parquet/arrow/push_decoder/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//! [`ParquetPushDecoder`]: decodes Parquet data with data provided by the
19//! caller (rather than from an underlying reader).
20
21mod reader_builder;
22mod remaining;
23
24use crate::DecodeResult;
25use crate::arrow::arrow_reader::{
26 ArrowReaderBuilder, ArrowReaderMetadata, ArrowReaderOptions, ParquetRecordBatchReader,
27};
28use crate::errors::ParquetError;
29use crate::file::metadata::ParquetMetaData;
30pub use crate::util::push_buffers::PushBuffers;
31use arrow_array::RecordBatch;
32use bytes::Bytes;
33use reader_builder::{RowBudget, RowGroupReaderBuilder, RowGroupReaderBuilderParts};
34use remaining::{RemainingRowGroups, RemainingRowGroupsParts};
35use std::ops::Range;
36use std::sync::Arc;
37
38/// A builder for [`ParquetPushDecoder`].
39///
40/// To create a new decoder, use [`ParquetPushDecoderBuilder::try_new_decoder`].
41///
42/// You can decode the metadata from a Parquet file using either
43/// [`ParquetMetadataReader`] or [`ParquetMetaDataPushDecoder`].
44///
45/// [`ParquetMetadataReader`]: crate::file::metadata::ParquetMetaDataReader
46/// [`ParquetMetaDataPushDecoder`]: crate::file::metadata::ParquetMetaDataPushDecoder
47///
48/// Note the "input" type is `u64` which represents the length of the Parquet file
49/// being decoded. This is needed to initialize the internal buffers that track
50/// what data has been provided to the decoder.
51///
52/// # Example
53/// ```
54/// # use std::ops::Range;
55/// # use std::sync::Arc;
56/// # use bytes::Bytes;
57/// # use arrow_array::record_batch;
58/// # use parquet::DecodeResult;
59/// # use parquet::arrow::push_decoder::ParquetPushDecoderBuilder;
60/// # use parquet::arrow::ArrowWriter;
61/// # use parquet::file::metadata::ParquetMetaDataPushDecoder;
62/// # let file_bytes = {
63/// # let mut buffer = vec![];
64/// # let batch = record_batch!(("a", Int32, [1, 2, 3])).unwrap();
65/// # let mut writer = ArrowWriter::try_new(&mut buffer, batch.schema(), None).unwrap();
66/// # writer.write(&batch).unwrap();
67/// # writer.close().unwrap();
68/// # Bytes::from(buffer)
69/// # };
70/// # // mimic IO by returning a function that returns the bytes for a given range
71/// # let get_range = |range: &Range<u64>| -> Bytes {
72/// # let start = range.start as usize;
73/// # let end = range.end as usize;
74/// # file_bytes.slice(start..end)
75/// # };
76/// # let file_length = file_bytes.len() as u64;
77/// # let mut metadata_decoder = ParquetMetaDataPushDecoder::try_new(file_length).unwrap();
78/// # metadata_decoder.push_ranges(vec![0..file_length], vec![file_bytes.clone()]).unwrap();
79/// # let DecodeResult::Data(parquet_metadata) = metadata_decoder.try_decode().unwrap() else { panic!("failed to decode metadata") };
80/// # let parquet_metadata = Arc::new(parquet_metadata);
81/// // The file length and metadata are required to create the decoder
82/// let mut decoder =
83/// ParquetPushDecoderBuilder::try_new_decoder(parquet_metadata)
84/// .unwrap()
85/// // Optionally configure the decoder, e.g. batch size
86/// .with_batch_size(1024)
87/// // Build the decoder
88/// .build()
89/// .unwrap();
90///
91/// // In a loop, ask the decoder what it needs next, and provide it with the required data
92/// loop {
93/// match decoder.try_decode().unwrap() {
94/// DecodeResult::NeedsData(ranges) => {
95/// // The decoder needs more data. Fetch the data for the given ranges
96/// let data = ranges.iter().map(|r| get_range(r)).collect::<Vec<_>>();
97/// // Push the data to the decoder
98/// decoder.push_ranges(ranges, data).unwrap();
99/// // After pushing the data, we can try to decode again on the next iteration
100/// }
101/// DecodeResult::Data(batch) => {
102/// // Successfully decoded a batch of data
103/// assert!(batch.num_rows() > 0);
104/// }
105/// DecodeResult::Finished => {
106/// // The decoder has finished decoding exit the loop
107/// break;
108/// }
109/// }
110/// }
111/// ```
112///
113/// # Adaptive scans
114///
115/// The scan strategy is not fixed once [`build`](Self::build) is called: it
116/// can be changed *while decoding*, at row-group boundaries.
117///
118/// The important API for this is [`ParquetPushDecoder::try_next_reader`].
119/// Unlike [`try_decode`](ParquetPushDecoder::try_decode), which barrels
120/// straight through row-group boundaries, `try_next_reader` returns once per
121/// row group — leaving a clean window *between* row groups. At any such
122/// boundary, [`ParquetPushDecoder::into_builder`] hands back a
123/// `ParquetPushDecoderBuilder` for the row groups not yet decoded. Change any
124/// option on it (projection, row filter, row selection policy, …) and
125/// [`build`](Self::build) a fresh decoder that resumes from the next row
126/// group. This is how a query engine promotes or demotes filters — for
127/// example turning a row filter on or off — based on the selectivity observed
128/// in the row groups decoded so far.
129///
130/// ```
131/// # use std::ops::Range;
132/// # use std::sync::Arc;
133/// # use bytes::Bytes;
134/// # use arrow_array::record_batch;
135/// # use parquet::DecodeResult;
136/// # use parquet::arrow::ProjectionMask;
137/// # use parquet::arrow::push_decoder::ParquetPushDecoderBuilder;
138/// # use parquet::arrow::ArrowWriter;
139/// # use parquet::file::metadata::ParquetMetaDataPushDecoder;
140/// # use parquet::file::properties::WriterProperties;
141/// # let file_bytes = {
142/// # let batch = record_batch!(
143/// # ("a", Int32, [1, 2, 3, 4, 5, 6]),
144/// # ("b", Int32, [6, 5, 4, 3, 2, 1])
145/// # ).unwrap();
146/// # // Small row groups so the test file has two of them.
147/// # let props = WriterProperties::builder().set_max_row_group_row_count(Some(3)).build();
148/// # let mut buffer = vec![];
149/// # let mut writer = ArrowWriter::try_new(&mut buffer, batch.schema(), Some(props)).unwrap();
150/// # writer.write(&batch).unwrap();
151/// # writer.close().unwrap();
152/// # Bytes::from(buffer)
153/// # };
154/// # let get_range = |r: &Range<u64>| file_bytes.slice(r.start as usize..r.end as usize);
155/// # let file_length = file_bytes.len() as u64;
156/// # let mut metadata_decoder = ParquetMetaDataPushDecoder::try_new(file_length).unwrap();
157/// # metadata_decoder.push_ranges(vec![0..file_length], vec![file_bytes.clone()]).unwrap();
158/// # let DecodeResult::Data(parquet_metadata) = metadata_decoder.try_decode().unwrap() else { panic!() };
159/// # let parquet_metadata = Arc::new(parquet_metadata);
160/// let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(parquet_metadata)
161/// .unwrap()
162/// .build()
163/// .unwrap();
164///
165/// // Drive the decoder one row group at a time with `try_next_reader`.
166/// loop {
167/// match decoder.try_next_reader().unwrap() {
168/// DecodeResult::NeedsData(ranges) => {
169/// // Fetch and hand over the bytes the decoder asked for.
170/// let data = ranges.iter().map(|r| get_range(r)).collect();
171/// decoder.push_ranges(ranges, data).unwrap();
172/// }
173/// DecodeResult::Data(reader) => {
174/// // Decode this row group's batches.
175/// for batch in reader {
176/// assert!(batch.unwrap().num_rows() > 0);
177/// }
178/// // We are now at a row-group boundary. Based on whatever stats
179/// // were gathered, optionally change strategy for the row groups
180/// // still to come: drop or promote a row filter, narrow or widen
181/// // the projection, etc.
182/// if decoder.is_at_row_group_boundary() && decoder.row_groups_remaining() > 0 {
183/// let builder = decoder.into_builder().unwrap();
184/// // e.g. column "b" turned out not to be needed.
185/// let projection = ProjectionMask::columns(builder.parquet_schema(), ["a"]);
186/// decoder = builder.with_projection(projection).build().unwrap();
187/// }
188/// }
189/// DecodeResult::Finished => break,
190/// }
191/// }
192/// ```
193pub type ParquetPushDecoderBuilder = ArrowReaderBuilder<PushDecoderInput>;
194
195/// The `input` of a [`ParquetPushDecoderBuilder`].
196///
197/// The shared [`ArrowReaderBuilder`] is generic over an `input`. The sync and
198/// async builders read from a file or async reader; the push decoder has no
199/// reader, so its input is the [`PushBuffers`] that caller-pushed bytes
200/// accumulate in (empty for a fresh builder).
201#[derive(Debug, Default)]
202pub struct PushDecoderInput {
203 /// Bytes pushed into the decoder, awaiting decode.
204 buffers: PushBuffers,
205}
206
207/// Methods for building a ParquetDecoder. See the base [`ArrowReaderBuilder`] for
208/// more options that can be configured.
209impl ParquetPushDecoderBuilder {
210 /// Create a new `ParquetDecoderBuilder` for configuring a Parquet decoder for the given file.
211 ///
212 /// See [`ParquetMetadataDecoder`] for a builder that can read the metadata from a Parquet file.
213 ///
214 /// [`ParquetMetadataDecoder`]: crate::file::metadata::ParquetMetaDataPushDecoder
215 ///
216 /// See example on [`ParquetPushDecoderBuilder`]
217 pub fn try_new_decoder(parquet_metadata: Arc<ParquetMetaData>) -> Result<Self, ParquetError> {
218 Self::try_new_decoder_with_options(parquet_metadata, ArrowReaderOptions::default())
219 }
220
221 /// Create a new `ParquetDecoderBuilder` for configuring a Parquet decoder for the given file
222 /// with the given reader options.
223 ///
224 /// This is similar to [`Self::try_new_decoder`] but allows configuring
225 /// options such as Arrow schema
226 pub fn try_new_decoder_with_options(
227 parquet_metadata: Arc<ParquetMetaData>,
228 arrow_reader_options: ArrowReaderOptions,
229 ) -> Result<Self, ParquetError> {
230 let arrow_reader_metadata =
231 ArrowReaderMetadata::try_new(parquet_metadata, arrow_reader_options)?;
232 Ok(Self::new_with_metadata(arrow_reader_metadata))
233 }
234
235 /// Create a new `ParquetDecoderBuilder` given [`ArrowReaderMetadata`].
236 ///
237 /// See [`ArrowReaderMetadata::try_new`] for how to create the metadata from
238 /// the Parquet metadata and reader options.
239 pub fn new_with_metadata(arrow_reader_metadata: ArrowReaderMetadata) -> Self {
240 Self::new_builder(PushDecoderInput::default(), arrow_reader_metadata)
241 }
242
243 /// Provide a preexisting [`PushBuffers`] for the built decoder to read
244 /// from, so bytes already fetched are not requested again.
245 pub fn with_buffers(self, buffers: PushBuffers) -> Self {
246 Self {
247 input: PushDecoderInput { buffers },
248 ..self
249 }
250 }
251
252 /// Create a [`ParquetPushDecoder`] with the configured options
253 pub fn build(self) -> Result<ParquetPushDecoder, ParquetError> {
254 let Self {
255 input: PushDecoderInput { buffers },
256 metadata: parquet_metadata,
257 schema,
258 fields,
259 batch_size,
260 row_groups,
261 projection,
262 filter,
263 selection,
264 limit,
265 offset,
266 metrics,
267 row_selection_policy,
268 max_predicate_cache_size,
269 } = self;
270
271 // If no row groups were specified, read all of them
272 let row_groups =
273 row_groups.unwrap_or_else(|| (0..parquet_metadata.num_row_groups()).collect());
274 let has_predicates = filter
275 .as_ref()
276 .is_some_and(|filter| !filter.predicates.is_empty());
277
278 // Prepare to build RowGroup readers. `buffers` carries any bytes the
279 // caller already pushed (preserved across `into_builder`); a fresh
280 // builder supplies an empty `PushBuffers`.
281 let row_group_reader_builder = RowGroupReaderBuilder::new(
282 batch_size,
283 projection,
284 Arc::clone(&parquet_metadata),
285 fields,
286 filter,
287 metrics,
288 max_predicate_cache_size,
289 buffers,
290 row_selection_policy,
291 );
292
293 // Initialize the decoder with the configured options
294 let remaining_row_groups = RemainingRowGroups::new(
295 schema,
296 parquet_metadata,
297 row_groups,
298 selection,
299 RowBudget::new(offset, limit),
300 has_predicates,
301 row_group_reader_builder,
302 );
303
304 Ok(ParquetPushDecoder {
305 state: ParquetDecoderState::ReadingRowGroup {
306 remaining_row_groups: Box::new(remaining_row_groups),
307 },
308 })
309 }
310}
311
312/// Reassemble a [`ParquetPushDecoderBuilder`] from a decoder's not-yet-decoded
313/// state — the inverse of [`ParquetPushDecoderBuilder::build`]. The rebuilt
314/// builder pins the remaining row groups and carries the remaining row
315/// selection, offset/limit budget, and buffered bytes.
316fn builder_from_remaining(parts: RemainingRowGroupsParts) -> ParquetPushDecoderBuilder {
317 let RemainingRowGroupsParts {
318 metadata,
319 schema,
320 row_groups,
321 selection,
322 offset,
323 limit,
324 reader_builder,
325 } = parts;
326 let RowGroupReaderBuilderParts {
327 batch_size,
328 projection,
329 fields,
330 filter,
331 max_predicate_cache_size,
332 metrics,
333 row_selection_policy,
334 buffers,
335 } = reader_builder;
336
337 ArrowReaderBuilder {
338 input: PushDecoderInput::default(),
339 metadata,
340 schema,
341 fields,
342 batch_size,
343 // The frontier tracks remaining row groups explicitly, so the rebuilt
344 // builder always pins them (even if the original left `row_groups` as
345 // `None` meaning "all").
346 row_groups: Some(row_groups),
347 projection,
348 filter,
349 selection,
350 row_selection_policy,
351 limit,
352 offset,
353 metrics,
354 max_predicate_cache_size,
355 }
356 // Carry the decoder's already-fetched bytes across the rebuild so the new
357 // decoder does not re-request them.
358 .with_buffers(buffers)
359}
360
361/// A push based Parquet Decoder
362///
363/// See [`ParquetPushDecoderBuilder`] for an example of how to build and use the decoder.
364///
365/// [`ParquetPushDecoder`] is a low level API for decoding Parquet data without an
366/// underlying reader for performing IO, and thus offers fine grained control
367/// over how data is fetched and decoded.
368///
369/// When more data is needed to make progress, instead of reading data directly
370/// from a reader, the decoder returns [`DecodeResult`] indicating what ranges
371/// are needed. Once the caller provides the requested ranges via
372/// [`Self::push_ranges`], they try to decode again by calling
373/// [`Self::try_decode`].
374///
375/// The decoder's internal state tracks what has been already decoded and what
376/// is needed next.
377#[derive(Debug)]
378pub struct ParquetPushDecoder {
379 /// The inner state.
380 ///
381 /// This state is consumed on every transition and a new state is produced
382 /// so the Rust compiler can ensure that the state is always valid and
383 /// transitions are not missed.
384 state: ParquetDecoderState,
385}
386
387impl ParquetPushDecoder {
388 /// Attempt to decode the next batch of data, or return what data is needed
389 ///
390 /// The the decoder communicates the next state with a [`DecodeResult`]
391 ///
392 /// See full example in [`ParquetPushDecoderBuilder`]
393 ///
394 /// ```no_run
395 /// # use parquet::arrow::push_decoder::ParquetPushDecoder;
396 /// use parquet::DecodeResult;
397 /// # fn get_decoder() -> ParquetPushDecoder { unimplemented!() }
398 /// # fn push_data(decoder: &mut ParquetPushDecoder, ranges: Vec<std::ops::Range<u64>>) { unimplemented!() }
399 /// let mut decoder = get_decoder();
400 /// loop {
401 /// match decoder.try_decode().unwrap() {
402 /// DecodeResult::NeedsData(ranges) => {
403 /// // The decoder needs more data. Fetch the data for the given ranges
404 /// // call decoder.push_ranges(ranges, data) and call again
405 /// push_data(&mut decoder, ranges);
406 /// }
407 /// DecodeResult::Data(batch) => {
408 /// // Successfully decoded the next batch of data
409 /// println!("Got batch with {} rows", batch.num_rows());
410 /// }
411 /// DecodeResult::Finished => {
412 /// // The decoder has finished decoding all data
413 /// break;
414 /// }
415 /// }
416 /// }
417 ///```
418 pub fn try_decode(&mut self) -> Result<DecodeResult<RecordBatch>, ParquetError> {
419 let current_state = std::mem::replace(&mut self.state, ParquetDecoderState::Finished);
420 let (new_state, decode_result) = current_state.try_next_batch()?;
421 self.state = new_state;
422 Ok(decode_result)
423 }
424
425 /// Return a [`ParquetRecordBatchReader`] that reads the next set of rows, or
426 /// return what data is needed to produce it.
427 ///
428 /// This API can be used to get a reader for decoding the next set of
429 /// RecordBatches while proceeding to begin fetching data for the set (e.g
430 /// row group)
431 ///
432 /// Example
433 /// ```no_run
434 /// # use parquet::arrow::push_decoder::ParquetPushDecoder;
435 /// use parquet::DecodeResult;
436 /// # fn get_decoder() -> ParquetPushDecoder { unimplemented!() }
437 /// # fn push_data(decoder: &mut ParquetPushDecoder, ranges: Vec<std::ops::Range<u64>>) { unimplemented!() }
438 /// let mut decoder = get_decoder();
439 /// loop {
440 /// match decoder.try_next_reader().unwrap() {
441 /// DecodeResult::NeedsData(ranges) => {
442 /// // The decoder needs more data. Fetch the data for the given ranges
443 /// // call decoder.push_ranges(ranges, data) and call again
444 /// push_data(&mut decoder, ranges);
445 /// }
446 /// DecodeResult::Data(reader) => {
447 /// // spawn a thread to read the batches in parallel
448 /// // with fetching the next row group / data
449 /// std::thread::spawn(move || {
450 /// for batch in reader {
451 /// let batch = batch.unwrap();
452 /// println!("Got batch with {} rows", batch.num_rows());
453 /// }
454 /// });
455 /// }
456 /// DecodeResult::Finished => {
457 /// // The decoder has finished decoding all data
458 /// break;
459 /// }
460 /// }
461 /// }
462 ///```
463 pub fn try_next_reader(
464 &mut self,
465 ) -> Result<DecodeResult<ParquetRecordBatchReader>, ParquetError> {
466 let current_state = std::mem::replace(&mut self.state, ParquetDecoderState::Finished);
467 let (new_state, decode_result) = current_state.try_next_reader()?;
468 self.state = new_state;
469 Ok(decode_result)
470 }
471
472 /// Push data into the decoder for processing
473 ///
474 /// This is a convenience wrapper around [`Self::push_ranges`] for pushing a
475 /// single range of data.
476 ///
477 /// Note this can be the entire file or just a part of it. If it is part of the file,
478 /// the ranges should correspond to the data ranges requested by the decoder.
479 ///
480 /// See example in [`ParquetPushDecoderBuilder`]
481 pub fn push_range(&mut self, range: Range<u64>, data: Bytes) -> Result<(), ParquetError> {
482 self.push_ranges(vec![range], vec![data])
483 }
484
485 /// Push data into the decoder for processing
486 ///
487 /// This should correspond to the data ranges requested by the decoder
488 pub fn push_ranges(
489 &mut self,
490 ranges: Vec<Range<u64>>,
491 data: Vec<Bytes>,
492 ) -> Result<(), ParquetError> {
493 let current_state = std::mem::replace(&mut self.state, ParquetDecoderState::Finished);
494 self.state = current_state.push_data(ranges, data)?;
495 Ok(())
496 }
497
498 /// Returns the total number of buffered bytes in the decoder
499 ///
500 /// This is the sum of the size of all [`Bytes`] that has been pushed to the
501 /// decoder but not yet consumed.
502 ///
503 /// Note that this does not include any overhead of the internal data
504 /// structures and that since [`Bytes`] are ref counted memory, this may not
505 /// reflect additional memory usage.
506 ///
507 /// This can be used to monitor memory usage of the decoder.
508 pub fn buffered_bytes(&self) -> u64 {
509 self.state.buffered_bytes()
510 }
511
512 /// Clear any staged byte ranges currently buffered for future decode work.
513 ///
514 /// This clears byte ranges still owned by the decoder's internal
515 /// `PushBuffers`. It does not affect any data that has already been handed
516 /// off to an active [`ParquetRecordBatchReader`].
517 pub fn clear_all_ranges(&mut self) {
518 self.state.clear_all_ranges();
519 }
520
521 /// True iff the decoder is at a row-group boundary, where
522 /// [`Self::into_builder`] can reconfigure the scan.
523 ///
524 /// A boundary is "between row groups": the previous row group's
525 /// [`ParquetRecordBatchReader`] has been fully extracted (via
526 /// [`Self::try_next_reader`]) or fully drained (via [`Self::try_decode`]),
527 /// and the next row group has not yet been planned. While
528 /// [`Self::try_decode`] is iterating an active row group's reader this
529 /// returns `false`; with [`Self::try_next_reader`] there is a clean
530 /// window between two consecutive returns where this is `true`.
531 pub fn is_at_row_group_boundary(&self) -> bool {
532 self.state.is_at_row_group_boundary()
533 }
534
535 /// Number of row groups left to decode after the one currently in flight.
536 /// Useful as a "should I bother reconfiguring the scan?" signal.
537 pub fn row_groups_remaining(&self) -> usize {
538 self.state.row_groups_remaining()
539 }
540
541 /// Returns the row-group index that the next call to
542 /// [`Self::try_next_reader`] will yield a reader for, after applying
543 /// any internal skipping (row selection emptiness, exhausted budget,
544 /// finished state).
545 ///
546 /// Safe to call at any time. When called mid-row-group (i.e. while a
547 /// previously-emitted [`ParquetRecordBatchReader`] is still being
548 /// drained), the returned index refers to the row group that
549 /// [`Self::try_next_reader`] will produce *after* the current one.
550 ///
551 /// Returns `Ok(None)` when:
552 /// - the decoder has no more row groups to read, or
553 /// - every remaining row group would be skipped.
554 ///
555 /// This method does not mutate decoder state. It is useful for
556 /// callers that maintain per-row-group state in lock-step with the
557 /// decoder (e.g. dynamic row-group pruners) to determine which row
558 /// group the next reader corresponds to, since
559 /// [`Self::try_next_reader`] may silently advance past row groups
560 /// based on filtering and other criteria.
561 pub fn peek_next_row_group(&self) -> Result<Option<usize>, ParquetError> {
562 self.state.peek_next_row_group()
563 }
564
565 /// Decompose this decoder back into a [`ParquetPushDecoderBuilder`] for the
566 /// row groups that have *not* yet been decoded.
567 ///
568 /// This is the API for *adaptive* scans. Drive the decoder with
569 /// [`Self::try_next_reader`]; at any row-group boundary, call
570 /// `into_builder` to recover a builder, adjust it with the usual
571 /// [`ParquetPushDecoderBuilder`] setters, and
572 /// [`build`](ParquetPushDecoderBuilder::build) a fresh decoder that resumes
573 /// from the next row group:
574 ///
575 /// ```no_run
576 /// # use parquet::arrow::push_decoder::ParquetPushDecoder;
577 /// # use parquet::arrow::arrow_reader::RowFilter;
578 /// # fn get_decoder() -> ParquetPushDecoder { unimplemented!() }
579 /// # fn new_filter() -> RowFilter { unimplemented!() }
580 /// let mut decoder = get_decoder();
581 /// // ... drive `decoder.try_next_reader()` for a few row groups ...
582 /// if decoder.is_at_row_group_boundary() && decoder.row_groups_remaining() > 0 {
583 /// decoder = decoder
584 /// .into_builder()
585 /// .unwrap()
586 /// // any builder option can be changed here, e.g. promote a
587 /// // filter into a row filter based on observed selectivity
588 /// .with_row_filter(new_filter())
589 /// .build()
590 /// .unwrap();
591 /// }
592 /// ```
593 ///
594 /// The returned builder pins the not-yet-decoded row groups (via
595 /// [`with_row_groups`](ArrowReaderBuilder::with_row_groups)) and carries the
596 /// not-yet-consumed row selection and offset/limit budget, so rows from
597 /// already-decoded row groups are not produced again. Every other option —
598 /// projection, row filter, row selection policy, batch size, metrics,
599 /// predicate-cache size — is left exactly as the decoder had it and can be
600 /// overridden before [`build`](ParquetPushDecoderBuilder::build).
601 ///
602 /// # Errors
603 ///
604 /// Returns `Err(ParquetError::General)` when the decoder is not at a
605 /// row-group boundary (check [`Self::is_at_row_group_boundary`] first) or
606 /// has already finished. The decoder is consumed either way.
607 ///
608 /// # Buffered bytes
609 ///
610 /// The decoder's buffered bytes are carried across the rebuild: bytes
611 /// already fetched for row groups the new configuration still reads are
612 /// not re-requested. Bytes the new configuration no longer needs stay
613 /// buffered until [`clear_all_ranges`](Self::clear_all_ranges) is called
614 /// or the rebuilt decoder is dropped.
615 pub fn into_builder(self) -> Result<ParquetPushDecoderBuilder, ParquetError> {
616 self.state.into_builder()
617 }
618}
619
620/// Internal state machine for the [`ParquetPushDecoder`]
621#[derive(Debug)]
622enum ParquetDecoderState {
623 /// Waiting for data needed to decode the next RowGroup
624 ReadingRowGroup {
625 remaining_row_groups: Box<RemainingRowGroups>,
626 },
627 /// The decoder is actively decoding a RowGroup
628 DecodingRowGroup {
629 /// Current active reader
630 record_batch_reader: Box<ParquetRecordBatchReader>,
631 remaining_row_groups: Box<RemainingRowGroups>,
632 },
633 /// The decoder has finished processing all data
634 Finished,
635}
636
637impl ParquetDecoderState {
638 /// If actively reading a RowGroup, return the currently active
639 /// ParquetRecordBatchReader and advance to the next group.
640 fn try_next_reader(
641 self,
642 ) -> Result<(Self, DecodeResult<ParquetRecordBatchReader>), ParquetError> {
643 let mut current_state = self;
644 loop {
645 let (next_state, decode_result) = current_state.transition()?;
646 // if more data is needed to transition, can't proceed further without it
647 match decode_result {
648 DecodeResult::NeedsData(ranges) => {
649 return Ok((next_state, DecodeResult::NeedsData(ranges)));
650 }
651 // act next based on state
652 DecodeResult::Data(()) | DecodeResult::Finished => {}
653 }
654 match next_state {
655 // not ready to read yet, continue transitioning
656 Self::ReadingRowGroup { .. } => current_state = next_state,
657 // have a reader ready, so return it and set ourself to ReadingRowGroup
658 Self::DecodingRowGroup {
659 record_batch_reader,
660 remaining_row_groups,
661 } => {
662 let result = DecodeResult::Data(*record_batch_reader);
663 let next_state = Self::ReadingRowGroup {
664 remaining_row_groups,
665 };
666 return Ok((next_state, result));
667 }
668 Self::Finished => {
669 return Ok((Self::Finished, DecodeResult::Finished));
670 }
671 }
672 }
673 }
674
675 /// Current state --> next state + output
676 ///
677 /// This function is called to get the next RecordBatch
678 ///
679 /// This structure is used to reduce the indentation level of the main loop
680 /// in try_build
681 fn try_next_batch(self) -> Result<(Self, DecodeResult<RecordBatch>), ParquetError> {
682 let mut current_state = self;
683 loop {
684 let (new_state, decode_result) = current_state.transition()?;
685 // if more data is needed to transition, can't proceed further without it
686 match decode_result {
687 DecodeResult::NeedsData(ranges) => {
688 return Ok((new_state, DecodeResult::NeedsData(ranges)));
689 }
690 // act next based on state
691 DecodeResult::Data(()) | DecodeResult::Finished => {}
692 }
693 match new_state {
694 // not ready to read yet, continue transitioning
695 Self::ReadingRowGroup { .. } => current_state = new_state,
696 // have a reader ready, so decode the next batch
697 Self::DecodingRowGroup {
698 mut record_batch_reader,
699 remaining_row_groups,
700 } => {
701 match record_batch_reader.next() {
702 // Successfully decoded a batch, return it
703 Some(Ok(batch)) => {
704 let result = DecodeResult::Data(batch);
705 let next_state = Self::DecodingRowGroup {
706 record_batch_reader,
707 remaining_row_groups,
708 };
709 return Ok((next_state, result));
710 }
711 // No more batches in this row group, move to the next row group
712 None => {
713 current_state = Self::ReadingRowGroup {
714 remaining_row_groups,
715 }
716 }
717 // some error occurred while decoding, so return that
718 Some(Err(e)) => {
719 // TODO: preserve ArrowError in ParquetError (rather than convert to a string)
720 return Err(ParquetError::ArrowError(e.to_string()));
721 }
722 }
723 }
724 Self::Finished => {
725 return Ok((Self::Finished, DecodeResult::Finished));
726 }
727 }
728 }
729 }
730
731 /// Transition to the next state with a reader (data can be produced), if not end of stream
732 ///
733 /// This function is called in a loop until the decoder is ready to return
734 /// data (has the required pages buffered) or is finished.
735 fn transition(self) -> Result<(Self, DecodeResult<()>), ParquetError> {
736 // result returned when there is data ready
737 let data_ready = DecodeResult::Data(());
738 match self {
739 Self::ReadingRowGroup {
740 mut remaining_row_groups,
741 } => {
742 match remaining_row_groups.try_next_reader()? {
743 // If we have a next reader, we can transition to decoding it
744 DecodeResult::Data(record_batch_reader) => {
745 // Transition to decoding the row group
746 Ok((
747 Self::DecodingRowGroup {
748 record_batch_reader: Box::new(record_batch_reader),
749 remaining_row_groups,
750 },
751 data_ready,
752 ))
753 }
754 DecodeResult::NeedsData(ranges) => {
755 // If we need more data, we return the ranges needed and stay in Reading
756 // RowGroup state
757 Ok((
758 Self::ReadingRowGroup {
759 remaining_row_groups,
760 },
761 DecodeResult::NeedsData(ranges),
762 ))
763 }
764 // If there are no more readers, we are finished
765 DecodeResult::Finished => {
766 // No more row groups to read, we are finished
767 Ok((Self::Finished, DecodeResult::Finished))
768 }
769 }
770 }
771 // if we are already in DecodingRowGroup, just return data ready
772 Self::DecodingRowGroup { .. } => Ok((self, data_ready)),
773 // if finished, just return finished
774 Self::Finished => Ok((self, DecodeResult::Finished)),
775 }
776 }
777
778 /// Push data, and transition state if needed
779 ///
780 /// This should correspond to the data ranges requested by the decoder
781 pub fn push_data(
782 self,
783 ranges: Vec<Range<u64>>,
784 data: Vec<Bytes>,
785 ) -> Result<Self, ParquetError> {
786 match self {
787 ParquetDecoderState::ReadingRowGroup {
788 mut remaining_row_groups,
789 } => {
790 // Push data to the RowGroupReaderBuilder
791 remaining_row_groups.push_data(ranges, data);
792 Ok(ParquetDecoderState::ReadingRowGroup {
793 remaining_row_groups,
794 })
795 }
796 // it is ok to get data before we asked for it
797 ParquetDecoderState::DecodingRowGroup {
798 record_batch_reader,
799 mut remaining_row_groups,
800 } => {
801 remaining_row_groups.push_data(ranges, data);
802 Ok(ParquetDecoderState::DecodingRowGroup {
803 record_batch_reader,
804 remaining_row_groups,
805 })
806 }
807 ParquetDecoderState::Finished => Err(ParquetError::General(
808 "Cannot push data to a finished decoder".to_string(),
809 )),
810 }
811 }
812
813 /// How many bytes are currently buffered in the decoder?
814 fn buffered_bytes(&self) -> u64 {
815 match self {
816 ParquetDecoderState::ReadingRowGroup {
817 remaining_row_groups,
818 } => remaining_row_groups.buffered_bytes(),
819 ParquetDecoderState::DecodingRowGroup {
820 record_batch_reader: _,
821 remaining_row_groups,
822 } => remaining_row_groups.buffered_bytes(),
823 ParquetDecoderState::Finished => 0,
824 }
825 }
826
827 /// Clear any staged ranges currently buffered in the decoder.
828 fn clear_all_ranges(&mut self) {
829 match self {
830 ParquetDecoderState::ReadingRowGroup {
831 remaining_row_groups,
832 } => remaining_row_groups.clear_all_ranges(),
833 ParquetDecoderState::DecodingRowGroup {
834 record_batch_reader: _,
835 remaining_row_groups,
836 } => remaining_row_groups.clear_all_ranges(),
837 ParquetDecoderState::Finished => {}
838 }
839 }
840
841 fn is_at_row_group_boundary(&self) -> bool {
842 match self {
843 ParquetDecoderState::ReadingRowGroup {
844 remaining_row_groups,
845 } => remaining_row_groups.is_at_row_group_boundary(),
846 // Mid-row-group: the active reader holds an `ArrayReader` and
847 // `ReadPlan` keyed to the *current* projection/filter; rebuilding
848 // would require throwing that work away.
849 ParquetDecoderState::DecodingRowGroup { .. } => false,
850 ParquetDecoderState::Finished => false,
851 }
852 }
853
854 fn row_groups_remaining(&self) -> usize {
855 match self {
856 ParquetDecoderState::ReadingRowGroup {
857 remaining_row_groups,
858 } => remaining_row_groups.row_groups_remaining(),
859 ParquetDecoderState::DecodingRowGroup {
860 remaining_row_groups,
861 ..
862 } => remaining_row_groups.row_groups_remaining(),
863 ParquetDecoderState::Finished => 0,
864 }
865 }
866
867 /// See [`ParquetPushDecoder::peek_next_row_group`] for the public
868 /// API contract. This inner method delegates to the underlying
869 /// [`RemainingRowGroups`] for both `ReadingRowGroup` and
870 /// `DecodingRowGroup`: mid-row-group the answer is the row group
871 /// `try_next_reader` will produce *after* the active one finishes,
872 /// which is exactly what `RemainingRowGroups::peek_next_row_group`
873 /// computes from the queued frontier.
874 fn peek_next_row_group(&self) -> Result<Option<usize>, ParquetError> {
875 match self {
876 ParquetDecoderState::ReadingRowGroup {
877 remaining_row_groups,
878 } => remaining_row_groups.peek_next_row_group(),
879 ParquetDecoderState::DecodingRowGroup {
880 remaining_row_groups,
881 ..
882 } => remaining_row_groups.peek_next_row_group(),
883 ParquetDecoderState::Finished => Ok(None),
884 }
885 }
886
887 fn into_builder(self) -> Result<ParquetPushDecoderBuilder, ParquetError> {
888 let remaining_row_groups = match self {
889 ParquetDecoderState::ReadingRowGroup {
890 remaining_row_groups,
891 } => remaining_row_groups,
892 ParquetDecoderState::DecodingRowGroup { .. } => {
893 return Err(ParquetError::General(
894 "into_builder called while a row group is being decoded; \
895 check is_at_row_group_boundary() first"
896 .to_string(),
897 ));
898 }
899 ParquetDecoderState::Finished => {
900 return Err(ParquetError::General(
901 "into_builder called on a finished decoder".to_string(),
902 ));
903 }
904 };
905 if !remaining_row_groups.is_at_row_group_boundary() {
906 return Err(ParquetError::General(
907 "into_builder called mid-row-group; check is_at_row_group_boundary() first"
908 .to_string(),
909 ));
910 }
911 Ok(builder_from_remaining(remaining_row_groups.into_parts()))
912 }
913}
914
915#[cfg(test)]
916mod test {
917 use super::*;
918 use crate::DecodeResult;
919 use crate::arrow::arrow_reader::{ArrowPredicateFn, RowFilter, RowSelection, RowSelector};
920 use crate::arrow::push_decoder::{ParquetPushDecoder, ParquetPushDecoderBuilder};
921 use crate::arrow::{ArrowWriter, ProjectionMask};
922 use crate::errors::ParquetError;
923 use crate::file::metadata::ParquetMetaDataPushDecoder;
924 use crate::file::properties::WriterProperties;
925 use arrow::compute::kernels::cmp::{gt, lt};
926 use arrow_array::cast::AsArray;
927 use arrow_array::types::Int64Type;
928 use arrow_array::{ArrayRef, Int64Array, RecordBatch, StringViewArray};
929 use arrow_select::concat::concat_batches;
930 use bytes::Bytes;
931 use std::fmt::Debug;
932 use std::ops::Range;
933 use std::sync::{Arc, LazyLock};
934
935 /// Test decoder struct size (as they are copied around on each transition, they
936 /// should not grow too large)
937 #[test]
938 fn test_decoder_size() {
939 assert_eq!(std::mem::size_of::<ParquetDecoderState>(), 24);
940 }
941
942 /// Decode the entire file at once, simulating a scenario where all data is
943 /// available in memory
944 #[test]
945 fn test_decoder_all_data() {
946 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
947 .unwrap()
948 .build()
949 .unwrap();
950
951 decoder
952 .push_range(test_file_range(), TEST_FILE_DATA.clone())
953 .unwrap();
954
955 let results = vec![
956 // first row group should be decoded without needing more data
957 expect_data(decoder.try_decode()),
958 // second row group should be decoded without needing more data
959 expect_data(decoder.try_decode()),
960 ];
961 expect_finished(decoder.try_decode());
962
963 let all_output = concat_batches(&TEST_BATCH.schema(), &results).unwrap();
964 // Check that the output matches the input batch
965 assert_eq!(all_output, *TEST_BATCH);
966 }
967
968 /// Decode the entire file incrementally, simulating a scenario where data is
969 /// fetched as needed
970 #[test]
971 fn test_decoder_incremental() {
972 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
973 .unwrap()
974 .build()
975 .unwrap();
976
977 let mut results = vec![];
978
979 // First row group, expect a single request
980 let ranges = expect_needs_data(decoder.try_decode());
981 let num_bytes_requested: u64 = ranges.iter().map(|r| r.end - r.start).sum();
982 push_ranges_to_decoder(&mut decoder, ranges);
983 // The decoder should currently only store the data it needs to decode the first row group
984 assert_eq!(decoder.buffered_bytes(), num_bytes_requested);
985 results.push(expect_data(decoder.try_decode()));
986 // the decoder should have consumed the data for the first row group and freed it
987 assert_eq!(decoder.buffered_bytes(), 0);
988
989 // Second row group,
990 let ranges = expect_needs_data(decoder.try_decode());
991 let num_bytes_requested: u64 = ranges.iter().map(|r| r.end - r.start).sum();
992 push_ranges_to_decoder(&mut decoder, ranges);
993 // The decoder should currently only store the data it needs to decode the second row group
994 assert_eq!(decoder.buffered_bytes(), num_bytes_requested);
995 results.push(expect_data(decoder.try_decode()));
996 // the decoder should have consumed the data for the second row group and freed it
997 assert_eq!(decoder.buffered_bytes(), 0);
998 expect_finished(decoder.try_decode());
999
1000 // Check that the output matches the input batch
1001 let all_output = concat_batches(&TEST_BATCH.schema(), &results).unwrap();
1002 assert_eq!(all_output, *TEST_BATCH);
1003 }
1004
1005 /// Releasing staged ranges should free speculative buffers without affecting
1006 /// the active row group reader.
1007 #[test]
1008 fn test_decoder_clear_all_ranges() {
1009 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1010 .unwrap()
1011 .with_batch_size(100)
1012 .build()
1013 .unwrap();
1014
1015 decoder
1016 .push_range(test_file_range(), TEST_FILE_DATA.clone())
1017 .unwrap();
1018 assert_eq!(decoder.buffered_bytes(), test_file_len());
1019
1020 // The current row group reader is built from the prefetched bytes, but
1021 // the speculative full-file range remains staged in the decoder.
1022 let batch1 = expect_data(decoder.try_decode());
1023 assert_eq!(batch1, TEST_BATCH.slice(0, 100));
1024 assert_eq!(decoder.buffered_bytes(), test_file_len());
1025
1026 // All of the buffer is released
1027 decoder.clear_all_ranges();
1028 assert_eq!(decoder.buffered_bytes(), 0);
1029
1030 // The active reader still owns the current row group's bytes, so it can
1031 // continue decoding without consulting PushBuffers.
1032 let batch2 = expect_data(decoder.try_decode());
1033 assert_eq!(batch2, TEST_BATCH.slice(100, 100));
1034 assert_eq!(decoder.buffered_bytes(), 0);
1035
1036 // Moving to the next row group now requires the decoder to ask for data
1037 // again because the staged speculative ranges were released.
1038 let ranges = expect_needs_data(decoder.try_decode());
1039 let num_bytes_requested: u64 = ranges.iter().map(|r| r.end - r.start).sum();
1040 push_ranges_to_decoder(&mut decoder, ranges);
1041 assert_eq!(decoder.buffered_bytes(), num_bytes_requested);
1042
1043 let batch3 = expect_data(decoder.try_decode());
1044 assert_eq!(batch3, TEST_BATCH.slice(200, 100));
1045 assert_eq!(decoder.buffered_bytes(), 0);
1046
1047 let batch4 = expect_data(decoder.try_decode());
1048 assert_eq!(batch4, TEST_BATCH.slice(300, 100));
1049 assert_eq!(decoder.buffered_bytes(), 0);
1050
1051 expect_finished(decoder.try_decode());
1052 }
1053
1054 /// Decode the entire file incrementally, simulating partial reads
1055 #[test]
1056 fn test_decoder_partial() {
1057 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1058 .unwrap()
1059 .build()
1060 .unwrap();
1061
1062 // First row group, expect a single request for all data needed to read "a" and "b"
1063 let ranges = expect_needs_data(decoder.try_decode());
1064 push_ranges_to_decoder(&mut decoder, ranges);
1065
1066 let batch1 = expect_data(decoder.try_decode());
1067 let expected1 = TEST_BATCH.slice(0, 200);
1068 assert_eq!(batch1, expected1);
1069
1070 // Second row group, this time provide the data in two steps
1071 let ranges = expect_needs_data(decoder.try_decode());
1072 let (ranges1, ranges2) = ranges.split_at(ranges.len() / 2);
1073 assert!(!ranges1.is_empty());
1074 assert!(!ranges2.is_empty());
1075 // push first half to simulate partial read
1076 push_ranges_to_decoder(&mut decoder, ranges1.to_vec());
1077
1078 // still expect more data
1079 let ranges = expect_needs_data(decoder.try_decode());
1080 assert_eq!(ranges, ranges2); // should be the remaining ranges
1081 // push empty ranges should be a no-op
1082 push_ranges_to_decoder(&mut decoder, vec![]);
1083 let ranges = expect_needs_data(decoder.try_decode());
1084 assert_eq!(ranges, ranges2); // should be the remaining ranges
1085 push_ranges_to_decoder(&mut decoder, ranges);
1086
1087 let batch2 = expect_data(decoder.try_decode());
1088 let expected2 = TEST_BATCH.slice(200, 200);
1089 assert_eq!(batch2, expected2);
1090
1091 expect_finished(decoder.try_decode());
1092 }
1093
1094 /// Decode multiple columns "a" and "b", expect that the decoder requests
1095 /// only a single request per row group
1096 #[test]
1097 fn test_decoder_selection_does_one_request() {
1098 let builder =
1099 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1100
1101 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1102
1103 let mut decoder = builder
1104 .with_projection(
1105 ProjectionMask::columns(&schema_descr, ["a", "b"]), // read "a", "b"
1106 )
1107 .build()
1108 .unwrap();
1109
1110 // First row group, expect a single request for all data needed to read "a" and "b"
1111 let ranges = expect_needs_data(decoder.try_decode());
1112 push_ranges_to_decoder(&mut decoder, ranges);
1113
1114 let batch1 = expect_data(decoder.try_decode());
1115 let expected1 = TEST_BATCH.slice(0, 200).project(&[0, 1]).unwrap();
1116 assert_eq!(batch1, expected1);
1117
1118 // Second row group, similarly expect a single request for all data needed to read "a" and "b"
1119 let ranges = expect_needs_data(decoder.try_decode());
1120 push_ranges_to_decoder(&mut decoder, ranges);
1121
1122 let batch2 = expect_data(decoder.try_decode());
1123 let expected2 = TEST_BATCH.slice(200, 200).project(&[0, 1]).unwrap();
1124 assert_eq!(batch2, expected2);
1125
1126 expect_finished(decoder.try_decode());
1127 }
1128
1129 /// Decode with a filter that requires multiple requests, but only provide part
1130 /// of the data needed for the filter at a time simulating partial reads.
1131 #[test]
1132 fn test_decoder_single_filter_partial() {
1133 let builder =
1134 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1135
1136 // Values in column "a" range 0..399
1137 // First filter: "a" > 250 (nothing in Row Group 0, both data pages in Row Group 1)
1138 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1139
1140 // a > 250
1141 let row_filter_a = ArrowPredicateFn::new(
1142 // claim to use both a and b so we get two ranges requests for the filter pages
1143 ProjectionMask::columns(&schema_descr, ["a", "b"]),
1144 |batch: RecordBatch| {
1145 let scalar_250 = Int64Array::new_scalar(250);
1146 let column = batch.column(0).as_primitive::<Int64Type>();
1147 gt(column, &scalar_250)
1148 },
1149 );
1150
1151 let mut decoder = builder
1152 .with_projection(
1153 // read only column "a" to test that filter pages are reused
1154 ProjectionMask::columns(&schema_descr, ["a"]), // read "a"
1155 )
1156 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1157 .build()
1158 .unwrap();
1159
1160 // First row group, evaluating filters
1161 let ranges = expect_needs_data(decoder.try_decode());
1162 // only provide half the ranges
1163 let (ranges1, ranges2) = ranges.split_at(ranges.len() / 2);
1164 assert!(!ranges1.is_empty());
1165 assert!(!ranges2.is_empty());
1166 push_ranges_to_decoder(&mut decoder, ranges1.to_vec());
1167 // still expect more data
1168 let ranges = expect_needs_data(decoder.try_decode());
1169 assert_eq!(ranges, ranges2); // should be the remaining ranges
1170 let ranges = expect_needs_data(decoder.try_decode());
1171 assert_eq!(ranges, ranges2); // should be the remaining ranges
1172 push_ranges_to_decoder(&mut decoder, ranges2.to_vec());
1173
1174 // Since no rows in the first row group pass the filters, there is no
1175 // additional requests to read data pages for "b" here
1176
1177 // Second row group
1178 let ranges = expect_needs_data(decoder.try_decode());
1179 push_ranges_to_decoder(&mut decoder, ranges);
1180
1181 let batch = expect_data(decoder.try_decode());
1182 let expected = TEST_BATCH.slice(251, 149).project(&[0]).unwrap();
1183 assert_eq!(batch, expected);
1184
1185 expect_finished(decoder.try_decode());
1186 }
1187
1188 /// Decode with a filter where we also skip one of the RowGroups via a RowSelection
1189 #[test]
1190 fn test_decoder_single_filter_and_row_selection() {
1191 let builder =
1192 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1193
1194 // Values in column "a" range 0..399
1195 // First filter: "a" > 250 (nothing in Row Group 0, last data page in Row Group 1)
1196 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1197
1198 // a > 250
1199 let row_filter_a = ArrowPredicateFn::new(
1200 ProjectionMask::columns(&schema_descr, ["a"]),
1201 |batch: RecordBatch| {
1202 let scalar_250 = Int64Array::new_scalar(250);
1203 let column = batch.column(0).as_primitive::<Int64Type>();
1204 gt(column, &scalar_250)
1205 },
1206 );
1207
1208 let mut decoder = builder
1209 .with_projection(
1210 // read only column "a" to test that filter pages are reused
1211 ProjectionMask::columns(&schema_descr, ["b"]), // read "b"
1212 )
1213 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1214 .with_row_selection(RowSelection::from(vec![
1215 RowSelector::skip(200), // skip first row group
1216 RowSelector::select(100), // first 100 rows of second row group
1217 RowSelector::skip(100),
1218 ]))
1219 .build()
1220 .unwrap();
1221
1222 // expect the first row group to be filtered out (no filter is evaluated due to row selection)
1223
1224 // First row group, first filter (a > 250)
1225 let ranges = expect_needs_data(decoder.try_decode());
1226 push_ranges_to_decoder(&mut decoder, ranges);
1227
1228 // Second row group
1229 let ranges = expect_needs_data(decoder.try_decode());
1230 push_ranges_to_decoder(&mut decoder, ranges);
1231
1232 let batch = expect_data(decoder.try_decode());
1233 let expected = TEST_BATCH.slice(251, 49).project(&[1]).unwrap();
1234 assert_eq!(batch, expected);
1235
1236 expect_finished(decoder.try_decode());
1237 }
1238
1239 /// Decode with multiple filters that require multiple requests
1240 #[test]
1241 fn test_decoder_multi_filters() {
1242 // Create a decoder for decoding parquet data (note it does not have any IO / readers)
1243 let builder =
1244 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1245
1246 // Values in column "a" range 0..399
1247 // Values in column "b" range 400..799
1248 // First filter: "a" > 175 (last data page in Row Group 0)
1249 // Second filter: "b" < 625 (last data page in Row Group 0 and first DataPage in RowGroup 1)
1250 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1251
1252 // a > 175
1253 let row_filter_a = ArrowPredicateFn::new(
1254 ProjectionMask::columns(&schema_descr, ["a"]),
1255 |batch: RecordBatch| {
1256 let scalar_175 = Int64Array::new_scalar(175);
1257 let column = batch.column(0).as_primitive::<Int64Type>();
1258 gt(column, &scalar_175)
1259 },
1260 );
1261
1262 // b < 625
1263 let row_filter_b = ArrowPredicateFn::new(
1264 ProjectionMask::columns(&schema_descr, ["b"]),
1265 |batch: RecordBatch| {
1266 let scalar_625 = Int64Array::new_scalar(625);
1267 let column = batch.column(0).as_primitive::<Int64Type>();
1268 lt(column, &scalar_625)
1269 },
1270 );
1271
1272 let mut decoder = builder
1273 .with_projection(
1274 ProjectionMask::columns(&schema_descr, ["c"]), // read "c"
1275 )
1276 .with_row_filter(RowFilter::new(vec![
1277 Box::new(row_filter_a),
1278 Box::new(row_filter_b),
1279 ]))
1280 .build()
1281 .unwrap();
1282
1283 // First row group, first filter (a > 175)
1284 let ranges = expect_needs_data(decoder.try_decode());
1285 push_ranges_to_decoder(&mut decoder, ranges);
1286
1287 // first row group, second filter (b < 625)
1288 let ranges = expect_needs_data(decoder.try_decode());
1289 push_ranges_to_decoder(&mut decoder, ranges);
1290
1291 // first row group, data pages for "c"
1292 let ranges = expect_needs_data(decoder.try_decode());
1293 push_ranges_to_decoder(&mut decoder, ranges);
1294
1295 // expect the first batch to be decoded: rows 176..199, column "c"
1296 let batch1 = expect_data(decoder.try_decode());
1297 let expected1 = TEST_BATCH.slice(176, 24).project(&[2]).unwrap();
1298 assert_eq!(batch1, expected1);
1299
1300 // Second row group, first filter (a > 175)
1301 let ranges = expect_needs_data(decoder.try_decode());
1302 push_ranges_to_decoder(&mut decoder, ranges);
1303
1304 // Second row group, second filter (b < 625)
1305 let ranges = expect_needs_data(decoder.try_decode());
1306 push_ranges_to_decoder(&mut decoder, ranges);
1307
1308 // Second row group, data pages for "c"
1309 let ranges = expect_needs_data(decoder.try_decode());
1310 push_ranges_to_decoder(&mut decoder, ranges);
1311
1312 // expect the second batch to be decoded: rows 200..224, column "c"
1313 let batch2 = expect_data(decoder.try_decode());
1314 let expected2 = TEST_BATCH.slice(200, 25).project(&[2]).unwrap();
1315 assert_eq!(batch2, expected2);
1316
1317 expect_finished(decoder.try_decode());
1318 }
1319
1320 /// Decode with a filter that uses a column that is also projected, and expect
1321 /// that the filter pages are reused (don't refetch them)
1322 #[test]
1323 fn test_decoder_reuses_filter_pages() {
1324 // Create a decoder for decoding parquet data (note it does not have any IO / readers)
1325 let builder =
1326 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1327
1328 // Values in column "a" range 0..399
1329 // First filter: "a" > 250 (nothing in Row Group 0, last data page in Row Group 1)
1330 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1331
1332 // a > 250
1333 let row_filter_a = ArrowPredicateFn::new(
1334 ProjectionMask::columns(&schema_descr, ["a"]),
1335 |batch: RecordBatch| {
1336 let scalar_250 = Int64Array::new_scalar(250);
1337 let column = batch.column(0).as_primitive::<Int64Type>();
1338 gt(column, &scalar_250)
1339 },
1340 );
1341
1342 let mut decoder = builder
1343 .with_projection(
1344 // read only column "a" to test that filter pages are reused
1345 ProjectionMask::columns(&schema_descr, ["a"]), // read "a"
1346 )
1347 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1348 .build()
1349 .unwrap();
1350
1351 // First row group, first filter (a > 175)
1352 let ranges = expect_needs_data(decoder.try_decode());
1353 push_ranges_to_decoder(&mut decoder, ranges);
1354
1355 // expect the first row group to be filtered out (no rows match)
1356
1357 // Second row group, first filter (a > 250)
1358 let ranges = expect_needs_data(decoder.try_decode());
1359 push_ranges_to_decoder(&mut decoder, ranges);
1360
1361 // expect that the second row group is decoded: rows 251..399, column "a"
1362 // Note that the filter pages for "a" should be reused and no additional data
1363 // should be requested
1364 let batch = expect_data(decoder.try_decode());
1365 let expected = TEST_BATCH.slice(251, 149).project(&[0]).unwrap();
1366 assert_eq!(batch, expected);
1367
1368 expect_finished(decoder.try_decode());
1369 }
1370
1371 #[test]
1372 fn test_decoder_empty_filters() {
1373 let builder =
1374 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1375 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1376
1377 // only read column "c", but with empty filters
1378 let mut decoder = builder
1379 .with_projection(
1380 ProjectionMask::columns(&schema_descr, ["c"]), // read "c"
1381 )
1382 .with_row_filter(RowFilter::new(vec![
1383 // empty filters should be ignored
1384 ]))
1385 .build()
1386 .unwrap();
1387
1388 // First row group
1389 let ranges = expect_needs_data(decoder.try_decode());
1390 push_ranges_to_decoder(&mut decoder, ranges);
1391
1392 // expect the first batch to be decoded: rows 0..199, column "c"
1393 let batch1 = expect_data(decoder.try_decode());
1394 let expected1 = TEST_BATCH.slice(0, 200).project(&[2]).unwrap();
1395 assert_eq!(batch1, expected1);
1396
1397 // Second row group,
1398 let ranges = expect_needs_data(decoder.try_decode());
1399 push_ranges_to_decoder(&mut decoder, ranges);
1400
1401 // expect the second batch to be decoded: rows 200..399, column "c"
1402 let batch2 = expect_data(decoder.try_decode());
1403 let expected2 = TEST_BATCH.slice(200, 200).project(&[2]).unwrap();
1404
1405 assert_eq!(batch2, expected2);
1406
1407 expect_finished(decoder.try_decode());
1408 }
1409
1410 /// When filter pushdown is combined with a `LIMIT`, the predicate must
1411 /// not be evaluated for rows beyond the `limit`-th match.
1412 ///
1413 /// Filter `a > 175` produces 24 matches in row group 0 (rows 176..199).
1414 /// With `limit = 10`, only the first 10 matches (rows 176..185) should be
1415 /// emitted, AND the predicate counter should observe that evaluation was
1416 /// short-circuited.
1417 #[test]
1418 fn test_decoder_filter_with_limit_short_circuits_within_row_group() {
1419 use std::sync::atomic::{AtomicUsize, Ordering};
1420
1421 let builder =
1422 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1423 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1424
1425 let rows_filtered = Arc::new(AtomicUsize::new(0));
1426 let rows_filtered_for_predicate = Arc::clone(&rows_filtered);
1427
1428 let row_filter_a = ArrowPredicateFn::new(
1429 ProjectionMask::columns(&schema_descr, ["a"]),
1430 move |batch: RecordBatch| {
1431 rows_filtered_for_predicate.fetch_add(batch.num_rows(), Ordering::Relaxed);
1432 let scalar_175 = Int64Array::new_scalar(175);
1433 let column = batch.column(0).as_primitive::<Int64Type>();
1434 gt(column, &scalar_175)
1435 },
1436 );
1437
1438 // Use a small batch size so the row group is evaluated across
1439 // multiple predicate batches; that is the regime where Layer 2's
1440 // short-circuit saves predicate evaluation work. Matching rows are
1441 // 176..199 (24 rows); with batch_size = 10 those span batches 17, 18,
1442 // and 19 (rows 170..199). A limit of 10 should stop filter evaluation
1443 // in the middle of batch 18.
1444 let mut decoder = builder
1445 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
1446 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1447 .with_batch_size(10)
1448 .with_limit(10)
1449 .build()
1450 .unwrap();
1451
1452 // First row group: filter columns fetch (predicate is evaluated here)
1453 let ranges = expect_needs_data(decoder.try_decode());
1454 push_ranges_to_decoder(&mut decoder, ranges);
1455
1456 // The first 10 matching rows come out: 176..185, column "a"
1457 let batch = expect_data(decoder.try_decode());
1458 let expected = TEST_BATCH.slice(176, 10).project(&[0]).unwrap();
1459 assert_eq!(batch, expected);
1460
1461 // no data for row group 1 should be requested — the limit
1462 // was satisfied by row group 0 and the `Start` state for row group 1
1463 // short-circuits to `Finished`.
1464 expect_finished(decoder.try_decode());
1465
1466 // Row 186 is the 11th match; the scan should stop no later than the
1467 // batch containing it (batch 18 of 10 rows = rows 180..189), so at
1468 // most 190 rows are evaluated.
1469 let evaluated = rows_filtered.load(Ordering::Relaxed);
1470 assert!(
1471 evaluated <= 190,
1472 "predicate evaluated {evaluated} rows; expected ≤ 190 (stop within batch containing 11th match)"
1473 );
1474 }
1475
1476 /// Once the limit has been satisfied by a prior row group, subsequent
1477 /// row groups should be skipped entirely — no data request for their
1478 /// filter columns.
1479 #[test]
1480 fn test_decoder_filter_with_limit_skips_later_row_groups() {
1481 let builder =
1482 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1483 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1484
1485 // `a > 175` matches rows 176..199 in row group 0 (24 matches) and
1486 // 200..399 in row group 1 (200 matches). With limit = 5, all matches
1487 // should come from row group 0.
1488 let row_filter_a = ArrowPredicateFn::new(
1489 ProjectionMask::columns(&schema_descr, ["a"]),
1490 |batch: RecordBatch| {
1491 let scalar_175 = Int64Array::new_scalar(175);
1492 let column = batch.column(0).as_primitive::<Int64Type>();
1493 gt(column, &scalar_175)
1494 },
1495 );
1496
1497 let mut decoder = builder
1498 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
1499 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1500 .with_limit(5)
1501 .build()
1502 .unwrap();
1503
1504 // Row group 0: fetch filter pages
1505 let ranges = expect_needs_data(decoder.try_decode());
1506 push_ranges_to_decoder(&mut decoder, ranges);
1507
1508 // First 5 matches: 176..180
1509 let batch = expect_data(decoder.try_decode());
1510 let expected = TEST_BATCH.slice(176, 5).project(&[0]).unwrap();
1511 assert_eq!(batch, expected);
1512
1513 // Row group 1 must NOT request data — the limit is already satisfied
1514 // so `Start` in row group 1 short-circuits to `Finished`.
1515 expect_finished(decoder.try_decode());
1516 }
1517
1518 /// The predicate short-circuit must account for `self.offset` as well as
1519 /// `self.limit`. The post-predicate `with_offset` step skips that many
1520 /// already-selected rows before `with_limit` counts output rows — so the
1521 /// predicate must retain at least `offset + limit` matches. Without the
1522 /// fix, Layer 2 caps at just `limit` and the later `with_offset` consumes
1523 /// all of them, producing 0 rows instead of `limit`.
1524 ///
1525 /// `a > 175` matches rows 176..199 in row group 0 (24 matches). With
1526 /// `offset = 10, limit = 5`, the expected output is rows 186..190 (the
1527 /// 11th through 15th matches).
1528 #[test]
1529 fn test_decoder_filter_with_offset_and_limit() {
1530 let builder =
1531 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1532 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1533
1534 let row_filter_a = ArrowPredicateFn::new(
1535 ProjectionMask::columns(&schema_descr, ["a"]),
1536 |batch: RecordBatch| {
1537 let scalar_175 = Int64Array::new_scalar(175);
1538 let column = batch.column(0).as_primitive::<Int64Type>();
1539 gt(column, &scalar_175)
1540 },
1541 );
1542
1543 let mut decoder = builder
1544 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
1545 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1546 .with_offset(10)
1547 .with_limit(5)
1548 .build()
1549 .unwrap();
1550
1551 let ranges = expect_needs_data(decoder.try_decode());
1552 push_ranges_to_decoder(&mut decoder, ranges);
1553
1554 let batch = expect_data(decoder.try_decode());
1555 let expected = TEST_BATCH.slice(186, 5).project(&[0]).unwrap();
1556 assert_eq!(batch, expected);
1557
1558 expect_finished(decoder.try_decode());
1559 }
1560
1561 /// The limit short-circuit must also be correct when the limited predicate
1562 /// is the last predicate in a multi-predicate chain.
1563 ///
1564 /// `a > 175` first narrows row group 0 to rows 176..199. The final
1565 /// predicate `b < 625` is then evaluated only over those 24 rows, all of
1566 /// which match. With `limit = 10`, the final output should still be rows
1567 /// 176..185, and the second predicate should stop before consuming all 24
1568 /// selected rows.
1569 #[test]
1570 fn test_decoder_multi_filters_with_limit() {
1571 use std::sync::atomic::{AtomicUsize, Ordering};
1572
1573 let builder =
1574 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1575 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1576
1577 let first_predicate_rows = Arc::new(AtomicUsize::new(0));
1578 let second_predicate_rows = Arc::new(AtomicUsize::new(0));
1579
1580 let first_predicate_rows_for_filter = Arc::clone(&first_predicate_rows);
1581 let row_filter_a = ArrowPredicateFn::new(
1582 ProjectionMask::columns(&schema_descr, ["a"]),
1583 move |batch: RecordBatch| {
1584 first_predicate_rows_for_filter.fetch_add(batch.num_rows(), Ordering::Relaxed);
1585 let scalar_175 = Int64Array::new_scalar(175);
1586 let column = batch.column(0).as_primitive::<Int64Type>();
1587 gt(column, &scalar_175)
1588 },
1589 );
1590
1591 let second_predicate_rows_for_filter = Arc::clone(&second_predicate_rows);
1592 let row_filter_b = ArrowPredicateFn::new(
1593 ProjectionMask::columns(&schema_descr, ["b"]),
1594 move |batch: RecordBatch| {
1595 second_predicate_rows_for_filter.fetch_add(batch.num_rows(), Ordering::Relaxed);
1596 let scalar_625 = Int64Array::new_scalar(625);
1597 let column = batch.column(0).as_primitive::<Int64Type>();
1598 lt(column, &scalar_625)
1599 },
1600 );
1601
1602 let mut decoder = builder
1603 .with_projection(ProjectionMask::columns(&schema_descr, ["c"]))
1604 .with_row_filter(RowFilter::new(vec![
1605 Box::new(row_filter_a),
1606 Box::new(row_filter_b),
1607 ]))
1608 .with_batch_size(10)
1609 .with_limit(10)
1610 .build()
1611 .unwrap();
1612
1613 // Row group 0, first predicate
1614 let ranges = expect_needs_data(decoder.try_decode());
1615 push_ranges_to_decoder(&mut decoder, ranges);
1616
1617 // Row group 0, second predicate
1618 let ranges = expect_needs_data(decoder.try_decode());
1619 push_ranges_to_decoder(&mut decoder, ranges);
1620
1621 // Final projected data
1622 let ranges = expect_needs_data(decoder.try_decode());
1623 push_ranges_to_decoder(&mut decoder, ranges);
1624
1625 let batch = expect_data(decoder.try_decode());
1626 let expected = TEST_BATCH.slice(176, 10).project(&[2]).unwrap();
1627 assert_eq!(batch, expected);
1628
1629 // The overall limit was satisfied by row group 0.
1630 expect_finished(decoder.try_decode());
1631
1632 assert_eq!(first_predicate_rows.load(Ordering::Relaxed), 200);
1633 assert!(
1634 second_predicate_rows.load(Ordering::Relaxed) < 24,
1635 "final predicate should short-circuit before consuming all 24 rows selected by the first predicate"
1636 );
1637 }
1638
1639 /// When a row selection already exists, limiting the predicate must still
1640 /// preserve alignment with that prior selection.
1641 ///
1642 /// The explicit selection narrows row group 0 to rows 150..199. Applying
1643 /// `a > 175` over that selection yields rows 176..199. With `limit = 10`,
1644 /// the decoder should emit rows 176..185 and stop without evaluating the
1645 /// remaining selected rows.
1646 #[test]
1647 fn test_decoder_filter_with_row_selection_and_limit() {
1648 use std::sync::atomic::{AtomicUsize, Ordering};
1649
1650 let builder =
1651 ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata()).unwrap();
1652 let schema_descr = builder.metadata().file_metadata().schema_descr_ptr();
1653
1654 let rows_filtered = Arc::new(AtomicUsize::new(0));
1655 let rows_filtered_for_predicate = Arc::clone(&rows_filtered);
1656
1657 let row_filter_a = ArrowPredicateFn::new(
1658 ProjectionMask::columns(&schema_descr, ["a"]),
1659 move |batch: RecordBatch| {
1660 rows_filtered_for_predicate.fetch_add(batch.num_rows(), Ordering::Relaxed);
1661 let scalar_175 = Int64Array::new_scalar(175);
1662 let column = batch.column(0).as_primitive::<Int64Type>();
1663 gt(column, &scalar_175)
1664 },
1665 );
1666
1667 let mut decoder = builder
1668 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
1669 .with_row_selection(RowSelection::from(vec![
1670 RowSelector::skip(150),
1671 RowSelector::select(50),
1672 ]))
1673 .with_row_filter(RowFilter::new(vec![Box::new(row_filter_a)]))
1674 .with_batch_size(10)
1675 .with_limit(10)
1676 .build()
1677 .unwrap();
1678
1679 let ranges = expect_needs_data(decoder.try_decode());
1680 push_ranges_to_decoder(&mut decoder, ranges);
1681
1682 let batch = expect_data(decoder.try_decode());
1683 let expected = TEST_BATCH.slice(176, 10).project(&[0]).unwrap();
1684 assert_eq!(batch, expected);
1685
1686 expect_finished(decoder.try_decode());
1687
1688 assert!(
1689 rows_filtered.load(Ordering::Relaxed) < 50,
1690 "predicate should short-circuit before consuming all 50 rows from the explicit row selection"
1691 );
1692 }
1693
1694 #[test]
1695 fn test_decoder_offset_limit() {
1696 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1697 .unwrap()
1698 // skip entire first row group (200 rows) and first 25 rows of second row group
1699 .with_offset(225)
1700 // and limit to 20 rows
1701 .with_limit(20)
1702 .build()
1703 .unwrap();
1704
1705 // First row group should be skipped,
1706
1707 // Second row group
1708 let ranges = expect_needs_data(decoder.try_decode());
1709 push_ranges_to_decoder(&mut decoder, ranges);
1710
1711 // expect the first and only batch to be decoded
1712 let batch1 = expect_data(decoder.try_decode());
1713 let expected1 = TEST_BATCH.slice(225, 20);
1714 assert_eq!(batch1, expected1);
1715
1716 expect_finished(decoder.try_decode());
1717 }
1718
1719 #[test]
1720 fn test_decoder_try_next_reader_offset_limit() {
1721 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1722 .unwrap()
1723 .with_offset(225)
1724 .with_limit(20)
1725 .build()
1726 .unwrap();
1727
1728 let ranges = expect_needs_data(decoder.try_next_reader());
1729 push_ranges_to_decoder(&mut decoder, ranges);
1730
1731 let reader = expect_data(decoder.try_next_reader());
1732 let batches = reader
1733 .map(|batch| batch.expect("expected decoded batch"))
1734 .collect::<Vec<_>>();
1735 let output = concat_batches(&TEST_BATCH.schema(), &batches).unwrap();
1736 assert_eq!(output, TEST_BATCH.slice(225, 20));
1737
1738 expect_finished(decoder.try_next_reader());
1739 }
1740
1741 #[test]
1742 fn test_decoder_row_group_selection() {
1743 // take only the second row group
1744 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1745 .unwrap()
1746 .with_row_groups(vec![1])
1747 .build()
1748 .unwrap();
1749
1750 // First row group should be skipped,
1751
1752 // Second row group
1753 let ranges = expect_needs_data(decoder.try_decode());
1754 push_ranges_to_decoder(&mut decoder, ranges);
1755
1756 // expect the first and only batch to be decoded
1757 let batch1 = expect_data(decoder.try_decode());
1758 let expected1 = TEST_BATCH.slice(200, 200);
1759 assert_eq!(batch1, expected1);
1760
1761 expect_finished(decoder.try_decode());
1762 }
1763
1764 #[test]
1765 fn test_decoder_row_selection() {
1766 // take only the second row group
1767 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1768 .unwrap()
1769 .with_row_selection(RowSelection::from(vec![
1770 RowSelector::skip(225), // skip first row group and 25 rows of second])
1771 RowSelector::select(20), // take 20 rows
1772 ]))
1773 .build()
1774 .unwrap();
1775
1776 // First row group should be skipped,
1777
1778 // Second row group
1779 let ranges = expect_needs_data(decoder.try_decode());
1780 push_ranges_to_decoder(&mut decoder, ranges);
1781
1782 // expect the first ane only batch to be decoded
1783 let batch1 = expect_data(decoder.try_decode());
1784 let expected1 = TEST_BATCH.slice(225, 20);
1785 assert_eq!(batch1, expected1);
1786
1787 expect_finished(decoder.try_decode());
1788 }
1789
1790 /// `peek_next_row_group` reports the index of the row group the
1791 /// next `try_next_reader` call will hand back, matching the
1792 /// frontier's internal skip logic.
1793 #[test]
1794 fn test_peek_next_row_group_basic() {
1795 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1796 .unwrap()
1797 .build()
1798 .unwrap();
1799
1800 // Two row groups (0, 1). At boundary before any read, peek should
1801 // see RG 0.
1802 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(0));
1803 assert!(decoder.is_at_row_group_boundary());
1804
1805 let ranges = expect_needs_data(decoder.try_next_reader());
1806 push_ranges_to_decoder(&mut decoder, ranges);
1807 let reader = expect_data(decoder.try_next_reader());
1808 // Once the reader for RG 0 has been handed off, the decoder is
1809 // back at a boundary waiting for RG 1 — peek must reflect that
1810 // (the active reader lives outside the decoder).
1811 assert!(decoder.is_at_row_group_boundary());
1812 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1813
1814 // Drain RG 0's reader and consume RG 1.
1815 for batch in reader {
1816 let _ = batch.unwrap();
1817 }
1818 let ranges = expect_needs_data(decoder.try_next_reader());
1819 push_ranges_to_decoder(&mut decoder, ranges);
1820 let reader = expect_data(decoder.try_next_reader());
1821 for batch in reader {
1822 let _ = batch.unwrap();
1823 }
1824
1825 // No row groups left.
1826 assert_eq!(decoder.peek_next_row_group().unwrap(), None);
1827 }
1828
1829 /// `peek_next_row_group` honors `with_row_groups` — restricting the
1830 /// scan to a single row group means peek reports only that one and
1831 /// then `None`.
1832 #[test]
1833 fn test_peek_next_row_group_respects_with_row_groups() {
1834 let decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1835 .unwrap()
1836 .with_row_groups(vec![1])
1837 .build()
1838 .unwrap();
1839
1840 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1841 }
1842
1843 /// When a row-selection segment leaves the next row group with zero
1844 /// selected rows, `peek_next_row_group` mirrors
1845 /// `next_readable_row_group`'s skip: it returns the *following*
1846 /// row group instead of the empty one.
1847 #[test]
1848 fn test_peek_next_row_group_skips_empty_selection() {
1849 // Each row group has 200 rows. Skip all 200 of RG 0 plus 50 of
1850 // RG 1; the next reader will be for RG 1, not RG 0.
1851 let decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1852 .unwrap()
1853 .with_row_selection(RowSelection::from(vec![
1854 RowSelector::skip(250),
1855 RowSelector::select(100),
1856 ]))
1857 .build()
1858 .unwrap();
1859
1860 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1861 }
1862
1863 /// `peek_next_row_group` returns `None` on a finished decoder.
1864 #[test]
1865 fn test_peek_next_row_group_finished() {
1866 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1867 .unwrap()
1868 .with_row_groups(vec![])
1869 .build()
1870 .unwrap();
1871
1872 // No row groups requested ⇒ already finished, no peek.
1873 expect_finished(decoder.try_next_reader());
1874 assert_eq!(decoder.peek_next_row_group().unwrap(), None);
1875 }
1876
1877 /// `peek_next_row_group` mirrors `next_readable_row_group`'s
1878 /// budget-skipping logic: with an `OFFSET` that consumes the
1879 /// entire first row group, peek must return the *following*
1880 /// row group, not RG 0 (no predicates, so budget skips apply).
1881 #[test]
1882 fn test_peek_next_row_group_skips_for_budget() {
1883 // Each row group has 200 rows. OFFSET 200 drains RG 0 entirely
1884 // and lands the decoder's first emitted reader at RG 1.
1885 let decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1886 .unwrap()
1887 .with_offset(200)
1888 .build()
1889 .unwrap();
1890
1891 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1892 }
1893
1894 /// OFFSET larger than every remaining row group's row count
1895 /// exhausts the budget, so peek should report `None` — matching
1896 /// `next_readable_row_group`'s behavior of producing `Finished`.
1897 #[test]
1898 fn test_peek_next_row_group_budget_drains_all() {
1899 // 2 row groups × 200 rows = 400 total. OFFSET 500 cannot land
1900 // anywhere — every RG is skipped under the budget.
1901 let decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1902 .unwrap()
1903 .with_offset(500)
1904 .build()
1905 .unwrap();
1906
1907 assert_eq!(decoder.peek_next_row_group().unwrap(), None);
1908 }
1909
1910 /// `peek_next_row_group` is safe to call while a row group's reader
1911 /// is still being drained via `try_decode`. In `DecodingRowGroup`
1912 /// state it must report the row group `try_next_reader` will yield
1913 /// a reader for *after* the active one — never `None` just because
1914 /// the decoder is mid-row-group.
1915 #[test]
1916 fn test_peek_next_row_group_during_decoding_row_group() {
1917 // Two row groups (200 rows each), small batch size so the
1918 // decoder stays in `DecodingRowGroup` across multiple
1919 // `try_decode` calls within RG 0.
1920 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1921 .unwrap()
1922 .with_batch_size(100)
1923 .build()
1924 .unwrap();
1925 decoder
1926 .push_range(test_file_range(), TEST_FILE_DATA.clone())
1927 .unwrap();
1928
1929 // First batch of RG 0 — after this, the decoder is in
1930 // `DecodingRowGroup` state with the reader retained internally
1931 // and one more 100-row batch still owed for RG 0. Peek must
1932 // therefore look past the active RG to RG 1.
1933 let _batch0 = expect_data(decoder.try_decode());
1934 assert!(!decoder.is_at_row_group_boundary());
1935 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1936
1937 // Drain the rest of RG 0; peek still reports RG 1.
1938 let _batch1 = expect_data(decoder.try_decode());
1939 assert_eq!(decoder.peek_next_row_group().unwrap(), Some(1));
1940
1941 // Move into RG 1 — peek now sees no further row groups.
1942 let _batch2 = expect_data(decoder.try_decode());
1943 assert!(!decoder.is_at_row_group_boundary());
1944 assert_eq!(decoder.peek_next_row_group().unwrap(), None);
1945
1946 let _batch3 = expect_data(decoder.try_decode());
1947 expect_finished(decoder.try_decode());
1948 }
1949
1950 /// Peeking is a read-only operation: calling it repeatedly between
1951 /// `try_next_reader` calls must never change which row group the
1952 /// reader path actually produces. Drives the decoder all the way
1953 /// through and asserts each peek/read pair agrees.
1954 #[test]
1955 fn test_peek_next_row_group_does_not_mutate_state() {
1956 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
1957 .unwrap()
1958 .build()
1959 .unwrap();
1960
1961 // Two row groups expected — drive both, asserting peek/read agree.
1962 for expected_rg in [0usize, 1usize] {
1963 assert!(decoder.is_at_row_group_boundary());
1964
1965 // Multiple peeks before reading must all agree, and must not
1966 // disturb the upcoming read.
1967 let first_peek = decoder.peek_next_row_group().unwrap();
1968 let second_peek = decoder.peek_next_row_group().unwrap();
1969 let third_peek = decoder.peek_next_row_group().unwrap();
1970 assert_eq!(first_peek, Some(expected_rg));
1971 assert_eq!(first_peek, second_peek);
1972 assert_eq!(second_peek, third_peek);
1973
1974 // Now read for real and confirm the decoder hands back exactly
1975 // what peek promised.
1976 let ranges = expect_needs_data(decoder.try_next_reader());
1977 push_ranges_to_decoder(&mut decoder, ranges);
1978 let reader = expect_data(decoder.try_next_reader());
1979 for batch in reader {
1980 let _ = batch.unwrap();
1981 }
1982 }
1983
1984 // Decoder is drained. Peek must agree with `try_next_reader`'s
1985 // terminal state.
1986 assert_eq!(decoder.peek_next_row_group().unwrap(), None);
1987 expect_finished(decoder.try_next_reader());
1988 }
1989
1990 /// `into_builder` between row groups recovers a builder for the
1991 /// not-yet-decoded row groups; rebuilding it with a new row filter
1992 /// applies that filter to the subsequent row groups while leaving the
1993 /// already-decoded row group's results untouched.
1994 ///
1995 /// See the "Adaptive scans" section of [`ParquetPushDecoderBuilder`] for
1996 /// the high-level overview.
1997 #[test]
1998 fn test_into_builder_installs_filter_between_row_groups() {
1999 let schema_descr = test_file_parquet_metadata()
2000 .file_metadata()
2001 .schema_descr_ptr();
2002 let mut decoder = prefetched_decoder(1024);
2003
2004 // Reader for row group 0 — no filter.
2005 let reader0 = expect_data(decoder.try_next_reader());
2006 let batches0: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2007 let batch0 = concat_batches(&TEST_BATCH.schema(), &batches0).unwrap();
2008 assert_eq!(batch0, TEST_BATCH.slice(0, 200));
2009
2010 // We're between row groups now. Rebuild with a filter on column "a".
2011 assert!(decoder.is_at_row_group_boundary());
2012 assert_eq!(decoder.row_groups_remaining(), 1);
2013 let filter =
2014 ArrowPredicateFn::new(ProjectionMask::columns(&schema_descr, ["a"]), |batch| {
2015 gt(batch.column(0), &Int64Array::new_scalar(250))
2016 });
2017 let mut decoder = decoder
2018 .into_builder()
2019 .unwrap()
2020 .with_row_filter(RowFilter::new(vec![Box::new(filter)]))
2021 .build()
2022 .unwrap();
2023
2024 // Reader for row group 1 — filter applied. The rebuilt decoder kept
2025 // the buffered bytes (see `test_into_builder_preserves_buffered_bytes`)
2026 // so no data needs to be re-supplied. Column "a" in RG1 has values
2027 // 200..399; `a > 250` keeps 251..399 = 149 rows.
2028 let reader1 = expect_data(decoder.try_next_reader());
2029 let batches1: Vec<_> = reader1.collect::<Result<_, _>>().unwrap();
2030 let batch1 = concat_batches(&TEST_BATCH.schema(), &batches1).unwrap();
2031 assert_eq!(batch1, TEST_BATCH.slice(251, 149));
2032 expect_finished(decoder.try_next_reader());
2033 }
2034
2035 /// `into_builder` is rejected while a row group's reader is being
2036 /// drained (`DecodingRowGroup`); the error points at
2037 /// `is_at_row_group_boundary`.
2038 #[test]
2039 fn test_into_builder_rejected_mid_row_group() {
2040 let mut decoder = prefetched_decoder(50);
2041
2042 // Decode one batch to land mid-row-group, inside `DecodingRowGroup`
2043 // with an active reader — not a boundary.
2044 expect_data(decoder.try_decode());
2045 assert!(!decoder.is_at_row_group_boundary());
2046
2047 let err = decoder.into_builder().unwrap_err();
2048 let err_msg = format!("{err}");
2049 assert!(
2050 err_msg.contains("is_at_row_group_boundary"),
2051 "unexpected error: {err_msg}"
2052 );
2053 }
2054
2055 /// `into_builder` is rejected once the decoder has finished.
2056 #[test]
2057 fn test_into_builder_rejected_on_finished_decoder() {
2058 let mut decoder = prefetched_decoder(1024);
2059 expect_data(decoder.try_decode());
2060 expect_data(decoder.try_decode());
2061 expect_finished(decoder.try_decode());
2062 assert!(!decoder.is_at_row_group_boundary());
2063
2064 let err = decoder.into_builder().unwrap_err();
2065 assert!(
2066 format!("{err}").contains("finished"),
2067 "unexpected error: {err}"
2068 );
2069 }
2070
2071 /// `try_next_reader` hands the active reader off to the caller and
2072 /// transitions the decoder back to `ReadingRowGroup` — so the caller
2073 /// can call `into_builder` even while still holding the returned
2074 /// reader. (The handed-off reader has no link back to the decoder's
2075 /// projection/filter; it has its own `ArrayReader` and `ReadPlan`.)
2076 #[test]
2077 fn test_into_builder_allowed_while_iterating_handed_off_reader() {
2078 let mut decoder = prefetched_decoder(1024);
2079
2080 let reader0 = expect_data(decoder.try_next_reader());
2081 // Decoder no longer owns the reader, so it considers itself
2082 // "between row groups".
2083 assert!(decoder.is_at_row_group_boundary());
2084 // Recovering the builder consumes the decoder but leaves `reader0`
2085 // valid: iterating it is independent of the decoder's state.
2086 let _builder = decoder.into_builder().unwrap();
2087 let batches: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2088 let batch0 = concat_batches(&TEST_BATCH.schema(), &batches).unwrap();
2089 assert_eq!(batch0, TEST_BATCH.slice(0, 200));
2090 }
2091
2092 /// `into_builder` recovers a builder for the *remaining* row groups and
2093 /// carries the not-yet-consumed offset/limit budget, so a rebuilt
2094 /// decoder resumes where the original left off rather than restarting.
2095 #[test]
2096 fn test_into_builder_resumes_remaining_budget() {
2097 // limit = 250 spans both 200-row row groups: all 200 rows of RG0
2098 // plus the first 50 rows of RG1.
2099 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
2100 .unwrap()
2101 .with_batch_size(1024)
2102 .with_limit(250)
2103 .build()
2104 .unwrap();
2105 prefetch_test_file(&mut decoder);
2106
2107 // RG0 contributes all 200 of its rows.
2108 let reader0 = expect_data(decoder.try_next_reader());
2109 let batches0: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2110 let batch0 = concat_batches(&TEST_BATCH.schema(), &batches0).unwrap();
2111 assert_eq!(batch0, TEST_BATCH.slice(0, 200));
2112
2113 // Rebuild without changing anything: the remaining 50-row limit and
2114 // the not-yet-decoded RG1 must carry through (as do the buffers, so
2115 // no data needs re-supplying).
2116 assert!(decoder.is_at_row_group_boundary());
2117 let mut decoder = decoder.into_builder().unwrap().build().unwrap();
2118
2119 let reader1 = expect_data(decoder.try_next_reader());
2120 let batches1: Vec<_> = reader1.collect::<Result<_, _>>().unwrap();
2121 let batch1 = concat_batches(&TEST_BATCH.schema(), &batches1).unwrap();
2122 // Only the first 50 rows of RG1 (200..249) — the rest of the limit.
2123 assert_eq!(batch1, TEST_BATCH.slice(200, 50));
2124 expect_finished(decoder.try_next_reader());
2125 }
2126
2127 /// `into_builder` carries the decoder's buffered bytes across the
2128 /// rebuild: the rebuilt decoder keeps them and does not re-request data
2129 /// it already holds.
2130 #[test]
2131 fn test_into_builder_preserves_buffered_bytes() {
2132 let mut decoder = prefetched_decoder(1024);
2133 assert_eq!(decoder.buffered_bytes(), test_file_len());
2134
2135 // Drain RG0.
2136 let reader0 = expect_data(decoder.try_next_reader());
2137 let _: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2138 // RG1's bytes are still staged inside the decoder.
2139 let buffered = decoder.buffered_bytes();
2140 assert!(buffered > 0);
2141
2142 // Rebuilding via into_builder keeps the staged bytes.
2143 let mut decoder = decoder.into_builder().unwrap().build().unwrap();
2144 assert_eq!(decoder.buffered_bytes(), buffered);
2145
2146 // RG1's bytes are already buffered, so it decodes without a
2147 // `NeedsData` round-trip.
2148 let reader1 = expect_data(decoder.try_next_reader());
2149 let batches1: Vec<_> = reader1.collect::<Result<_, _>>().unwrap();
2150 let batch1 = concat_batches(&TEST_BATCH.schema(), &batches1).unwrap();
2151 assert_eq!(batch1, TEST_BATCH.slice(200, 200));
2152 expect_finished(decoder.try_next_reader());
2153 }
2154
2155 /// Drive the decoder incrementally. Start with a narrow projection,
2156 /// drain RG0, then `into_builder` and widen the projection to all three
2157 /// columns. The rebuilt decoder's `NeedsData` for RG1 must request
2158 /// bytes for *all three* columns, not just the originally-projected
2159 /// "a". The expected ranges are hardcoded because `TEST_BATCH` and the
2160 /// writer settings are static; this pins the layout cleanly without a
2161 /// parallel reference decoder.
2162 #[test]
2163 fn test_into_builder_expand_projection_requests_new_bytes() {
2164 let metadata = test_file_parquet_metadata();
2165 let schema_descr = metadata.file_metadata().schema_descr_ptr();
2166
2167 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(metadata)
2168 .unwrap()
2169 .with_batch_size(1024)
2170 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
2171 .build()
2172 .unwrap();
2173
2174 // RG0: incrementally satisfy the narrow request — a single
2175 // contiguous range for the "a"-only projection.
2176 let ranges_rg0 = expect_needs_data(decoder.try_next_reader());
2177 assert_eq!(ranges_rg0, vec![4..1860]);
2178 push_ranges_to_decoder(&mut decoder, ranges_rg0);
2179
2180 let reader0 = expect_data(decoder.try_next_reader());
2181 let batches0: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2182 let batch0 = concat_batches(&batches0[0].schema(), &batches0).unwrap();
2183 assert_eq!(batch0, TEST_BATCH.slice(0, 200).project(&[0]).unwrap());
2184
2185 // Widen the projection at the boundary.
2186 assert!(decoder.is_at_row_group_boundary());
2187 let mut decoder = decoder
2188 .into_builder()
2189 .unwrap()
2190 .with_projection(ProjectionMask::columns(&schema_descr, ["a", "b", "c"]))
2191 .build()
2192 .unwrap();
2193
2194 // RG1 now requests "a", "b", and "c" column chunks: ~1.8KiB each
2195 // for "a" and "b", ~7.5KiB for the StringView column "c".
2196 let ranges_rg1 = expect_needs_data(decoder.try_next_reader());
2197 assert_eq!(ranges_rg1, vec![11062..12918, 12918..14774, 14774..22230]);
2198 push_ranges_to_decoder(&mut decoder, ranges_rg1);
2199
2200 let reader1 = expect_data(decoder.try_next_reader());
2201 let batches1: Vec<_> = reader1.collect::<Result<_, _>>().unwrap();
2202 let batch1 = concat_batches(&TEST_BATCH.schema(), &batches1).unwrap();
2203 assert_eq!(batch1, TEST_BATCH.slice(200, 200));
2204 expect_finished(decoder.try_next_reader());
2205 }
2206
2207 /// Mirror of [`test_into_builder_expand_projection_requests_new_bytes`]:
2208 /// start with the full projection, drain RG0, then `into_builder` and
2209 /// narrow the projection to just column "a". RG1's `NeedsData` must
2210 /// request only the single "a" column-chunk range, not the three a
2211 /// wide projection would.
2212 #[test]
2213 fn test_into_builder_narrow_projection_requests_fewer_bytes() {
2214 let metadata = test_file_parquet_metadata();
2215 let schema_descr = metadata.file_metadata().schema_descr_ptr();
2216
2217 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(metadata)
2218 .unwrap()
2219 .with_batch_size(1024)
2220 .build()
2221 .unwrap();
2222
2223 // RG0 with the default (full) projection — three column ranges.
2224 let ranges_rg0 = expect_needs_data(decoder.try_next_reader());
2225 assert_eq!(ranges_rg0, vec![4..1860, 1860..3716, 3716..11062]);
2226 push_ranges_to_decoder(&mut decoder, ranges_rg0);
2227
2228 let reader0 = expect_data(decoder.try_next_reader());
2229 let batches0: Vec<_> = reader0.collect::<Result<_, _>>().unwrap();
2230 let batch0 = concat_batches(&TEST_BATCH.schema(), &batches0).unwrap();
2231 assert_eq!(batch0, TEST_BATCH.slice(0, 200));
2232
2233 // Narrow the projection at the boundary.
2234 assert!(decoder.is_at_row_group_boundary());
2235 let mut decoder = decoder
2236 .into_builder()
2237 .unwrap()
2238 .with_projection(ProjectionMask::columns(&schema_descr, ["a"]))
2239 .build()
2240 .unwrap();
2241
2242 // RG1 now requests column "a" only — a single 1856-byte range.
2243 let ranges_rg1 = expect_needs_data(decoder.try_next_reader());
2244 assert_eq!(ranges_rg1, vec![11062..12918]);
2245 push_ranges_to_decoder(&mut decoder, ranges_rg1);
2246
2247 let reader1 = expect_data(decoder.try_next_reader());
2248 let batches1: Vec<_> = reader1.collect::<Result<_, _>>().unwrap();
2249 let batch1 = concat_batches(&batches1[0].schema(), &batches1).unwrap();
2250 assert_eq!(batch1, TEST_BATCH.slice(200, 200).project(&[0]).unwrap());
2251 expect_finished(decoder.try_next_reader());
2252 }
2253
2254 /// Returns a batch with 400 rows, with 3 columns: "a", "b", "c"
2255 ///
2256 /// Note c is a different types (so the data page sizes will be different)
2257 static TEST_BATCH: LazyLock<RecordBatch> = LazyLock::new(|| {
2258 let a: ArrayRef = Arc::new(Int64Array::from_iter_values(0..400));
2259 let b: ArrayRef = Arc::new(Int64Array::from_iter_values(400..800));
2260 let c: ArrayRef = Arc::new(StringViewArray::from_iter_values((0..400).map(|i| {
2261 if i % 2 == 0 {
2262 format!("string_{i}")
2263 } else {
2264 format!("A string larger than 12 bytes and thus not inlined {i}")
2265 }
2266 })));
2267
2268 RecordBatch::try_from_iter(vec![("a", a), ("b", b), ("c", c)]).unwrap()
2269 });
2270
2271 /// Create a parquet file in memory for testing.
2272 ///
2273 /// See [`TEST_BATCH`] for the data in the file.
2274 ///
2275 /// Each column is written in 4 data pages, each with 100 rows, across 2
2276 /// row groups. Each column in each row group has two data pages.
2277 ///
2278 /// The data is split across row groups like this
2279 ///
2280 /// Column | Values | Data Page | Row Group
2281 /// -------|------------------------|-----------|-----------
2282 /// a | 0..99 | 1 | 0
2283 /// a | 100..199 | 2 | 0
2284 /// a | 200..299 | 1 | 1
2285 /// a | 300..399 | 2 | 1
2286 ///
2287 /// b | 400..499 | 1 | 0
2288 /// b | 500..599 | 2 | 0
2289 /// b | 600..699 | 1 | 1
2290 /// b | 700..799 | 2 | 1
2291 ///
2292 /// c | "string_0".."string_99" | 1 | 0
2293 /// c | "string_100".."string_199" | 2 | 0
2294 /// c | "string_200".."string_299" | 1 | 1
2295 /// c | "string_300".."string_399" | 2 | 1
2296 static TEST_FILE_DATA: LazyLock<Bytes> = LazyLock::new(|| {
2297 let input_batch = &TEST_BATCH;
2298 let mut output = Vec::new();
2299
2300 let writer_options = WriterProperties::builder()
2301 .set_max_row_group_row_count(Some(200))
2302 .set_data_page_row_count_limit(100)
2303 .build();
2304 let mut writer =
2305 ArrowWriter::try_new(&mut output, input_batch.schema(), Some(writer_options)).unwrap();
2306
2307 // since the limits are only enforced on batch boundaries, write the input
2308 // batch in chunks of 50
2309 let mut row_remain = input_batch.num_rows();
2310 while row_remain > 0 {
2311 let chunk_size = row_remain.min(50);
2312 let chunk = input_batch.slice(input_batch.num_rows() - row_remain, chunk_size);
2313 writer.write(&chunk).unwrap();
2314 row_remain -= chunk_size;
2315 }
2316 writer.close().unwrap();
2317 Bytes::from(output)
2318 });
2319
2320 /// Return the length of [`TEST_FILE_DATA`], in bytes
2321 fn test_file_len() -> u64 {
2322 TEST_FILE_DATA.len() as u64
2323 }
2324
2325 /// Return a range that covers the entire [`TEST_FILE_DATA`]
2326 fn test_file_range() -> Range<u64> {
2327 0..test_file_len()
2328 }
2329
2330 /// Return a slice of the test file data from the given range
2331 pub fn test_file_slice(range: Range<u64>) -> Bytes {
2332 let start: usize = range.start.try_into().unwrap();
2333 let end: usize = range.end.try_into().unwrap();
2334 TEST_FILE_DATA.slice(start..end)
2335 }
2336
2337 /// return the metadata for the test file
2338 pub fn test_file_parquet_metadata() -> Arc<crate::file::metadata::ParquetMetaData> {
2339 let mut metadata_decoder = ParquetMetaDataPushDecoder::try_new(test_file_len()).unwrap();
2340 push_ranges_to_metadata_decoder(&mut metadata_decoder, vec![test_file_range()]);
2341 let metadata = metadata_decoder.try_decode().unwrap();
2342 let DecodeResult::Data(metadata) = metadata else {
2343 panic!("Expected metadata to be decoded successfully");
2344 };
2345 Arc::new(metadata)
2346 }
2347
2348 /// Push the given ranges to the metadata decoder, simulating reading from a file
2349 fn push_ranges_to_metadata_decoder(
2350 metadata_decoder: &mut ParquetMetaDataPushDecoder,
2351 ranges: Vec<Range<u64>>,
2352 ) {
2353 let data = ranges
2354 .iter()
2355 .map(|range| test_file_slice(range.clone()))
2356 .collect::<Vec<_>>();
2357 metadata_decoder.push_ranges(ranges, data).unwrap();
2358 }
2359
2360 /// Push the entire test file into `decoder`.
2361 fn prefetch_test_file(decoder: &mut ParquetPushDecoder) {
2362 decoder
2363 .push_range(test_file_range(), TEST_FILE_DATA.clone())
2364 .unwrap();
2365 }
2366
2367 /// Build a decoder over the test file with the given batch size and
2368 /// prefetch the whole file into it.
2369 fn prefetched_decoder(batch_size: usize) -> ParquetPushDecoder {
2370 let mut decoder = ParquetPushDecoderBuilder::try_new_decoder(test_file_parquet_metadata())
2371 .unwrap()
2372 .with_batch_size(batch_size)
2373 .build()
2374 .unwrap();
2375 prefetch_test_file(&mut decoder);
2376 decoder
2377 }
2378
2379 fn push_ranges_to_decoder(decoder: &mut ParquetPushDecoder, ranges: Vec<Range<u64>>) {
2380 let data = ranges
2381 .iter()
2382 .map(|range| test_file_slice(range.clone()))
2383 .collect::<Vec<_>>();
2384 decoder.push_ranges(ranges, data).unwrap();
2385 }
2386
2387 /// Expect that the [`DecodeResult`] is a [`DecodeResult::Data`] and return the corresponding element
2388 fn expect_data<T: Debug>(result: Result<DecodeResult<T>, ParquetError>) -> T {
2389 match result.expect("Expected Ok(DecodeResult::Data(T))") {
2390 DecodeResult::Data(data) => data,
2391 result => panic!("Expected DecodeResult::Data, got {result:?}"),
2392 }
2393 }
2394
2395 /// Expect that the [`DecodeResult`] is a [`DecodeResult::NeedsData`] and return the corresponding ranges
2396 fn expect_needs_data<T: Debug>(
2397 result: Result<DecodeResult<T>, ParquetError>,
2398 ) -> Vec<Range<u64>> {
2399 match result.expect("Expected Ok(DecodeResult::NeedsData{ranges})") {
2400 DecodeResult::NeedsData(ranges) => ranges,
2401 result => panic!("Expected DecodeResult::NeedsData, got {result:?}"),
2402 }
2403 }
2404
2405 fn expect_finished<T: Debug>(result: Result<DecodeResult<T>, ParquetError>) {
2406 match result.expect("Expected Ok(DecodeResult::Finished)") {
2407 DecodeResult::Finished => {}
2408 result => panic!("Expected DecodeResult::Finished, got {result:?}"),
2409 }
2410 }
2411}