Skip to main content

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}