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