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