Skip to main content

parquet/arrow/arrow_reader/
read_plan.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//! [`ReadPlan`] and [`ReadPlanBuilder`] for determining which rows to read
19//! from a Parquet file
20
21use crate::arrow::array_reader::ArrayReader;
22use crate::arrow::arrow_reader::selection::{
23    LoadedRowRanges, RowSelectionInner, RowSelectionPolicy, RowSelectionStrategy,
24};
25use crate::arrow::arrow_reader::{
26    ArrowPredicate, ParquetRecordBatchReader, RowSelection, RowSelectionCursor, RowSelector,
27};
28use crate::errors::{ParquetError, Result};
29use arrow_array::{Array, BooleanArray};
30use arrow_buffer::{BooleanBuffer, BooleanBufferBuilder};
31use arrow_select::filter::prep_null_mask_filter;
32use std::sync::Arc;
33
34/// Options for [`ReadPlanBuilder::with_predicate_options`].
35pub struct PredicateOptions<'a> {
36    array_reader: Box<dyn ArrayReader>,
37    predicate: &'a mut dyn ArrowPredicate,
38    limit: Option<usize>,
39    total_rows: usize,
40}
41
42impl<'a> PredicateOptions<'a> {
43    /// Create options for evaluating `predicate` against rows produced by
44    /// `array_reader`.
45    ///
46    /// By default there is no match-count limit; the predicate is evaluated
47    /// over every row the reader yields. Use [`Self::with_limit`] to enable
48    /// early termination.
49    pub fn new(array_reader: Box<dyn ArrayReader>, predicate: &'a mut dyn ArrowPredicate) -> Self {
50        Self {
51            array_reader,
52            predicate,
53            limit: None,
54            total_rows: 0,
55        }
56    }
57
58    /// Stop scanning `array_reader` once `limit` matches have accumulated.
59    ///
60    /// Performance optimization for `LIMIT` / TopK: when the cumulative
61    /// `true_count` reaches `limit`, the current filter batch is truncated
62    /// at the `limit`-th match and remaining batches are never decoded.
63    ///
64    /// `limit` counts predicate matches, not output rows — callers applying
65    /// an offset must pass `offset + limit`.
66    ///
67    /// `total_rows` is the row count `array_reader` would yield if iterated
68    /// to completion. It is used to pad un-evaluated trailing rows as "not
69    /// selected" so the returned [`RowSelection`] covers the full row group.
70    ///
71    /// Only valid for the *last* predicate in a filter chain: intermediate
72    /// predicates' match counts do not map 1:1 to output rows.
73    pub fn with_limit(mut self, limit: usize, total_rows: usize) -> Self {
74        self.limit = Some(limit);
75        self.total_rows = total_rows;
76        self
77    }
78}
79
80/// A builder for [`ReadPlan`]
81#[derive(Clone, Debug)]
82pub struct ReadPlanBuilder {
83    batch_size: usize,
84    /// Which rows to select. Includes the result of all filters applied so far
85    selection: Option<RowSelection>,
86    /// Policy to use when materializing the row selection
87    row_selection_policy: RowSelectionPolicy,
88    /// Row ranges with page data loaded for the current projection.
89    loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
90}
91
92impl ReadPlanBuilder {
93    /// Create a `ReadPlanBuilder` with the given batch size
94    pub fn new(batch_size: usize) -> Self {
95        Self {
96            batch_size,
97            selection: None,
98            row_selection_policy: RowSelectionPolicy::default(),
99            loaded_row_ranges: None,
100        }
101    }
102
103    /// Set the current selection to the given value
104    pub fn with_selection(mut self, selection: Option<RowSelection>) -> Self {
105        self.selection = selection;
106        self
107    }
108
109    /// Configure the policy to use when materialising the [`RowSelection`]
110    ///
111    /// Defaults to [`RowSelectionPolicy::Auto`]
112    pub fn with_row_selection_policy(mut self, policy: RowSelectionPolicy) -> Self {
113        self.row_selection_policy = policy;
114        self
115    }
116
117    pub(crate) fn with_loaded_row_ranges(mut self, ranges: Option<LoadedRowRanges>) -> Self {
118        self.loaded_row_ranges = ranges.map(Arc::new);
119        self
120    }
121
122    /// Returns the current row selection policy
123    pub fn row_selection_policy(&self) -> &RowSelectionPolicy {
124        &self.row_selection_policy
125    }
126
127    /// Returns the current selection, if any
128    pub fn selection(&self) -> Option<&RowSelection> {
129        self.selection.as_ref()
130    }
131
132    /// Specifies the number of rows in the row group, before filtering is applied.
133    ///
134    /// Returns a [`LimitedReadPlanBuilder`] that can apply
135    /// offset and limit.
136    ///
137    /// Call [`LimitedReadPlanBuilder::build_limited`] to apply the limits to this
138    /// selection.
139    pub(crate) fn limited(self, row_count: usize) -> LimitedReadPlanBuilder {
140        LimitedReadPlanBuilder::new(self, row_count)
141    }
142
143    /// Returns true if the current plan selects any rows
144    pub fn selects_any(&self) -> bool {
145        self.selection
146            .as_ref()
147            .map(|s| s.selects_any())
148            .unwrap_or(true)
149    }
150
151    /// Returns the number of rows selected, or `None` if all rows are selected.
152    pub fn num_rows_selected(&self) -> Option<usize> {
153        self.selection.as_ref().map(|s| s.row_count())
154    }
155
156    /// Returns the [`RowSelectionStrategy`] for this plan.
157    ///
158    /// Guarantees to return either `Selectors` or `Mask`, never `Auto`.
159    pub(crate) fn resolve_selection_strategy(&self) -> RowSelectionStrategy {
160        match self.row_selection_policy {
161            RowSelectionPolicy::Selectors => RowSelectionStrategy::Selectors,
162            RowSelectionPolicy::Mask => RowSelectionStrategy::Mask,
163            RowSelectionPolicy::Auto { threshold, .. } => {
164                let Some(selection) = self.selection.as_ref() else {
165                    return RowSelectionStrategy::Selectors;
166                };
167
168                selection.auto_selection_strategy(threshold)
169            }
170        }
171    }
172
173    /// Evaluates an [`ArrowPredicate`], updating this plan's `selection`
174    ///
175    /// If the current `selection` is `Some`, the resulting [`RowSelection`]
176    /// will be the conjunction of the existing selection and the rows selected
177    /// by `predicate`.
178    ///
179    /// Note: pre-existing selections may come from evaluating a previous predicate
180    /// or if the [`ParquetRecordBatchReader`] specified an explicit
181    /// [`RowSelection`] in addition to one or more predicates.
182    pub fn with_predicate(
183        self,
184        array_reader: Box<dyn ArrayReader>,
185        predicate: &mut dyn ArrowPredicate,
186    ) -> Result<Self> {
187        self.with_predicate_options(PredicateOptions::new(array_reader, predicate))
188    }
189
190    /// Evaluates an [`ArrowPredicate`] with the given [`PredicateOptions`],
191    /// updating this plan's `selection`.
192    ///
193    /// Like [`Self::with_predicate`], but allows additional options such as a
194    /// match-count limit for early termination (see
195    /// [`PredicateOptions::with_limit`]).
196    pub fn with_predicate_options(mut self, options: PredicateOptions<'_>) -> Result<Self> {
197        let PredicateOptions {
198            array_reader,
199            predicate,
200            limit,
201            total_rows,
202        } = options;
203
204        // Target length for the concatenated filter output:
205        // - Prior selection ⇒ the reader yields that many rows; `and_then`
206        //   below requires the filter output to match.
207        // - No prior selection ⇒ the reader yields `total_rows`. We only
208        //   need to pad when `limit` may short-circuit the loop; otherwise
209        //   iteration naturally exhausts.
210        let expected_rows = match self.selection.as_ref() {
211            Some(s) => Some(s.row_count()),
212            None => limit.map(|_| total_rows),
213        };
214
215        let reader = ParquetRecordBatchReader::new(array_reader, self.clone().build());
216        let mut filters = vec![];
217        let mut processed_rows: usize = 0;
218        let mut matched_rows: usize = 0;
219        for maybe_batch in reader {
220            let maybe_batch = maybe_batch?;
221            let input_rows = maybe_batch.num_rows();
222            let filter = predicate.evaluate(maybe_batch)?;
223            // Since user supplied predicate, check error here to catch bugs quickly
224            if filter.len() != input_rows {
225                return Err(arrow_err!(
226                    "ArrowPredicate predicate returned {} rows, expected {input_rows}",
227                    filter.len()
228                ));
229            }
230            let filter = match filter.null_count() {
231                0 => filter,
232                // RowSelection::from_filters expects non-null filters. Convert
233                // NULL predicate results to false so they are not selected.
234                _ => prep_null_mask_filter(&filter),
235            };
236
237            processed_rows += input_rows;
238
239            match limit {
240                Some(limit) if limit - matched_rows <= filter.len() => {
241                    let truncated = filter.take_n_true(limit - matched_rows);
242                    matched_rows += truncated.true_count();
243                    filters.push(truncated);
244                    if matched_rows >= limit {
245                        break;
246                    }
247                }
248                _ => {
249                    matched_rows += filter.true_count();
250                    filters.push(filter);
251                }
252            }
253        }
254
255        // Pad the tail so the filters cover `expected_rows` total. This keeps
256        // the invariant that the resulting `RowSelection` spans every row the
257        // reader would have produced — rows past the early break are marked
258        // "not selected". When no limit is set the loop always exhausts and
259        // no padding is needed.
260        if let Some(expected) = expected_rows
261            && processed_rows < expected
262        {
263            let pad_len = expected - processed_rows;
264            filters.push(BooleanArray::new(BooleanBuffer::new_unset(pad_len), None));
265        }
266
267        // If the predicate selected all rows, applying it is a no-op. With no
268        // prior selection this keeps selection as None, enabling coalesced page
269        // fetches; with a prior selection it avoids rebuilding the same
270        // selection.
271        let all_selected = filters.iter().all(|f| f.true_count() == f.len());
272        if all_selected {
273            return Ok(self);
274        }
275        let raw = if self
276            .selection
277            .as_ref()
278            .is_some_and(|s| s.as_mask().is_some())
279        {
280            RowSelection::from_boolean_buffer(filters_to_boolean_buffer(&filters))
281        } else {
282            RowSelection::from_filters(&filters)
283        };
284        self.selection = match self.selection.take() {
285            Some(selection) => Some(selection.and_then(&raw)),
286            None => Some(raw),
287        };
288        Ok(self)
289    }
290
291    /// Create a final `ReadPlan` the read plan for the scan
292    pub fn build(mut self) -> ReadPlan {
293        // If selection is empty, truncate
294        if !self.selects_any() {
295            self.selection = Some(RowSelection::from(vec![]));
296        }
297
298        // Preferred strategy must not be Auto
299        let selection_strategy = self.resolve_selection_strategy();
300
301        let Self {
302            batch_size,
303            selection,
304            row_selection_policy: _,
305            loaded_row_ranges,
306        } = self;
307
308        let row_selection_cursor = selection
309            .map(|s| build_cursor(s.trim(), selection_strategy, loaded_row_ranges))
310            .unwrap_or_else(RowSelectionCursor::new_all);
311
312        ReadPlan {
313            batch_size,
314            row_selection_cursor,
315        }
316    }
317}
318
319/// Lower a [`RowSelection`] to the cursor form requested by the resolved strategy.
320fn build_cursor(
321    selection: RowSelection,
322    strategy: RowSelectionStrategy,
323    loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
324) -> RowSelectionCursor {
325    match (strategy, selection.into_inner()) {
326        (RowSelectionStrategy::Mask, RowSelectionInner::Mask(mask)) => {
327            RowSelectionCursor::new_mask_from_buffer((*mask).into_mask(), loaded_row_ranges)
328        }
329        (RowSelectionStrategy::Mask, RowSelectionInner::Selectors(selectors)) => {
330            RowSelectionCursor::new_mask_from_selectors(selectors, loaded_row_ranges)
331        }
332        (RowSelectionStrategy::Selectors, RowSelectionInner::Selectors(selectors)) => {
333            RowSelectionCursor::new_selectors(selectors)
334        }
335        (RowSelectionStrategy::Selectors, RowSelectionInner::Mask(mask)) => {
336            RowSelectionCursor::new_selectors((*mask).into_selectors())
337        }
338    }
339}
340
341/// Builder for [`ReadPlan`] that applies a limit and offset to the read plan
342///
343/// See [`ReadPlanBuilder::limited`] to create this builder.
344pub(crate) struct LimitedReadPlanBuilder {
345    /// The underlying builder
346    inner: ReadPlanBuilder,
347    /// Total number of rows in the row group before the selection, limit or
348    /// offset are applied
349    row_count: usize,
350    /// The offset to apply, if any
351    offset: Option<usize>,
352    /// The limit to apply, if any
353    limit: Option<usize>,
354}
355
356impl LimitedReadPlanBuilder {
357    /// Create a new `LimitedReadPlanBuilder` from the existing builder and number of rows
358    fn new(inner: ReadPlanBuilder, row_count: usize) -> Self {
359        Self {
360            inner,
361            row_count,
362            offset: None,
363            limit: None,
364        }
365    }
366
367    /// Set the offset to apply to the read plan
368    pub(crate) fn with_offset(mut self, offset: Option<usize>) -> Self {
369        self.offset = offset;
370        self
371    }
372
373    /// Set the limit to apply to the read plan
374    pub(crate) fn with_limit(mut self, limit: Option<usize>) -> Self {
375        self.limit = limit;
376        self
377    }
378
379    /// Apply offset and limit, updating the selection on the underlying builder
380    /// and returning it.
381    pub(crate) fn build_limited(self) -> ReadPlanBuilder {
382        let Self {
383            mut inner,
384            row_count,
385            offset,
386            limit,
387        } = self;
388
389        // If the selection is empty, truncate
390        if !inner.selects_any() {
391            inner.selection = Some(RowSelection::from(vec![]));
392        }
393
394        // If an offset is defined, apply it to the `selection`
395        if let Some(offset) = offset {
396            inner.selection = Some(match row_count.checked_sub(offset) {
397                None => RowSelection::from(vec![]),
398                Some(remaining) => inner
399                    .selection
400                    .map(|selection| selection.offset(offset))
401                    .unwrap_or_else(|| {
402                        RowSelection::from(vec![
403                            RowSelector::skip(offset),
404                            RowSelector::select(remaining),
405                        ])
406                    }),
407            });
408        }
409
410        // If a limit is defined, apply it to the final `selection`
411        if let Some(limit) = limit {
412            inner.selection = Some(
413                inner
414                    .selection
415                    .map(|selection| selection.limit(limit))
416                    .unwrap_or_else(|| {
417                        RowSelection::from(vec![RowSelector::select(limit.min(row_count))])
418                    }),
419            );
420        }
421
422        inner
423    }
424}
425
426fn filters_to_boolean_buffer(filters: &[BooleanArray]) -> BooleanBuffer {
427    let total_rows = filters.iter().map(|f| f.len()).sum();
428    let mut builder = BooleanBufferBuilder::new(total_rows);
429    for filter in filters {
430        assert_eq!(filter.null_count(), 0);
431        builder.append_buffer(filter.values());
432    }
433    builder.finish()
434}
435
436/// A plan reading specific rows from a Parquet Row Group.
437///
438/// See [`ReadPlanBuilder`] to create `ReadPlan`s
439#[derive(Debug)]
440pub struct ReadPlan {
441    /// The number of rows to read in each batch
442    batch_size: usize,
443    /// Row ranges to be selected from the data source
444    row_selection_cursor: RowSelectionCursor,
445}
446
447impl ReadPlan {
448    /// Returns a mutable reference to the row selection cursor
449    pub fn row_selection_cursor_mut(&mut self) -> &mut RowSelectionCursor {
450        &mut self.row_selection_cursor
451    }
452
453    /// Return the number of rows to read in each output batch
454    #[inline(always)]
455    pub fn batch_size(&self) -> usize {
456        self.batch_size
457    }
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    fn builder_with_selection(selection: RowSelection) -> ReadPlanBuilder {
465        ReadPlanBuilder::new(1024).with_selection(Some(selection))
466    }
467
468    #[test]
469    fn preferred_selection_strategy_prefers_mask_by_default() {
470        let selection = RowSelection::from(vec![RowSelector::select(8)]);
471        let builder = builder_with_selection(selection);
472        assert_eq!(
473            builder.resolve_selection_strategy(),
474            RowSelectionStrategy::Mask
475        );
476    }
477
478    #[test]
479    fn preferred_selection_strategy_prefers_selectors_when_threshold_small() {
480        let selection = RowSelection::from(vec![RowSelector::select(8)]);
481        let builder = builder_with_selection(selection)
482            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 1 });
483        assert_eq!(
484            builder.resolve_selection_strategy(),
485            RowSelectionStrategy::Selectors
486        );
487    }
488
489    #[test]
490    fn preferred_selection_strategy_handles_dense_mask_backing() {
491        let bits: Vec<_> = (0..16).map(|i| i % 2 == 0).collect();
492        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
493        let builder = builder_with_selection(selection)
494            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 4 });
495        assert_eq!(
496            builder.resolve_selection_strategy(),
497            RowSelectionStrategy::Mask
498        );
499    }
500
501    #[test]
502    fn preferred_selection_strategy_handles_sparse_mask_backing() {
503        let bits: Vec<_> = (0..128).map(|i| i < 64).collect();
504        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
505        let builder = builder_with_selection(selection)
506            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 32 });
507        assert_eq!(
508            builder.resolve_selection_strategy(),
509            RowSelectionStrategy::Selectors
510        );
511    }
512
513    #[test]
514    fn preferred_selection_strategy_preserves_mask_threshold_boundaries() {
515        let mask = BooleanBuffer::from(vec![true; 8]);
516
517        let empty = builder_with_selection(RowSelection::from_boolean_buffer(
518            BooleanBuffer::new_unset(0),
519        ));
520        assert_eq!(
521            empty.resolve_selection_strategy(),
522            RowSelectionStrategy::Mask
523        );
524
525        let disabled = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
526            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 0 });
527        assert_eq!(
528            disabled.resolve_selection_strategy(),
529            RowSelectionStrategy::Selectors
530        );
531
532        // Equality does not satisfy the policy's strict inequality: 8 < 1 * 8.
533        let equal = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
534            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 8 });
535        assert_eq!(
536            equal.resolve_selection_strategy(),
537            RowSelectionStrategy::Selectors
538        );
539
540        let above = builder_with_selection(RowSelection::from_boolean_buffer(mask))
541            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 9 });
542        assert_eq!(
543            above.resolve_selection_strategy(),
544            RowSelectionStrategy::Mask
545        );
546    }
547
548    #[test]
549    fn preferred_selection_strategy_mask_matches_selector_backing() {
550        use rand::{RngExt, rng};
551
552        let mut rand = rng();
553        for _ in 0..200 {
554            let len = rand.random_range(0..256);
555            let bits: Vec<_> = (0..len).map(|_| rand.random_bool(0.5)).collect();
556            let mask_backed = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits.clone()));
557            let selector_backed = RowSelection::from_filters(&[BooleanArray::from(bits)]);
558
559            for threshold in [0, 1, 2, 8, 32, 64, usize::MAX] {
560                assert_eq!(
561                    mask_backed.auto_selection_strategy(threshold),
562                    selector_backed.auto_selection_strategy(threshold),
563                    "strategy differs for len {len} and threshold {threshold}"
564                );
565            }
566        }
567    }
568
569    #[test]
570    fn mask_plan_trims_trailing_skips_before_chunking() {
571        let mut plan = ReadPlanBuilder::new(8)
572            .with_selection(Some(RowSelection::from(vec![
573                RowSelector::select(1),
574                RowSelector::skip(7),
575            ])))
576            .with_row_selection_policy(RowSelectionPolicy::Mask)
577            .build();
578        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
579            panic!("expected a Mask cursor");
580        };
581
582        let chunk = cursor.next_chunk(8).unwrap();
583        assert_eq!(chunk.chunk_rows, 1);
584        assert_eq!(chunk.selected_rows, 1);
585        assert!(cursor.is_empty());
586    }
587
588    #[test]
589    fn selectors_policy_lowers_mask_backed_selection() {
590        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
591            true, false, false, true, true,
592        ]));
593        let mut plan = ReadPlanBuilder::new(4)
594            .with_selection(Some(selection))
595            .with_row_selection_policy(RowSelectionPolicy::Selectors)
596            .build();
597        let RowSelectionCursor::Selectors(cursor) = plan.row_selection_cursor_mut() else {
598            panic!("expected a Selectors cursor");
599        };
600
601        assert_eq!(cursor.next_selector(), RowSelector::select(1));
602        assert_eq!(cursor.next_selector(), RowSelector::skip(2));
603        assert_eq!(cursor.next_selector(), RowSelector::select(2));
604        assert!(cursor.is_empty());
605    }
606
607    #[test]
608    fn mask_backed_plan_respects_loaded_row_ranges() {
609        // Start from a non-byte-aligned BooleanBuffer::slice so the mask-backed
610        // cursor path (new_mask_from_buffer) is exercised with a bit offset.
611        // Rows 0 and 11 of 12 are selected; pages for rows [4, 10) are unloaded.
612        let mut bits = vec![false; 5];
613        bits.push(true); // row 0
614        bits.extend(std::iter::repeat_n(false, 10)); // rows 1..=10
615        bits.push(true); // row 11
616        bits.extend([true, false, true]); // trailing padding outside the slice
617        let mask = BooleanBuffer::from(bits).slice(5, 12);
618        let selection = RowSelection::from_boolean_buffer(mask);
619        assert!(selection.as_mask().is_some());
620
621        let loaded = LoadedRowRanges::from_selection(RowSelection::from(vec![
622            RowSelector::select(4),
623            RowSelector::skip(6),
624            RowSelector::select(2),
625        ]));
626
627        let mut plan = ReadPlanBuilder::new(12)
628            .with_selection(Some(selection))
629            .with_row_selection_policy(RowSelectionPolicy::Mask)
630            .with_loaded_row_ranges(Some(loaded))
631            .build();
632        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
633            panic!("expected a Mask cursor");
634        };
635
636        // The first chunk stops at its final selected row instead of carrying
637        // trailing skipped rows to the loaded range boundary.
638        let first = cursor.next_chunk(12).unwrap();
639        assert_eq!(first.initial_skip, 0);
640        assert_eq!(first.chunk_rows, 1);
641        assert_eq!(first.selected_rows, 1);
642
643        // The second chunk skips directly to the next selected row.
644        let second = cursor.next_chunk(12).unwrap();
645        assert_eq!(second.initial_skip, 10);
646        assert_eq!(second.chunk_rows, 1);
647        assert_eq!(second.selected_rows, 1);
648        assert!(cursor.is_empty());
649    }
650
651    #[test]
652    fn with_predicate_options_limit_pads_tail_when_no_prior_selection() {
653        use crate::arrow::ProjectionMask;
654        use crate::arrow::array_reader::StructArrayReader;
655        use crate::arrow::array_reader::test_util::make_int32_page_reader;
656        use crate::arrow::arrow_reader::ArrowPredicateFn;
657        use arrow_schema::{DataType as ArrowType, Field, Fields};
658
659        // 100 rows, all match the predicate. Limit stops the loop after 10
660        // matches — but the resulting RowSelection must still describe the
661        // full 100-row row group (90 trailing rows as "not selected"), not
662        // only the 10 rows we happened to evaluate before breaking.
663        const TOTAL_ROWS: usize = 100;
664        const LIMIT: usize = 10;
665
666        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
667        let levels = vec![0; TOTAL_ROWS];
668        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
669        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
670            "c0",
671            ArrowType::Int32,
672            false,
673        )]));
674        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
675
676        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
677            Ok(BooleanArray::from(vec![true; batch.num_rows()]))
678        });
679
680        let builder = ReadPlanBuilder::new(16)
681            .with_predicate_options(
682                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
683                    .with_limit(LIMIT, TOTAL_ROWS),
684            )
685            .unwrap();
686
687        let selection = builder
688            .selection()
689            .expect("limit-driven early break must produce a selection");
690
691        // `row_count` counts selected rows — must equal the limit.
692        assert_eq!(selection.row_count(), LIMIT);
693
694        // Total rows covered (selects + skips) must equal the full row group
695        // so downstream offset/limit math stays in absolute-row space.
696        assert_eq!(
697            selection.total_row_count(),
698            TOTAL_ROWS,
699            "selection must span the full row group, not only the prefix evaluated before the limit"
700        );
701    }
702
703    #[test]
704    fn with_predicate_options_preserves_mask_selection() {
705        use crate::arrow::ProjectionMask;
706        use crate::arrow::array_reader::StructArrayReader;
707        use crate::arrow::array_reader::test_util::make_int32_page_reader;
708        use crate::arrow::arrow_reader::ArrowPredicateFn;
709        use arrow_schema::{DataType as ArrowType, Field, Fields};
710
711        let data: Vec<i32> = (0..6).collect();
712        let levels = vec![0; data.len()];
713        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
714        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
715            "c0",
716            ArrowType::Int32,
717            false,
718        )]));
719        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
720
721        let prior = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
722            true, false, true, true, false, true,
723        ]));
724        let mut filters = vec![BooleanArray::from(vec![true, false, true, false])];
725        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
726            assert_eq!(batch.num_rows(), 4);
727            Ok(filters.remove(0))
728        });
729
730        let builder = ReadPlanBuilder::new(16)
731            .with_selection(Some(prior))
732            .with_predicate_options(PredicateOptions::new(
733                Box::new(struct_reader),
734                &mut predicate,
735            ))
736            .unwrap();
737
738        let selection = builder.selection().unwrap();
739        assert!(selection.as_mask().is_some());
740
741        let expected = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
742            true, false, false, true, false, false,
743        ]));
744        assert_eq!(selection, &expected);
745    }
746
747    #[test]
748    fn with_predicate_options_limit_handles_null_filters() {
749        use crate::arrow::ProjectionMask;
750        use crate::arrow::array_reader::StructArrayReader;
751        use crate::arrow::array_reader::test_util::make_int32_page_reader;
752        use crate::arrow::arrow_reader::ArrowPredicateFn;
753        use arrow_schema::{DataType as ArrowType, Field, Fields};
754
755        const TOTAL_ROWS: usize = 100;
756        const LIMIT: usize = 10;
757
758        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
759        let levels = vec![0; TOTAL_ROWS];
760        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
761        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
762            "c0",
763            ArrowType::Int32,
764            false,
765        )]));
766        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
767
768        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
769            Ok((0..batch.num_rows())
770                .map(|i| match i % 4 {
771                    0 | 2 => Some(true),
772                    1 => None,
773                    _ => Some(false),
774                })
775                .collect::<BooleanArray>())
776        });
777
778        let builder = ReadPlanBuilder::new(16)
779            .with_predicate_options(
780                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
781                    .with_limit(LIMIT, TOTAL_ROWS),
782            )
783            .unwrap();
784
785        let selection = builder
786            .selection()
787            .expect("limit-driven early break must produce a selection");
788
789        assert_eq!(selection.row_count(), LIMIT);
790
791        assert_eq!(selection.total_row_count(), TOTAL_ROWS);
792    }
793}