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;