Skip to main content

parquet/file/metadata/
reader.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18#[cfg(feature = "arrow")]
19use crate::arrow::ProjectionMask;
20#[cfg(feature = "encryption")]
21use crate::encryption::decrypt::FileDecryptionProperties;
22use crate::errors::{ParquetError, Result};
23use crate::file::FOOTER_SIZE;
24#[cfg(feature = "arrow")]
25use crate::file::metadata::dictionary::decode_dictionary_page;
26use crate::file::metadata::parser::decode_metadata;
27use crate::file::metadata::thrift::parquet_schema_from_bytes;
28use crate::file::metadata::{
29    FooterTail, ParquetMetaData, ParquetMetaDataOptions, ParquetMetaDataPushDecoder,
30};
31use crate::file::reader::ChunkReader;
32use crate::schema::types::SchemaDescriptor;
33#[cfg(feature = "arrow")]
34use arrow_array::ArrayRef;
35use bytes::Bytes;
36use std::collections::BTreeSet;
37use std::iter::FusedIterator;
38use std::slice::Iter;
39use std::sync::Arc;
40use std::{io::Read, ops::Range};
41
42use crate::DecodeResult;
43#[cfg(all(feature = "async", feature = "arrow"))]
44use crate::arrow::async_reader::{MetadataFetch, MetadataSuffixFetch};
45
46/// Reads [`ParquetMetaData`] from a byte stream, with either synchronous or
47/// asynchronous I/O.
48///
49/// There are two flavors of APIs:
50/// * Synchronous: [`Self::try_parse()`], [`Self::try_parse_sized()`], [`Self::parse_and_finish()`], etc.
51/// * Asynchronous (requires `async` and `arrow` features): [`Self::try_load()`], etc
52///
53///  See the [`ParquetMetaDataPushDecoder`] for an API that does not require I/O.
54///
55/// # Format Notes
56///
57/// Parquet metadata is not necessarily contiguous in a Parquet file: a portion is stored
58/// in the footer (the last bytes of the file), but other portions (such as the
59/// PageIndex) can be stored elsewhere.
60/// See [`crate::file::metadata::ParquetMetaDataWriter#output-format`] for more details of
61/// Parquet metadata.
62///
63/// This reader handles reading the footer as well as the non contiguous parts
64/// of the metadata (`PageIndex` and `ColumnIndex`). It does not handle reading Bloom Filters.
65///
66/// # Example
67/// ```no_run
68/// # use parquet::file::metadata::{PageIndexPolicy, ParquetMetaDataReader};
69/// # fn open_parquet_file(path: &str) -> std::fs::File { unimplemented!(); }
70/// // read parquet metadata including page indexes from a file
71/// let file = open_parquet_file("some_path.parquet");
72/// let mut reader = ParquetMetaDataReader::new()
73///     .with_page_index_policy(PageIndexPolicy::Required);
74/// reader.try_parse(&file).unwrap();
75/// let metadata = reader.finish().unwrap();
76/// assert!(metadata.page_index().is_some());
77/// ```
78#[derive(Default, Debug)]
79pub struct ParquetMetaDataReader {
80    metadata: Option<ParquetMetaData>,
81    column_index: PageIndexPolicy,
82    offset_index: PageIndexPolicy,
83    column_index_mask: ColumnChunkMask,
84    offset_index_mask: ColumnChunkMask,
85    prefetch_hint: Option<usize>,
86    metadata_options: Option<Arc<ParquetMetaDataOptions>>,
87    // Size of the serialized thrift metadata plus the 8 byte footer. Only set if
88    // `self.parse_metadata` is called.
89    metadata_size: Option<usize>,
90    #[cfg(feature = "encryption")]
91    file_decryption_properties: Option<Arc<FileDecryptionProperties>>,
92}
93
94// TODO: https://github.com/apache/arrow-rs/issues/11395 is tracking unifying PageIndexPolicy
95// with the ColumnChunkMask to reduce the dizzying number of APIs that exist to control reading
96// the page indexes.
97
98/// Describes the policy for reading page indexes
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
100pub enum PageIndexPolicy {
101    /// Do not read the page index.
102    #[default]
103    Skip,
104    /// Read the page index if it exists, otherwise do not error.
105    Optional,
106    /// Require offset indexes for every selected column chunk when any selected
107    /// page index ranges exist.
108    ///
109    /// For compatibility, a file with no page index ranges is accepted. This is only
110    /// enforced for the offset index; this is the same as [`Self::Optional`] for the
111    /// column index.
112    Required,
113}
114
115impl From<bool> for PageIndexPolicy {
116    fn from(value: bool) -> Self {
117        match value {
118            true => Self::Required,
119            false => Self::Skip,
120        }
121    }
122}
123
124/// A cheaply cloneable struct to specify column chunks for which metadata is required.
125///
126/// Column chunks are identified by row group index and leaf column index (the index of the
127/// column in [`SchemaDescriptor::columns`], not the index of a root or Arrow field). This struct
128/// allows for specifying vertical slices of column chunk data (via [`Self::columns`]),
129/// horizontal slices (via [`Self::row_groups`]), or the intersection of the two
130/// (via [`Self::row_groups_and_columns`]).
131///
132/// Because Parquet collection cannot contain more than [`i32::MAX`] values, user provided indices
133/// outside that range will be discarded.
134///
135/// At present this is only used to select elements of the [Page Index] for decoding.
136///
137/// # Examples
138///
139/// To select columns 0 and 1 from all row groups:
140/// ```rust
141/// # use parquet::file::metadata::ColumnChunkMask;
142/// let mask = ColumnChunkMask::columns([0, 1]);
143/// ```
144///
145/// To select all columns from row group 2:
146/// ```rust
147/// # use parquet::file::metadata::ColumnChunkMask;
148/// let mask = ColumnChunkMask::row_groups([2]);
149/// ```
150///
151/// To select columns 1 and 3 from row group 0:
152/// ```rust
153/// # use parquet::file::metadata::ColumnChunkMask;
154/// let mask = ColumnChunkMask::row_groups_and_columns([0], [1, 3]);
155/// ```
156///
157/// [Page Index]: https://parquet.apache.org/docs/file-format/pageindex/
158#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
159pub struct ColumnChunkMask {
160    // `row_groups` and `columns` are optional `u32` slices, with elements
161    // sorted in ascending order (to permit binary searching for member indices).
162    // `None` means all indices are included, while an empty slice means none are.
163    // Indices are stored as `u32` because Parquet/Thrift collections cannot contain
164    // more than i32::MAX entries.
165    row_groups: Option<Arc<[u32]>>,
166    columns: Option<Arc<[u32]>>,
167}
168
169impl ColumnChunkMask {
170    /// Select all row groups and columns.
171    pub fn all() -> Self {
172        Self::default()
173    }
174
175    /// Select no row groups or columns.
176    pub fn none() -> Self {
177        Self {
178            row_groups: Some(Arc::from([])),
179            columns: Some(Arc::from([])),
180        }
181    }
182
183    /// Select only the listed columns.
184    ///
185    /// Passing an empty iterator selects no columns.
186    pub fn columns(columns: impl IntoIterator<Item = usize>) -> Self {
187        Self {
188            row_groups: None,
189            columns: Self::iter_to_set(columns),
190        }
191    }
192
193    /// Select only the listed row groups.
194    ///
195    /// Passing an empty iterator selects no row groups.
196    pub fn row_groups(row_groups: impl IntoIterator<Item = usize>) -> Self {
197        Self {
198            row_groups: Self::iter_to_set(row_groups),
199            columns: None,
200        }
201    }
202
203    /// Select only the listed row groups and columns.
204    ///
205    /// An empty iterator for either dimension selects no column chunks.
206    pub fn row_groups_and_columns(
207        row_groups: impl IntoIterator<Item = usize>,
208        columns: impl IntoIterator<Item = usize>,
209    ) -> Self {
210        Self {
211            row_groups: Self::iter_to_set(row_groups),
212            columns: Self::iter_to_set(columns),
213        }
214    }
215
216    /// Returns whether row group `idx` is selected and within the bounds `[0, num_row_groups)`.
217    pub fn includes_row_group(&self, idx: usize, num_row_groups: usize) -> bool {
218        idx < num_row_groups && Self::includes_index(self.row_groups.as_ref(), idx)
219    }
220
221    /// Returns whether column `idx` is selected and within the bounds `[0, num_columns)`.
222    pub fn includes_column(&self, idx: usize, num_columns: usize) -> bool {
223        idx < num_columns && Self::includes_index(self.columns.as_ref(), idx)
224    }
225
226    fn includes_index(keep: Option<&Arc<[u32]>>, idx: usize) -> bool {
227        // return false for out-of-bounds index
228        let Ok(idx) = i32::try_from(idx) else {
229            return false;
230        };
231        let idx = idx as u32;
232        keep.is_none_or(|keep| keep.binary_search(&idx).is_ok())
233    }
234
235    /// Returns `true` when this mask selects every column chunk.
236    pub fn is_all(&self) -> bool {
237        self.row_groups.is_none() && self.columns.is_none()
238    }
239
240    /// Returns `true` when either configured axis is empty (and thus would select no column chunks).
241    ///
242    /// This does not consider the dimensions of a particular Parquet file, so may return `false`
243    /// when [`Self::column_indices`] or [`Self::row_group_indices`] returns an empty iterator due
244    /// to all elements exceeding the provided upper bound.
245    pub fn is_none(&self) -> bool {
246        self.row_groups.as_ref().is_some_and(|i| i.is_empty())
247            || self.columns.as_ref().is_some_and(|i| i.is_empty())
248    }
249
250    /// Creates a mask selecting the leaf columns in an Arrow projection.
251    #[cfg(feature = "arrow")]
252    pub fn from_projection(projection: &ProjectionMask, schema: &SchemaDescriptor) -> Self {
253        Self::columns((0..schema.num_columns()).filter(|&i| projection.leaf_included(i)))
254    }
255
256    /// Returns an iterator over the row group indices selected by this mask. If the mask is `all`
257    /// then all indices in the range `[0, num_row_groups)` are returned.
258    pub fn row_group_indices(&self, num_row_groups: usize) -> impl Iterator<Item = usize> + '_ {
259        Self::axis_indices(self.row_groups.as_deref(), num_row_groups)
260    }
261
262    /// Returns an iterator over the column indices selected by this mask. If the mask is `all`
263    /// then all indices in the range `[0, num_columns)` are returned.
264    pub fn column_indices(&self, num_columns: usize) -> impl Iterator<Item = usize> + '_ {
265        Self::axis_indices(self.columns.as_deref(), num_columns)
266    }
267
268    fn axis_indices(axis: Option<&[u32]>, len: usize) -> AxisIndices<'_> {
269        match axis {
270            None => AxisIndices::All(0..len),
271            Some(indices) => {
272                let end = indices.partition_point(|&i| (i as usize) < len);
273                AxisIndices::Selected(indices[..end].iter())
274            }
275        }
276    }
277
278    // Used during initialization, this helper takes an iterator over `usize` elements,
279    // discards out-of-range values and then sorts.
280    fn iter_to_set(indices: impl IntoIterator<Item = usize>) -> Option<Arc<[u32]>> {
281        Some(
282            indices
283                .into_iter()
284                .filter_map(|i| i32::try_from(i).ok()) // remove out-of-bounds values
285                .map(|i| i as u32) // store as u32. safe because 0 <= i <= i32::MAX
286                .collect::<BTreeSet<_>>()
287                .into_iter()
288                .collect::<Vec<_>>()
289                .into(),
290        )
291    }
292}
293
294// helper for ColumnChunkMask selected index traversal
295enum AxisIndices<'a> {
296    All(Range<usize>),
297    Selected(Iter<'a, u32>),
298}
299
300impl Iterator for AxisIndices<'_> {
301    type Item = usize;
302
303    fn next(&mut self) -> Option<Self::Item> {
304        match self {
305            Self::All(indices) => indices.next(),
306            Self::Selected(indices) => indices.next().map(|&i| i as usize),
307        }
308    }
309
310    fn size_hint(&self) -> (usize, Option<usize>) {
311        match self {
312            Self::All(indices) => indices.size_hint(),
313            Self::Selected(indices) => indices.size_hint(),
314        }
315    }
316}
317
318impl ExactSizeIterator for AxisIndices<'_> {}
319impl FusedIterator for AxisIndices<'_> {}
320
321impl ParquetMetaDataReader {
322    /// Create a new [`ParquetMetaDataReader`]
323    pub fn new() -> Self {
324        Default::default()
325    }
326
327    /// Create a new [`ParquetMetaDataReader`] populated with a [`ParquetMetaData`] struct
328    /// obtained via other means.
329    pub fn new_with_metadata(metadata: ParquetMetaData) -> Self {
330        Self {
331            metadata: Some(metadata),
332            ..Default::default()
333        }
334    }
335
336    /// Sets the [`PageIndexPolicy`] for the column and offset indexes
337    pub fn with_page_index_policy(self, policy: PageIndexPolicy) -> Self {
338        self.with_column_index_policy(policy)
339            .with_offset_index_policy(policy)
340    }
341
342    /// Sets the [`PageIndexPolicy`] for the column index
343    pub fn with_column_index_policy(mut self, policy: PageIndexPolicy) -> Self {
344        self.column_index = policy;
345        self
346    }
347
348    /// Sets the [`PageIndexPolicy`] for the offset index
349    pub fn with_offset_index_policy(mut self, policy: PageIndexPolicy) -> Self {
350        self.offset_index = policy;
351        self
352    }
353
354    // TODO: deprecate the following as part of https://github.com/apache/arrow-rs/issues/11395
355
356    /// Selects the row groups and leaf columns for which both page index structures are read.
357    ///
358    /// The mask applies only to index structures whose policy is not [`PageIndexPolicy::Skip`].
359    /// This reader fetches one byte range covering the selected indexes, so a sparse mask
360    /// primarily reduces decoding and memory rather than necessarily reducing I/O.
361    pub fn with_page_index_mask(self, mask: ColumnChunkMask) -> Self {
362        self.with_column_index_mask(mask.clone())
363            .with_offset_index_mask(mask)
364    }
365
366    /// Selects the row groups and leaf columns for which column indexes are read.
367    ///
368    /// The mask applies only when the column index policy is not [`PageIndexPolicy::Skip`].
369    /// [`PageIndexPolicy::Required`] validates only selected column chunks.
370    pub fn with_column_index_mask(mut self, mask: ColumnChunkMask) -> Self {
371        self.column_index_mask = mask;
372        self
373    }
374
375    /// Selects the row groups and leaf columns for which offset indexes are read.
376    ///
377    /// The mask applies only when the offset index policy is not [`PageIndexPolicy::Skip`].
378    /// [`PageIndexPolicy::Required`] validates only selected column chunks.
379    pub fn with_offset_index_mask(mut self, mask: ColumnChunkMask) -> Self {
380        self.offset_index_mask = mask;
381        self
382    }
383
384    /// Sets the [`ParquetMetaDataOptions`] to use when decoding
385    pub fn with_metadata_options(mut self, options: Option<ParquetMetaDataOptions>) -> Self {
386        self.metadata_options = options.map(Arc::new);
387        self
388    }
389
390    /// Provide a hint as to the number of bytes needed to fully parse the [`ParquetMetaData`].
391    /// Only used for the asynchronous [`Self::try_load()`] method.
392    ///
393    /// By default, the reader will first fetch the last 8 bytes of the input file to obtain the
394    /// size of the footer metadata. A second fetch will be performed to obtain the needed bytes.
395    /// After parsing the footer metadata, a third fetch will be performed to obtain the bytes
396    /// needed to decode the page index structures, if they have been requested. To avoid
397    /// unnecessary fetches, `prefetch` can be set to an estimate of the number of bytes needed
398    /// to fully decode the [`ParquetMetaData`], which can reduce the number of fetch requests and
399    /// reduce latency. Setting `prefetch` too small will not trigger an error, but will result
400    /// in extra fetches being performed.
401    pub fn with_prefetch_hint(mut self, prefetch: Option<usize>) -> Self {
402        self.prefetch_hint = prefetch;
403        self
404    }
405
406    /// Provide the FileDecryptionProperties to use when decrypting the file.
407    ///
408    /// This is only necessary when the file is encrypted.
409    #[cfg(feature = "encryption")]
410    pub fn with_decryption_properties(
411        mut self,
412        properties: Option<std::sync::Arc<FileDecryptionProperties>>,
413    ) -> Self {
414        self.file_decryption_properties = properties;
415        self
416    }
417
418    /// Indicates whether this reader has a [`ParquetMetaData`] internally.
419    pub fn has_metadata(&self) -> bool {
420        self.metadata.is_some()
421    }
422
423    /// Return the parsed [`ParquetMetaData`] struct, leaving `None` in its place.
424    pub fn finish(&mut self) -> Result<ParquetMetaData> {
425        self.metadata
426            .take()
427            .ok_or_else(|| general_err!("could not parse parquet metadata"))
428    }
429
430    /// Given a [`ChunkReader`], parse and return the [`ParquetMetaData`] in a single pass.
431    ///
432    /// If `reader` is [`Bytes`] based, then the buffer must contain sufficient bytes to complete
433    /// the request, and must include the Parquet footer. If page indexes are desired, the buffer
434    /// must contain the entire file, or [`Self::try_parse_sized()`] should be used.
435    ///
436    /// This call will consume `self`.
437    ///
438    /// # Example
439    /// ```no_run
440    /// # use parquet::file::metadata::{PageIndexPolicy, ParquetMetaDataReader};
441    /// # fn open_parquet_file(path: &str) -> std::fs::File { unimplemented!(); }
442    /// // read parquet metadata including page indexes
443    /// let file = open_parquet_file("some_path.parquet");
444    /// let metadata = ParquetMetaDataReader::new()
445    ///     .with_page_index_policy(PageIndexPolicy::Required)
446    ///     .parse_and_finish(&file).unwrap();
447    /// ```
448    pub fn parse_and_finish<R: ChunkReader>(mut self, reader: &R) -> Result<ParquetMetaData> {
449        self.try_parse(reader)?;
450        self.finish()
451    }
452
453    /// Attempts to parse the footer metadata (and optionally page indexes) given a [`ChunkReader`].
454    ///
455    /// If `reader` is [`Bytes`] based, then the buffer must contain sufficient bytes to complete
456    /// the request, and must include the Parquet footer. If page indexes are desired, the buffer
457    /// must contain the entire file, or [`Self::try_parse_sized()`] should be used.
458    pub fn try_parse<R: ChunkReader>(&mut self, reader: &R) -> Result<()> {
459        self.try_parse_sized(reader, reader.len())
460    }
461
462    /// Same as [`Self::try_parse()`], but provide the original file size in the case that `reader`
463    /// is a [`Bytes`] struct that does not contain the entire file. This information is necessary
464    /// when the page indexes are desired. `reader` must have access to the Parquet footer.
465    ///
466    /// Using this function also allows for retrying with a larger buffer.
467    ///
468    /// # Errors
469    ///
470    /// This function will return [`ParquetError::NeedMoreData`] in the event `reader` does not
471    /// provide enough data to fully parse the metadata (see example below). The returned error
472    /// will be populated with a `usize` field indicating the number of bytes required from the
473    /// tail of the file to completely parse the requested metadata.
474    ///
475    /// Other errors returned include [`ParquetError::General`] and [`ParquetError::EOF`].
476    ///
477    /// # Example
478    /// ```no_run
479    /// # use parquet::file::metadata::{PageIndexPolicy, ParquetMetaDataReader};
480    /// # use parquet::errors::ParquetError;
481    /// # use crate::parquet::file::reader::Length;
482    /// # fn get_bytes(file: &std::fs::File, range: std::ops::Range<u64>) -> bytes::Bytes { unimplemented!(); }
483    /// # fn open_parquet_file(path: &str) -> std::fs::File { unimplemented!(); }
484    /// let file = open_parquet_file("some_path.parquet");
485    /// let len = file.len();
486    /// // Speculatively read 1 kilobyte from the end of the file
487    /// let bytes = get_bytes(&file, len - 1024..len);
488    /// let mut reader = ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Required);
489    /// match reader.try_parse_sized(&bytes, len) {
490    ///     Ok(_) => (),
491    ///     Err(ParquetError::NeedMoreData(needed)) => {
492    ///         // Read the needed number of bytes from the end of the file
493    ///         let bytes = get_bytes(&file, len - needed as u64..len);
494    ///         reader.try_parse_sized(&bytes, len).unwrap();
495    ///     }
496    ///     _ => panic!("unexpected error")
497    /// }
498    /// let metadata = reader.finish().unwrap();
499    /// ```
500    ///
501    /// Note that it is possible for the file metadata to be completely read, but there are
502    /// insufficient bytes available to read the page indexes. [`Self::has_metadata()`] can be used
503    /// to test for this. In the event the file metadata is present, re-parsing of the file
504    /// metadata can be skipped by using [`Self::read_page_indexes_sized()`], as shown below.
505    /// ```no_run
506    /// # use parquet::file::metadata::{PageIndexPolicy, ParquetMetaDataReader};
507    /// # use parquet::errors::ParquetError;
508    /// # use crate::parquet::file::reader::Length;
509    /// # fn get_bytes(file: &std::fs::File, range: std::ops::Range<u64>) -> bytes::Bytes { unimplemented!(); }
510    /// # fn open_parquet_file(path: &str) -> std::fs::File { unimplemented!(); }
511    /// let file = open_parquet_file("some_path.parquet");
512    /// let len = file.len();
513    /// // Speculatively read 1 kilobyte from the end of the file
514    /// let mut bytes = get_bytes(&file, len - 1024..len);
515    /// let mut reader = ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Required);
516    /// // Loop until `bytes` is large enough
517    /// loop {
518    ///     match reader.try_parse_sized(&bytes, len) {
519    ///         Ok(_) => break,
520    ///         Err(ParquetError::NeedMoreData(needed)) => {
521    ///             // Read the needed number of bytes from the end of the file
522    ///             bytes = get_bytes(&file, len - needed as u64..len);
523    ///             // If file metadata was read only read page indexes, otherwise continue loop
524    ///             if reader.has_metadata() {
525    ///                 reader.read_page_indexes_sized(&bytes, len).unwrap();
526    ///                 break;
527    ///             }
528    ///         }
529    ///         _ => panic!("unexpected error")
530    ///     }
531    /// }
532    /// let metadata = reader.finish().unwrap();
533    /// ```
534    pub fn try_parse_sized<R: ChunkReader>(&mut self, reader: &R, file_size: u64) -> Result<()> {
535        self.metadata = match self.parse_metadata(reader) {
536            Ok(metadata) => Some(metadata),
537            Err(ParquetError::NeedMoreData(needed)) => {
538                // If reader is the same length as `file_size` then presumably there is no more to
539                // read, so return an EOF error.
540                return if file_size == reader.len() || needed as u64 > file_size {
541                    Err(eof_err!(
542                        "Parquet file too small. Size is {} but need {}",
543                        file_size,
544                        needed
545                    ))
546                } else {
547                    // Ask for a larger buffer
548                    Err(ParquetError::NeedMoreData(needed))
549                };
550            }
551            Err(e) => return Err(e),
552        };
553
554        // we can return if page indexes aren't requested
555        if self.column_index == PageIndexPolicy::Skip && self.offset_index == PageIndexPolicy::Skip
556        {
557            return Ok(());
558        }
559
560        self.read_page_indexes_sized(reader, file_size)
561    }
562
563    /// Read the page index structures when a [`ParquetMetaData`] has already been obtained.
564    /// See [`Self::new_with_metadata()`] and [`Self::has_metadata()`].
565    ///
566    /// # Behavior on multiple calls
567    ///
568    /// On success, this method replaces any existing page index in the metadata. Index types
569    /// configured as [`PageIndexPolicy::Skip`] are cleared; other index types contain only the
570    /// indexes selected by their configured masks and available according to their policies. If
571    /// neither index type is populated, the page index is cleared.
572    pub fn read_page_indexes<R: ChunkReader>(&mut self, reader: &R) -> Result<()> {
573        self.read_page_indexes_sized(reader, reader.len())
574    }
575
576    /// Read the page index structures when a [`ParquetMetaData`] has already been obtained.
577    /// This variant is used when `reader` cannot access the entire Parquet file (e.g. it is
578    /// a [`Bytes`] struct containing the tail of the file).
579    /// See [`Self::new_with_metadata()`] and [`Self::has_metadata()`]. Like
580    /// [`Self::try_parse_sized()`] this function may return [`ParquetError::NeedMoreData`].
581    ///
582    /// See [`Self::read_page_indexes()`] for a description of the replacement behavior on
583    /// multiple calls.
584    pub fn read_page_indexes_sized<R: ChunkReader>(
585        &mut self,
586        reader: &R,
587        file_size: u64,
588    ) -> Result<()> {
589        let Some(metadata) = self.metadata.take() else {
590            return Err(general_err!(
591                "Tried to read page indexes without ParquetMetaData metadata"
592            ));
593        };
594
595        let push_decoder = ParquetMetaDataPushDecoder::try_new_with_metadata(file_size, metadata)?
596            .with_offset_index_policy(self.offset_index)
597            .with_column_index_policy(self.column_index)
598            .with_offset_index_mask(self.offset_index_mask.clone())
599            .with_column_index_mask(self.column_index_mask.clone())
600            .with_metadata_options(self.metadata_options.clone());
601        let mut push_decoder = self.prepare_push_decoder(push_decoder);
602
603        // Get bounds needed for page indexes (if any are present in the file).
604        let range = match needs_index_data(&mut push_decoder)? {
605            NeedsIndexData::No(metadata) => {
606                self.metadata = Some(metadata);
607                return Ok(());
608            }
609            NeedsIndexData::Yes(range) => range,
610        };
611
612        // Check to see if needed range is within `file_range`. Checking `range.end` seems
613        // redundant, but it guards against `range_for_page_index()` returning garbage.
614        let file_range = file_size.saturating_sub(reader.len())..file_size;
615        if !(file_range.contains(&range.start) && file_range.contains(&range.end)) {
616            // Requested range starts beyond EOF
617            return if range.end > file_size {
618                Err(eof_err!(
619                    "Parquet file too small. Range {range:?} is beyond file bounds {file_size}",
620                ))
621            } else {
622                // Ask for a larger buffer
623                Err(ParquetError::NeedMoreData(
624                    (file_size - range.start).try_into()?,
625                ))
626            };
627        }
628
629        // Perform extra sanity check to make sure `range` and the footer metadata don't
630        // overlap.
631        if let Some(metadata_size) = self.metadata_size {
632            let metadata_range = file_size.saturating_sub(metadata_size as u64)..file_size;
633            if range.end > metadata_range.start {
634                return Err(eof_err!(
635                    "Parquet file too small. Page index range {range:?} overlaps with file metadata {metadata_range:?}",
636                ));
637            }
638        }
639
640        // add the needed ranges to the decoder
641        let bytes_needed = usize::try_from(range.end - range.start)?;
642        let bytes = reader.get_bytes(range.start - file_range.start, bytes_needed)?;
643
644        push_decoder.push_range(range, bytes)?;
645        let metadata = parse_index_data(&mut push_decoder)?;
646        self.metadata = Some(metadata);
647
648        Ok(())
649    }
650
651    /// Given a [`MetadataFetch`], parse and return the [`ParquetMetaData`] in a single pass.
652    ///
653    /// This call will consume `self`.
654    ///
655    /// See [`Self::with_prefetch_hint`] for a discussion of how to reduce the number of fetches
656    /// performed by this function.
657    #[cfg(all(feature = "async", feature = "arrow"))]
658    pub async fn load_and_finish<F: MetadataFetch>(
659        mut self,
660        fetch: F,
661        file_size: u64,
662    ) -> Result<ParquetMetaData> {
663        self.try_load(fetch, file_size).await?;
664        self.finish()
665    }
666
667    /// Given a [`MetadataSuffixFetch`], parse and return the [`ParquetMetaData`] in a single pass.
668    ///
669    /// This call will consume `self`.
670    ///
671    /// See [`Self::with_prefetch_hint`] for a discussion of how to reduce the number of fetches
672    /// performed by this function.
673    #[cfg(all(feature = "async", feature = "arrow"))]
674    pub async fn load_via_suffix_and_finish<F: MetadataSuffixFetch>(
675        mut self,
676        fetch: F,
677    ) -> Result<ParquetMetaData> {
678        self.try_load_via_suffix(fetch).await?;
679        self.finish()
680    }
681    /// Attempts to (asynchronously) parse the footer metadata (and optionally page indexes)
682    /// given a [`MetadataFetch`].
683    ///
684    /// See [`Self::with_prefetch_hint`] for a discussion of how to reduce the number of fetches
685    /// performed by this function.
686    #[cfg(all(feature = "async", feature = "arrow"))]
687    pub async fn try_load<F: MetadataFetch>(&mut self, mut fetch: F, file_size: u64) -> Result<()> {
688        let (metadata, remainder) = self.load_metadata(&mut fetch, file_size).await?;
689
690        self.metadata = Some(metadata);
691
692        // we can return if page indexes aren't requested
693        if self.column_index == PageIndexPolicy::Skip && self.offset_index == PageIndexPolicy::Skip
694        {
695            return Ok(());
696        }
697
698        self.load_page_index_with_remainder(fetch, remainder).await
699    }
700
701    /// Attempts to (asynchronously) parse the footer metadata (and optionally page indexes)
702    /// given a [`MetadataSuffixFetch`].
703    ///
704    /// See [`Self::with_prefetch_hint`] for a discussion of how to reduce the number of fetches
705    /// performed by this function.
706    #[cfg(all(feature = "async", feature = "arrow"))]
707    pub async fn try_load_via_suffix<F: MetadataSuffixFetch>(
708        &mut self,
709        mut fetch: F,
710    ) -> Result<()> {
711        let metadata = self.load_metadata_via_suffix(&mut fetch).await?;
712
713        self.metadata = Some(metadata);
714
715        // we can return if page indexes aren't requested
716        if self.column_index == PageIndexPolicy::Skip && self.offset_index == PageIndexPolicy::Skip
717        {
718            return Ok(());
719        }
720
721        self.load_page_index_with_remainder(fetch, None).await
722    }
723
724    /// Asynchronously fetch the page index structures when a [`ParquetMetaData`] has already
725    /// been obtained. See [`Self::new_with_metadata()`].
726    ///
727    /// See [`Self::read_page_indexes()`] for a description of the replacement behavior on
728    /// multiple calls.
729    #[cfg(all(feature = "async", feature = "arrow"))]
730    pub async fn load_page_index<F: MetadataFetch>(&mut self, fetch: F) -> Result<()> {
731        self.load_page_index_with_remainder(fetch, None).await
732    }
733
734    /// Reads and decodes the dictionary page of a column chunk into an Arrow array.
735    ///
736    /// Returns `Ok(None)` if the column chunk has no dictionary page, or if
737    /// its physical type is not `BYTE_ARRAY` (the only physical type
738    /// currently supported).
739    ///
740    /// The returned array contains raw `Binary` values, even for columns
741    /// annotated as strings. Callers can compare byte slices directly or
742    /// convert values to UTF-8 explicitly.
743    ///
744    /// This can be used to inspect dictionary values when selecting or pruning
745    /// row groups before reading their data pages.
746    ///
747    /// Note this does not verify that the *entire* column chunk is
748    /// dictionary-encoded (i.e. that the dictionary contains every value in
749    /// the chunk) -- callers that need that guarantee should check
750    /// [`crate::file::metadata::ColumnChunkMetaData::page_encoding_stats_mask`].
751    #[cfg(feature = "arrow")]
752    pub fn read_column_dictionary<R: ChunkReader>(
753        reader: &R,
754        metadata: &ParquetMetaData,
755        row_group_idx: usize,
756        column_idx: usize,
757    ) -> Result<Option<ArrayRef>> {
758        let Some(range) = dictionary_page_byte_range(metadata, row_group_idx, column_idx)? else {
759            return Ok(None);
760        };
761        let length = usize::try_from(range.end - range.start)?;
762        let buffer = reader.get_bytes(range.start, length)?;
763        decode_dictionary_page(buffer, metadata, row_group_idx, column_idx).map(Some)
764    }
765
766    /// Asynchronous version of [`Self::read_column_dictionary`].
767    #[cfg(all(feature = "async", feature = "arrow"))]
768    pub async fn read_column_dictionary_async<F: MetadataFetch>(
769        mut fetch: F,
770        metadata: &ParquetMetaData,
771        row_group_idx: usize,
772        column_idx: usize,
773    ) -> Result<Option<ArrayRef>> {
774        let Some(range) = dictionary_page_byte_range(metadata, row_group_idx, column_idx)? else {
775            return Ok(None);
776        };
777        let buffer = fetch.fetch(range).await?;
778        decode_dictionary_page(buffer, metadata, row_group_idx, column_idx).map(Some)
779    }
780
781    #[cfg(all(feature = "async", feature = "arrow"))]
782    async fn load_page_index_with_remainder<F: MetadataFetch>(
783        &mut self,
784        mut fetch: F,
785        remainder: Option<(usize, Bytes)>,
786    ) -> Result<()> {
787        let Some(metadata) = self.metadata.take() else {
788            return Err(general_err!("Footer metadata is not present"));
789        };
790
791        // in this case we don't actually know what the file size is, so just use u64::MAX
792        // this is ok since the offsets in the metadata are always valid
793        let file_size = u64::MAX;
794        let push_decoder = ParquetMetaDataPushDecoder::try_new_with_metadata(file_size, metadata)?
795            .with_offset_index_policy(self.offset_index)
796            .with_column_index_policy(self.column_index)
797            .with_offset_index_mask(self.offset_index_mask.clone())
798            .with_column_index_mask(self.column_index_mask.clone())
799            .with_metadata_options(self.metadata_options.clone());
800        let mut push_decoder = self.prepare_push_decoder(push_decoder);
801
802        // Get bounds needed for page indexes (if any are present in the file).
803        let range = match needs_index_data(&mut push_decoder)? {
804            NeedsIndexData::No(metadata) => {
805                self.metadata = Some(metadata);
806                return Ok(());
807            }
808            NeedsIndexData::Yes(range) => range,
809        };
810
811        let bytes = match &remainder {
812            Some((remainder_start, remainder)) if *remainder_start as u64 <= range.start => {
813                let remainder_start = *remainder_start as u64;
814                let offset = usize::try_from(range.start - remainder_start)?;
815                let end = usize::try_from(range.end - remainder_start)?;
816                if end > remainder.len() {
817                    return Err(general_err!(
818                        "Corrupted parquet file: index data range ({:?}) exceeds remainder length ({})",
819                        range,
820                        remainder.len()
821                    ));
822                }
823                remainder.slice(offset..end)
824            }
825            // Note: this will potentially fetch data already in remainder, this keeps things simple
826            _ => fetch.fetch(range.clone()).await?,
827        };
828
829        // Sanity check
830        if bytes.len() as u64 != range.end - range.start {
831            return Err(general_err!(
832                "Corrupted parquet file: index data length mismatch, expected {}, got {}",
833                range.end - range.start,
834                bytes.len()
835            ));
836        }
837        push_decoder.push_range(range.clone(), bytes)?;
838        let metadata = parse_index_data(&mut push_decoder)?;
839        self.metadata = Some(metadata);
840        Ok(())
841    }
842
843    // One-shot parse of footer.
844    // Side effect: this will set `self.metadata_size`
845    fn parse_metadata<R: ChunkReader>(&mut self, chunk_reader: &R) -> Result<ParquetMetaData> {
846        // check file is large enough to hold footer
847        let file_size = chunk_reader.len();
848        if file_size < (FOOTER_SIZE as u64) {
849            return Err(ParquetError::NeedMoreData(FOOTER_SIZE));
850        }
851
852        let mut footer = [0_u8; FOOTER_SIZE];
853        chunk_reader
854            .get_read(file_size - FOOTER_SIZE as u64)?
855            .read_exact(&mut footer)?;
856
857        let footer = FooterTail::try_new(&footer)?;
858        let metadata_len = footer.metadata_length();
859        let footer_metadata_len = FOOTER_SIZE + metadata_len;
860        self.metadata_size = Some(footer_metadata_len);
861
862        if footer_metadata_len as u64 > file_size {
863            return Err(ParquetError::NeedMoreData(footer_metadata_len));
864        }
865
866        let start = file_size - footer_metadata_len as u64;
867        let bytes = chunk_reader.get_bytes(start, metadata_len)?;
868        self.decode_footer_metadata(bytes, file_size, footer)
869    }
870
871    /// Size of the serialized thrift metadata plus the 8 byte footer. Only set if
872    /// `self.parse_metadata` is called.
873    pub fn metadata_size(&self) -> Option<usize> {
874        self.metadata_size
875    }
876
877    /// Return the number of bytes to read in the initial pass. If `prefetch_size` has
878    /// been provided, then return that value if it is larger than the size of the Parquet
879    /// file footer (8 bytes). Otherwise returns `8`.
880    #[cfg(all(feature = "async", feature = "arrow"))]
881    fn get_prefetch_size(&self) -> usize {
882        if let Some(prefetch) = self.prefetch_hint
883            && prefetch > FOOTER_SIZE
884        {
885            return prefetch;
886        }
887        FOOTER_SIZE
888    }
889
890    #[cfg(all(feature = "async", feature = "arrow"))]
891    async fn load_metadata<F: MetadataFetch>(
892        &self,
893        fetch: &mut F,
894        file_size: u64,
895    ) -> Result<(ParquetMetaData, Option<(usize, Bytes)>)> {
896        let prefetch = self.get_prefetch_size() as u64;
897
898        if file_size < FOOTER_SIZE as u64 {
899            return Err(eof_err!("file size of {} is less than footer", file_size));
900        }
901
902        // If a size hint is provided, read more than the minimum size
903        // to try and avoid a second fetch.
904        // Note: prefetch > file_size is ok since we're using saturating_sub.
905        let footer_start = file_size.saturating_sub(prefetch);
906
907        let suffix = fetch.fetch(footer_start..file_size).await?;
908        let suffix_len = suffix.len();
909        let fetch_len = (file_size - footer_start)
910            .try_into()
911            .expect("footer size should never be larger than u32");
912        if suffix_len < fetch_len {
913            return Err(eof_err!(
914                "metadata requires {} bytes, but could only read {}",
915                fetch_len,
916                suffix_len
917            ));
918        }
919
920        let mut footer = [0; FOOTER_SIZE];
921        footer.copy_from_slice(&suffix[suffix_len - FOOTER_SIZE..suffix_len]);
922
923        let footer = FooterTail::try_new(&footer)?;
924        let length = footer.metadata_length();
925
926        if file_size < (length + FOOTER_SIZE) as u64 {
927            return Err(eof_err!(
928                "file size of {} is less than footer + metadata {}",
929                file_size,
930                length + FOOTER_SIZE
931            ));
932        }
933
934        // Did not fetch the entire file metadata in the initial read, need to make a second request
935        if length > suffix_len - FOOTER_SIZE {
936            let metadata_start = file_size - (length + FOOTER_SIZE) as u64;
937            let meta = fetch
938                .fetch(metadata_start..(file_size - FOOTER_SIZE as u64))
939                .await?;
940            Ok((self.decode_footer_metadata(meta, file_size, footer)?, None))
941        } else {
942            let metadata_start = (file_size - (length + FOOTER_SIZE) as u64 - footer_start)
943                .try_into()
944                .expect("metadata length should never be larger than u32");
945            let slice = suffix.slice(metadata_start..suffix_len - FOOTER_SIZE);
946            Ok((
947                self.decode_footer_metadata(slice, file_size, footer)?,
948                Some((footer_start as usize, suffix.slice(..metadata_start))),
949            ))
950        }
951    }
952
953    #[cfg(all(feature = "async", feature = "arrow"))]
954    // Unlike load_metadata, the file size is not known so it is not safe
955    // to use any leftover bytes that may have been pre-fetched. Thus this
956    // returns only the metadata.
957    async fn load_metadata_via_suffix<F: MetadataSuffixFetch>(
958        &self,
959        fetch: &mut F,
960    ) -> Result<ParquetMetaData> {
961        let prefetch = self.get_prefetch_size();
962
963        let suffix = fetch.fetch_suffix(prefetch).await?;
964        let suffix_len = suffix.len();
965
966        if suffix_len < FOOTER_SIZE {
967            return Err(eof_err!(
968                "footer metadata requires {} bytes, but could only read {}",
969                FOOTER_SIZE,
970                suffix_len
971            ));
972        }
973
974        let mut footer = [0; FOOTER_SIZE];
975        footer.copy_from_slice(&suffix[suffix_len - FOOTER_SIZE..suffix_len]);
976
977        let footer = FooterTail::try_new(&footer)?;
978        let length = footer.metadata_length();
979        // fake file size as we are only parsing the footer metadata here
980        // (cant be parsing page indexes without the full file size)
981        let file_size = (length + FOOTER_SIZE) as u64;
982
983        // Did not fetch the entire file metadata in the initial read, need to make a second request
984        let metadata_offset = length + FOOTER_SIZE;
985        if length > suffix_len - FOOTER_SIZE {
986            let meta = fetch.fetch_suffix(metadata_offset).await?;
987
988            if meta.len() < metadata_offset {
989                return Err(eof_err!(
990                    "metadata requires {} bytes, but could only read {}",
991                    metadata_offset,
992                    meta.len()
993                ));
994            }
995
996            // need to slice off the footer or decryption fails
997            let meta = meta.slice(0..length);
998            Ok(self.decode_footer_metadata(meta, file_size, footer)?)
999        } else {
1000            let metadata_start = suffix_len - metadata_offset;
1001            let slice = suffix.slice(metadata_start..suffix_len - FOOTER_SIZE);
1002            Ok(self.decode_footer_metadata(slice, file_size, footer)?)
1003        }
1004    }
1005
1006    /// Decodes [`ParquetMetaData`] from the provided bytes.
1007    ///
1008    /// Typically, this is used to decode the metadata from the end of a parquet
1009    /// file. The format of `buf` is the Thrift compact binary protocol, as specified
1010    /// by the [Parquet Spec].
1011    ///
1012    /// It does **NOT** include the 8-byte footer.
1013    ///
1014    /// This method handles using either `decode_metadata` or
1015    /// `decode_metadata_with_encryption` depending on whether the encryption
1016    /// feature is enabled.
1017    ///
1018    /// [Parquet Spec]: https://github.com/apache/parquet-format#metadata
1019    pub(crate) fn decode_footer_metadata(
1020        &self,
1021        buf: Bytes,
1022        file_size: u64,
1023        footer_tail: FooterTail,
1024    ) -> Result<ParquetMetaData> {
1025        // The push decoder expects the metadata to be at the end of the file
1026        // (... data ...) + (metadata) + (footer)
1027        // so we need to provide the starting offset of the metadata
1028        // within the file.
1029        let ending_offset = file_size.checked_sub(FOOTER_SIZE as u64).ok_or_else(|| {
1030            general_err!(
1031                "file size {file_size} is smaller than footer size {}",
1032                FOOTER_SIZE
1033            )
1034        })?;
1035
1036        let starting_offset = ending_offset.checked_sub(buf.len() as u64).ok_or_else(|| {
1037            general_err!(
1038                "file size {file_size} is smaller than buffer size {} + footer size {}",
1039                buf.len(),
1040                FOOTER_SIZE
1041            )
1042        })?;
1043
1044        let range = starting_offset..ending_offset;
1045
1046        let push_decoder =
1047            ParquetMetaDataPushDecoder::try_new_with_footer_tail(file_size, footer_tail)?
1048                // NOTE: DO NOT enable page indexes here, they are handled separately
1049                .with_page_index_policy(PageIndexPolicy::Skip)
1050                .with_metadata_options(self.metadata_options.clone());
1051
1052        let mut push_decoder = self.prepare_push_decoder(push_decoder);
1053        push_decoder.push_range(range, buf)?;
1054        match push_decoder.try_decode()? {
1055            DecodeResult::Data(metadata) => Ok(metadata),
1056            DecodeResult::Finished => Err(general_err!(
1057                "could not parse parquet metadata -- previously finished"
1058            )),
1059            DecodeResult::NeedsData(ranges) => Err(general_err!(
1060                "could not parse parquet metadata, needs ranges {:?}",
1061                ranges
1062            )),
1063        }
1064    }
1065
1066    /// Prepares a push decoder and runs it to decode the metadata.
1067    #[cfg(feature = "encryption")]
1068    fn prepare_push_decoder(
1069        &self,
1070        push_decoder: ParquetMetaDataPushDecoder,
1071    ) -> ParquetMetaDataPushDecoder {
1072        push_decoder.with_file_decryption_properties(
1073            self.file_decryption_properties
1074                .as_ref()
1075                .map(std::sync::Arc::clone),
1076        )
1077    }
1078    #[cfg(not(feature = "encryption"))]
1079    fn prepare_push_decoder(
1080        &self,
1081        push_decoder: ParquetMetaDataPushDecoder,
1082    ) -> ParquetMetaDataPushDecoder {
1083        push_decoder
1084    }
1085
1086    /// Decodes [`ParquetMetaData`] from the provided bytes.
1087    ///
1088    /// Typically this is used to decode the metadata from the end of a parquet
1089    /// file. The format of `buf` is the Thrift compact binary protocol, as specified
1090    /// by the [Parquet Spec].
1091    ///
1092    /// [Parquet Spec]: https://github.com/apache/parquet-format#metadata
1093    pub fn decode_metadata(buf: &[u8]) -> Result<ParquetMetaData> {
1094        decode_metadata(buf, None)
1095    }
1096
1097    /// Decodes [`ParquetMetaData`] from the provided bytes.
1098    ///
1099    /// Like [`Self::decode_metadata`] but this also accepts
1100    /// metadata parsing options.
1101    pub fn decode_metadata_with_options(
1102        buf: &[u8],
1103        options: Option<&ParquetMetaDataOptions>,
1104    ) -> Result<ParquetMetaData> {
1105        decode_metadata(buf, options)
1106    }
1107
1108    /// Decodes the schema from the Parquet footer in `buf`. Returned as
1109    /// a [`SchemaDescriptor`].
1110    pub fn decode_schema(buf: &[u8]) -> Result<Arc<SchemaDescriptor>> {
1111        Ok(Arc::new(parquet_schema_from_bytes(buf)?))
1112    }
1113}
1114
1115/// The bounds needed to read page indexes
1116// this is an internal enum, so it is ok to allow differences in enum size
1117enum NeedsIndexData {
1118    /// no additional data is needed (e.g. the indexes weren't requested)
1119    No(ParquetMetaData),
1120    /// Additional data is needed, with the range that are required
1121    Yes(Range<u64>),
1122}
1123
1124/// Determines a single combined range of bytes needed to read the page indexes,
1125/// or returns the metadata if no additional data is needed (e.g. if no page indexes are requested)
1126fn needs_index_data(push_decoder: &mut ParquetMetaDataPushDecoder) -> Result<NeedsIndexData> {
1127    match push_decoder.try_decode()? {
1128        DecodeResult::NeedsData(ranges) => {
1129            let range = ranges
1130                .into_iter()
1131                .reduce(|a, b| a.start.min(b.start)..a.end.max(b.end))
1132                .ok_or_else(|| general_err!("Internal error: no ranges provided"))?;
1133            Ok(NeedsIndexData::Yes(range))
1134        }
1135        DecodeResult::Data(metadata) => Ok(NeedsIndexData::No(metadata)),
1136        DecodeResult::Finished => Err(general_err!("Internal error: decoder was finished")),
1137    }
1138}
1139
1140/// Given a push decoder that has had the needed ranges pushed to it,
1141/// attempt to decode indexes and return the updated metadata.
1142fn parse_index_data(push_decoder: &mut ParquetMetaDataPushDecoder) -> Result<ParquetMetaData> {
1143    match push_decoder.try_decode()? {
1144        DecodeResult::NeedsData(_) => Err(general_err!(
1145            "Internal error: decoder still needs data after reading required range"
1146        )),
1147        DecodeResult::Data(metadata) => Ok(metadata),
1148        DecodeResult::Finished => Err(general_err!("Internal error: decoder was finished")),
1149    }
1150}
1151
1152/// Returns the `[start, end)` byte range of a column chunk's dictionary
1153/// page, or `None` if the column chunk has no dictionary page or is not a
1154/// `BYTE_ARRAY` column (the only physical type [`decode_dictionary_page`]
1155/// currently supports).
1156#[cfg(feature = "arrow")]
1157fn dictionary_page_byte_range(
1158    metadata: &ParquetMetaData,
1159    row_group_idx: usize,
1160    column_idx: usize,
1161) -> Result<Option<Range<u64>>> {
1162    let column_metadata = metadata.row_group(row_group_idx).column(column_idx);
1163    let column_descriptor = column_metadata.column_descr();
1164
1165    if column_descriptor.physical_type() != crate::basic::Type::BYTE_ARRAY {
1166        return Ok(None);
1167    }
1168    let Some(start) = column_metadata.dictionary_page_offset() else {
1169        return Ok(None);
1170    };
1171    let start: u64 = start
1172        .try_into()
1173        .map_err(|_| ParquetError::General("Dictionary page offset is invalid".to_string()))?;
1174    let end: u64 = column_metadata
1175        .data_page_offset()
1176        .try_into()
1177        .map_err(|_| ParquetError::General("Data page offset is invalid".to_string()))?;
1178    if end < start {
1179        return Err(ParquetError::General(
1180            "Data page offset precedes dictionary page offset".to_string(),
1181        ));
1182    }
1183
1184    Ok(Some(start..end))
1185}
1186
1187#[cfg(test)]
1188mod tests {
1189    use super::*;
1190    use crate::file::reader::Length;
1191    use crate::util::test_common::file_util::get_test_file;
1192    use std::ops::Range;
1193
1194    #[test]
1195    fn test_parse_metadata_size_smaller_than_footer() {
1196        let test_file = tempfile::tempfile().unwrap();
1197        let err = ParquetMetaDataReader::new()
1198            .parse_metadata(&test_file)
1199            .unwrap_err();
1200        assert!(matches!(err, ParquetError::NeedMoreData(FOOTER_SIZE)));
1201    }
1202
1203    #[test]
1204    fn test_parse_metadata_corrupt_footer() {
1205        let data = Bytes::from(vec![1, 2, 3, 4, 5, 6, 7, 8]);
1206        let reader_result = ParquetMetaDataReader::new().parse_metadata(&data);
1207        assert_eq!(
1208            reader_result.unwrap_err().to_string(),
1209            "Parquet error: Invalid Parquet file. Corrupt footer"
1210        );
1211    }
1212
1213    #[test]
1214    fn test_parse_metadata_invalid_start() {
1215        let test_file = Bytes::from(vec![255, 0, 0, 0, b'P', b'A', b'R', b'1']);
1216        let err = ParquetMetaDataReader::new()
1217            .parse_metadata(&test_file)
1218            .unwrap_err();
1219        assert!(matches!(err, ParquetError::NeedMoreData(263)));
1220    }
1221
1222    #[test]
1223    #[cfg_attr(miri, ignore)] // Takes too long
1224    fn test_try_parse() {
1225        let file = get_test_file("alltypes_tiny_pages.parquet");
1226        let len = file.len();
1227
1228        let mut reader =
1229            ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Required);
1230
1231        let bytes_for_range = |range: Range<u64>| {
1232            file.get_bytes(range.start, (range.end - range.start).try_into().unwrap())
1233                .unwrap()
1234        };
1235
1236        // read entire file
1237        let bytes = bytes_for_range(0..len);
1238        reader.try_parse(&bytes).unwrap();
1239        let metadata = reader.finish().unwrap();
1240        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1241
1242        // read more than enough of file
1243        let bytes = bytes_for_range(320000..len);
1244        reader.try_parse_sized(&bytes, len).unwrap();
1245        let metadata = reader.finish().unwrap();
1246        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1247
1248        // exactly enough
1249        let bytes = bytes_for_range(323583..len);
1250        reader.try_parse_sized(&bytes, len).unwrap();
1251        let metadata = reader.finish().unwrap();
1252        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1253
1254        // not enough for page index
1255        let bytes = bytes_for_range(323584..len);
1256        // should fail
1257        match reader.try_parse_sized(&bytes, len).unwrap_err() {
1258            // expected error, try again with provided bounds
1259            ParquetError::NeedMoreData(needed) => {
1260                let bytes = bytes_for_range(len - needed as u64..len);
1261                reader.try_parse_sized(&bytes, len).unwrap();
1262                let metadata = reader.finish().unwrap();
1263                assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1264            }
1265            _ => panic!("unexpected error"),
1266        }
1267
1268        // not enough for file metadata, but keep trying until page indexes are read
1269        let mut reader =
1270            ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Required);
1271        let mut bytes = bytes_for_range(452505..len);
1272        loop {
1273            match reader.try_parse_sized(&bytes, len) {
1274                Ok(()) => break,
1275                Err(ParquetError::NeedMoreData(needed)) => {
1276                    bytes = bytes_for_range(len - needed as u64..len);
1277                    if reader.has_metadata() {
1278                        reader.read_page_indexes_sized(&bytes, len).unwrap();
1279                        break;
1280                    }
1281                }
1282                _ => panic!("unexpected error"),
1283            }
1284        }
1285        let metadata = reader.finish().unwrap();
1286        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1287
1288        // not enough for page index but lie about file size
1289        let bytes = bytes_for_range(323584..len);
1290        let reader_result = reader.try_parse_sized(&bytes, len - 323584).unwrap_err();
1291        assert_eq!(
1292            reader_result.to_string(),
1293            "EOF: Parquet file too small. Range 323583..452504 is beyond file bounds 130649"
1294        );
1295
1296        // not enough for file metadata
1297        let mut reader = ParquetMetaDataReader::new();
1298        let bytes = bytes_for_range(452505..len);
1299        // should fail
1300        match reader.try_parse_sized(&bytes, len).unwrap_err() {
1301            // expected error, try again with provided bounds
1302            ParquetError::NeedMoreData(needed) => {
1303                let bytes = bytes_for_range(len - needed as u64..len);
1304                reader.try_parse_sized(&bytes, len).unwrap();
1305                reader.finish().unwrap();
1306            }
1307            _ => panic!("unexpected error"),
1308        }
1309
1310        // not enough for file metadata but use try_parse()
1311        let reader_result = reader.try_parse(&bytes).unwrap_err();
1312        assert_eq!(
1313            reader_result.to_string(),
1314            "EOF: Parquet file too small. Size is 1728 but need 1729"
1315        );
1316
1317        // read head of file rather than tail
1318        let bytes = bytes_for_range(0..1000);
1319        let reader_result = reader.try_parse_sized(&bytes, len).unwrap_err();
1320        assert_eq!(
1321            reader_result.to_string(),
1322            "Parquet error: Invalid Parquet file. Corrupt footer"
1323        );
1324
1325        // lie about file size
1326        let bytes = bytes_for_range(452510..len);
1327        let reader_result = reader.try_parse_sized(&bytes, len - 452505).unwrap_err();
1328        assert_eq!(
1329            reader_result.to_string(),
1330            "EOF: Parquet file too small. Size is 1728 but need 1729"
1331        );
1332    }
1333
1334    #[test]
1335    fn test_chunk_mask() {
1336        let mask = ColumnChunkMask::row_groups_and_columns([0], [1]);
1337        assert!(mask.includes_row_group(0, 1));
1338        assert!(!mask.includes_row_group(1, 1));
1339        assert!(!mask.includes_column(0, 2));
1340        assert!(mask.includes_column(1, 2));
1341
1342        let mask = ColumnChunkMask::columns([]);
1343        assert!(!mask.includes_column(0, 3));
1344        assert_eq!(mask.column_indices(3).count(), 0);
1345
1346        let all = ColumnChunkMask::all();
1347        assert!(all.is_all());
1348        assert!(all.includes_column(2, 3));
1349        assert!(!all.includes_column(3, 3));
1350
1351        assert_eq!(all.row_group_indices(3).collect::<Vec<_>>(), [0, 1, 2]);
1352        assert_eq!(all.column_indices(2).collect::<Vec<_>>(), [0, 1]);
1353
1354        let none = ColumnChunkMask::none();
1355        assert_eq!(none.row_group_indices(3).count(), 0);
1356        assert_eq!(none.column_indices(3).count(), 0);
1357
1358        let sparse = ColumnChunkMask::row_groups_and_columns([0, 2, 4], [1, 3, 5]);
1359        assert_eq!(sparse.row_group_indices(3).collect::<Vec<_>>(), [0, 2]);
1360        assert_eq!(sparse.column_indices(4).collect::<Vec<_>>(), [1, 3]);
1361    }
1362}
1363
1364#[cfg(all(feature = "async", feature = "arrow", test))]
1365mod async_tests {
1366    use super::*;
1367
1368    use arrow::{array::Int32Array, datatypes::DataType};
1369    use arrow_array::RecordBatch;
1370    use arrow_schema::{Field, Schema};
1371    use bytes::Bytes;
1372    use futures::FutureExt;
1373    use futures::future::BoxFuture;
1374    use std::fs::File;
1375    use std::future::Future;
1376    use std::io::{Read, Seek, SeekFrom};
1377    use std::ops::Range;
1378    use std::sync::Arc;
1379    use std::sync::atomic::{AtomicUsize, Ordering};
1380    use tempfile::NamedTempFile;
1381
1382    use crate::arrow::ArrowWriter;
1383    use crate::file::properties::WriterProperties;
1384    use crate::file::reader::Length;
1385    use crate::util::test_common::file_util::get_test_file;
1386
1387    struct MetadataFetchFn<F>(F);
1388
1389    impl<F, Fut> MetadataFetch for MetadataFetchFn<F>
1390    where
1391        F: FnMut(Range<u64>) -> Fut + Send,
1392        Fut: Future<Output = Result<Bytes>> + Send,
1393    {
1394        fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
1395            async move { self.0(range).await }.boxed()
1396        }
1397    }
1398
1399    struct MetadataSuffixFetchFn<F1, F2>(F1, F2);
1400
1401    impl<F1, Fut, F2> MetadataFetch for MetadataSuffixFetchFn<F1, F2>
1402    where
1403        F1: FnMut(Range<u64>) -> Fut + Send,
1404        Fut: Future<Output = Result<Bytes>> + Send,
1405        F2: Send,
1406    {
1407        fn fetch(&mut self, range: Range<u64>) -> BoxFuture<'_, Result<Bytes>> {
1408            async move { self.0(range).await }.boxed()
1409        }
1410    }
1411
1412    impl<F1, Fut, F2> MetadataSuffixFetch for MetadataSuffixFetchFn<F1, F2>
1413    where
1414        F1: FnMut(Range<u64>) -> Fut + Send,
1415        F2: FnMut(usize) -> Fut + Send,
1416        Fut: Future<Output = Result<Bytes>> + Send,
1417    {
1418        fn fetch_suffix(&mut self, suffix: usize) -> BoxFuture<'_, Result<Bytes>> {
1419            async move { self.1(suffix).await }.boxed()
1420        }
1421    }
1422
1423    fn read_range(file: &mut File, range: Range<u64>) -> Result<Bytes> {
1424        file.seek(SeekFrom::Start(range.start))?;
1425        let len = range.end - range.start;
1426        let mut buf = Vec::with_capacity(len.try_into().unwrap());
1427        file.take(len).read_to_end(&mut buf)?;
1428        Ok(buf.into())
1429    }
1430
1431    fn read_suffix(file: &mut File, suffix: usize) -> Result<Bytes> {
1432        let file_len = file.len();
1433        // Don't seek before beginning of file
1434        file.seek(SeekFrom::End(0 - suffix.min(file_len as _) as i64))?;
1435        let mut buf = Vec::with_capacity(suffix);
1436        file.take(suffix as _).read_to_end(&mut buf)?;
1437        Ok(buf.into())
1438    }
1439
1440    #[tokio::test]
1441    async fn test_simple() {
1442        let mut file = get_test_file("nulls.snappy.parquet");
1443        let len = file.len();
1444
1445        let expected = ParquetMetaDataReader::new()
1446            .parse_and_finish(&file)
1447            .unwrap();
1448        let expected = expected.file_metadata().schema();
1449        let fetch_count = AtomicUsize::new(0);
1450
1451        let mut fetch = |range| {
1452            fetch_count.fetch_add(1, Ordering::SeqCst);
1453            futures::future::ready(read_range(&mut file, range))
1454        };
1455
1456        let input = MetadataFetchFn(&mut fetch);
1457        let actual = ParquetMetaDataReader::new()
1458            .load_and_finish(input, len)
1459            .await
1460            .unwrap();
1461        assert_eq!(actual.file_metadata().schema(), expected);
1462        assert_eq!(fetch_count.load(Ordering::SeqCst), 2);
1463
1464        // Metadata hint too small - below footer size
1465        fetch_count.store(0, Ordering::SeqCst);
1466        let input = MetadataFetchFn(&mut fetch);
1467        let actual = ParquetMetaDataReader::new()
1468            .with_prefetch_hint(Some(7))
1469            .load_and_finish(input, len)
1470            .await
1471            .unwrap();
1472        assert_eq!(actual.file_metadata().schema(), expected);
1473        assert_eq!(fetch_count.load(Ordering::SeqCst), 2);
1474
1475        // Metadata hint too small
1476        fetch_count.store(0, Ordering::SeqCst);
1477        let input = MetadataFetchFn(&mut fetch);
1478        let actual = ParquetMetaDataReader::new()
1479            .with_prefetch_hint(Some(10))
1480            .load_and_finish(input, len)
1481            .await
1482            .unwrap();
1483        assert_eq!(actual.file_metadata().schema(), expected);
1484        assert_eq!(fetch_count.load(Ordering::SeqCst), 2);
1485
1486        // Metadata hint too large
1487        fetch_count.store(0, Ordering::SeqCst);
1488        let input = MetadataFetchFn(&mut fetch);
1489        let actual = ParquetMetaDataReader::new()
1490            .with_prefetch_hint(Some(500))
1491            .load_and_finish(input, len)
1492            .await
1493            .unwrap();
1494        assert_eq!(actual.file_metadata().schema(), expected);
1495        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1496
1497        // Metadata hint exactly correct
1498        fetch_count.store(0, Ordering::SeqCst);
1499        let input = MetadataFetchFn(&mut fetch);
1500        let actual = ParquetMetaDataReader::new()
1501            .with_prefetch_hint(Some(428))
1502            .load_and_finish(input, len)
1503            .await
1504            .unwrap();
1505        assert_eq!(actual.file_metadata().schema(), expected);
1506        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1507
1508        let input = MetadataFetchFn(&mut fetch);
1509        let err = ParquetMetaDataReader::new()
1510            .load_and_finish(input, 4)
1511            .await
1512            .unwrap_err()
1513            .to_string();
1514        assert_eq!(err, "EOF: file size of 4 is less than footer");
1515
1516        let input = MetadataFetchFn(&mut fetch);
1517        let err = ParquetMetaDataReader::new()
1518            .load_and_finish(input, 20)
1519            .await
1520            .unwrap_err()
1521            .to_string();
1522        assert_eq!(err, "Parquet error: Invalid Parquet file. Corrupt footer");
1523    }
1524
1525    #[tokio::test]
1526    async fn test_suffix() {
1527        let mut file = get_test_file("nulls.snappy.parquet");
1528        let mut file2 = file.try_clone().unwrap();
1529
1530        let expected = ParquetMetaDataReader::new()
1531            .parse_and_finish(&file)
1532            .unwrap();
1533        let expected = expected.file_metadata().schema();
1534        let fetch_count = AtomicUsize::new(0);
1535        let suffix_fetch_count = AtomicUsize::new(0);
1536
1537        let mut fetch = |range| {
1538            fetch_count.fetch_add(1, Ordering::SeqCst);
1539            futures::future::ready(read_range(&mut file, range))
1540        };
1541        let mut suffix_fetch = |suffix| {
1542            suffix_fetch_count.fetch_add(1, Ordering::SeqCst);
1543            futures::future::ready(read_suffix(&mut file2, suffix))
1544        };
1545
1546        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1547        let actual = ParquetMetaDataReader::new()
1548            .load_via_suffix_and_finish(input)
1549            .await
1550            .unwrap();
1551        assert_eq!(actual.file_metadata().schema(), expected);
1552        assert_eq!(fetch_count.load(Ordering::SeqCst), 0);
1553        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 2);
1554
1555        // Metadata hint too small - below footer size
1556        fetch_count.store(0, Ordering::SeqCst);
1557        suffix_fetch_count.store(0, Ordering::SeqCst);
1558        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1559        let actual = ParquetMetaDataReader::new()
1560            .with_prefetch_hint(Some(7))
1561            .load_via_suffix_and_finish(input)
1562            .await
1563            .unwrap();
1564        assert_eq!(actual.file_metadata().schema(), expected);
1565        assert_eq!(fetch_count.load(Ordering::SeqCst), 0);
1566        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 2);
1567
1568        // Metadata hint too small
1569        fetch_count.store(0, Ordering::SeqCst);
1570        suffix_fetch_count.store(0, Ordering::SeqCst);
1571        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1572        let actual = ParquetMetaDataReader::new()
1573            .with_prefetch_hint(Some(10))
1574            .load_via_suffix_and_finish(input)
1575            .await
1576            .unwrap();
1577        assert_eq!(actual.file_metadata().schema(), expected);
1578        assert_eq!(fetch_count.load(Ordering::SeqCst), 0);
1579        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 2);
1580
1581        // Metadata hint too large
1582        fetch_count.store(0, Ordering::SeqCst);
1583        suffix_fetch_count.store(0, Ordering::SeqCst);
1584        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1585        let actual = ParquetMetaDataReader::new()
1586            .with_prefetch_hint(Some(500))
1587            .load_via_suffix_and_finish(input)
1588            .await
1589            .unwrap();
1590        assert_eq!(actual.file_metadata().schema(), expected);
1591        assert_eq!(fetch_count.load(Ordering::SeqCst), 0);
1592        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 1);
1593
1594        // Metadata hint exactly correct
1595        fetch_count.store(0, Ordering::SeqCst);
1596        suffix_fetch_count.store(0, Ordering::SeqCst);
1597        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1598        let actual = ParquetMetaDataReader::new()
1599            .with_prefetch_hint(Some(428))
1600            .load_via_suffix_and_finish(input)
1601            .await
1602            .unwrap();
1603        assert_eq!(actual.file_metadata().schema(), expected);
1604        assert_eq!(fetch_count.load(Ordering::SeqCst), 0);
1605        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 1);
1606    }
1607
1608    #[cfg(feature = "encryption")]
1609    #[tokio::test]
1610    async fn test_suffix_with_encryption() {
1611        let mut file = get_test_file("uniform_encryption.parquet.encrypted");
1612        let mut file2 = file.try_clone().unwrap();
1613
1614        let mut fetch = |range| futures::future::ready(read_range(&mut file, range));
1615        let mut suffix_fetch = |suffix| futures::future::ready(read_suffix(&mut file2, suffix));
1616
1617        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1618
1619        let key_code: &[u8] = b"0123456789012345";
1620        let decryption_properties = FileDecryptionProperties::builder(key_code.to_vec())
1621            .build()
1622            .unwrap();
1623
1624        // just make sure the metadata is properly decrypted and read
1625        let expected = ParquetMetaDataReader::new()
1626            .with_decryption_properties(Some(decryption_properties))
1627            .load_via_suffix_and_finish(input)
1628            .await
1629            .unwrap();
1630        assert_eq!(expected.num_row_groups(), 1);
1631    }
1632
1633    #[tokio::test]
1634    async fn test_page_index() {
1635        let mut file = get_test_file("alltypes_tiny_pages.parquet");
1636        let len = file.len();
1637        let fetch_count = AtomicUsize::new(0);
1638        let mut fetch = |range| {
1639            fetch_count.fetch_add(1, Ordering::SeqCst);
1640            futures::future::ready(read_range(&mut file, range))
1641        };
1642
1643        let f = MetadataFetchFn(&mut fetch);
1644        let mut loader =
1645            ParquetMetaDataReader::new().with_page_index_policy(PageIndexPolicy::Required);
1646        loader.try_load(f, len).await.unwrap();
1647        assert_eq!(fetch_count.load(Ordering::SeqCst), 3);
1648        let metadata = loader.finish().unwrap();
1649        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1650
1651        // Prefetch just footer exactly
1652        fetch_count.store(0, Ordering::SeqCst);
1653        let f = MetadataFetchFn(&mut fetch);
1654        let mut loader = ParquetMetaDataReader::new()
1655            .with_page_index_policy(PageIndexPolicy::Required)
1656            .with_prefetch_hint(Some(1729));
1657        loader.try_load(f, len).await.unwrap();
1658        assert_eq!(fetch_count.load(Ordering::SeqCst), 2);
1659        let metadata = loader.finish().unwrap();
1660        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1661
1662        // Prefetch more than footer but not enough
1663        fetch_count.store(0, Ordering::SeqCst);
1664        let f = MetadataFetchFn(&mut fetch);
1665        let mut loader = ParquetMetaDataReader::new()
1666            .with_page_index_policy(PageIndexPolicy::Required)
1667            .with_prefetch_hint(Some(130649));
1668        loader.try_load(f, len).await.unwrap();
1669        assert_eq!(fetch_count.load(Ordering::SeqCst), 2);
1670        let metadata = loader.finish().unwrap();
1671        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1672
1673        // Prefetch exactly enough
1674        fetch_count.store(0, Ordering::SeqCst);
1675        let f = MetadataFetchFn(&mut fetch);
1676        let metadata = ParquetMetaDataReader::new()
1677            .with_page_index_policy(PageIndexPolicy::Required)
1678            .with_prefetch_hint(Some(130650))
1679            .load_and_finish(f, len)
1680            .await
1681            .unwrap();
1682        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1683        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1684
1685        // Prefetch more than enough but less than the entire file
1686        fetch_count.store(0, Ordering::SeqCst);
1687        let f = MetadataFetchFn(&mut fetch);
1688        let metadata = ParquetMetaDataReader::new()
1689            .with_page_index_policy(PageIndexPolicy::Required)
1690            .with_prefetch_hint(Some((len - 1000) as usize)) // prefetch entire file
1691            .load_and_finish(f, len)
1692            .await
1693            .unwrap();
1694        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1695        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1696
1697        // Prefetch the entire file
1698        fetch_count.store(0, Ordering::SeqCst);
1699        let f = MetadataFetchFn(&mut fetch);
1700        let metadata = ParquetMetaDataReader::new()
1701            .with_page_index_policy(PageIndexPolicy::Required)
1702            .with_prefetch_hint(Some(len as usize)) // prefetch entire file
1703            .load_and_finish(f, len)
1704            .await
1705            .unwrap();
1706        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1707        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1708
1709        // Prefetch more than the entire file
1710        fetch_count.store(0, Ordering::SeqCst);
1711        let f = MetadataFetchFn(&mut fetch);
1712        let metadata = ParquetMetaDataReader::new()
1713            .with_page_index_policy(PageIndexPolicy::Required)
1714            .with_prefetch_hint(Some((len + 1000) as usize)) // prefetch entire file
1715            .load_and_finish(f, len)
1716            .await
1717            .unwrap();
1718        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1719        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1720    }
1721
1722    #[tokio::test]
1723    async fn test_page_index_via_suffix_with_prefetch() {
1724        let mut file = get_test_file("alltypes_tiny_pages.parquet");
1725        let mut suffix_file = file.try_clone().unwrap();
1726        let len = file.len();
1727        let fetch_count = AtomicUsize::new(0);
1728        let suffix_fetch_count = AtomicUsize::new(0);
1729
1730        let mut fetch = |range| {
1731            fetch_count.fetch_add(1, Ordering::SeqCst);
1732            futures::future::ready(read_range(&mut file, range))
1733        };
1734        let mut suffix_fetch = |suffix| {
1735            suffix_fetch_count.fetch_add(1, Ordering::SeqCst);
1736            futures::future::ready(read_suffix(&mut suffix_file, suffix))
1737        };
1738
1739        let input = MetadataSuffixFetchFn(&mut fetch, &mut suffix_fetch);
1740        let metadata = ParquetMetaDataReader::new()
1741            .with_page_index_policy(PageIndexPolicy::Required)
1742            .with_prefetch_hint(Some((len - 1000) as usize))
1743            .load_via_suffix_and_finish(input)
1744            .await
1745            .unwrap();
1746
1747        assert_eq!(suffix_fetch_count.load(Ordering::SeqCst), 1);
1748        assert_eq!(fetch_count.load(Ordering::SeqCst), 1);
1749        assert!(metadata.page_index().is_some_and(|idx| idx.is_complete()));
1750    }
1751
1752    fn write_parquet_file(offset_index_disabled: bool) -> Result<NamedTempFile> {
1753        let schema = Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]));
1754        let batch = RecordBatch::try_new(
1755            schema.clone(),
1756            vec![Arc::new(Int32Array::from(vec![1, 2, 3]))],
1757        )?;
1758
1759        let file = NamedTempFile::new().unwrap();
1760
1761        // Write properties with page index disabled
1762        let props = WriterProperties::builder()
1763            .set_offset_index_disabled(offset_index_disabled)
1764            .build();
1765
1766        let mut writer = ArrowWriter::try_new(file.reopen()?, schema, Some(props))?;
1767        writer.write(&batch)?;
1768        writer.close()?;
1769
1770        Ok(file)
1771    }
1772
1773    fn read_and_check(file: &File, policy: PageIndexPolicy) -> Result<ParquetMetaData> {
1774        let mut reader = ParquetMetaDataReader::new().with_page_index_policy(policy);
1775        reader.try_parse(file)?;
1776        reader.finish()
1777    }
1778
1779    #[test]
1780    fn test_page_index_policy() {
1781        // With page index
1782        let f = write_parquet_file(false).unwrap();
1783        read_and_check(f.as_file(), PageIndexPolicy::Required).unwrap();
1784        read_and_check(f.as_file(), PageIndexPolicy::Optional).unwrap();
1785        read_and_check(f.as_file(), PageIndexPolicy::Skip).unwrap();
1786
1787        // Without page index
1788        let f = write_parquet_file(true).unwrap();
1789        let res = read_and_check(f.as_file(), PageIndexPolicy::Required);
1790        assert!(matches!(
1791            res,
1792            Err(ParquetError::General(e)) if e == "missing offset index"
1793        ));
1794        read_and_check(f.as_file(), PageIndexPolicy::Optional).unwrap();
1795        read_and_check(f.as_file(), PageIndexPolicy::Skip).unwrap();
1796    }
1797}