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;
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 = match (self.selection.as_ref(), self.row_selection_policy) {
276            (Some(selection), _) if selection.as_mask().is_some() => {
277                RowSelection::from_filters_mask(&filters)
278            }
279            (None, RowSelectionPolicy::Auto { threshold }) => {
280                RowSelection::from_filters_auto(&filters, threshold)
281            }
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
426/// A plan reading specific rows from a Parquet Row Group.
427///
428/// See [`ReadPlanBuilder`] to create `ReadPlan`s
429#[derive(Debug)]
430pub struct ReadPlan {
431    /// The number of rows to read in each batch
432    batch_size: usize,
433    /// Row ranges to be selected from the data source
434    row_selection_cursor: RowSelectionCursor,
435}
436
437impl ReadPlan {
438    /// Returns a mutable reference to the row selection cursor
439    pub fn row_selection_cursor_mut(&mut self) -> &mut RowSelectionCursor {
440        &mut self.row_selection_cursor
441    }
442
443    /// Return the number of rows to read in each output batch
444    #[inline(always)]
445    pub fn batch_size(&self) -> usize {
446        self.batch_size
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use super::*;
453
454    const DEFAULT_AUTO_THRESHOLD: usize = 32;
455
456    fn builder_with_selection(selection: RowSelection) -> ReadPlanBuilder {
457        ReadPlanBuilder::new(1024).with_selection(Some(selection))
458    }
459
460    fn predicate_plan(
461        pattern: Vec<bool>,
462        batch_size: usize,
463        limit: Option<usize>,
464    ) -> ReadPlanBuilder {
465        use crate::arrow::ProjectionMask;
466        use crate::arrow::array_reader::StructArrayReader;
467        use crate::arrow::array_reader::test_util::make_int32_page_reader;
468        use crate::arrow::arrow_reader::ArrowPredicateFn;
469        use arrow_schema::{DataType as ArrowType, Field, Fields};
470
471        let total_rows = pattern.len();
472        let data: Vec<i32> = (0..total_rows as i32).collect();
473        let levels = vec![0; total_rows];
474        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
475        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
476            "c0",
477            ArrowType::Int32,
478            false,
479        )]));
480        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
481
482        let mut offset = 0usize;
483        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
484            let end = offset + batch.num_rows();
485            let filter = BooleanArray::from(pattern[offset..end].to_vec());
486            offset = end;
487            Ok(filter)
488        });
489        let options = PredicateOptions::new(Box::new(struct_reader), &mut predicate);
490        let options = match limit {
491            Some(limit) => options.with_limit(limit, total_rows),
492            None => options,
493        };
494
495        ReadPlanBuilder::new(batch_size)
496            .with_predicate_options(options)
497            .unwrap()
498    }
499
500    fn first_n_matches(pattern: &[bool], limit: usize) -> Vec<bool> {
501        let mut remaining = limit;
502        pattern
503            .iter()
504            .map(|selected| {
505                if *selected && remaining != 0 {
506                    remaining -= 1;
507                    true
508                } else {
509                    false
510                }
511            })
512            .collect()
513    }
514
515    fn assert_limit_case(name: &str, pattern: Vec<bool>, batch_size: usize, limit: usize) {
516        let expected_bits = first_n_matches(&pattern, limit);
517        let expected = RowSelection::from_filters(&[BooleanArray::from(expected_bits)]);
518        let builder = predicate_plan(pattern, batch_size, Some(limit));
519        let actual = builder
520            .selection()
521            .unwrap_or_else(|| panic!("{name}: limited mixed predicate must produce a selection"));
522
523        assert_eq!(actual, &expected, "{name}: logical selection");
524
525        let current_strategy = expected.auto_selection_strategy(DEFAULT_AUTO_THRESHOLD);
526        assert_eq!(
527            builder.resolve_selection_strategy(),
528            current_strategy,
529            "{name}: Auto strategy"
530        );
531        assert_eq!(
532            actual.as_mask().is_some(),
533            current_strategy == RowSelectionStrategy::Mask,
534            "{name}: backing selected by capped Auto"
535        );
536    }
537
538    #[test]
539    fn preferred_selection_strategy_prefers_mask_by_default() {
540        let selection = RowSelection::from(vec![RowSelector::select(8)]);
541        let builder = builder_with_selection(selection);
542        assert_eq!(
543            builder.resolve_selection_strategy(),
544            RowSelectionStrategy::Mask
545        );
546    }
547
548    #[test]
549    fn preferred_selection_strategy_prefers_selectors_when_threshold_small() {
550        let selection = RowSelection::from(vec![RowSelector::select(8)]);
551        let builder = builder_with_selection(selection)
552            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 1 });
553        assert_eq!(
554            builder.resolve_selection_strategy(),
555            RowSelectionStrategy::Selectors
556        );
557    }
558
559    #[test]
560    fn preferred_selection_strategy_handles_dense_mask_backing() {
561        let bits: Vec<_> = (0..16).map(|i| i % 2 == 0).collect();
562        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
563        let builder = builder_with_selection(selection)
564            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 4 });
565        assert_eq!(
566            builder.resolve_selection_strategy(),
567            RowSelectionStrategy::Mask
568        );
569    }
570
571    #[test]
572    fn preferred_selection_strategy_handles_sparse_mask_backing() {
573        let bits: Vec<_> = (0..128).map(|i| i < 64).collect();
574        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
575        let builder = builder_with_selection(selection)
576            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 32 });
577        assert_eq!(
578            builder.resolve_selection_strategy(),
579            RowSelectionStrategy::Selectors
580        );
581    }
582
583    #[test]
584    fn preferred_selection_strategy_preserves_mask_threshold_boundaries() {
585        let mask = BooleanBuffer::from(vec![true; 8]);
586
587        let empty = builder_with_selection(RowSelection::from_boolean_buffer(
588            BooleanBuffer::new_unset(0),
589        ));
590        assert_eq!(
591            empty.resolve_selection_strategy(),
592            RowSelectionStrategy::Mask
593        );
594
595        let disabled = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
596            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 0 });
597        assert_eq!(
598            disabled.resolve_selection_strategy(),
599            RowSelectionStrategy::Selectors
600        );
601
602        // Equality does not satisfy the policy's strict inequality: 8 < 1 * 8.
603        let equal = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
604            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 8 });
605        assert_eq!(
606            equal.resolve_selection_strategy(),
607            RowSelectionStrategy::Selectors
608        );
609
610        let above = builder_with_selection(RowSelection::from_boolean_buffer(mask))
611            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 9 });
612        assert_eq!(
613            above.resolve_selection_strategy(),
614            RowSelectionStrategy::Mask
615        );
616    }
617
618    #[test]
619    #[cfg_attr(miri, ignore)] // Takes too long
620    fn preferred_selection_strategy_mask_matches_selector_backing() {
621        use rand::{RngExt, rng};
622
623        let mut rand = rng();
624        for _ in 0..200 {
625            let len = rand.random_range(0..256);
626            let bits: Vec<_> = (0..len).map(|_| rand.random_bool(0.5)).collect();
627            let mask_backed = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits.clone()));
628            let selector_backed = RowSelection::from_filters(&[BooleanArray::from(bits)]);
629
630            for threshold in [0, 1, 2, 8, 32, 64, usize::MAX] {
631                assert_eq!(
632                    mask_backed.auto_selection_strategy(threshold),
633                    selector_backed.auto_selection_strategy(threshold),
634                    "strategy differs for len {len} and threshold {threshold}"
635                );
636            }
637        }
638    }
639
640    #[test]
641    fn mask_plan_trims_trailing_skips_before_chunking() {
642        let mut plan = ReadPlanBuilder::new(8)
643            .with_selection(Some(RowSelection::from(vec![
644                RowSelector::select(1),
645                RowSelector::skip(7),
646            ])))
647            .with_row_selection_policy(RowSelectionPolicy::Mask)
648            .build();
649        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
650            panic!("expected a Mask cursor");
651        };
652
653        let chunk = cursor.next_chunk(8).unwrap();
654        assert_eq!(chunk.chunk_rows, 1);
655        assert_eq!(chunk.selected_rows, 1);
656        assert!(cursor.is_empty());
657    }
658
659    #[test]
660    fn selectors_policy_lowers_mask_backed_selection() {
661        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
662            true, false, false, true, true,
663        ]));
664        let mut plan = ReadPlanBuilder::new(4)
665            .with_selection(Some(selection))
666            .with_row_selection_policy(RowSelectionPolicy::Selectors)
667            .build();
668        let RowSelectionCursor::Selectors(cursor) = plan.row_selection_cursor_mut() else {
669            panic!("expected a Selectors cursor");
670        };
671
672        assert_eq!(cursor.next_selector(), RowSelector::select(1));
673        assert_eq!(cursor.next_selector(), RowSelector::skip(2));
674        assert_eq!(cursor.next_selector(), RowSelector::select(2));
675        assert!(cursor.is_empty());
676    }
677
678    #[test]
679    fn mask_backed_plan_respects_loaded_row_ranges() {
680        // Start from a non-byte-aligned BooleanBuffer::slice so the mask-backed
681        // cursor path (new_mask_from_buffer) is exercised with a bit offset.
682        // Rows 0 and 11 of 12 are selected; pages for rows [4, 10) are unloaded.
683        let mut bits = vec![false; 5];
684        bits.push(true); // row 0
685        bits.extend(std::iter::repeat_n(false, 10)); // rows 1..=10
686        bits.push(true); // row 11
687        bits.extend([true, false, true]); // trailing padding outside the slice
688        let mask = BooleanBuffer::from(bits).slice(5, 12);
689        let selection = RowSelection::from_boolean_buffer(mask);
690        assert!(selection.as_mask().is_some());
691
692        let loaded = LoadedRowRanges::from_selection(RowSelection::from(vec![
693            RowSelector::select(4),
694            RowSelector::skip(6),
695            RowSelector::select(2),
696        ]));
697
698        let mut plan = ReadPlanBuilder::new(12)
699            .with_selection(Some(selection))
700            .with_row_selection_policy(RowSelectionPolicy::Mask)
701            .with_loaded_row_ranges(Some(loaded))
702            .build();
703        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
704            panic!("expected a Mask cursor");
705        };
706
707        // The first chunk stops at its final selected row instead of carrying
708        // trailing skipped rows to the loaded range boundary.
709        let first = cursor.next_chunk(12).unwrap();
710        assert_eq!(first.initial_skip, 0);
711        assert_eq!(first.chunk_rows, 1);
712        assert_eq!(first.selected_rows, 1);
713
714        // The second chunk skips directly to the next selected row.
715        let second = cursor.next_chunk(12).unwrap();
716        assert_eq!(second.initial_skip, 10);
717        assert_eq!(second.chunk_rows, 1);
718        assert_eq!(second.selected_rows, 1);
719        assert!(cursor.is_empty());
720    }
721
722    #[test]
723    fn with_predicate_options_capped_auto_preserves_limit_and_padding_boundaries() {
724        let fragmented_early_limit = (0..37)
725            .map(|row| matches!(row, 0 | 3 | 7 | 9 | 12 | 18 | 24 | 36))
726            .collect();
727        assert_limit_case("fragmented early limit", fragmented_early_limit, 16, 3);
728
729        assert_limit_case(
730            "selector-friendly padded tail",
731            vec![true; 4_097],
732            1_024,
733            1_024,
734        );
735    }
736
737    #[test]
738    fn with_predicate_options_limit_pads_tail_when_no_prior_selection() {
739        use crate::arrow::ProjectionMask;
740        use crate::arrow::array_reader::StructArrayReader;
741        use crate::arrow::array_reader::test_util::make_int32_page_reader;
742        use crate::arrow::arrow_reader::ArrowPredicateFn;
743        use arrow_schema::{DataType as ArrowType, Field, Fields};
744
745        // 100 rows, all match the predicate. Limit stops the loop after 10
746        // matches — but the resulting RowSelection must still describe the
747        // full 100-row row group (90 trailing rows as "not selected"), not
748        // only the 10 rows we happened to evaluate before breaking.
749        const TOTAL_ROWS: usize = 100;
750        const LIMIT: usize = 10;
751
752        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
753        let levels = vec![0; TOTAL_ROWS];
754        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
755        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
756            "c0",
757            ArrowType::Int32,
758            false,
759        )]));
760        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
761
762        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
763            Ok(BooleanArray::from(vec![true; batch.num_rows()]))
764        });
765
766        let builder = ReadPlanBuilder::new(16)
767            .with_predicate_options(
768                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
769                    .with_limit(LIMIT, TOTAL_ROWS),
770            )
771            .unwrap();
772
773        let selection = builder
774            .selection()
775            .expect("limit-driven early break must produce a selection");
776
777        // `row_count` counts selected rows — must equal the limit.
778        assert_eq!(selection.row_count(), LIMIT);
779
780        // Total rows covered (selects + skips) must equal the full row group
781        // so downstream offset/limit math stays in absolute-row space.
782        assert_eq!(
783            selection.total_row_count(),
784            TOTAL_ROWS,
785            "selection must span the full row group, not only the prefix evaluated before the limit"
786        );
787    }
788
789    #[test]
790    fn with_predicate_options_preserves_mask_selection() {
791        use crate::arrow::ProjectionMask;
792        use crate::arrow::array_reader::StructArrayReader;
793        use crate::arrow::array_reader::test_util::make_int32_page_reader;
794        use crate::arrow::arrow_reader::ArrowPredicateFn;
795        use arrow_schema::{DataType as ArrowType, Field, Fields};
796
797        let data: Vec<i32> = (0..6).collect();
798        let levels = vec![0; data.len()];
799        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
800        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
801            "c0",
802            ArrowType::Int32,
803            false,
804        )]));
805        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
806
807        let prior = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
808            true, false, true, true, false, true,
809        ]));
810        let mut filters = vec![BooleanArray::from(vec![true, false, true, false])];
811        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
812            assert_eq!(batch.num_rows(), 4);
813            Ok(filters.remove(0))
814        });
815
816        let builder = ReadPlanBuilder::new(16)
817            .with_selection(Some(prior))
818            .with_predicate_options(PredicateOptions::new(
819                Box::new(struct_reader),
820                &mut predicate,
821            ))
822            .unwrap();
823
824        let selection = builder.selection().unwrap();
825        assert!(selection.as_mask().is_some());
826
827        let expected = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
828            true, false, false, true, false, false,
829        ]));
830        assert_eq!(selection, &expected);
831    }
832
833    #[test]
834    fn with_predicate_options_limit_handles_null_filters() {
835        use crate::arrow::ProjectionMask;
836        use crate::arrow::array_reader::StructArrayReader;
837        use crate::arrow::array_reader::test_util::make_int32_page_reader;
838        use crate::arrow::arrow_reader::ArrowPredicateFn;
839        use arrow_schema::{DataType as ArrowType, Field, Fields};
840
841        const TOTAL_ROWS: usize = 100;
842        const LIMIT: usize = 10;
843
844        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
845        let levels = vec![0; TOTAL_ROWS];
846        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
847        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
848            "c0",
849            ArrowType::Int32,
850            false,
851        )]));
852        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
853
854        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
855            Ok((0..batch.num_rows())
856                .map(|i| match i % 4 {
857                    0 | 2 => Some(true),
858                    1 => None,
859                    _ => Some(false),
860                })
861                .collect::<BooleanArray>())
862        });
863
864        let builder = ReadPlanBuilder::new(16)
865            .with_predicate_options(
866                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
867                    .with_limit(LIMIT, TOTAL_ROWS),
868            )
869            .unwrap();
870
871        let selection = builder
872            .selection()
873            .expect("limit-driven early break must produce a selection");
874
875        assert_eq!(selection.row_count(), LIMIT);
876
877        assert_eq!(selection.total_row_count(), TOTAL_ROWS);
878    }
879}