Skip to main content

parquet/arrow/arrow_reader/
mod.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//! Contains reader which reads parquet data into arrow [`RecordBatch`]
19
20use arrow_array::cast::AsArray;
21use arrow_array::{BooleanArray, RecordBatch, RecordBatchReader};
22use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder};
23use arrow_schema::{ArrowError, DataType as ArrowType, FieldRef, Schema, SchemaRef};
24use arrow_select::filter::filter_record_batch;
25pub use filter::{ArrowPredicate, ArrowPredicateFn, RowFilter};
26use selection::MaskCursor;
27pub use selection::{
28    MaskRunIter, RowSelection, RowSelectionCursor, RowSelectionPolicy, RowSelector,
29};
30use std::fmt::{Debug, Formatter};
31use std::sync::Arc;
32
33pub use crate::arrow::array_reader::RowGroups;
34use crate::arrow::array_reader::{ArrayReader, ArrayReaderBuilder};
35use crate::arrow::schema::{
36    ParquetField, parquet_to_arrow_schema_and_fields, virtual_type::is_virtual_column,
37};
38use crate::arrow::{FieldLevels, ProjectionMask, parquet_to_arrow_field_levels_with_virtual};
39use crate::basic::{BloomFilterAlgorithm, BloomFilterCompression, BloomFilterHash};
40use crate::bloom_filter::{
41    SBBF_HEADER_SIZE_ESTIMATE, Sbbf, chunk_read_bloom_filter_header_and_offset,
42};
43use crate::column::page::{PageIterator, PageReader};
44#[cfg(feature = "encryption")]
45use crate::encryption::decrypt::FileDecryptionProperties;
46use crate::errors::{ParquetError, Result};
47use crate::file::metadata::{
48    PageIndexPolicy, ParquetMetaData, ParquetMetaDataOptions, ParquetMetaDataReader,
49    ParquetStatisticsPolicy, RowGroupMetaData,
50};
51use crate::file::reader::{ChunkReader, SerializedPageReader};
52use crate::schema::types::SchemaDescriptor;
53
54use crate::arrow::arrow_reader::metrics::ArrowReaderMetrics;
55// Exposed so integration tests and benchmarks can temporarily override the threshold.
56pub use read_plan::{PredicateOptions, ReadPlan, ReadPlanBuilder};
57
58mod filter;
59pub mod metrics;
60mod read_plan;
61pub(crate) mod selection;
62pub mod statistics;
63
64/// Default batch size for reading parquet files
65pub const DEFAULT_BATCH_SIZE: usize = 1024;
66
67/// A row group and its optional row-group-local [`RowSelection`].
68///
69/// A row-group-local selection is relative to the rows in this row group. For
70/// example, an offset of 100 refers to the row at offset 100 within the row
71/// group, not within the Parquet file.
72///
73/// A `None` selection reads the entire row group. Omitting a row group skips
74/// it. Entries are decoded in the supplied order.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct RowGroupSelection {
77    pub(crate) row_group_index: usize,
78    pub(crate) selection: Option<RowSelection>,
79}
80
81impl RowGroupSelection {
82    /// Creates a row-group-local selection.
83    pub fn new(row_group_index: usize, selection: Option<RowSelection>) -> Self {
84        Self {
85            row_group_index,
86            selection,
87        }
88    }
89
90    /// The index of the row group this selection applies to.
91    pub fn row_group_index(&self) -> usize {
92        self.row_group_index
93    }
94
95    /// The row-group-local selection, or `None` if the entire row group is
96    /// read.
97    pub fn selection(&self) -> Option<&RowSelection> {
98        self.selection.as_ref()
99    }
100}
101
102/// Row-selection configuration shared by the Arrow reader builders.
103#[derive(Debug)]
104pub(crate) enum RowGroupPlan {
105    /// First select `row_groups`, if provided, and then apply `selection`
106    /// across the concatenated rows from those row groups.
107    ///
108    /// This is formed by [`ArrowReaderBuilder::with_row_groups`] and
109    /// [`ArrowReaderBuilder::with_row_selection`].
110    Global {
111        row_groups: Option<Vec<usize>>,
112        selection: Option<RowSelection>,
113    },
114    /// Apply each row-group-local selection independently, in the order
115    /// supplied.
116    ///
117    /// This is formed by `with_row_group_selections` on the push decoder and
118    /// async stream builders.
119    PerRowGroup(Vec<RowGroupSelection>),
120    /// Mutually exclusive global and per-row-group configuration was supplied.
121    /// This is reported as an error when the reader is built.
122    Conflicting,
123}
124
125impl RowGroupPlan {
126    fn set_row_groups(&mut self, new_row_groups: Vec<usize>) {
127        match self {
128            Self::Global { row_groups, .. } => *row_groups = Some(new_row_groups),
129            Self::PerRowGroup(_) => *self = Self::Conflicting,
130            Self::Conflicting => {}
131        }
132    }
133
134    fn set_row_selection(&mut self, new_selection: RowSelection) {
135        match self {
136            Self::Global { selection, .. } => *selection = Some(new_selection),
137            Self::PerRowGroup(_) => *self = Self::Conflicting,
138            Self::Conflicting => {}
139        }
140    }
141
142    pub(crate) fn set_row_group_selections(
143        &mut self,
144        row_group_selections: Vec<RowGroupSelection>,
145    ) {
146        match self {
147            Self::Global {
148                row_groups: None,
149                selection: None,
150            }
151            | Self::PerRowGroup(_) => {
152                *self = Self::PerRowGroup(row_group_selections);
153            }
154            Self::Global { .. } => *self = Self::Conflicting,
155            Self::Conflicting => {}
156        }
157    }
158
159    pub(crate) fn conflict_error() -> ParquetError {
160        ParquetError::General(
161            "with_row_group_selections cannot be combined with with_row_groups or with_row_selection"
162                .to_string(),
163        )
164    }
165
166    fn into_global(self) -> Result<(Option<Vec<usize>>, Option<RowSelection>)> {
167        match self {
168            Self::Global {
169                row_groups,
170                selection,
171            } => Ok((row_groups, selection)),
172            Self::PerRowGroup(_) => Err(ParquetError::General(
173                "Row-group-local selections are not supported by the synchronous reader"
174                    .to_string(),
175            )),
176            Self::Conflicting => Err(Self::conflict_error()),
177        }
178    }
179}
180
181/// Builder for constructing Parquet readers that decode into [Apache Arrow]
182/// arrays.
183///
184/// Most users should use one of the following specializations:
185///
186/// * synchronous API: [`ParquetRecordBatchReaderBuilder`]
187/// * `async` API: [`ParquetRecordBatchStreamBuilder`]
188/// * decoder API: [`ParquetPushDecoderBuilder`]
189///
190/// # Features
191/// * Projection pushdown: [`Self::with_projection`]
192/// * Cached metadata: [`ArrowReaderMetadata::load`]
193/// * Offset skipping: [`Self::with_offset`] and [`Self::with_limit`]
194/// * Row group filtering: [`Self::with_row_groups`]
195/// * Range filtering: [`Self::with_row_selection`]
196/// * Row level filtering: [`Self::with_row_filter`]
197///
198/// # Implementing Predicate Pushdown
199///
200/// [`Self::with_row_filter`] permits filter evaluation *during* the decoding
201/// process, which is efficient and allows the most low level optimizations.
202///
203/// However, most Parquet based systems will apply filters at many steps prior
204/// to decoding such as pruning files, row groups and data pages. This crate
205/// provides the low level APIs needed to implement such filtering, but does not
206/// include any logic to actually evaluate predicates. For example:
207///
208/// * [`Self::with_row_groups`] for Row Group pruning
209/// * [`Self::with_row_selection`] for data page pruning
210/// * [`StatisticsConverter`] to convert Parquet statistics to Arrow arrays
211///
212/// The rationale for this design is that implementing predicate pushdown is a
213/// complex topic and varies significantly from system to system. For example
214///
215/// 1. Predicates supported (do you support predicates like prefix matching, user defined functions, etc)
216/// 2. Evaluating predicates on multiple files (with potentially different but compatible schemas)
217/// 3. Evaluating predicates using information from an external metadata catalog (e.g. Apache Iceberg or similar)
218/// 4. Interleaving fetching metadata, evaluating predicates, and decoding files
219///
220/// You can read more about this design in the [Querying Parquet with
221/// Millisecond Latency] Arrow blog post.
222///
223/// [`ParquetRecordBatchStreamBuilder`]: crate::arrow::async_reader::ParquetRecordBatchStreamBuilder
224/// [`ParquetPushDecoderBuilder`]: crate::arrow::push_decoder::ParquetPushDecoderBuilder
225/// [Apache Arrow]: https://arrow.apache.org/
226/// [`StatisticsConverter`]: statistics::StatisticsConverter
227/// [Querying Parquet with Millisecond Latency]: https://arrow.apache.org/blog/2022/12/26/querying-parquet-with-millisecond-latency/
228pub struct ArrowReaderBuilder<T> {
229    /// The "input" to read parquet data from.
230    ///
231    /// Note in the case of the [`ParquetPushDecoderBuilder`] there is no
232    /// underlying reader; the input is instead [`PushDecoderInput`], the buffer that
233    /// caller-pushed bytes accumulate in.
234    ///
235    /// [`ParquetPushDecoderBuilder`]: crate::arrow::push_decoder::ParquetPushDecoderBuilder
236    /// [`PushDecoderInput`]: crate::arrow::push_decoder::PushDecoderInput
237    pub(crate) input: T,
238
239    pub(crate) metadata: Arc<ParquetMetaData>,
240
241    pub(crate) schema: SchemaRef,
242
243    pub(crate) fields: Option<Arc<ParquetField>>,
244
245    pub(crate) batch_size: usize,
246
247    pub(crate) row_group_plan: RowGroupPlan,
248
249    pub(crate) projection: ProjectionMask,
250
251    pub(crate) filter: Option<RowFilter>,
252
253    pub(crate) row_selection_policy: RowSelectionPolicy,
254
255    pub(crate) limit: Option<usize>,
256
257    pub(crate) offset: Option<usize>,
258
259    pub(crate) metrics: ArrowReaderMetrics,
260
261    pub(crate) max_predicate_cache_size: usize,
262}
263
264impl<T: Debug> Debug for ArrowReaderBuilder<T> {
265    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("ArrowReaderBuilder<T>")
267            .field("input", &self.input)
268            .field("metadata", &self.metadata)
269            .field("schema", &self.schema)
270            .field("fields", &self.fields)
271            .field("batch_size", &self.batch_size)
272            .field("row_group_plan", &self.row_group_plan)
273            .field("projection", &self.projection)
274            .field("filter", &self.filter)
275            .field("row_selection_policy", &self.row_selection_policy)
276            .field("limit", &self.limit)
277            .field("offset", &self.offset)
278            .field("metrics", &self.metrics)
279            .finish()
280    }
281}
282
283impl<T> ArrowReaderBuilder<T> {
284    pub(crate) fn new_builder(input: T, metadata: ArrowReaderMetadata) -> Self {
285        Self {
286            input,
287            metadata: metadata.metadata,
288            schema: metadata.schema,
289            fields: metadata.fields,
290            batch_size: DEFAULT_BATCH_SIZE,
291            row_group_plan: RowGroupPlan::Global {
292                row_groups: None,
293                selection: None,
294            },
295            projection: ProjectionMask::all(),
296            filter: None,
297            row_selection_policy: RowSelectionPolicy::default(),
298            limit: None,
299            offset: None,
300            metrics: ArrowReaderMetrics::Disabled,
301            max_predicate_cache_size: 100 * 1024 * 1024, // 100MB default cache size
302        }
303    }
304
305    /// Returns a reference to the [`ParquetMetaData`] for this parquet file
306    pub fn metadata(&self) -> &Arc<ParquetMetaData> {
307        &self.metadata
308    }
309
310    /// Returns the parquet [`SchemaDescriptor`] for this parquet file
311    pub fn parquet_schema(&self) -> &SchemaDescriptor {
312        self.metadata.file_metadata().schema_descr()
313    }
314
315    /// Returns the arrow [`SchemaRef`] for this parquet file
316    pub fn schema(&self) -> &SchemaRef {
317        &self.schema
318    }
319
320    /// Set the size of [`RecordBatch`] to produce. Defaults to [`DEFAULT_BATCH_SIZE`].
321    ///
322    /// This may be used as a hint for internal allocations, but does not
323    /// guarantee exact internal buffer capacities.
324    ///
325    /// If `batch_size` is more than the file row count, use the file row count.
326    pub fn with_batch_size(self, batch_size: usize) -> Self {
327        // Try to avoid allocate large buffer
328        let batch_size = batch_size.min(self.metadata.file_metadata().num_rows() as usize);
329        Self { batch_size, ..self }
330    }
331
332    /// Only read data from the provided row group indexes
333    ///
334    /// This is also called row group filtering
335    ///
336    /// On [`ParquetPushDecoderBuilder`] and [`ParquetRecordBatchStreamBuilder`],
337    /// which additionally offer `with_row_group_selections`, this cannot be
338    /// combined with that method; attempting to do so returns an error from
339    /// `build`.
340    ///
341    /// [`ParquetPushDecoderBuilder`]: crate::arrow::push_decoder::ParquetPushDecoderBuilder
342    /// [`ParquetRecordBatchStreamBuilder`]: crate::arrow::async_reader::ParquetRecordBatchStreamBuilder
343    pub fn with_row_groups(mut self, row_groups: Vec<usize>) -> Self {
344        self.row_group_plan.set_row_groups(row_groups);
345        self
346    }
347
348    /// Only read data from the provided column indexes
349    pub fn with_projection(self, mask: ProjectionMask) -> Self {
350        Self {
351            projection: mask,
352            ..self
353        }
354    }
355
356    /// Configure how row selections should be materialised during execution
357    ///
358    /// See [`RowSelectionPolicy`] for more details
359    pub fn with_row_selection_policy(self, policy: RowSelectionPolicy) -> Self {
360        Self {
361            row_selection_policy: policy,
362            ..self
363        }
364    }
365
366    /// Provide a [`RowSelection`] to filter out rows, and avoid fetching their
367    /// data into memory.
368    ///
369    /// This feature is used to restrict which rows are decoded within row
370    /// groups, skipping ranges of rows that are not needed. Such selections
371    /// could be determined by evaluating predicates against the parquet page
372    /// [`Index`] or some other external information available to a query
373    /// engine.
374    ///
375    /// # Notes
376    ///
377    /// Row group filtering (see [`Self::with_row_groups`]) is applied prior to
378    /// applying the row selection, and therefore rows from skipped row groups
379    /// should not be included in the [`RowSelection`] (see example below)
380    ///
381    /// On [`ParquetPushDecoderBuilder`] and [`ParquetRecordBatchStreamBuilder`],
382    /// which additionally offer `with_row_group_selections`, this cannot be
383    /// combined with that method; attempting to do so returns an error from
384    /// `build`.
385    ///
386    /// [`ParquetPushDecoderBuilder`]: crate::arrow::push_decoder::ParquetPushDecoderBuilder
387    /// [`ParquetRecordBatchStreamBuilder`]: crate::arrow::async_reader::ParquetRecordBatchStreamBuilder
388    ///
389    /// It is recommended to enable writing the page index if using this
390    /// functionality, to allow more efficient skipping over data pages. See
391    /// [`ArrowReaderOptions::with_page_index_policy`].
392    ///
393    /// # Example
394    ///
395    /// Given a parquet file with 4 row groups, and a row group filter of `[0,
396    /// 2, 3]`, in order to scan rows 50-100 in row group 2 and rows 200-300 in
397    /// row group 3:
398    ///
399    /// ```text
400    ///   Row Group 0, 1000 rows (selected)
401    ///   Row Group 1, 1000 rows (skipped)
402    ///   Row Group 2, 1000 rows (selected, but want to only scan rows 50-100)
403    ///   Row Group 3, 1000 rows (selected, but want to only scan rows 200-300)
404    /// ```
405    ///
406    /// You could pass the following [`RowSelection`]:
407    ///
408    /// ```text
409    ///  Select 1000    (scan all rows in row group 0)
410    ///  Skip 50        (skip the first 50 rows in row group 2)
411    ///  Select 50      (scan rows 50-100 in row group 2)
412    ///  Skip 900       (skip the remaining rows in row group 2)
413    ///  Skip 200       (skip the first 200 rows in row group 3)
414    ///  Select 100     (scan rows 200-300 in row group 3)
415    ///  Skip 700       (skip the remaining rows in row group 3)
416    /// ```
417    /// Note there is no entry for the (entirely) skipped row group 1.
418    ///
419    /// Note you can represent the same selection with fewer entries. Instead of
420    ///
421    /// ```text
422    ///  Skip 900       (skip the remaining rows in row group 2)
423    ///  Skip 200       (skip the first 200 rows in row group 3)
424    /// ```
425    ///
426    /// you could use
427    ///
428    /// ```text
429    /// Skip 1100      (skip the remaining 900 rows in row group 2 and the first 200 rows in row group 3)
430    /// ```
431    ///
432    /// [`Index`]: crate::file::page_index::column_index::ColumnIndexMetaData
433    pub fn with_row_selection(mut self, selection: RowSelection) -> Self {
434        self.row_group_plan.set_row_selection(selection);
435        self
436    }
437
438    /// Provide a [`RowFilter`] to skip decoding rows
439    ///
440    /// Row filters are applied after row group selection and row selection
441    ///
442    /// It is recommended to enable reading the page index if using this functionality, to allow
443    /// more efficient skipping over data pages. See [`ArrowReaderOptions::with_page_index_policy`].
444    ///
445    /// See the [blog post on late materialization] for a more technical explanation.
446    ///
447    /// [blog post on late materialization]: https://arrow.apache.org/blog/2025/12/11/parquet-late-materialization-deep-dive
448    ///
449    /// # Example
450    /// ```rust
451    /// # use std::fs::File;
452    /// # use arrow_array::Int32Array;
453    /// # use parquet::arrow::ProjectionMask;
454    /// # use parquet::arrow::arrow_reader::{ArrowPredicateFn, ParquetRecordBatchReaderBuilder, RowFilter};
455    /// # fn main() -> Result<(), parquet::errors::ParquetError> {
456    /// # let testdata = arrow::util::test_util::parquet_test_data();
457    /// # let path = format!("{testdata}/alltypes_plain.parquet");
458    /// # let file = File::open(&path)?;
459    /// let builder = ParquetRecordBatchReaderBuilder::try_new(file)?;
460    /// let schema_desc = builder.metadata().file_metadata().schema_descr_ptr();
461    /// // Create predicate that evaluates `int_col != 1`.
462    /// // `int_col` column has index 4 (zero based) in the schema
463    /// let projection = ProjectionMask::leaves(&schema_desc, [4]);
464    /// // Only the projection columns are passed to the predicate so
465    /// // int_col is column 0 in the predicate
466    /// let predicate = ArrowPredicateFn::new(projection, |batch| {
467    ///     let int_col = batch.column(0);
468    ///     arrow::compute::kernels::cmp::neq(int_col, &Int32Array::new_scalar(1))
469    /// });
470    /// let row_filter = RowFilter::new(vec![Box::new(predicate)]);
471    /// // The filter will be invoked during the reading process
472    /// let reader = builder.with_row_filter(row_filter).build()?;
473    /// # for b in reader { let _ = b?; }
474    /// # Ok(())
475    /// # }
476    /// ```
477    pub fn with_row_filter(self, filter: RowFilter) -> Self {
478        Self {
479            filter: Some(filter),
480            ..self
481        }
482    }
483
484    /// Provide a limit to the number of rows to be read
485    ///
486    /// The limit will be applied after any [`Self::with_row_selection`] and [`Self::with_row_filter`]
487    /// allowing it to limit the final set of rows decoded after any pushed down predicates
488    ///
489    /// It is recommended to enable reading the page index if using this functionality, to allow
490    /// more efficient skipping over data pages. See [`ArrowReaderOptions::with_page_index_policy`]
491    pub fn with_limit(self, limit: usize) -> Self {
492        Self {
493            limit: Some(limit),
494            ..self
495        }
496    }
497
498    /// Provide an offset to skip over the given number of rows
499    ///
500    /// The offset will be applied after any [`Self::with_row_selection`] and [`Self::with_row_filter`]
501    /// allowing it to skip rows after any pushed down predicates
502    ///
503    /// It is recommended to enable reading the page index if using this functionality, to allow
504    /// more efficient skipping over data pages. See [`ArrowReaderOptions::with_page_index_policy`]
505    pub fn with_offset(self, offset: usize) -> Self {
506        Self {
507            offset: Some(offset),
508            ..self
509        }
510    }
511
512    /// Specify metrics collection during reading
513    ///
514    /// To access the metrics, create an [`ArrowReaderMetrics`] and pass a
515    /// clone of the provided metrics to the builder.
516    ///
517    /// For example:
518    ///
519    /// ```rust
520    /// # use std::sync::Arc;
521    /// # use bytes::Bytes;
522    /// # use arrow_array::{Int32Array, RecordBatch};
523    /// # use arrow_schema::{DataType, Field, Schema};
524    /// # use parquet::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
525    /// use parquet::arrow::arrow_reader::metrics::ArrowReaderMetrics;
526    /// # use parquet::arrow::ArrowWriter;
527    /// # let mut file: Vec<u8> = Vec::with_capacity(1024);
528    /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
529    /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
530    /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
531    /// # writer.write(&batch).unwrap();
532    /// # writer.close().unwrap();
533    /// # let file = Bytes::from(file);
534    /// // Create metrics object to pass into the reader
535    /// let metrics = ArrowReaderMetrics::enabled();
536    /// let reader = ParquetRecordBatchReaderBuilder::try_new(file).unwrap()
537    ///   // Configure the builder to use the metrics by passing a clone
538    ///   .with_metrics(metrics.clone())
539    ///   // Build the reader
540    ///   .build().unwrap();
541    /// // .. read data from the reader ..
542    ///
543    /// // check the metrics
544    /// assert!(metrics.records_read_from_inner().is_some());
545    /// ```
546    pub fn with_metrics(self, metrics: ArrowReaderMetrics) -> Self {
547        Self { metrics, ..self }
548    }
549
550    /// Set the maximum size (per row group) of the predicate cache in bytes for
551    /// the async decoder.
552    ///
553    /// Defaults to 100MB (across all columns). Set to `usize::MAX` to use
554    /// unlimited cache size.
555    ///
556    /// This cache is used to store decoded arrays that are used in
557    /// predicate evaluation ([`Self::with_row_filter`]).
558    ///
559    /// This cache is only used for the "async" decoder, [`ParquetRecordBatchStream`]. See
560    /// [this ticket] for more details and alternatives.
561    ///
562    /// [`ParquetRecordBatchStream`]: https://docs.rs/parquet/latest/parquet/arrow/async_reader/struct.ParquetRecordBatchStream.html
563    /// [this ticket]: https://github.com/apache/arrow-rs/issues/8000
564    pub fn with_max_predicate_cache_size(self, max_predicate_cache_size: usize) -> Self {
565        Self {
566            max_predicate_cache_size,
567            ..self
568        }
569    }
570}
571
572/// Options that control how [`ParquetMetaData`] is read when constructing
573/// an Arrow reader.
574///
575/// To use these options, pass them to one of the following methods:
576/// * [`ParquetRecordBatchReaderBuilder::try_new_with_options`]
577/// * [`ParquetRecordBatchStreamBuilder::new_with_options`]
578///
579/// For fine-grained control over metadata loading, use
580/// [`ArrowReaderMetadata::load`] to load metadata with these options,
581///
582/// See [`ArrowReaderBuilder`] for how to configure how the column data
583/// is then read from the file, including projection and filter pushdown
584///
585/// [`ParquetRecordBatchStreamBuilder::new_with_options`]: crate::arrow::async_reader::ParquetRecordBatchStreamBuilder::new_with_options
586#[derive(Debug, Clone, Default)]
587pub struct ArrowReaderOptions {
588    /// Should the reader strip any user defined metadata from the Arrow schema
589    skip_arrow_metadata: bool,
590    /// If provided, used as the schema hint when determining the Arrow schema,
591    /// otherwise the schema hint is read from the [ARROW_SCHEMA_META_KEY]
592    ///
593    /// [ARROW_SCHEMA_META_KEY]: crate::arrow::ARROW_SCHEMA_META_KEY
594    supplied_schema: Option<SchemaRef>,
595
596    pub(crate) column_index: PageIndexPolicy,
597    pub(crate) offset_index: PageIndexPolicy,
598
599    /// Options to control reading of Parquet metadata
600    metadata_options: ParquetMetaDataOptions,
601    /// If encryption is enabled, the file decryption properties can be provided
602    #[cfg(feature = "encryption")]
603    pub(crate) file_decryption_properties: Option<Arc<FileDecryptionProperties>>,
604
605    virtual_columns: Vec<FieldRef>,
606}
607
608impl ArrowReaderOptions {
609    /// Create a new [`ArrowReaderOptions`] with the default settings
610    pub fn new() -> Self {
611        Self::default()
612    }
613
614    /// Skip decoding the embedded arrow metadata (defaults to `false`)
615    ///
616    /// Parquet files generated by some writers may contain embedded arrow
617    /// schema and metadata.
618    /// This may not be correct or compatible with your system,
619    /// for example, see [ARROW-16184](https://issues.apache.org/jira/browse/ARROW-16184)
620    pub fn with_skip_arrow_metadata(self, skip_arrow_metadata: bool) -> Self {
621        Self {
622            skip_arrow_metadata,
623            ..self
624        }
625    }
626
627    /// Provide a schema hint to use when reading the Parquet file.
628    ///
629    /// If provided, this schema takes precedence over any arrow schema embedded
630    /// in the metadata (see the [`arrow`] documentation for more details).
631    ///
632    /// If the provided schema is not compatible with the data stored in the
633    /// parquet file schema, an error will be returned when constructing the
634    /// builder.
635    ///
636    /// This option is only required if you want to explicitly control the
637    /// conversion of Parquet types to Arrow types, such as casting a column to
638    /// a different type. For example, if you wanted to read an Int64 in
639    /// a Parquet file to a [`TimestampMicrosecondArray`] in the Arrow schema.
640    ///
641    /// [`arrow`]: crate::arrow
642    /// [`TimestampMicrosecondArray`]: arrow_array::TimestampMicrosecondArray
643    ///
644    /// # Notes
645    ///
646    /// The provided schema must have the same number of columns as the parquet schema and
647    /// the column names must be the same.
648    ///
649    /// # Example
650    /// ```
651    /// # use std::sync::Arc;
652    /// # use bytes::Bytes;
653    /// # use arrow_array::{ArrayRef, Int32Array, RecordBatch};
654    /// # use arrow_schema::{DataType, Field, Schema, TimeUnit};
655    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
656    /// # use parquet::arrow::ArrowWriter;
657    /// // Write data - schema is inferred from the data to be Int32
658    /// let mut file = Vec::new();
659    /// let batch = RecordBatch::try_from_iter(vec![
660    ///     ("col_1", Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef),
661    /// ]).unwrap();
662    /// let mut writer = ArrowWriter::try_new(&mut file, batch.schema(), None).unwrap();
663    /// writer.write(&batch).unwrap();
664    /// writer.close().unwrap();
665    /// let file = Bytes::from(file);
666    ///
667    /// // Read the file back.
668    /// // Supply a schema that interprets the Int32 column as a Timestamp.
669    /// let supplied_schema = Arc::new(Schema::new(vec![
670    ///     Field::new("col_1", DataType::Timestamp(TimeUnit::Nanosecond, None), false)
671    /// ]));
672    /// let options = ArrowReaderOptions::new().with_schema(supplied_schema.clone());
673    /// let mut builder = ParquetRecordBatchReaderBuilder::try_new_with_options(
674    ///     file.clone(),
675    ///     options
676    /// ).expect("Error if the schema is not compatible with the parquet file schema.");
677    ///
678    /// // Create the reader and read the data using the supplied schema.
679    /// let mut reader = builder.build().unwrap();
680    /// let _batch = reader.next().unwrap().unwrap();
681    /// ```
682    ///
683    /// # Example: Preserving Dictionary Encoding
684    ///
685    /// By default, Parquet string columns are read as `Utf8Array` (or `LargeUtf8Array`),
686    /// even if the underlying Parquet data uses dictionary encoding. You can preserve
687    /// the dictionary encoding by specifying a `Dictionary` type in the schema hint:
688    ///
689    /// ```
690    /// # use std::sync::Arc;
691    /// # use bytes::Bytes;
692    /// # use arrow_array::{ArrayRef, RecordBatch, StringArray};
693    /// # use arrow_schema::{DataType, Field, Schema};
694    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
695    /// # use parquet::arrow::ArrowWriter;
696    /// // Write a Parquet file with string data
697    /// let mut file = Vec::new();
698    /// let schema = Arc::new(Schema::new(vec![
699    ///     Field::new("city", DataType::Utf8, false)
700    /// ]));
701    /// let cities = StringArray::from(vec!["Berlin", "Berlin", "Paris", "Berlin", "Paris"]);
702    /// let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(cities)]).unwrap();
703    ///
704    /// let mut writer = ArrowWriter::try_new(&mut file, batch.schema(), None).unwrap();
705    /// writer.write(&batch).unwrap();
706    /// writer.close().unwrap();
707    /// let file = Bytes::from(file);
708    ///
709    /// // Read the file back, requesting dictionary encoding preservation
710    /// let dict_schema = Arc::new(Schema::new(vec![
711    ///     Field::new("city", DataType::Dictionary(
712    ///         Box::new(DataType::Int32),
713    ///         Box::new(DataType::Utf8)
714    ///     ), false)
715    /// ]));
716    /// let options = ArrowReaderOptions::new().with_schema(dict_schema);
717    /// let builder = ParquetRecordBatchReaderBuilder::try_new_with_options(
718    ///     file.clone(),
719    ///     options
720    /// ).unwrap();
721    ///
722    /// let mut reader = builder.build().unwrap();
723    /// let batch = reader.next().unwrap().unwrap();
724    ///
725    /// // The column is now a DictionaryArray
726    /// assert!(matches!(
727    ///     batch.column(0).data_type(),
728    ///     DataType::Dictionary(_, _)
729    /// ));
730    /// ```
731    ///
732    /// **Note**: Dictionary encoding preservation works best when:
733    /// 1. The original column was dictionary encoded (the default for string columns)
734    /// 2. There are a small number of distinct values
735    pub fn with_schema(self, schema: SchemaRef) -> Self {
736        Self {
737            supplied_schema: Some(schema),
738            skip_arrow_metadata: true,
739            ..self
740        }
741    }
742
743    /// Sets the [`PageIndexPolicy`] for both the column and offset indexes.
744    ///
745    /// The `PageIndex` can be used to push down predicates to the parquet scan,
746    /// potentially eliminating unnecessary IO, by some query engines.
747    /// The `PageIndex` consists of two structures: the `ColumnIndex` and `OffsetIndex`.
748    /// This method sets the same policy for both. For fine-grained control, use
749    /// [`Self::with_column_index_policy`] and [`Self::with_offset_index_policy`].
750    pub fn with_page_index_policy(self, policy: PageIndexPolicy) -> Self {
751        self.with_column_index_policy(policy)
752            .with_offset_index_policy(policy)
753    }
754
755    /// Sets the [`PageIndexPolicy`] for the Parquet [ColumnIndex] structure.
756    ///
757    /// The `ColumnIndex` contains min/max statistics for each page, which can be used
758    /// for predicate pushdown and page-level pruning.
759    ///
760    /// [ColumnIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
761    pub fn with_column_index_policy(mut self, policy: PageIndexPolicy) -> Self {
762        self.column_index = policy;
763        self
764    }
765
766    /// Sets the [`PageIndexPolicy`] for the Parquet [OffsetIndex] structure.
767    ///
768    /// The `OffsetIndex` contains the locations and sizes of each page, which enables
769    /// efficient page-level skipping and random access within column chunks.
770    ///
771    /// [OffsetIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
772    pub fn with_offset_index_policy(mut self, policy: PageIndexPolicy) -> Self {
773        self.offset_index = policy;
774        self
775    }
776
777    /// Provide a Parquet schema to use when decoding the metadata. The schema in the Parquet
778    /// footer will be skipped.
779    ///
780    /// This can be used to avoid reparsing the schema from the file when it is
781    /// already known.
782    pub fn with_parquet_schema(mut self, schema: Arc<SchemaDescriptor>) -> Self {
783        self.metadata_options.set_schema(schema);
784        self
785    }
786
787    /// Set whether to convert the [`encoding_stats`] in the Parquet `ColumnMetaData` to a bitmask
788    /// (defaults to `false`).
789    ///
790    /// See [`ColumnChunkMetaData::page_encoding_stats_mask`] for an explanation of why this
791    /// might be desirable.
792    ///
793    /// [`ColumnChunkMetaData::page_encoding_stats_mask`]:
794    /// crate::file::metadata::ColumnChunkMetaData::page_encoding_stats_mask
795    /// [`encoding_stats`]:
796    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L917
797    pub fn with_encoding_stats_as_mask(mut self, val: bool) -> Self {
798        self.metadata_options.set_encoding_stats_as_mask(val);
799        self
800    }
801
802    /// Sets the decoding policy for [`encoding_stats`] in the Parquet `ColumnMetaData`.
803    ///
804    /// [`encoding_stats`]:
805    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L917
806    pub fn with_encoding_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
807        self.metadata_options.set_encoding_stats_policy(policy);
808        self
809    }
810
811    /// Sets the decoding policy for [`statistics`] in the Parquet `ColumnMetaData`.
812    ///
813    /// [`statistics`]:
814    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L912
815    pub fn with_column_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
816        self.metadata_options.set_column_stats_policy(policy);
817        self
818    }
819
820    /// Sets the decoding policy for [`size_statistics`] in the Parquet `ColumnMetaData`.
821    ///
822    /// [`size_statistics`]:
823    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L936
824    pub fn with_size_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
825        self.metadata_options.set_size_stats_policy(policy);
826        self
827    }
828
829    /// Provide the file decryption properties to use when reading encrypted parquet files.
830    ///
831    /// If encryption is enabled and the file is encrypted, the `file_decryption_properties` must be provided.
832    #[cfg(feature = "encryption")]
833    pub fn with_file_decryption_properties(
834        self,
835        file_decryption_properties: Arc<FileDecryptionProperties>,
836    ) -> Self {
837        Self {
838            file_decryption_properties: Some(file_decryption_properties),
839            ..self
840        }
841    }
842
843    /// Include virtual columns in the output.
844    ///
845    /// Virtual columns are columns that are not part of the Parquet schema, but are added to the output by the reader such as row numbers and row group indices.
846    ///
847    /// # Example
848    /// ```
849    /// # use std::sync::Arc;
850    /// # use bytes::Bytes;
851    /// # use arrow_array::{ArrayRef, Int64Array, RecordBatch};
852    /// # use arrow_schema::{DataType, Field, Schema};
853    /// # use parquet::arrow::{ArrowWriter, RowNumber};
854    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
855    /// #
856    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
857    /// // Create a simple record batch with some data
858    /// let values = Arc::new(Int64Array::from(vec![1, 2, 3])) as ArrayRef;
859    /// let batch = RecordBatch::try_from_iter(vec![("value", values)])?;
860    ///
861    /// // Write the batch to an in-memory buffer
862    /// let mut file = Vec::new();
863    /// let mut writer = ArrowWriter::try_new(
864    ///     &mut file,
865    ///     batch.schema(),
866    ///     None
867    /// )?;
868    /// writer.write(&batch)?;
869    /// writer.close()?;
870    /// let file = Bytes::from(file);
871    ///
872    /// // Create a virtual column for row numbers
873    /// let row_number_field = Arc::new(Field::new("row_number", DataType::Int64, false)
874    ///     .with_extension_type(RowNumber));
875    ///
876    /// // Configure options with virtual columns
877    /// let options = ArrowReaderOptions::new()
878    ///     .with_virtual_columns(vec![row_number_field])?;
879    ///
880    /// // Create a reader with the options
881    /// let mut reader = ParquetRecordBatchReaderBuilder::try_new_with_options(
882    ///     file,
883    ///     options
884    /// )?
885    /// .build()?;
886    ///
887    /// // Read the batch - it will include both the original column and the virtual row_number column
888    /// let result_batch = reader.next().unwrap()?;
889    /// assert_eq!(result_batch.num_columns(), 2); // "value" + "row_number"
890    /// assert_eq!(result_batch.num_rows(), 3);
891    /// #
892    /// # Ok(())
893    /// # }
894    /// ```
895    pub fn with_virtual_columns(self, virtual_columns: Vec<FieldRef>) -> Result<Self> {
896        // Validate that all fields are virtual columns
897        for field in &virtual_columns {
898            if !is_virtual_column(field) {
899                return Err(ParquetError::General(format!(
900                    "Field '{}' is not a virtual column. Virtual columns must have extension type names starting with 'arrow.virtual.'",
901                    field.name()
902                )));
903            }
904        }
905        Ok(Self {
906            virtual_columns,
907            ..self
908        })
909    }
910
911    /// Retrieve the currently set [`PageIndexPolicy`] for the offset index.
912    ///
913    /// This can be set via [`with_offset_index_policy`][Self::with_offset_index_policy]
914    /// or [`with_page_index_policy`][Self::with_page_index_policy].
915    pub fn offset_index_policy(&self) -> PageIndexPolicy {
916        self.offset_index
917    }
918
919    /// Retrieve the currently set [`PageIndexPolicy`] for the column index.
920    ///
921    /// This can be set via [`with_column_index_policy`][Self::with_column_index_policy]
922    /// or [`with_page_index_policy`][Self::with_page_index_policy].
923    pub fn column_index_policy(&self) -> PageIndexPolicy {
924        self.column_index
925    }
926
927    /// Retrieve the currently set metadata decoding options.
928    pub fn metadata_options(&self) -> &ParquetMetaDataOptions {
929        &self.metadata_options
930    }
931
932    /// Retrieve the currently set file decryption properties.
933    ///
934    /// This can be set via
935    /// [`file_decryption_properties`][Self::with_file_decryption_properties].
936    #[cfg(feature = "encryption")]
937    pub fn file_decryption_properties(&self) -> Option<&Arc<FileDecryptionProperties>> {
938        self.file_decryption_properties.as_ref()
939    }
940}
941
942impl ParquetMetaDataReader {
943    /// Applies the metadata related settings from [`ArrowReaderOptions`],
944    /// such as the [`ParquetMetaDataOptions`], decryption properties, and
945    /// [`PageIndexPolicy`] to this reader.
946    ///
947    /// The page index policies are only applied if at least one of them is not
948    /// [`PageIndexPolicy::Skip`], so policies previously configured on this
949    /// reader (e.g. from a preload setting) are preserved when the options do
950    /// not request the page index.
951    ///
952    /// This encodes the canonical way to construct a `ParquetMetaDataReader`
953    /// inside `AsyncFileReader::get_metadata` (available with the `async`
954    /// feature), so implementations outside this crate do not need to
955    /// duplicate it.
956    pub fn with_arrow_reader_options(mut self, options: Option<&ArrowReaderOptions>) -> Self {
957        let Some(options) = options else { return self };
958
959        self = self.with_metadata_options(Some(options.metadata_options().clone()));
960
961        #[cfg(feature = "encryption")]
962        {
963            self = self.with_decryption_properties(
964                options.file_decryption_properties.as_ref().map(Arc::clone),
965            );
966        }
967
968        if options.column_index_policy() != PageIndexPolicy::Skip
969            || options.offset_index_policy() != PageIndexPolicy::Skip
970        {
971            self = self
972                .with_column_index_policy(options.column_index_policy())
973                .with_offset_index_policy(options.offset_index_policy());
974        }
975
976        self
977    }
978}
979
980/// The metadata necessary to construct a [`ArrowReaderBuilder`]
981///
982/// Note this structure is cheaply clone-able as it consists of several arcs.
983///
984/// This structure allows
985///
986/// 1. Loading metadata for a file once and then using that same metadata to
987///    construct multiple separate readers, for example, to distribute readers
988///    across multiple threads
989///
990/// 2. Using a cached copy of the [`ParquetMetadata`] rather than reading it
991///    from the file each time a reader is constructed.
992///
993/// [`ParquetMetadata`]: crate::file::metadata::ParquetMetaData
994#[derive(Debug, Clone)]
995pub struct ArrowReaderMetadata {
996    /// The Parquet Metadata, if known aprior
997    pub(crate) metadata: Arc<ParquetMetaData>,
998    /// The Arrow Schema
999    pub(crate) schema: SchemaRef,
1000    /// The Parquet schema (root field)
1001    pub(crate) fields: Option<Arc<ParquetField>>,
1002}
1003
1004impl ArrowReaderMetadata {
1005    /// Create [`ArrowReaderMetadata`] from the provided [`ArrowReaderOptions`]
1006    /// and [`ChunkReader`]
1007    ///
1008    /// See [`ParquetRecordBatchReaderBuilder::new_with_metadata`] for an
1009    /// example of how this can be used
1010    ///
1011    /// # Notes
1012    ///
1013    /// If `options` indicates the page index should be read, but
1014    /// `Self::metadata` is missing the page index, this function will attempt
1015    /// to load the page index by making an object store request.
1016    ///
1017    /// See [`ArrowReaderOptions::with_page_index_policy`] for more information on the page index.
1018    pub fn load<T: ChunkReader>(reader: &T, options: ArrowReaderOptions) -> Result<Self> {
1019        let metadata = ParquetMetaDataReader::new()
1020            .with_column_index_policy(options.column_index_policy())
1021            .with_offset_index_policy(options.offset_index_policy())
1022            .with_metadata_options(Some(options.metadata_options.clone()));
1023        #[cfg(feature = "encryption")]
1024        let metadata = metadata.with_decryption_properties(
1025            options.file_decryption_properties.as_ref().map(Arc::clone),
1026        );
1027        let metadata = metadata.parse_and_finish(reader)?;
1028        Self::try_new(Arc::new(metadata), options)
1029    }
1030
1031    /// Create a new [`ArrowReaderMetadata`] from a pre-existing
1032    /// [`ParquetMetaData`] and [`ArrowReaderOptions`].
1033    ///
1034    /// # Notes
1035    ///
1036    /// This function will not attempt to load the PageIndex if not present in the metadata, regardless
1037    /// of the settings in `options`. See [`Self::load`] to load metadata including the page index if needed.
1038    pub fn try_new(metadata: Arc<ParquetMetaData>, options: ArrowReaderOptions) -> Result<Self> {
1039        match options.supplied_schema {
1040            Some(supplied_schema) => Self::with_supplied_schema(
1041                metadata,
1042                supplied_schema.clone(),
1043                &options.virtual_columns,
1044            ),
1045            None => {
1046                let kv_metadata = match options.skip_arrow_metadata {
1047                    true => None,
1048                    false => metadata.file_metadata().key_value_metadata(),
1049                };
1050
1051                let (schema, fields) = parquet_to_arrow_schema_and_fields(
1052                    metadata.file_metadata().schema_descr(),
1053                    ProjectionMask::all(),
1054                    kv_metadata,
1055                    &options.virtual_columns,
1056                )?;
1057
1058                Ok(Self {
1059                    metadata,
1060                    schema: Arc::new(schema),
1061                    fields: fields.map(Arc::new),
1062                })
1063            }
1064        }
1065    }
1066
1067    fn with_supplied_schema(
1068        metadata: Arc<ParquetMetaData>,
1069        supplied_schema: SchemaRef,
1070        virtual_columns: &[FieldRef],
1071    ) -> Result<Self> {
1072        let parquet_schema = metadata.file_metadata().schema_descr();
1073        let field_levels = parquet_to_arrow_field_levels_with_virtual(
1074            parquet_schema,
1075            ProjectionMask::all(),
1076            Some(supplied_schema.fields()),
1077            virtual_columns,
1078        )?;
1079        let fields = field_levels.fields;
1080        let inferred_len = fields.len();
1081        let supplied_len = supplied_schema.fields().len() + virtual_columns.len();
1082        // Ensure the supplied schema has the same number of columns as the parquet schema.
1083        // parquet_to_arrow_field_levels is expected to throw an error if the schemas have
1084        // different lengths, but we check here to be safe.
1085        if inferred_len != supplied_len {
1086            return Err(arrow_err!(format!(
1087                "Incompatible supplied Arrow schema: expected {} columns received {}",
1088                inferred_len, supplied_len
1089            )));
1090        }
1091
1092        let mut errors = Vec::new();
1093
1094        let field_iter = supplied_schema.fields().iter().zip(fields.iter());
1095
1096        for (field1, field2) in field_iter {
1097            if field1.data_type() != field2.data_type() {
1098                errors.push(format!(
1099                    "data type mismatch for field {}: requested {} but found {}",
1100                    field1.name(),
1101                    field1.data_type(),
1102                    field2.data_type()
1103                ));
1104            }
1105            if field1.is_nullable() != field2.is_nullable() {
1106                errors.push(format!(
1107                    "nullability mismatch for field {}: expected {:?} but found {:?}",
1108                    field1.name(),
1109                    field1.is_nullable(),
1110                    field2.is_nullable()
1111                ));
1112            }
1113            if field1.metadata() != field2.metadata() {
1114                errors.push(format!(
1115                    "metadata mismatch for field {}: expected {:?} but found {:?}",
1116                    field1.name(),
1117                    field1.metadata(),
1118                    field2.metadata()
1119                ));
1120            }
1121        }
1122
1123        if !errors.is_empty() {
1124            let message = errors.join(", ");
1125            return Err(ParquetError::ArrowError(format!(
1126                "Incompatible supplied Arrow schema: {message}",
1127            )));
1128        }
1129
1130        // `fields` is the supplied fields followed by the virtual columns, so the reported
1131        // schema has to be built from it or the virtual columns go missing.
1132        let schema = if virtual_columns.is_empty() {
1133            supplied_schema
1134        } else {
1135            Arc::new(Schema::new_with_metadata(
1136                fields,
1137                supplied_schema.metadata().clone(),
1138            ))
1139        };
1140
1141        Ok(Self {
1142            metadata,
1143            schema,
1144            fields: field_levels.levels.map(Arc::new),
1145        })
1146    }
1147
1148    /// Returns a reference to the [`ParquetMetaData`] for this parquet file
1149    pub fn metadata(&self) -> &Arc<ParquetMetaData> {
1150        &self.metadata
1151    }
1152
1153    /// Returns the parquet [`SchemaDescriptor`] for this parquet file
1154    pub fn parquet_schema(&self) -> &SchemaDescriptor {
1155        self.metadata.file_metadata().schema_descr()
1156    }
1157
1158    /// Returns the arrow [`SchemaRef`] for this parquet file
1159    pub fn schema(&self) -> &SchemaRef {
1160        &self.schema
1161    }
1162}
1163
1164#[doc(hidden)]
1165// A newtype used within `ReaderOptionsBuilder` to distinguish sync readers from async
1166pub struct SyncReader<T: ChunkReader>(T);
1167
1168impl<T: Debug + ChunkReader> Debug for SyncReader<T> {
1169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1170        f.debug_tuple("SyncReader").field(&self.0).finish()
1171    }
1172}
1173
1174/// Creates [`ParquetRecordBatchReader`] for reading Parquet files into Arrow [`RecordBatch`]es
1175///
1176/// # See Also
1177/// * [`crate::arrow::async_reader::ParquetRecordBatchStreamBuilder`] for an async API
1178/// * [`crate::arrow::push_decoder::ParquetPushDecoderBuilder`] for a SansIO decoder API
1179/// * [`ArrowReaderBuilder`] for additional member functions
1180pub type ParquetRecordBatchReaderBuilder<T> = ArrowReaderBuilder<SyncReader<T>>;
1181
1182impl<T: ChunkReader + 'static> ParquetRecordBatchReaderBuilder<T> {
1183    /// Create a new [`ParquetRecordBatchReaderBuilder`]
1184    ///
1185    /// ```
1186    /// # use std::sync::Arc;
1187    /// # use bytes::Bytes;
1188    /// # use arrow_array::{Int32Array, RecordBatch};
1189    /// # use arrow_schema::{DataType, Field, Schema};
1190    /// # use parquet::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
1191    /// # use parquet::arrow::ArrowWriter;
1192    /// # let mut file: Vec<u8> = Vec::with_capacity(1024);
1193    /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
1194    /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
1195    /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
1196    /// # writer.write(&batch).unwrap();
1197    /// # writer.close().unwrap();
1198    /// # let file = Bytes::from(file);
1199    /// // Build the reader from anything that implements `ChunkReader`
1200    /// // such as a `File`, or `Bytes`
1201    /// let mut builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
1202    /// // The builder has access to ParquetMetaData such
1203    /// // as the number and layout of row groups
1204    /// assert_eq!(builder.metadata().num_row_groups(), 1);
1205    /// // Call build to create the reader
1206    /// let mut reader: ParquetRecordBatchReader = builder.build().unwrap();
1207    /// // Read data
1208    /// while let Some(batch) = reader.next().transpose()? {
1209    ///     println!("Read {} rows", batch.num_rows());
1210    /// }
1211    /// # Ok::<(), parquet::errors::ParquetError>(())
1212    /// ```
1213    pub fn try_new(reader: T) -> Result<Self> {
1214        Self::try_new_with_options(reader, Default::default())
1215    }
1216
1217    /// Create a new [`ParquetRecordBatchReaderBuilder`] with [`ArrowReaderOptions`]
1218    ///
1219    /// Use this method if you want to control the options for reading the
1220    /// [`ParquetMetaData`]
1221    pub fn try_new_with_options(reader: T, options: ArrowReaderOptions) -> Result<Self> {
1222        let metadata = ArrowReaderMetadata::load(&reader, options)?;
1223        Ok(Self::new_with_metadata(reader, metadata))
1224    }
1225
1226    /// Create a [`ParquetRecordBatchReaderBuilder`] from the provided [`ArrowReaderMetadata`]
1227    ///
1228    /// Use this method if you already have [`ParquetMetaData`] for a file.
1229    /// This interface allows:
1230    ///
1231    /// 1. Loading metadata once and using it to create multiple builders with
1232    ///    potentially different settings or run on different threads
1233    ///
1234    /// 2. Using a cached copy of the metadata rather than re-reading it from the
1235    ///    file each time a reader is constructed.
1236    ///
1237    /// See the docs on [`ArrowReaderMetadata`] for more details
1238    ///
1239    /// # Example
1240    /// ```
1241    /// # use std::fs::metadata;
1242    /// # use std::sync::Arc;
1243    /// # use bytes::Bytes;
1244    /// # use arrow_array::{Int32Array, RecordBatch};
1245    /// # use arrow_schema::{DataType, Field, Schema};
1246    /// # use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
1247    /// # use parquet::arrow::ArrowWriter;
1248    /// #
1249    /// # let mut file: Vec<u8> = Vec::with_capacity(1024);
1250    /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
1251    /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
1252    /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
1253    /// # writer.write(&batch).unwrap();
1254    /// # writer.close().unwrap();
1255    /// # let file = Bytes::from(file);
1256    /// #
1257    /// let metadata = ArrowReaderMetadata::load(&file, Default::default()).unwrap();
1258    /// let mut a = ParquetRecordBatchReaderBuilder::new_with_metadata(file.clone(), metadata.clone()).build().unwrap();
1259    /// let mut b = ParquetRecordBatchReaderBuilder::new_with_metadata(file, metadata).build().unwrap();
1260    ///
1261    /// // Should be able to read from both in parallel
1262    /// assert_eq!(a.next().unwrap().unwrap(), b.next().unwrap().unwrap());
1263    /// ```
1264    pub fn new_with_metadata(input: T, metadata: ArrowReaderMetadata) -> Self {
1265        Self::new_builder(SyncReader(input), metadata)
1266    }
1267
1268    /// Read bloom filter for a column in a row group
1269    ///
1270    /// Returns `None` if the column does not have a bloom filter
1271    ///
1272    /// We should call this function after other forms pruning, such as projection and predicate pushdown.
1273    pub fn get_row_group_column_bloom_filter(
1274        &self,
1275        row_group_idx: usize,
1276        column_idx: usize,
1277    ) -> Result<Option<Sbbf>> {
1278        let metadata = self.metadata.row_group(row_group_idx);
1279        let column_metadata = metadata.column(column_idx);
1280
1281        let offset: u64 = if let Some(offset) = column_metadata.bloom_filter_offset() {
1282            offset
1283                .try_into()
1284                .map_err(|_| ParquetError::General("Bloom filter offset is invalid".to_string()))?
1285        } else {
1286            return Ok(None);
1287        };
1288
1289        let buffer = match column_metadata.bloom_filter_length() {
1290            Some(length) => self.input.0.get_bytes(offset, length as usize),
1291            None => self.input.0.get_bytes(offset, SBBF_HEADER_SIZE_ESTIMATE),
1292        }?;
1293
1294        let (header, bitset_offset) =
1295            chunk_read_bloom_filter_header_and_offset(offset, buffer.clone())?;
1296
1297        match header.algorithm {
1298            BloomFilterAlgorithm::BLOCK => {
1299                // this match exists to future proof the singleton algorithm enum
1300            }
1301        }
1302        match header.compression {
1303            BloomFilterCompression::UNCOMPRESSED => {
1304                // this match exists to future proof the singleton compression enum
1305            }
1306        }
1307        match header.hash {
1308            BloomFilterHash::XXHASH => {
1309                // this match exists to future proof the singleton hash enum
1310            }
1311        }
1312
1313        let bitset = match column_metadata.bloom_filter_length() {
1314            Some(_) => {
1315                let bitset_start = bitset_offset
1316                    .checked_sub(offset)
1317                    .and_then(|start| usize::try_from(start).ok())
1318                    .ok_or_else(|| {
1319                        ParquetError::General("Bloom filter offset is invalid".to_string())
1320                    })?;
1321                buffer.slice(bitset_start..)
1322            }
1323            None => {
1324                let bitset_length: usize = header.num_bytes.try_into().map_err(|_| {
1325                    ParquetError::General("Bloom filter length is invalid".to_string())
1326                })?;
1327                self.input.0.get_bytes(bitset_offset, bitset_length)?
1328            }
1329        };
1330        Ok(Some(Sbbf::new(&bitset)))
1331    }
1332
1333    /// Build a [`ParquetRecordBatchReader`]
1334    ///
1335    /// Note: this will eagerly evaluate any `RowFilter` before returning
1336    pub fn build(self) -> Result<ParquetRecordBatchReader> {
1337        let Self {
1338            input,
1339            metadata,
1340            schema: _,
1341            fields,
1342            batch_size,
1343            row_group_plan,
1344            projection,
1345            mut filter,
1346            row_selection_policy,
1347            limit,
1348            offset,
1349            metrics,
1350            // Not used for the sync reader, see https://github.com/apache/arrow-rs/issues/8000
1351            max_predicate_cache_size: _,
1352        } = self;
1353
1354        // Try to avoid allocate large buffer
1355        let batch_size = batch_size.min(metadata.file_metadata().num_rows() as usize);
1356
1357        let (row_groups, selection) = row_group_plan.into_global()?;
1358
1359        let row_groups = row_groups.unwrap_or_else(|| (0..metadata.num_row_groups()).collect());
1360
1361        let reader = ReaderRowGroups {
1362            reader: Arc::new(input.0),
1363            metadata,
1364            row_groups,
1365        };
1366
1367        let mut plan_builder = ReadPlanBuilder::new(batch_size)
1368            .with_selection(selection)
1369            .with_row_selection_policy(row_selection_policy);
1370
1371        // Update selection based on any filters
1372        if let Some(filter) = filter.as_mut() {
1373            for predicate in &mut filter.predicates {
1374                // break early if we have ruled out all rows
1375                if !plan_builder.selects_any() {
1376                    break;
1377                }
1378
1379                let mut cache_projection = predicate.projection().clone();
1380                cache_projection.intersect(&projection);
1381
1382                let array_reader = ArrayReaderBuilder::new(&reader, &metrics)
1383                    .with_batch_size(batch_size)
1384                    .with_parquet_metadata(&reader.metadata)
1385                    .build_array_reader(fields.as_deref(), predicate.projection())?;
1386
1387                plan_builder = plan_builder.with_predicate(array_reader, predicate.as_mut())?;
1388            }
1389        }
1390
1391        let array_reader = ArrayReaderBuilder::new(&reader, &metrics)
1392            .with_batch_size(batch_size)
1393            .with_parquet_metadata(&reader.metadata)
1394            .build_array_reader(fields.as_deref(), &projection)?;
1395
1396        let read_plan = plan_builder
1397            .limited(reader.num_rows())
1398            .with_offset(offset)
1399            .with_limit(limit)
1400            .build_limited()
1401            .build();
1402
1403        Ok(ParquetRecordBatchReader::new(array_reader, read_plan))
1404    }
1405}
1406
1407struct ReaderRowGroups<T: ChunkReader> {
1408    reader: Arc<T>,
1409
1410    metadata: Arc<ParquetMetaData>,
1411    /// Optional list of row group indices to scan
1412    row_groups: Vec<usize>,
1413}
1414
1415impl<T: ChunkReader + 'static> RowGroups for ReaderRowGroups<T> {
1416    fn num_rows(&self) -> usize {
1417        let meta = self.metadata.row_groups();
1418        self.row_groups
1419            .iter()
1420            .map(|x| meta[*x].num_rows() as usize)
1421            .sum()
1422    }
1423
1424    fn column_chunks(&self, i: usize) -> Result<Box<dyn PageIterator>> {
1425        Ok(Box::new(ReaderPageIterator {
1426            column_idx: i,
1427            reader: self.reader.clone(),
1428            metadata: self.metadata.clone(),
1429            row_groups: self.row_groups.clone().into_iter(),
1430        }))
1431    }
1432
1433    fn row_groups(&self) -> Box<dyn Iterator<Item = &RowGroupMetaData> + '_> {
1434        Box::new(
1435            self.row_groups
1436                .iter()
1437                .map(move |i| self.metadata.row_group(*i)),
1438        )
1439    }
1440
1441    fn metadata(&self) -> &ParquetMetaData {
1442        self.metadata.as_ref()
1443    }
1444}
1445
1446struct ReaderPageIterator<T: ChunkReader> {
1447    reader: Arc<T>,
1448    column_idx: usize,
1449    row_groups: std::vec::IntoIter<usize>,
1450    metadata: Arc<ParquetMetaData>,
1451}
1452
1453impl<T: ChunkReader + 'static> ReaderPageIterator<T> {
1454    /// Return the next SerializedPageReader
1455    fn next_page_reader(&self, rg_idx: usize) -> Result<SerializedPageReader<T>> {
1456        let rg = self.metadata.row_group(rg_idx);
1457        let column_chunk_metadata = rg.column(self.column_idx);
1458        let page_locations = self
1459            .metadata
1460            .page_index()
1461            .map(|i| i.page_locations(rg_idx, self.column_idx).cloned())
1462            .unwrap_or(None);
1463        let total_rows = rg.num_rows() as usize;
1464        let reader = self.reader.clone();
1465
1466        SerializedPageReader::new(reader, column_chunk_metadata, total_rows, page_locations)?
1467            .add_crypto_context(
1468                rg_idx,
1469                self.column_idx,
1470                self.metadata.as_ref(),
1471                column_chunk_metadata,
1472            )
1473    }
1474}
1475
1476impl<T: ChunkReader + 'static> Iterator for ReaderPageIterator<T> {
1477    type Item = Result<Box<dyn PageReader>>;
1478
1479    fn next(&mut self) -> Option<Self::Item> {
1480        let rg_idx = self.row_groups.next()?;
1481        let page_reader = self
1482            .next_page_reader(rg_idx)
1483            .map(|page_reader| Box::new(page_reader) as _);
1484        Some(page_reader)
1485    }
1486}
1487
1488impl<T: ChunkReader + 'static> PageIterator for ReaderPageIterator<T> {}
1489
1490/// Reads Parquet data as Arrow [`RecordBatch`]es
1491///
1492/// This struct implements the [`RecordBatchReader`] trait and is an
1493/// `Iterator<Item = ArrowResult<RecordBatch>>` that yields [`RecordBatch`]es.
1494///
1495/// Typically, either reads from a file or an in memory buffer [`Bytes`]
1496///
1497/// Created by [`ParquetRecordBatchReaderBuilder`]
1498///
1499/// [`Bytes`]: bytes::Bytes
1500pub struct ParquetRecordBatchReader {
1501    array_reader: Box<dyn ArrayReader>,
1502    schema: SchemaRef,
1503    read_plan: ReadPlan,
1504}
1505
1506/// Accumulates filter masks for decoded chunks in one logical output batch.
1507///
1508/// The first chunk keeps its [`BooleanBuffer`] without copying. A second chunk
1509/// promotes the accumulator to a [`BooleanBufferBuilder`], and later chunks are
1510/// appended to it. For example, chunks `1001` and `1` become `10011`:
1511///
1512/// ```text
1513///           append(1001)             append(1)
1514///   Empty ───────────────▶ Single ───────────────▶ Combined
1515///                          1001                    10011
1516///                          (zero copy)             (promoted to builder)
1517/// ```
1518///
1519/// The combined mask lines up with the rows that [`ArrayReader::read_records`]
1520/// buffered across the decoded chunks (see [`read_mask_batch`]). Consuming the
1521/// buffered batch and filtering it with the accumulated mask yields the output.
1522///
1523/// ```text
1524///   decoded rows:   0 1 2 3   11   <-- buffered by the array reader
1525///   chunk masks:   [1 0 0 1] [1]
1526///   finish():       1 0 0 1   1    <-- filters the whole batch in one pass
1527/// ```
1528#[derive(Default)]
1529enum FilterMaskAccumulator {
1530    #[default]
1531    Empty,
1532    Single(BooleanBuffer),
1533    Combined(BooleanBufferBuilder),
1534}
1535
1536impl FilterMaskAccumulator {
1537    fn append(&mut self, mask: BooleanBuffer) {
1538        *self = match std::mem::take(self) {
1539            Self::Empty => Self::Single(mask),
1540            Self::Single(first) => {
1541                let mut combined = BooleanBufferBuilder::new(first.len() + mask.len());
1542                combined.append_buffer(&first);
1543                combined.append_buffer(&mask);
1544                Self::Combined(combined)
1545            }
1546            Self::Combined(mut combined) => {
1547                combined.append_buffer(&mask);
1548                Self::Combined(combined)
1549            }
1550        };
1551    }
1552
1553    fn finish(self) -> Option<BooleanBuffer> {
1554        match self {
1555            Self::Empty => None,
1556            Self::Single(mask) => Some(mask),
1557            Self::Combined(combined) => Some(combined.build()),
1558        }
1559    }
1560}
1561
1562/// Converts the projection buffered by `array_reader` into a record batch.
1563fn consume_record_batch(array_reader: &mut dyn ArrayReader) -> Result<RecordBatch> {
1564    let array = array_reader.consume_batch()?;
1565    let struct_array = array.as_struct_opt().ok_or_else(|| {
1566        ArrowError::ParquetError("Struct array reader should return struct array".to_string())
1567    })?;
1568    Ok(RecordBatch::from(struct_array))
1569}
1570
1571/// Reads one logical Mask batch, potentially spanning multiple loaded ranges.
1572///
1573/// Each [`MaskCursor`] chunk is safe to decode because it stays within loaded
1574/// pages. Gaps are crossed with [`ArrayReader::skip_records`], while decoded
1575/// arrays and their mask fragments remain buffered. Once `batch_size` selected
1576/// rows have accumulated, this consumes the underlying batch and filters it
1577/// once with the combined mask.
1578fn read_mask_batch(
1579    array_reader: &mut dyn ArrayReader,
1580    mask_cursor: &mut MaskCursor,
1581    batch_size: usize,
1582) -> Result<Option<RecordBatch>> {
1583    let mut selected_rows = 0;
1584    let mut filter_mask = FilterMaskAccumulator::default();
1585
1586    while selected_rows < batch_size && !mask_cursor.is_empty() {
1587        let mask_chunk = mask_cursor.next_chunk(batch_size - selected_rows)?;
1588
1589        if mask_chunk.initial_skip > 0 {
1590            let skipped = array_reader.skip_records(mask_chunk.initial_skip)?;
1591            if skipped != mask_chunk.initial_skip {
1592                return Err(general_err!(
1593                    "failed to skip rows, expected {}, got {}",
1594                    mask_chunk.initial_skip,
1595                    skipped
1596                ));
1597            }
1598        }
1599
1600        let mask = mask_cursor.mask_values_for(&mask_chunk)?;
1601        let read = array_reader.read_records(mask_chunk.chunk_rows)?;
1602        if read == 0 {
1603            return Err(general_err!(
1604                "reached end of column while expecting {} rows",
1605                mask_chunk.chunk_rows
1606            ));
1607        }
1608        if read != mask_chunk.chunk_rows {
1609            return Err(general_err!(
1610                "insufficient rows read from array reader - expected {}, got {}",
1611                mask_chunk.chunk_rows,
1612                read
1613            ));
1614        }
1615
1616        filter_mask.append(mask.values().clone());
1617        selected_rows += mask_chunk.selected_rows;
1618    }
1619
1620    if selected_rows == 0 {
1621        return Ok(None);
1622    }
1623
1624    let filter_mask = filter_mask
1625        .finish()
1626        .ok_or_else(|| general_err!("Internal Error: decoded Mask batch has no filter values"))?;
1627    let batch = consume_record_batch(array_reader)?;
1628    let filtered_batch = filter_record_batch(&batch, &BooleanArray::from(filter_mask))?;
1629    if filtered_batch.num_rows() != selected_rows {
1630        return Err(general_err!(
1631            "filtered rows mismatch selection - expected {}, got {}",
1632            selected_rows,
1633            filtered_batch.num_rows()
1634        ));
1635    }
1636
1637    Ok(Some(filtered_batch))
1638}
1639
1640impl Debug for ParquetRecordBatchReader {
1641    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1642        f.debug_struct("ParquetRecordBatchReader")
1643            .field("array_reader", &"...")
1644            .field("schema", &self.schema)
1645            .field("read_plan", &self.read_plan)
1646            .finish()
1647    }
1648}
1649
1650impl Iterator for ParquetRecordBatchReader {
1651    type Item = Result<RecordBatch, ArrowError>;
1652
1653    fn next(&mut self) -> Option<Self::Item> {
1654        self.next_inner()
1655            .map_err(|arrow_err| arrow_err.into())
1656            .transpose()
1657    }
1658}
1659
1660impl ParquetRecordBatchReader {
1661    /// Returns the next `RecordBatch` from the reader, or `None` if the reader
1662    /// has reached the end of the file.
1663    ///
1664    /// Returns `Result<Option<..>>` rather than `Option<Result<..>>` to
1665    /// simplify error handling with `?`
1666    fn next_inner(&mut self) -> Result<Option<RecordBatch>> {
1667        let mut read_records = 0;
1668        let batch_size = self.batch_size();
1669        if batch_size == 0 {
1670            return Ok(None);
1671        }
1672        match self.read_plan.row_selection_cursor_mut() {
1673            RowSelectionCursor::Mask(mask_cursor) => {
1674                return read_mask_batch(self.array_reader.as_mut(), mask_cursor, batch_size);
1675            }
1676            RowSelectionCursor::Selectors(selectors_cursor) => {
1677                while read_records < batch_size && !selectors_cursor.is_empty() {
1678                    let front = selectors_cursor.next_selector();
1679                    if front.skip {
1680                        let skipped = self.array_reader.skip_records(front.row_count)?;
1681
1682                        if skipped != front.row_count {
1683                            return Err(general_err!(
1684                                "failed to skip rows, expected {}, got {}",
1685                                front.row_count,
1686                                skipped
1687                            ));
1688                        }
1689                        continue;
1690                    }
1691
1692                    //Currently, when RowSelectors with row_count = 0 are included then its interpreted as end of reader.
1693                    //Fix is to skip such entries. See https://github.com/apache/arrow-rs/issues/2669
1694                    if front.row_count == 0 {
1695                        continue;
1696                    }
1697
1698                    // try to read record
1699                    let need_read = batch_size - read_records;
1700                    let to_read = match front.row_count.checked_sub(need_read) {
1701                        Some(remaining) if remaining != 0 => {
1702                            // if page row count less than batch_size we must set batch size to page row count.
1703                            // add check avoid dead loop
1704                            selectors_cursor.return_selector(RowSelector::select(remaining));
1705                            need_read
1706                        }
1707                        _ => front.row_count,
1708                    };
1709                    match self.array_reader.read_records(to_read)? {
1710                        0 => break,
1711                        rec => read_records += rec,
1712                    }
1713                }
1714            }
1715            RowSelectionCursor::All => {
1716                self.array_reader.read_records(batch_size)?;
1717            }
1718        }
1719
1720        let batch = consume_record_batch(self.array_reader.as_mut())?;
1721        Ok(if batch.num_rows() > 0 {
1722            Some(batch)
1723        } else {
1724            None
1725        })
1726    }
1727}
1728
1729impl RecordBatchReader for ParquetRecordBatchReader {
1730    /// Returns the projected [`SchemaRef`] for reading the parquet file.
1731    ///
1732    /// Note that the schema metadata will be stripped here. See
1733    /// [`ParquetRecordBatchReaderBuilder::schema`] if the metadata is desired.
1734    fn schema(&self) -> SchemaRef {
1735        self.schema.clone()
1736    }
1737}
1738
1739impl ParquetRecordBatchReader {
1740    /// Create a new [`ParquetRecordBatchReader`] from the provided chunk reader
1741    ///
1742    /// See [`ParquetRecordBatchReaderBuilder`] for more options
1743    pub fn try_new<T: ChunkReader + 'static>(reader: T, batch_size: usize) -> Result<Self> {
1744        ParquetRecordBatchReaderBuilder::try_new(reader)?
1745            .with_batch_size(batch_size)
1746            .build()
1747    }
1748
1749    /// Create a new [`ParquetRecordBatchReader`] from the provided [`RowGroups`]
1750    ///
1751    /// Note: this is a low-level interface see [`ParquetRecordBatchReader::try_new`] for a
1752    /// higher-level interface for reading parquet data from a file
1753    pub fn try_new_with_row_groups(
1754        levels: &FieldLevels,
1755        row_groups: &dyn RowGroups,
1756        batch_size: usize,
1757        selection: Option<RowSelection>,
1758    ) -> Result<Self> {
1759        // note metrics are not supported in this API
1760        let metrics = ArrowReaderMetrics::disabled();
1761        let array_reader = ArrayReaderBuilder::new(row_groups, &metrics)
1762            .with_batch_size(batch_size)
1763            .with_parquet_metadata(row_groups.metadata())
1764            .build_array_reader(levels.levels.as_ref(), &ProjectionMask::all())?;
1765
1766        let read_plan = ReadPlanBuilder::new(batch_size)
1767            .with_selection(selection)
1768            .build();
1769
1770        Ok(Self {
1771            array_reader,
1772            schema: Arc::new(Schema::new(levels.fields.clone())),
1773            read_plan,
1774        })
1775    }
1776
1777    /// Create a new [`ParquetRecordBatchReader`] that will read at most `batch_size` rows at
1778    /// a time from [`ArrayReader`] based on the configured `selection`. If `selection` is `None`
1779    /// all rows will be returned
1780    pub(crate) fn new(array_reader: Box<dyn ArrayReader>, read_plan: ReadPlan) -> Self {
1781        let schema = match array_reader.get_data_type() {
1782            ArrowType::Struct(fields) => Schema::new(fields.clone()),
1783            _ => unreachable!("Struct array reader's data type is not struct!"),
1784        };
1785
1786        Self {
1787            array_reader,
1788            schema: Arc::new(schema),
1789            read_plan,
1790        }
1791    }
1792
1793    #[inline(always)]
1794    pub(crate) fn batch_size(&self) -> usize {
1795        self.read_plan.batch_size()
1796    }
1797}
1798
1799#[cfg(test)]
1800pub(crate) mod tests;