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    ColumnChunkMask, PageIndexPolicy, ParquetMetaData, ParquetMetaDataOptions,
49    ParquetMetaDataReader, 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    column_index: PageIndexPolicy,
597    offset_index: PageIndexPolicy,
598    column_index_mask: ColumnChunkMask,
599    offset_index_mask: ColumnChunkMask,
600
601    /// Options to control reading of Parquet metadata
602    metadata_options: ParquetMetaDataOptions,
603    /// If encryption is enabled, the file decryption properties can be provided
604    #[cfg(feature = "encryption")]
605    pub(crate) file_decryption_properties: Option<Arc<FileDecryptionProperties>>,
606
607    virtual_columns: Vec<FieldRef>,
608}
609
610impl ArrowReaderOptions {
611    /// Create a new [`ArrowReaderOptions`] with the default settings
612    pub fn new() -> Self {
613        Self::default()
614    }
615
616    /// Skip decoding the embedded arrow metadata (defaults to `false`)
617    ///
618    /// Parquet files generated by some writers may contain embedded arrow
619    /// schema and metadata.
620    /// This may not be correct or compatible with your system,
621    /// for example, see [ARROW-16184](https://issues.apache.org/jira/browse/ARROW-16184)
622    pub fn with_skip_arrow_metadata(self, skip_arrow_metadata: bool) -> Self {
623        Self {
624            skip_arrow_metadata,
625            ..self
626        }
627    }
628
629    /// Provide a schema hint to use when reading the Parquet file.
630    ///
631    /// If provided, this schema takes precedence over any arrow schema embedded
632    /// in the metadata (see the [`arrow`] documentation for more details).
633    ///
634    /// If the provided schema is not compatible with the data stored in the
635    /// parquet file schema, an error will be returned when constructing the
636    /// builder.
637    ///
638    /// This option is only required if you want to explicitly control the
639    /// conversion of Parquet types to Arrow types, such as casting a column to
640    /// a different type. For example, if you wanted to read an Int64 in
641    /// a Parquet file to a [`TimestampMicrosecondArray`] in the Arrow schema.
642    ///
643    /// [`arrow`]: crate::arrow
644    /// [`TimestampMicrosecondArray`]: arrow_array::TimestampMicrosecondArray
645    ///
646    /// # Notes
647    ///
648    /// The provided schema must have the same number of columns as the parquet schema and
649    /// the column names must be the same.
650    ///
651    /// # Example
652    /// ```
653    /// # use std::sync::Arc;
654    /// # use bytes::Bytes;
655    /// # use arrow_array::{ArrayRef, Int32Array, RecordBatch};
656    /// # use arrow_schema::{DataType, Field, Schema, TimeUnit};
657    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
658    /// # use parquet::arrow::ArrowWriter;
659    /// // Write data - schema is inferred from the data to be Int32
660    /// let mut file = Vec::new();
661    /// let batch = RecordBatch::try_from_iter(vec![
662    ///     ("col_1", Arc::new(Int32Array::from(vec![1, 2, 3])) as ArrayRef),
663    /// ]).unwrap();
664    /// let mut writer = ArrowWriter::try_new(&mut file, batch.schema(), None).unwrap();
665    /// writer.write(&batch).unwrap();
666    /// writer.close().unwrap();
667    /// let file = Bytes::from(file);
668    ///
669    /// // Read the file back.
670    /// // Supply a schema that interprets the Int32 column as a Timestamp.
671    /// let supplied_schema = Arc::new(Schema::new(vec![
672    ///     Field::new("col_1", DataType::Timestamp(TimeUnit::Nanosecond, None), false)
673    /// ]));
674    /// let options = ArrowReaderOptions::new().with_schema(supplied_schema.clone());
675    /// let mut builder = ParquetRecordBatchReaderBuilder::try_new_with_options(
676    ///     file.clone(),
677    ///     options
678    /// ).expect("Error if the schema is not compatible with the parquet file schema.");
679    ///
680    /// // Create the reader and read the data using the supplied schema.
681    /// let mut reader = builder.build().unwrap();
682    /// let _batch = reader.next().unwrap().unwrap();
683    /// ```
684    ///
685    /// # Example: Preserving Dictionary Encoding
686    ///
687    /// By default, Parquet string columns are read as `Utf8Array` (or `LargeUtf8Array`),
688    /// even if the underlying Parquet data uses dictionary encoding. You can preserve
689    /// the dictionary encoding by specifying a `Dictionary` type in the schema hint:
690    ///
691    /// ```
692    /// # use std::sync::Arc;
693    /// # use bytes::Bytes;
694    /// # use arrow_array::{ArrayRef, RecordBatch, StringArray};
695    /// # use arrow_schema::{DataType, Field, Schema};
696    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
697    /// # use parquet::arrow::ArrowWriter;
698    /// // Write a Parquet file with string data
699    /// let mut file = Vec::new();
700    /// let schema = Arc::new(Schema::new(vec![
701    ///     Field::new("city", DataType::Utf8, false)
702    /// ]));
703    /// let cities = StringArray::from(vec!["Berlin", "Berlin", "Paris", "Berlin", "Paris"]);
704    /// let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(cities)]).unwrap();
705    ///
706    /// let mut writer = ArrowWriter::try_new(&mut file, batch.schema(), None).unwrap();
707    /// writer.write(&batch).unwrap();
708    /// writer.close().unwrap();
709    /// let file = Bytes::from(file);
710    ///
711    /// // Read the file back, requesting dictionary encoding preservation
712    /// let dict_schema = Arc::new(Schema::new(vec![
713    ///     Field::new("city", DataType::Dictionary(
714    ///         Box::new(DataType::Int32),
715    ///         Box::new(DataType::Utf8)
716    ///     ), false)
717    /// ]));
718    /// let options = ArrowReaderOptions::new().with_schema(dict_schema);
719    /// let builder = ParquetRecordBatchReaderBuilder::try_new_with_options(
720    ///     file.clone(),
721    ///     options
722    /// ).unwrap();
723    ///
724    /// let mut reader = builder.build().unwrap();
725    /// let batch = reader.next().unwrap().unwrap();
726    ///
727    /// // The column is now a DictionaryArray
728    /// assert!(matches!(
729    ///     batch.column(0).data_type(),
730    ///     DataType::Dictionary(_, _)
731    /// ));
732    /// ```
733    ///
734    /// **Note**: Dictionary encoding preservation works best when:
735    /// 1. The original column was dictionary encoded (the default for string columns)
736    /// 2. There are a small number of distinct values
737    pub fn with_schema(self, schema: SchemaRef) -> Self {
738        Self {
739            supplied_schema: Some(schema),
740            skip_arrow_metadata: true,
741            ..self
742        }
743    }
744
745    /// Sets the [`PageIndexPolicy`] for both the column and offset indexes.
746    ///
747    /// The `PageIndex` can be used to push down predicates to the parquet scan,
748    /// potentially eliminating unnecessary IO, by some query engines.
749    /// The `PageIndex` consists of two structures: the `ColumnIndex` and `OffsetIndex`.
750    /// This method sets the same policy for both. For fine-grained control, use
751    /// [`Self::with_column_index_policy`] and [`Self::with_offset_index_policy`].
752    pub fn with_page_index_policy(self, policy: PageIndexPolicy) -> Self {
753        self.with_column_index_policy(policy)
754            .with_offset_index_policy(policy)
755    }
756
757    /// Sets the [`PageIndexPolicy`] for the Parquet [ColumnIndex] structure.
758    ///
759    /// The `ColumnIndex` contains min/max statistics for each page, which can be used
760    /// for predicate pushdown and page-level pruning.
761    ///
762    /// [ColumnIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
763    pub fn with_column_index_policy(mut self, policy: PageIndexPolicy) -> Self {
764        self.column_index = policy;
765        self
766    }
767
768    /// Sets the [`PageIndexPolicy`] for the Parquet [OffsetIndex] structure.
769    ///
770    /// The `OffsetIndex` contains the locations and sizes of each page, which enables
771    /// efficient page-level skipping and random access within column chunks.
772    ///
773    /// [OffsetIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
774    pub fn with_offset_index_policy(mut self, policy: PageIndexPolicy) -> Self {
775        self.offset_index = policy;
776        self
777    }
778
779    // TODO: deprecate the following as part of https://github.com/apache/arrow-rs/issues/11395
780
781    /// Sets the same [`ColumnChunkMask`] for both page-index structures.
782    pub fn with_page_index_mask(self, mask: ColumnChunkMask) -> Self {
783        self.with_column_index_mask(mask.clone())
784            .with_offset_index_mask(mask)
785    }
786
787    /// Sets the [`ColumnChunkMask`] for the Parquet [ColumnIndex] structure.
788    ///
789    /// The column index can be costly to decode and store, especially when it is needed
790    /// only for a subset of row groups or columns (such as when filtering by a predicate
791    /// on a single column). Providing a [`ColumnChunkMask`] can greatly decrease
792    /// the time needed to decode this metadata.
793    ///
794    /// The mask applies only if the column-index policy is not [`PageIndexPolicy::Skip`]
795    /// (the default), or an underlying reader is configured to preload the index. It is
796    /// honored by loading APIs such as [`ArrowReaderMetadata::load`];
797    /// [`ArrowReaderMetadata::try_new`] does not load or filter page indexes.
798    ///
799    /// [ColumnIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
800    pub fn with_column_index_mask(mut self, mask: ColumnChunkMask) -> Self {
801        self.column_index_mask = mask;
802        self
803    }
804
805    /// Sets the [`ColumnChunkMask`] for the Parquet [OffsetIndex] structure.
806    ///
807    /// The offset index can be costly to decode and store, especially when it is needed
808    /// only for a subset of row groups or columns (such as when projecting a small subset
809    /// of columns). Providing a [`ColumnChunkMask`] can greatly decrease
810    /// the time needed to decode this metadata.
811    ///
812    /// Page pruning also needs the offset index for predicate columns, so callers should
813    /// include those columns in addition to projected columns. The same loading and policy
814    /// qualifications as [`Self::with_column_index_mask`] apply.
815    ///
816    /// [OffsetIndex]: https://github.com/apache/parquet-format/blob/master/PageIndex.md
817    pub fn with_offset_index_mask(mut self, mask: ColumnChunkMask) -> Self {
818        self.offset_index_mask = mask;
819        self
820    }
821
822    /// Provide a Parquet schema to use when decoding the metadata. The schema in the Parquet
823    /// footer will be skipped.
824    ///
825    /// This can be used to avoid reparsing the schema from the file when it is
826    /// already known.
827    pub fn with_parquet_schema(mut self, schema: Arc<SchemaDescriptor>) -> Self {
828        self.metadata_options.set_schema(schema);
829        self
830    }
831
832    /// Set whether to convert the [`encoding_stats`] in the Parquet `ColumnMetaData` to a bitmask
833    /// (defaults to `false`).
834    ///
835    /// See [`ColumnChunkMetaData::page_encoding_stats_mask`] for an explanation of why this
836    /// might be desirable.
837    ///
838    /// [`ColumnChunkMetaData::page_encoding_stats_mask`]:
839    /// crate::file::metadata::ColumnChunkMetaData::page_encoding_stats_mask
840    /// [`encoding_stats`]:
841    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L917
842    pub fn with_encoding_stats_as_mask(mut self, val: bool) -> Self {
843        self.metadata_options.set_encoding_stats_as_mask(val);
844        self
845    }
846
847    /// Sets the decoding policy for [`encoding_stats`] in the Parquet `ColumnMetaData`.
848    ///
849    /// [`encoding_stats`]:
850    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L917
851    pub fn with_encoding_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
852        self.metadata_options.set_encoding_stats_policy(policy);
853        self
854    }
855
856    /// Sets the decoding policy for [`statistics`] in the Parquet `ColumnMetaData`.
857    ///
858    /// [`statistics`]:
859    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L912
860    pub fn with_column_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
861        self.metadata_options.set_column_stats_policy(policy);
862        self
863    }
864
865    /// Sets the decoding policy for [`size_statistics`] in the Parquet `ColumnMetaData`.
866    ///
867    /// [`size_statistics`]:
868    /// https://github.com/apache/parquet-format/blob/786142e26740487930ddc3ec5e39d780bd930907/src/main/thrift/parquet.thrift#L936
869    pub fn with_size_stats_policy(mut self, policy: ParquetStatisticsPolicy) -> Self {
870        self.metadata_options.set_size_stats_policy(policy);
871        self
872    }
873
874    /// Provide the file decryption properties to use when reading encrypted parquet files.
875    ///
876    /// If encryption is enabled and the file is encrypted, the `file_decryption_properties` must be provided.
877    #[cfg(feature = "encryption")]
878    pub fn with_file_decryption_properties(
879        self,
880        file_decryption_properties: Arc<FileDecryptionProperties>,
881    ) -> Self {
882        Self {
883            file_decryption_properties: Some(file_decryption_properties),
884            ..self
885        }
886    }
887
888    /// Include virtual columns in the output.
889    ///
890    /// 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.
891    ///
892    /// # Example
893    /// ```
894    /// # use std::sync::Arc;
895    /// # use bytes::Bytes;
896    /// # use arrow_array::{ArrayRef, Int64Array, RecordBatch};
897    /// # use arrow_schema::{DataType, Field, Schema};
898    /// # use parquet::arrow::{ArrowWriter, RowNumber};
899    /// # use parquet::arrow::arrow_reader::{ArrowReaderOptions, ParquetRecordBatchReaderBuilder};
900    /// #
901    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
902    /// // Create a simple record batch with some data
903    /// let values = Arc::new(Int64Array::from(vec![1, 2, 3])) as ArrayRef;
904    /// let batch = RecordBatch::try_from_iter(vec![("value", values)])?;
905    ///
906    /// // Write the batch to an in-memory buffer
907    /// let mut file = Vec::new();
908    /// let mut writer = ArrowWriter::try_new(
909    ///     &mut file,
910    ///     batch.schema(),
911    ///     None
912    /// )?;
913    /// writer.write(&batch)?;
914    /// writer.close()?;
915    /// let file = Bytes::from(file);
916    ///
917    /// // Create a virtual column for row numbers
918    /// let row_number_field = Arc::new(Field::new("row_number", DataType::Int64, false)
919    ///     .with_extension_type(RowNumber));
920    ///
921    /// // Configure options with virtual columns
922    /// let options = ArrowReaderOptions::new()
923    ///     .with_virtual_columns(vec![row_number_field])?;
924    ///
925    /// // Create a reader with the options
926    /// let mut reader = ParquetRecordBatchReaderBuilder::try_new_with_options(
927    ///     file,
928    ///     options
929    /// )?
930    /// .build()?;
931    ///
932    /// // Read the batch - it will include both the original column and the virtual row_number column
933    /// let result_batch = reader.next().unwrap()?;
934    /// assert_eq!(result_batch.num_columns(), 2); // "value" + "row_number"
935    /// assert_eq!(result_batch.num_rows(), 3);
936    /// #
937    /// # Ok(())
938    /// # }
939    /// ```
940    pub fn with_virtual_columns(self, virtual_columns: Vec<FieldRef>) -> Result<Self> {
941        // Validate that all fields are virtual columns
942        for field in &virtual_columns {
943            if !is_virtual_column(field) {
944                return Err(ParquetError::General(format!(
945                    "Field '{}' is not a virtual column. Virtual columns must have extension type names starting with 'arrow.virtual.'",
946                    field.name()
947                )));
948            }
949        }
950        Ok(Self {
951            virtual_columns,
952            ..self
953        })
954    }
955
956    /// Retrieve the currently set [`PageIndexPolicy`] for the offset index.
957    ///
958    /// This can be set via [`with_offset_index_policy`][Self::with_offset_index_policy]
959    /// or [`with_page_index_policy`][Self::with_page_index_policy].
960    pub fn offset_index_policy(&self) -> PageIndexPolicy {
961        self.offset_index
962    }
963
964    /// Retrieve the currently set [`PageIndexPolicy`] for the column index.
965    ///
966    /// This can be set via [`with_column_index_policy`][Self::with_column_index_policy]
967    /// or [`with_page_index_policy`][Self::with_page_index_policy].
968    pub fn column_index_policy(&self) -> PageIndexPolicy {
969        self.column_index
970    }
971
972    // TODO: deprecate the following as part of https://github.com/apache/arrow-rs/issues/11395
973
974    /// Retrieve the currently set [`ColumnChunkMask`] for the offset index.
975    ///
976    /// This can be set via [`with_offset_index_mask`][Self::with_offset_index_mask].
977    pub fn offset_index_mask(&self) -> &ColumnChunkMask {
978        &self.offset_index_mask
979    }
980
981    /// Retrieve the currently set [`ColumnChunkMask`] for the column index.
982    ///
983    /// This can be set via [`with_column_index_mask`][Self::with_column_index_mask].
984    pub fn column_index_mask(&self) -> &ColumnChunkMask {
985        &self.column_index_mask
986    }
987
988    /// Retrieve the currently set metadata decoding options.
989    pub fn metadata_options(&self) -> &ParquetMetaDataOptions {
990        &self.metadata_options
991    }
992
993    /// Retrieve the currently set file decryption properties.
994    ///
995    /// This can be set via
996    /// [`file_decryption_properties`][Self::with_file_decryption_properties].
997    #[cfg(feature = "encryption")]
998    pub fn file_decryption_properties(&self) -> Option<&Arc<FileDecryptionProperties>> {
999        self.file_decryption_properties.as_ref()
1000    }
1001}
1002
1003impl ParquetMetaDataReader {
1004    /// Applies the metadata related settings from [`ArrowReaderOptions`],
1005    /// such as the [`ParquetMetaDataOptions`], decryption properties, and
1006    /// [`PageIndexPolicy`] to this reader.
1007    ///
1008    /// The page index policies are only applied if at least one of them is not
1009    /// [`PageIndexPolicy::Skip`], so policies previously configured on this
1010    /// reader (e.g. from a preload setting) are preserved when the options do
1011    /// not request the page index.
1012    ///
1013    /// This encodes the canonical way to construct a `ParquetMetaDataReader`
1014    /// inside `AsyncFileReader::get_metadata` (available with the `async`
1015    /// feature), so implementations outside this crate do not need to
1016    /// duplicate it.
1017    pub fn with_arrow_reader_options(mut self, options: Option<&ArrowReaderOptions>) -> Self {
1018        let Some(options) = options else { return self };
1019
1020        self = self.with_metadata_options(Some(options.metadata_options().clone()));
1021
1022        #[cfg(feature = "encryption")]
1023        {
1024            self = self.with_decryption_properties(
1025                options.file_decryption_properties.as_ref().map(Arc::clone),
1026            );
1027        }
1028
1029        if options.column_index_policy() != PageIndexPolicy::Skip
1030            || options.offset_index_policy() != PageIndexPolicy::Skip
1031        {
1032            self = self
1033                .with_column_index_policy(options.column_index_policy())
1034                .with_offset_index_policy(options.offset_index_policy());
1035        }
1036
1037        // Preload settings on the underlying reader may enable an index even when the
1038        // corresponding options policy is `Skip`, so apply non-default masks independently.
1039        //
1040        // TODO: https://github.com/apache/arrow-rs/issues/11395 may deprecate this API. Decide
1041        // at that time if masks should be added to ParquetObjectReader.
1042        if !options.column_index_mask().is_all() {
1043            self = self.with_column_index_mask(options.column_index_mask().clone());
1044        }
1045        if !options.offset_index_mask().is_all() {
1046            self = self.with_offset_index_mask(options.offset_index_mask().clone());
1047        }
1048
1049        self
1050    }
1051}
1052
1053/// The metadata necessary to construct a [`ArrowReaderBuilder`]
1054///
1055/// Note this structure is cheaply clone-able as it consists of several arcs.
1056///
1057/// This structure allows
1058///
1059/// 1. Loading metadata for a file once and then using that same metadata to
1060///    construct multiple separate readers, for example, to distribute readers
1061///    across multiple threads
1062///
1063/// 2. Using a cached copy of the [`ParquetMetadata`] rather than reading it
1064///    from the file each time a reader is constructed.
1065///
1066/// [`ParquetMetadata`]: crate::file::metadata::ParquetMetaData
1067#[derive(Debug, Clone)]
1068pub struct ArrowReaderMetadata {
1069    /// The Parquet Metadata, if known aprior
1070    pub(crate) metadata: Arc<ParquetMetaData>,
1071    /// The Arrow Schema
1072    pub(crate) schema: SchemaRef,
1073    /// The Parquet schema (root field)
1074    pub(crate) fields: Option<Arc<ParquetField>>,
1075}
1076
1077impl ArrowReaderMetadata {
1078    /// Create [`ArrowReaderMetadata`] from the provided [`ArrowReaderOptions`]
1079    /// and [`ChunkReader`]
1080    ///
1081    /// See [`ParquetRecordBatchReaderBuilder::new_with_metadata`] for an
1082    /// example of how this can be used
1083    ///
1084    /// # Notes
1085    ///
1086    /// If `options` indicates the page index should be read, but
1087    /// `Self::metadata` is missing the page index, this function will attempt
1088    /// to load the page index by making an object store request.
1089    ///
1090    /// See [`ArrowReaderOptions::with_page_index_policy`] for more information on the page index.
1091    pub fn load<T: ChunkReader>(reader: &T, options: ArrowReaderOptions) -> Result<Self> {
1092        let metadata = ParquetMetaDataReader::new()
1093            .with_column_index_policy(options.column_index_policy())
1094            .with_offset_index_policy(options.offset_index_policy())
1095            .with_column_index_mask(options.column_index_mask().clone())
1096            .with_offset_index_mask(options.offset_index_mask().clone())
1097            .with_metadata_options(Some(options.metadata_options.clone()));
1098        #[cfg(feature = "encryption")]
1099        let metadata = metadata.with_decryption_properties(
1100            options.file_decryption_properties.as_ref().map(Arc::clone),
1101        );
1102        let metadata = metadata.parse_and_finish(reader)?;
1103        Self::try_new(Arc::new(metadata), options)
1104    }
1105
1106    /// Create a new [`ArrowReaderMetadata`] from a pre-existing
1107    /// [`ParquetMetaData`] and [`ArrowReaderOptions`].
1108    ///
1109    /// # Notes
1110    ///
1111    /// This function will not attempt to load the PageIndex if not present in the metadata, regardless
1112    /// of the settings in `options`. See [`Self::load`] to load metadata including the page index if needed.
1113    pub fn try_new(metadata: Arc<ParquetMetaData>, options: ArrowReaderOptions) -> Result<Self> {
1114        match options.supplied_schema {
1115            Some(supplied_schema) => Self::with_supplied_schema(
1116                metadata,
1117                supplied_schema.clone(),
1118                &options.virtual_columns,
1119            ),
1120            None => {
1121                let kv_metadata = match options.skip_arrow_metadata {
1122                    true => None,
1123                    false => metadata.file_metadata().key_value_metadata(),
1124                };
1125
1126                let (schema, fields) = parquet_to_arrow_schema_and_fields(
1127                    metadata.file_metadata().schema_descr(),
1128                    ProjectionMask::all(),
1129                    kv_metadata,
1130                    &options.virtual_columns,
1131                )?;
1132
1133                Ok(Self {
1134                    metadata,
1135                    schema: Arc::new(schema),
1136                    fields: fields.map(Arc::new),
1137                })
1138            }
1139        }
1140    }
1141
1142    fn with_supplied_schema(
1143        metadata: Arc<ParquetMetaData>,
1144        supplied_schema: SchemaRef,
1145        virtual_columns: &[FieldRef],
1146    ) -> Result<Self> {
1147        let parquet_schema = metadata.file_metadata().schema_descr();
1148        let field_levels = parquet_to_arrow_field_levels_with_virtual(
1149            parquet_schema,
1150            ProjectionMask::all(),
1151            Some(supplied_schema.fields()),
1152            virtual_columns,
1153        )?;
1154        let fields = field_levels.fields;
1155        let inferred_len = fields.len();
1156        let supplied_len = supplied_schema.fields().len() + virtual_columns.len();
1157        // Ensure the supplied schema has the same number of columns as the parquet schema.
1158        // parquet_to_arrow_field_levels is expected to throw an error if the schemas have
1159        // different lengths, but we check here to be safe.
1160        if inferred_len != supplied_len {
1161            return Err(arrow_err!(format!(
1162                "Incompatible supplied Arrow schema: expected {} columns received {}",
1163                inferred_len, supplied_len
1164            )));
1165        }
1166
1167        let mut errors = Vec::new();
1168
1169        let field_iter = supplied_schema.fields().iter().zip(fields.iter());
1170
1171        for (field1, field2) in field_iter {
1172            if field1.data_type() != field2.data_type() {
1173                errors.push(format!(
1174                    "data type mismatch for field {}: requested {} but found {}",
1175                    field1.name(),
1176                    field1.data_type(),
1177                    field2.data_type()
1178                ));
1179            }
1180            if field1.is_nullable() != field2.is_nullable() {
1181                errors.push(format!(
1182                    "nullability mismatch for field {}: expected {:?} but found {:?}",
1183                    field1.name(),
1184                    field1.is_nullable(),
1185                    field2.is_nullable()
1186                ));
1187            }
1188            if field1.metadata() != field2.metadata() {
1189                errors.push(format!(
1190                    "metadata mismatch for field {}: expected {:?} but found {:?}",
1191                    field1.name(),
1192                    field1.metadata(),
1193                    field2.metadata()
1194                ));
1195            }
1196        }
1197
1198        if !errors.is_empty() {
1199            let message = errors.join(", ");
1200            return Err(ParquetError::ArrowError(format!(
1201                "Incompatible supplied Arrow schema: {message}",
1202            )));
1203        }
1204
1205        // `fields` is the supplied fields followed by the virtual columns, so the reported
1206        // schema has to be built from it or the virtual columns go missing.
1207        let schema = if virtual_columns.is_empty() {
1208            supplied_schema
1209        } else {
1210            Arc::new(Schema::new_with_metadata(
1211                fields,
1212                supplied_schema.metadata().clone(),
1213            ))
1214        };
1215
1216        Ok(Self {
1217            metadata,
1218            schema,
1219            fields: field_levels.levels.map(Arc::new),
1220        })
1221    }
1222
1223    /// Returns a reference to the [`ParquetMetaData`] for this parquet file
1224    pub fn metadata(&self) -> &Arc<ParquetMetaData> {
1225        &self.metadata
1226    }
1227
1228    /// Returns the parquet [`SchemaDescriptor`] for this parquet file
1229    pub fn parquet_schema(&self) -> &SchemaDescriptor {
1230        self.metadata.file_metadata().schema_descr()
1231    }
1232
1233    /// Returns the arrow [`SchemaRef`] for this parquet file
1234    pub fn schema(&self) -> &SchemaRef {
1235        &self.schema
1236    }
1237}
1238
1239#[doc(hidden)]
1240// A newtype used within `ReaderOptionsBuilder` to distinguish sync readers from async
1241pub struct SyncReader<T: ChunkReader>(T);
1242
1243impl<T: Debug + ChunkReader> Debug for SyncReader<T> {
1244    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1245        f.debug_tuple("SyncReader").field(&self.0).finish()
1246    }
1247}
1248
1249/// Creates [`ParquetRecordBatchReader`] for reading Parquet files into Arrow [`RecordBatch`]es
1250///
1251/// # See Also
1252/// * [`crate::arrow::async_reader::ParquetRecordBatchStreamBuilder`] for an async API
1253/// * [`crate::arrow::push_decoder::ParquetPushDecoderBuilder`] for a SansIO decoder API
1254/// * [`ArrowReaderBuilder`] for additional member functions
1255pub type ParquetRecordBatchReaderBuilder<T> = ArrowReaderBuilder<SyncReader<T>>;
1256
1257impl<T: ChunkReader + 'static> ParquetRecordBatchReaderBuilder<T> {
1258    /// Create a new [`ParquetRecordBatchReaderBuilder`]
1259    ///
1260    /// ```
1261    /// # use std::sync::Arc;
1262    /// # use bytes::Bytes;
1263    /// # use arrow_array::{Int32Array, RecordBatch};
1264    /// # use arrow_schema::{DataType, Field, Schema};
1265    /// # use parquet::arrow::arrow_reader::{ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
1266    /// # use parquet::arrow::ArrowWriter;
1267    /// # let mut file: Vec<u8> = Vec::with_capacity(1024);
1268    /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
1269    /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
1270    /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
1271    /// # writer.write(&batch).unwrap();
1272    /// # writer.close().unwrap();
1273    /// # let file = Bytes::from(file);
1274    /// // Build the reader from anything that implements `ChunkReader`
1275    /// // such as a `File`, or `Bytes`
1276    /// let mut builder = ParquetRecordBatchReaderBuilder::try_new(file).unwrap();
1277    /// // The builder has access to ParquetMetaData such
1278    /// // as the number and layout of row groups
1279    /// assert_eq!(builder.metadata().num_row_groups(), 1);
1280    /// // Call build to create the reader
1281    /// let mut reader: ParquetRecordBatchReader = builder.build().unwrap();
1282    /// // Read data
1283    /// while let Some(batch) = reader.next().transpose()? {
1284    ///     println!("Read {} rows", batch.num_rows());
1285    /// }
1286    /// # Ok::<(), parquet::errors::ParquetError>(())
1287    /// ```
1288    pub fn try_new(reader: T) -> Result<Self> {
1289        Self::try_new_with_options(reader, Default::default())
1290    }
1291
1292    /// Create a new [`ParquetRecordBatchReaderBuilder`] with [`ArrowReaderOptions`]
1293    ///
1294    /// Use this method if you want to control the options for reading the
1295    /// [`ParquetMetaData`]
1296    pub fn try_new_with_options(reader: T, options: ArrowReaderOptions) -> Result<Self> {
1297        let metadata = ArrowReaderMetadata::load(&reader, options)?;
1298        Ok(Self::new_with_metadata(reader, metadata))
1299    }
1300
1301    /// Create a [`ParquetRecordBatchReaderBuilder`] from the provided [`ArrowReaderMetadata`]
1302    ///
1303    /// Use this method if you already have [`ParquetMetaData`] for a file.
1304    /// This interface allows:
1305    ///
1306    /// 1. Loading metadata once and using it to create multiple builders with
1307    ///    potentially different settings or run on different threads
1308    ///
1309    /// 2. Using a cached copy of the metadata rather than re-reading it from the
1310    ///    file each time a reader is constructed.
1311    ///
1312    /// See the docs on [`ArrowReaderMetadata`] for more details
1313    ///
1314    /// # Example
1315    /// ```
1316    /// # use std::fs::metadata;
1317    /// # use std::sync::Arc;
1318    /// # use bytes::Bytes;
1319    /// # use arrow_array::{Int32Array, RecordBatch};
1320    /// # use arrow_schema::{DataType, Field, Schema};
1321    /// # use parquet::arrow::arrow_reader::{ArrowReaderMetadata, ParquetRecordBatchReader, ParquetRecordBatchReaderBuilder};
1322    /// # use parquet::arrow::ArrowWriter;
1323    /// #
1324    /// # let mut file: Vec<u8> = Vec::with_capacity(1024);
1325    /// # let schema = Arc::new(Schema::new(vec![Field::new("i32", DataType::Int32, false)]));
1326    /// # let mut writer = ArrowWriter::try_new(&mut file, schema.clone(), None).unwrap();
1327    /// # let batch = RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(vec![1, 2, 3]))]).unwrap();
1328    /// # writer.write(&batch).unwrap();
1329    /// # writer.close().unwrap();
1330    /// # let file = Bytes::from(file);
1331    /// #
1332    /// let metadata = ArrowReaderMetadata::load(&file, Default::default()).unwrap();
1333    /// let mut a = ParquetRecordBatchReaderBuilder::new_with_metadata(file.clone(), metadata.clone()).build().unwrap();
1334    /// let mut b = ParquetRecordBatchReaderBuilder::new_with_metadata(file, metadata).build().unwrap();
1335    ///
1336    /// // Should be able to read from both in parallel
1337    /// assert_eq!(a.next().unwrap().unwrap(), b.next().unwrap().unwrap());
1338    /// ```
1339    pub fn new_with_metadata(input: T, metadata: ArrowReaderMetadata) -> Self {
1340        Self::new_builder(SyncReader(input), metadata)
1341    }
1342
1343    /// Read bloom filter for a column in a row group
1344    ///
1345    /// Returns `None` if the column does not have a bloom filter
1346    ///
1347    /// We should call this function after other forms pruning, such as projection and predicate pushdown.
1348    pub fn get_row_group_column_bloom_filter(
1349        &self,
1350        row_group_idx: usize,
1351        column_idx: usize,
1352    ) -> Result<Option<Sbbf>> {
1353        let metadata = self.metadata.row_group(row_group_idx);
1354        let column_metadata = metadata.column(column_idx);
1355
1356        let offset: u64 = if let Some(offset) = column_metadata.bloom_filter_offset() {
1357            offset
1358                .try_into()
1359                .map_err(|_| ParquetError::General("Bloom filter offset is invalid".to_string()))?
1360        } else {
1361            return Ok(None);
1362        };
1363
1364        let buffer = match column_metadata.bloom_filter_length() {
1365            Some(length) => self.input.0.get_bytes(offset, length as usize),
1366            None => self.input.0.get_bytes(offset, SBBF_HEADER_SIZE_ESTIMATE),
1367        }?;
1368
1369        let (header, bitset_offset) =
1370            chunk_read_bloom_filter_header_and_offset(offset, buffer.clone())?;
1371
1372        match header.algorithm {
1373            BloomFilterAlgorithm::BLOCK => {
1374                // this match exists to future proof the singleton algorithm enum
1375            }
1376        }
1377        match header.compression {
1378            BloomFilterCompression::UNCOMPRESSED => {
1379                // this match exists to future proof the singleton compression enum
1380            }
1381        }
1382        match header.hash {
1383            BloomFilterHash::XXHASH => {
1384                // this match exists to future proof the singleton hash enum
1385            }
1386        }
1387
1388        let bitset = match column_metadata.bloom_filter_length() {
1389            Some(_) => {
1390                let bitset_start = bitset_offset
1391                    .checked_sub(offset)
1392                    .and_then(|start| usize::try_from(start).ok())
1393                    .ok_or_else(|| {
1394                        ParquetError::General("Bloom filter offset is invalid".to_string())
1395                    })?;
1396                buffer.slice(bitset_start..)
1397            }
1398            None => {
1399                let bitset_length: usize = header.num_bytes.try_into().map_err(|_| {
1400                    ParquetError::General("Bloom filter length is invalid".to_string())
1401                })?;
1402                self.input.0.get_bytes(bitset_offset, bitset_length)?
1403            }
1404        };
1405        Ok(Some(Sbbf::new(&bitset)))
1406    }
1407
1408    /// Build a [`ParquetRecordBatchReader`]
1409    ///
1410    /// Note: this will eagerly evaluate any `RowFilter` before returning
1411    pub fn build(self) -> Result<ParquetRecordBatchReader> {
1412        let Self {
1413            input,
1414            metadata,
1415            schema: _,
1416            fields,
1417            batch_size,
1418            row_group_plan,
1419            projection,
1420            mut filter,
1421            row_selection_policy,
1422            limit,
1423            offset,
1424            metrics,
1425            // Not used for the sync reader, see https://github.com/apache/arrow-rs/issues/8000
1426            max_predicate_cache_size: _,
1427        } = self;
1428
1429        // Try to avoid allocate large buffer
1430        let batch_size = batch_size.min(metadata.file_metadata().num_rows() as usize);
1431
1432        let (row_groups, selection) = row_group_plan.into_global()?;
1433
1434        let row_groups = row_groups.unwrap_or_else(|| (0..metadata.num_row_groups()).collect());
1435
1436        let reader = ReaderRowGroups {
1437            reader: Arc::new(input.0),
1438            metadata,
1439            row_groups,
1440        };
1441
1442        let mut plan_builder = ReadPlanBuilder::new(batch_size)
1443            .with_selection(selection)
1444            .with_row_selection_policy(row_selection_policy);
1445
1446        // Update selection based on any filters
1447        if let Some(filter) = filter.as_mut() {
1448            for predicate in &mut filter.predicates {
1449                // break early if we have ruled out all rows
1450                if !plan_builder.selects_any() {
1451                    break;
1452                }
1453
1454                let mut cache_projection = predicate.projection().clone();
1455                cache_projection.intersect(&projection);
1456
1457                let array_reader = ArrayReaderBuilder::new(&reader, &metrics)
1458                    .with_batch_size(batch_size)
1459                    .with_parquet_metadata(&reader.metadata)
1460                    .build_array_reader(fields.as_deref(), predicate.projection())?;
1461
1462                plan_builder = plan_builder.with_predicate(array_reader, predicate.as_mut())?;
1463            }
1464        }
1465
1466        let array_reader = ArrayReaderBuilder::new(&reader, &metrics)
1467            .with_batch_size(batch_size)
1468            .with_parquet_metadata(&reader.metadata)
1469            .build_array_reader(fields.as_deref(), &projection)?;
1470
1471        let read_plan = plan_builder
1472            .limited(reader.num_rows())
1473            .with_offset(offset)
1474            .with_limit(limit)
1475            .build_limited()
1476            .build();
1477
1478        Ok(ParquetRecordBatchReader::new(array_reader, read_plan))
1479    }
1480}
1481
1482struct ReaderRowGroups<T: ChunkReader> {
1483    reader: Arc<T>,
1484
1485    metadata: Arc<ParquetMetaData>,
1486    /// Optional list of row group indices to scan
1487    row_groups: Vec<usize>,
1488}
1489
1490impl<T: ChunkReader + 'static> RowGroups for ReaderRowGroups<T> {
1491    fn num_rows(&self) -> usize {
1492        let meta = self.metadata.row_groups();
1493        self.row_groups
1494            .iter()
1495            .map(|x| meta[*x].num_rows() as usize)
1496            .sum()
1497    }
1498
1499    fn column_chunks(&self, i: usize) -> Result<Box<dyn PageIterator>> {
1500        Ok(Box::new(ReaderPageIterator {
1501            column_idx: i,
1502            reader: self.reader.clone(),
1503            metadata: self.metadata.clone(),
1504            row_groups: self.row_groups.clone().into_iter(),
1505        }))
1506    }
1507
1508    fn row_groups(&self) -> Box<dyn Iterator<Item = &RowGroupMetaData> + '_> {
1509        Box::new(
1510            self.row_groups
1511                .iter()
1512                .map(move |i| self.metadata.row_group(*i)),
1513        )
1514    }
1515
1516    fn metadata(&self) -> &ParquetMetaData {
1517        self.metadata.as_ref()
1518    }
1519}
1520
1521struct ReaderPageIterator<T: ChunkReader> {
1522    reader: Arc<T>,
1523    column_idx: usize,
1524    row_groups: std::vec::IntoIter<usize>,
1525    metadata: Arc<ParquetMetaData>,
1526}
1527
1528impl<T: ChunkReader + 'static> ReaderPageIterator<T> {
1529    /// Return the next SerializedPageReader
1530    fn next_page_reader(&self, rg_idx: usize) -> Result<SerializedPageReader<T>> {
1531        let rg = self.metadata.row_group(rg_idx);
1532        let column_chunk_metadata = rg.column(self.column_idx);
1533        let page_locations = self
1534            .metadata
1535            .page_index()
1536            .map(|i| i.page_locations(rg_idx, self.column_idx).cloned())
1537            .unwrap_or(None);
1538        let total_rows = rg.num_rows() as usize;
1539        let reader = self.reader.clone();
1540
1541        SerializedPageReader::new(reader, column_chunk_metadata, total_rows, page_locations)?
1542            .add_crypto_context(
1543                rg_idx,
1544                self.column_idx,
1545                self.metadata.as_ref(),
1546                column_chunk_metadata,
1547            )
1548    }
1549}
1550
1551impl<T: ChunkReader + 'static> Iterator for ReaderPageIterator<T> {
1552    type Item = Result<Box<dyn PageReader>>;
1553
1554    fn next(&mut self) -> Option<Self::Item> {
1555        let rg_idx = self.row_groups.next()?;
1556        let page_reader = self
1557            .next_page_reader(rg_idx)
1558            .map(|page_reader| Box::new(page_reader) as _);
1559        Some(page_reader)
1560    }
1561}
1562
1563impl<T: ChunkReader + 'static> PageIterator for ReaderPageIterator<T> {}
1564
1565/// Reads Parquet data as Arrow [`RecordBatch`]es
1566///
1567/// This struct implements the [`RecordBatchReader`] trait and is an
1568/// `Iterator<Item = ArrowResult<RecordBatch>>` that yields [`RecordBatch`]es.
1569///
1570/// Typically, either reads from a file or an in memory buffer [`Bytes`]
1571///
1572/// Created by [`ParquetRecordBatchReaderBuilder`]
1573///
1574/// [`Bytes`]: bytes::Bytes
1575pub struct ParquetRecordBatchReader {
1576    array_reader: Box<dyn ArrayReader>,
1577    schema: SchemaRef,
1578    read_plan: ReadPlan,
1579}
1580
1581/// Accumulates filter masks for decoded chunks in one logical output batch.
1582///
1583/// The first chunk keeps its [`BooleanBuffer`] without copying. A second chunk
1584/// promotes the accumulator to a [`BooleanBufferBuilder`], and later chunks are
1585/// appended to it. For example, chunks `1001` and `1` become `10011`:
1586///
1587/// ```text
1588///           append(1001)             append(1)
1589///   Empty ───────────────▶ Single ───────────────▶ Combined
1590///                          1001                    10011
1591///                          (zero copy)             (promoted to builder)
1592/// ```
1593///
1594/// The combined mask lines up with the rows that [`ArrayReader::read_records`]
1595/// buffered across the decoded chunks (see [`read_mask_batch`]). Consuming the
1596/// buffered batch and filtering it with the accumulated mask yields the output.
1597///
1598/// ```text
1599///   decoded rows:   0 1 2 3   11   <-- buffered by the array reader
1600///   chunk masks:   [1 0 0 1] [1]
1601///   finish():       1 0 0 1   1    <-- filters the whole batch in one pass
1602/// ```
1603#[derive(Default)]
1604enum FilterMaskAccumulator {
1605    #[default]
1606    Empty,
1607    Single(BooleanBuffer),
1608    Combined(BooleanBufferBuilder),
1609}
1610
1611impl FilterMaskAccumulator {
1612    fn append(&mut self, mask: BooleanBuffer) {
1613        *self = match std::mem::take(self) {
1614            Self::Empty => Self::Single(mask),
1615            Self::Single(first) => {
1616                let mut combined = BooleanBufferBuilder::new(first.len() + mask.len());
1617                combined.append_buffer(&first);
1618                combined.append_buffer(&mask);
1619                Self::Combined(combined)
1620            }
1621            Self::Combined(mut combined) => {
1622                combined.append_buffer(&mask);
1623                Self::Combined(combined)
1624            }
1625        };
1626    }
1627
1628    fn finish(self) -> Option<BooleanBuffer> {
1629        match self {
1630            Self::Empty => None,
1631            Self::Single(mask) => Some(mask),
1632            Self::Combined(combined) => Some(combined.build()),
1633        }
1634    }
1635}
1636
1637/// Converts the projection buffered by `array_reader` into a record batch.
1638fn consume_record_batch(array_reader: &mut dyn ArrayReader) -> Result<RecordBatch> {
1639    let array = array_reader.consume_batch()?;
1640    let struct_array = array.as_struct_opt().ok_or_else(|| {
1641        ArrowError::ParquetError("Struct array reader should return struct array".to_string())
1642    })?;
1643    Ok(RecordBatch::from(struct_array))
1644}
1645
1646/// Reads one logical Mask batch, potentially spanning multiple loaded ranges.
1647///
1648/// Each [`MaskCursor`] chunk is safe to decode because it stays within loaded
1649/// pages. Gaps are crossed with [`ArrayReader::skip_records`], while decoded
1650/// arrays and their mask fragments remain buffered. Once `batch_size` selected
1651/// rows have accumulated, this consumes the underlying batch and filters it
1652/// once with the combined mask.
1653fn read_mask_batch(
1654    array_reader: &mut dyn ArrayReader,
1655    mask_cursor: &mut MaskCursor,
1656    batch_size: usize,
1657) -> Result<Option<RecordBatch>> {
1658    let mut selected_rows = 0;
1659    let mut filter_mask = FilterMaskAccumulator::default();
1660
1661    while selected_rows < batch_size && !mask_cursor.is_empty() {
1662        let mask_chunk = mask_cursor.next_chunk(batch_size - selected_rows)?;
1663
1664        if mask_chunk.initial_skip > 0 {
1665            let skipped = array_reader.skip_records(mask_chunk.initial_skip)?;
1666            if skipped != mask_chunk.initial_skip {
1667                return Err(general_err!(
1668                    "failed to skip rows, expected {}, got {}",
1669                    mask_chunk.initial_skip,
1670                    skipped
1671                ));
1672            }
1673        }
1674
1675        let mask = mask_cursor.mask_values_for(&mask_chunk)?;
1676        let read = array_reader.read_records(mask_chunk.chunk_rows)?;
1677        if read == 0 {
1678            return Err(general_err!(
1679                "reached end of column while expecting {} rows",
1680                mask_chunk.chunk_rows
1681            ));
1682        }
1683        if read != mask_chunk.chunk_rows {
1684            return Err(general_err!(
1685                "insufficient rows read from array reader - expected {}, got {}",
1686                mask_chunk.chunk_rows,
1687                read
1688            ));
1689        }
1690
1691        filter_mask.append(mask.values().clone());
1692        selected_rows += mask_chunk.selected_rows;
1693    }
1694
1695    if selected_rows == 0 {
1696        return Ok(None);
1697    }
1698
1699    let filter_mask = filter_mask
1700        .finish()
1701        .ok_or_else(|| general_err!("Internal Error: decoded Mask batch has no filter values"))?;
1702    let batch = consume_record_batch(array_reader)?;
1703    let filtered_batch = filter_record_batch(&batch, &BooleanArray::from(filter_mask))?;
1704    if filtered_batch.num_rows() != selected_rows {
1705        return Err(general_err!(
1706            "filtered rows mismatch selection - expected {}, got {}",
1707            selected_rows,
1708            filtered_batch.num_rows()
1709        ));
1710    }
1711
1712    Ok(Some(filtered_batch))
1713}
1714
1715impl Debug for ParquetRecordBatchReader {
1716    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1717        f.debug_struct("ParquetRecordBatchReader")
1718            .field("array_reader", &"...")
1719            .field("schema", &self.schema)
1720            .field("read_plan", &self.read_plan)
1721            .finish()
1722    }
1723}
1724
1725impl Iterator for ParquetRecordBatchReader {
1726    type Item = Result<RecordBatch, ArrowError>;
1727
1728    fn next(&mut self) -> Option<Self::Item> {
1729        self.next_inner()
1730            .map_err(|arrow_err| arrow_err.into())
1731            .transpose()
1732    }
1733}
1734
1735impl ParquetRecordBatchReader {
1736    /// Returns the next `RecordBatch` from the reader, or `None` if the reader
1737    /// has reached the end of the file.
1738    ///
1739    /// Returns `Result<Option<..>>` rather than `Option<Result<..>>` to
1740    /// simplify error handling with `?`
1741    fn next_inner(&mut self) -> Result<Option<RecordBatch>> {
1742        let mut read_records = 0;
1743        let batch_size = self.batch_size();
1744        if batch_size == 0 {
1745            return Ok(None);
1746        }
1747        match self.read_plan.row_selection_cursor_mut() {
1748            RowSelectionCursor::Mask(mask_cursor) => {
1749                return read_mask_batch(self.array_reader.as_mut(), mask_cursor, batch_size);
1750            }
1751            RowSelectionCursor::Selectors(selectors_cursor) => {
1752                while read_records < batch_size && !selectors_cursor.is_empty() {
1753                    let front = selectors_cursor.next_selector();
1754                    if front.skip {
1755                        let skipped = self.array_reader.skip_records(front.row_count)?;
1756
1757                        if skipped != front.row_count {
1758                            return Err(general_err!(
1759                                "failed to skip rows, expected {}, got {}",
1760                                front.row_count,
1761                                skipped
1762                            ));
1763                        }
1764                        continue;
1765                    }
1766
1767                    //Currently, when RowSelectors with row_count = 0 are included then its interpreted as end of reader.
1768                    //Fix is to skip such entries. See https://github.com/apache/arrow-rs/issues/2669
1769                    if front.row_count == 0 {
1770                        continue;
1771                    }
1772
1773                    // try to read record
1774                    let need_read = batch_size - read_records;
1775                    let to_read = match front.row_count.checked_sub(need_read) {
1776                        Some(remaining) if remaining != 0 => {
1777                            // if page row count less than batch_size we must set batch size to page row count.
1778                            // add check avoid dead loop
1779                            selectors_cursor.return_selector(RowSelector::select(remaining));
1780                            need_read
1781                        }
1782                        _ => front.row_count,
1783                    };
1784                    match self.array_reader.read_records(to_read)? {
1785                        0 => break,
1786                        rec => read_records += rec,
1787                    }
1788                }
1789            }
1790            RowSelectionCursor::All => {
1791                self.array_reader.read_records(batch_size)?;
1792            }
1793        }
1794
1795        let batch = consume_record_batch(self.array_reader.as_mut())?;
1796        Ok(if batch.num_rows() > 0 {
1797            Some(batch)
1798        } else {
1799            None
1800        })
1801    }
1802}
1803
1804impl RecordBatchReader for ParquetRecordBatchReader {
1805    /// Returns the projected [`SchemaRef`] for reading the parquet file.
1806    ///
1807    /// Note that the schema metadata will be stripped here. See
1808    /// [`ParquetRecordBatchReaderBuilder::schema`] if the metadata is desired.
1809    fn schema(&self) -> SchemaRef {
1810        self.schema.clone()
1811    }
1812}
1813
1814impl ParquetRecordBatchReader {
1815    /// Create a new [`ParquetRecordBatchReader`] from the provided chunk reader
1816    ///
1817    /// See [`ParquetRecordBatchReaderBuilder`] for more options
1818    pub fn try_new<T: ChunkReader + 'static>(reader: T, batch_size: usize) -> Result<Self> {
1819        ParquetRecordBatchReaderBuilder::try_new(reader)?
1820            .with_batch_size(batch_size)
1821            .build()
1822    }
1823
1824    /// Create a new [`ParquetRecordBatchReader`] from the provided [`RowGroups`]
1825    ///
1826    /// Note: this is a low-level interface see [`ParquetRecordBatchReader::try_new`] for a
1827    /// higher-level interface for reading parquet data from a file
1828    pub fn try_new_with_row_groups(
1829        levels: &FieldLevels,
1830        row_groups: &dyn RowGroups,
1831        batch_size: usize,
1832        selection: Option<RowSelection>,
1833    ) -> Result<Self> {
1834        // note metrics are not supported in this API
1835        let metrics = ArrowReaderMetrics::disabled();
1836        let array_reader = ArrayReaderBuilder::new(row_groups, &metrics)
1837            .with_batch_size(batch_size)
1838            .with_parquet_metadata(row_groups.metadata())
1839            .build_array_reader(levels.levels.as_ref(), &ProjectionMask::all())?;
1840
1841        let read_plan = ReadPlanBuilder::new(batch_size)
1842            .with_selection(selection)
1843            .build();
1844
1845        Ok(Self {
1846            array_reader,
1847            schema: Arc::new(Schema::new(levels.fields.clone())),
1848            read_plan,
1849        })
1850    }
1851
1852    /// Create a new [`ParquetRecordBatchReader`] that will read at most `batch_size` rows at
1853    /// a time from [`ArrayReader`] based on the configured `selection`. If `selection` is `None`
1854    /// all rows will be returned
1855    pub(crate) fn new(array_reader: Box<dyn ArrayReader>, read_plan: ReadPlan) -> Self {
1856        let schema = match array_reader.get_data_type() {
1857            ArrowType::Struct(fields) => Schema::new(fields.clone()),
1858            _ => unreachable!("Struct array reader's data type is not struct!"),
1859        };
1860
1861        Self {
1862            array_reader,
1863            schema: Arc::new(schema),
1864            read_plan,
1865        }
1866    }
1867
1868    #[inline(always)]
1869    pub(crate) fn batch_size(&self) -> usize {
1870        self.read_plan.batch_size()
1871    }
1872}
1873
1874#[cfg(test)]
1875pub(crate) mod tests;