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;