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, 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        self.row_selection_policy.resolve(self.selection.as_ref())
161    }
162
163    /// Evaluates an [`ArrowPredicate`], updating this plan's `selection`
164    ///
165    /// If the current `selection` is `Some`, the resulting [`RowSelection`]
166    /// will be the conjunction of the existing selection and the rows selected
167    /// by `predicate`.
168    ///
169    /// Note: pre-existing selections may come from evaluating a previous predicate
170    /// or if the [`ParquetRecordBatchReader`] specified an explicit
171    /// [`RowSelection`] in addition to one or more predicates.
172    pub fn with_predicate(
173        self,
174        array_reader: Box<dyn ArrayReader>,
175        predicate: &mut dyn ArrowPredicate,
176    ) -> Result<Self> {
177        self.with_predicate_options(PredicateOptions::new(array_reader, predicate))
178    }
179
180    /// Evaluates an [`ArrowPredicate`] with the given [`PredicateOptions`],
181    /// updating this plan's `selection`.
182    ///
183    /// Like [`Self::with_predicate`], but allows additional options such as a
184    /// match-count limit for early termination (see
185    /// [`PredicateOptions::with_limit`]).
186    pub fn with_predicate_options(mut self, options: PredicateOptions<'_>) -> Result<Self> {
187        let PredicateOptions {
188            array_reader,
189            predicate,
190            limit,
191            total_rows,
192        } = options;
193
194        // Target length for the concatenated filter output:
195        // - Prior selection ⇒ the reader yields that many rows; `and_then`
196        //   below requires the filter output to match.
197        // - No prior selection ⇒ the reader yields `total_rows`. We only
198        //   need to pad when `limit` may short-circuit the loop; otherwise
199        //   iteration naturally exhausts.
200        let expected_rows = match self.selection.as_ref() {
201            Some(s) => Some(s.row_count()),
202            None => limit.map(|_| total_rows),
203        };
204
205        let reader = ParquetRecordBatchReader::new(array_reader, self.clone().build());
206        let mut filters = vec![];
207        let mut processed_rows: usize = 0;
208        let mut matched_rows: usize = 0;
209        for maybe_batch in reader {
210            let maybe_batch = maybe_batch?;
211            let input_rows = maybe_batch.num_rows();
212            let filter = predicate.evaluate(maybe_batch)?;
213            // Since user supplied predicate, check error here to catch bugs quickly
214            if filter.len() != input_rows {
215                return Err(arrow_err!(
216                    "ArrowPredicate predicate returned {} rows, expected {input_rows}",
217                    filter.len()
218                ));
219            }
220            let filter = match filter.null_count() {
221                0 => filter,
222                // RowSelection::from_filters expects non-null filters. Convert
223                // NULL predicate results to false so they are not selected.
224                _ => prep_null_mask_filter(&filter),
225            };
226
227            processed_rows += input_rows;
228
229            match limit {
230                Some(limit) if limit - matched_rows <= filter.len() => {
231                    let truncated = filter.take_n_true(limit - matched_rows);
232                    matched_rows += truncated.true_count();
233                    filters.push(truncated);
234                    if matched_rows >= limit {
235                        break;
236                    }
237                }
238                _ => {
239                    matched_rows += filter.true_count();
240                    filters.push(filter);
241                }
242            }
243        }
244
245        // Pad the tail so the filters cover `expected_rows` total. This keeps
246        // the invariant that the resulting `RowSelection` spans every row the
247        // reader would have produced — rows past the early break are marked
248        // "not selected". When no limit is set the loop always exhausts and
249        // no padding is needed.
250        if let Some(expected) = expected_rows
251            && processed_rows < expected
252        {
253            let pad_len = expected - processed_rows;
254            filters.push(BooleanArray::new(BooleanBuffer::new_unset(pad_len), None));
255        }
256
257        // If the predicate selected all rows, applying it is a no-op. With no
258        // prior selection this keeps selection as None, enabling coalesced page
259        // fetches; with a prior selection it avoids rebuilding the same
260        // selection.
261        let all_selected = filters.iter().all(|f| f.true_count() == f.len());
262        if all_selected {
263            return Ok(self);
264        }
265        let raw = match (self.selection.as_ref(), self.row_selection_policy) {
266            (Some(selection), _) if selection.as_mask().is_some() => {
267                RowSelection::from_filters_mask(&filters)
268            }
269            (None, RowSelectionPolicy::Auto { threshold }) => {
270                RowSelection::from_filters_auto(&filters, threshold)
271            }
272            _ => RowSelection::from_filters(&filters),
273        };
274        self.selection = match self.selection.take() {
275            Some(selection) => Some(selection.and_then(&raw)),
276            None => Some(raw),
277        };
278        Ok(self)
279    }
280
281    /// Create a final `ReadPlan` the read plan for the scan
282    pub fn build(mut self) -> ReadPlan {
283        // If selection is empty, truncate
284        if !self.selects_any() {
285            self.selection = Some(RowSelection::from(vec![]));
286        }
287
288        // Preferred strategy must not be Auto
289        let selection_strategy = self.resolve_selection_strategy();
290
291        let Self {
292            batch_size,
293            selection,
294            row_selection_policy: _,
295            loaded_row_ranges,
296        } = self;
297
298        let row_selection_cursor = selection
299            .map(|s| selection_strategy.build_cursor(s.trim(), loaded_row_ranges))
300            .unwrap_or_else(RowSelectionCursor::new_all);
301
302        ReadPlan {
303            batch_size,
304            row_selection_cursor,
305        }
306    }
307}
308
309/// Builder for [`ReadPlan`] that applies a limit and offset to the read plan
310///
311/// See [`ReadPlanBuilder::limited`] to create this builder.
312pub(crate) struct LimitedReadPlanBuilder {
313    /// The underlying builder
314    inner: ReadPlanBuilder,
315    /// Total number of rows in the row group before the selection, limit or
316    /// offset are applied
317    row_count: usize,
318    /// The offset to apply, if any
319    offset: Option<usize>,
320    /// The limit to apply, if any
321    limit: Option<usize>,
322}
323
324impl LimitedReadPlanBuilder {
325    /// Create a new `LimitedReadPlanBuilder` from the existing builder and number of rows
326    fn new(inner: ReadPlanBuilder, row_count: usize) -> Self {
327        Self {
328            inner,
329            row_count,
330            offset: None,
331            limit: None,
332        }
333    }
334
335    /// Set the offset to apply to the read plan
336    pub(crate) fn with_offset(mut self, offset: Option<usize>) -> Self {
337        self.offset = offset;
338        self
339    }
340
341    /// Set the limit to apply to the read plan
342    pub(crate) fn with_limit(mut self, limit: Option<usize>) -> Self {
343        self.limit = limit;
344        self
345    }
346
347    /// Apply offset and limit, updating the selection on the underlying builder
348    /// and returning it.
349    pub(crate) fn build_limited(self) -> ReadPlanBuilder {
350        let Self {
351            mut inner,
352            row_count,
353            offset,
354            limit,
355        } = self;
356
357        // If the selection is empty, truncate
358        if !inner.selects_any() {
359            inner.selection = Some(RowSelection::from(vec![]));
360        }
361
362        // If an offset is defined, apply it to the `selection`
363        if let Some(offset) = offset {
364            inner.selection = Some(match row_count.checked_sub(offset) {
365                None => RowSelection::from(vec![]),
366                Some(remaining) => inner
367                    .selection
368                    .map(|selection| selection.offset(offset))
369                    .unwrap_or_else(|| {
370                        RowSelection::from(vec![
371                            RowSelector::skip(offset),
372                            RowSelector::select(remaining),
373                        ])
374                    }),
375            });
376        }
377
378        // If a limit is defined, apply it to the final `selection`
379        if let Some(limit) = limit {
380            inner.selection = Some(
381                inner
382                    .selection
383                    .map(|selection| selection.limit(limit))
384                    .unwrap_or_else(|| {
385                        RowSelection::from(vec![RowSelector::select(limit.min(row_count))])
386                    }),
387            );
388        }
389
390        inner
391    }
392}
393
394/// A plan reading specific rows from a Parquet Row Group.
395///
396/// See [`ReadPlanBuilder`] to create `ReadPlan`s
397#[derive(Debug)]
398pub struct ReadPlan {
399    /// The number of rows to read in each batch
400    batch_size: usize,
401    /// Row ranges to be selected from the data source
402    row_selection_cursor: RowSelectionCursor,
403}
404
405impl ReadPlan {
406    /// Returns a mutable reference to the row selection cursor
407    pub fn row_selection_cursor_mut(&mut self) -> &mut RowSelectionCursor {
408        &mut self.row_selection_cursor
409    }
410
411    /// Return the number of rows to read in each output batch
412    #[inline(always)]
413    pub fn batch_size(&self) -> usize {
414        self.batch_size
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    const DEFAULT_AUTO_THRESHOLD: usize = 32;
423
424    fn builder_with_selection(selection: RowSelection) -> ReadPlanBuilder {
425        ReadPlanBuilder::new(1024).with_selection(Some(selection))
426    }
427
428    fn predicate_plan(
429        pattern: Vec<bool>,
430        batch_size: usize,
431        limit: Option<usize>,
432    ) -> ReadPlanBuilder {
433        use crate::arrow::ProjectionMask;
434        use crate::arrow::array_reader::StructArrayReader;
435        use crate::arrow::array_reader::test_util::make_int32_page_reader;
436        use crate::arrow::arrow_reader::ArrowPredicateFn;
437        use arrow_schema::{DataType as ArrowType, Field, Fields};
438
439        let total_rows = pattern.len();
440        let data: Vec<i32> = (0..total_rows as i32).collect();
441        let levels = vec![0; total_rows];
442        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
443        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
444            "c0",
445            ArrowType::Int32,
446            false,
447        )]));
448        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
449
450        let mut offset = 0usize;
451        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
452            let end = offset + batch.num_rows();
453            let filter = BooleanArray::from(pattern[offset..end].to_vec());
454            offset = end;
455            Ok(filter)
456        });
457        let options = PredicateOptions::new(Box::new(struct_reader), &mut predicate);
458        let options = match limit {
459            Some(limit) => options.with_limit(limit, total_rows),
460            None => options,
461        };
462
463        ReadPlanBuilder::new(batch_size)
464            .with_predicate_options(options)
465            .unwrap()
466    }
467
468    fn first_n_matches(pattern: &[bool], limit: usize) -> Vec<bool> {
469        let mut remaining = limit;
470        pattern
471            .iter()
472            .map(|selected| {
473                if *selected && remaining != 0 {
474                    remaining -= 1;
475                    true
476                } else {
477                    false
478                }
479            })
480            .collect()
481    }
482
483    fn assert_limit_case(name: &str, pattern: Vec<bool>, batch_size: usize, limit: usize) {
484        let expected_bits = first_n_matches(&pattern, limit);
485        let expected = RowSelection::from_filters(&[BooleanArray::from(expected_bits)]);
486        let builder = predicate_plan(pattern, batch_size, Some(limit));
487        let actual = builder
488            .selection()
489            .unwrap_or_else(|| panic!("{name}: limited mixed predicate must produce a selection"));
490
491        assert_eq!(actual, &expected, "{name}: logical selection");
492
493        let current_strategy = expected.auto_selection_strategy(DEFAULT_AUTO_THRESHOLD);
494        assert_eq!(
495            builder.resolve_selection_strategy(),
496            current_strategy,
497            "{name}: Auto strategy"
498        );
499        assert_eq!(
500            actual.as_mask().is_some(),
501            current_strategy == RowSelectionStrategy::Mask,
502            "{name}: backing selected by capped Auto"
503        );
504    }
505
506    #[test]
507    fn preferred_selection_strategy_prefers_mask_by_default() {
508        let selection = RowSelection::from(vec![RowSelector::select(8)]);
509        let builder = builder_with_selection(selection);
510        assert_eq!(
511            builder.resolve_selection_strategy(),
512            RowSelectionStrategy::Mask
513        );
514    }
515
516    #[test]
517    fn preferred_selection_strategy_prefers_selectors_when_threshold_small() {
518        let selection = RowSelection::from(vec![RowSelector::select(8)]);
519        let builder = builder_with_selection(selection)
520            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 1 });
521        assert_eq!(
522            builder.resolve_selection_strategy(),
523            RowSelectionStrategy::Selectors
524        );
525    }
526
527    #[test]
528    fn preferred_selection_strategy_handles_dense_mask_backing() {
529        let bits: Vec<_> = (0..16).map(|i| i % 2 == 0).collect();
530        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
531        let builder = builder_with_selection(selection)
532            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 4 });
533        assert_eq!(
534            builder.resolve_selection_strategy(),
535            RowSelectionStrategy::Mask
536        );
537    }
538
539    #[test]
540    fn preferred_selection_strategy_handles_sparse_mask_backing() {
541        let bits: Vec<_> = (0..128).map(|i| i < 64).collect();
542        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits));
543        let builder = builder_with_selection(selection)
544            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 32 });
545        assert_eq!(
546            builder.resolve_selection_strategy(),
547            RowSelectionStrategy::Selectors
548        );
549    }
550
551    #[test]
552    fn preferred_selection_strategy_preserves_mask_threshold_boundaries() {
553        let mask = BooleanBuffer::from(vec![true; 8]);
554
555        let empty = builder_with_selection(RowSelection::from_boolean_buffer(
556            BooleanBuffer::new_unset(0),
557        ));
558        assert_eq!(
559            empty.resolve_selection_strategy(),
560            RowSelectionStrategy::Mask
561        );
562
563        let disabled = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
564            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 0 });
565        assert_eq!(
566            disabled.resolve_selection_strategy(),
567            RowSelectionStrategy::Selectors
568        );
569
570        // Equality does not satisfy the policy's strict inequality: 8 < 1 * 8.
571        let equal = builder_with_selection(RowSelection::from_boolean_buffer(mask.clone()))
572            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 8 });
573        assert_eq!(
574            equal.resolve_selection_strategy(),
575            RowSelectionStrategy::Selectors
576        );
577
578        let above = builder_with_selection(RowSelection::from_boolean_buffer(mask))
579            .with_row_selection_policy(RowSelectionPolicy::Auto { threshold: 9 });
580        assert_eq!(
581            above.resolve_selection_strategy(),
582            RowSelectionStrategy::Mask
583        );
584    }
585
586    #[test]
587    #[cfg_attr(miri, ignore)] // Takes too long
588    fn preferred_selection_strategy_mask_matches_selector_backing() {
589        use rand::{RngExt, rng};
590
591        let mut rand = rng();
592        for _ in 0..200 {
593            let len = rand.random_range(0..256);
594            let bits: Vec<_> = (0..len).map(|_| rand.random_bool(0.5)).collect();
595            let mask_backed = RowSelection::from_boolean_buffer(BooleanBuffer::from(bits.clone()));
596            let selector_backed = RowSelection::from_filters(&[BooleanArray::from(bits)]);
597
598            for threshold in [0, 1, 2, 8, 32, 64, usize::MAX] {
599                assert_eq!(
600                    mask_backed.auto_selection_strategy(threshold),
601                    selector_backed.auto_selection_strategy(threshold),
602                    "strategy differs for len {len} and threshold {threshold}"
603                );
604            }
605        }
606    }
607
608    #[test]
609    fn mask_plan_trims_trailing_skips_before_chunking() {
610        let mut plan = ReadPlanBuilder::new(8)
611            .with_selection(Some(RowSelection::from(vec![
612                RowSelector::select(1),
613                RowSelector::skip(7),
614            ])))
615            .with_row_selection_policy(RowSelectionPolicy::Mask)
616            .build();
617        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
618            panic!("expected a Mask cursor");
619        };
620
621        let chunk = cursor.next_chunk(8).unwrap();
622        assert_eq!(chunk.chunk_rows, 1);
623        assert_eq!(chunk.selected_rows, 1);
624        assert!(cursor.is_empty());
625    }
626
627    #[test]
628    fn selectors_policy_lowers_mask_backed_selection() {
629        let selection = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
630            true, false, false, true, true,
631        ]));
632        let mut plan = ReadPlanBuilder::new(4)
633            .with_selection(Some(selection))
634            .with_row_selection_policy(RowSelectionPolicy::Selectors)
635            .build();
636        let RowSelectionCursor::Selectors(cursor) = plan.row_selection_cursor_mut() else {
637            panic!("expected a Selectors cursor");
638        };
639
640        assert_eq!(cursor.next_selector(), RowSelector::select(1));
641        assert_eq!(cursor.next_selector(), RowSelector::skip(2));
642        assert_eq!(cursor.next_selector(), RowSelector::select(2));
643        assert!(cursor.is_empty());
644    }
645
646    #[test]
647    fn mask_backed_plan_respects_loaded_row_ranges() {
648        // Start from a non-byte-aligned BooleanBuffer::slice so the mask-backed
649        // cursor path (new_mask_from_buffer) is exercised with a bit offset.
650        // Rows 0 and 11 of 12 are selected; pages for rows [4, 10) are unloaded.
651        let mut bits = vec![false; 5];
652        bits.push(true); // row 0
653        bits.extend(std::iter::repeat_n(false, 10)); // rows 1..=10
654        bits.push(true); // row 11
655        bits.extend([true, false, true]); // trailing padding outside the slice
656        let mask = BooleanBuffer::from(bits).slice(5, 12);
657        let selection = RowSelection::from_boolean_buffer(mask);
658        assert!(selection.as_mask().is_some());
659
660        let loaded = LoadedRowRanges::from_selection(RowSelection::from(vec![
661            RowSelector::select(4),
662            RowSelector::skip(6),
663            RowSelector::select(2),
664        ]));
665
666        let mut plan = ReadPlanBuilder::new(12)
667            .with_selection(Some(selection))
668            .with_row_selection_policy(RowSelectionPolicy::Mask)
669            .with_loaded_row_ranges(Some(loaded))
670            .build();
671        let RowSelectionCursor::Mask(cursor) = plan.row_selection_cursor_mut() else {
672            panic!("expected a Mask cursor");
673        };
674
675        // The first chunk stops at its final selected row instead of carrying
676        // trailing skipped rows to the loaded range boundary.
677        let first = cursor.next_chunk(12).unwrap();
678        assert_eq!(first.initial_skip, 0);
679        assert_eq!(first.chunk_rows, 1);
680        assert_eq!(first.selected_rows, 1);
681
682        // The second chunk skips directly to the next selected row.
683        let second = cursor.next_chunk(12).unwrap();
684        assert_eq!(second.initial_skip, 10);
685        assert_eq!(second.chunk_rows, 1);
686        assert_eq!(second.selected_rows, 1);
687        assert!(cursor.is_empty());
688    }
689
690    #[test]
691    fn with_predicate_options_capped_auto_preserves_limit_and_padding_boundaries() {
692        let fragmented_early_limit = (0..37)
693            .map(|row| matches!(row, 0 | 3 | 7 | 9 | 12 | 18 | 24 | 36))
694            .collect();
695        assert_limit_case("fragmented early limit", fragmented_early_limit, 16, 3);
696
697        assert_limit_case(
698            "selector-friendly padded tail",
699            vec![true; 4_097],
700            1_024,
701            1_024,
702        );
703    }
704
705    #[test]
706    fn with_predicate_options_limit_pads_tail_when_no_prior_selection() {
707        use crate::arrow::ProjectionMask;
708        use crate::arrow::array_reader::StructArrayReader;
709        use crate::arrow::array_reader::test_util::make_int32_page_reader;
710        use crate::arrow::arrow_reader::ArrowPredicateFn;
711        use arrow_schema::{DataType as ArrowType, Field, Fields};
712
713        // 100 rows, all match the predicate. Limit stops the loop after 10
714        // matches — but the resulting RowSelection must still describe the
715        // full 100-row row group (90 trailing rows as "not selected"), not
716        // only the 10 rows we happened to evaluate before breaking.
717        const TOTAL_ROWS: usize = 100;
718        const LIMIT: usize = 10;
719
720        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
721        let levels = vec![0; TOTAL_ROWS];
722        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
723        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
724            "c0",
725            ArrowType::Int32,
726            false,
727        )]));
728        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
729
730        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
731            Ok(BooleanArray::from(vec![true; batch.num_rows()]))
732        });
733
734        let builder = ReadPlanBuilder::new(16)
735            .with_predicate_options(
736                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
737                    .with_limit(LIMIT, TOTAL_ROWS),
738            )
739            .unwrap();
740
741        let selection = builder
742            .selection()
743            .expect("limit-driven early break must produce a selection");
744
745        // `row_count` counts selected rows — must equal the limit.
746        assert_eq!(selection.row_count(), LIMIT);
747
748        // Total rows covered (selects + skips) must equal the full row group
749        // so downstream offset/limit math stays in absolute-row space.
750        assert_eq!(
751            selection.total_row_count(),
752            TOTAL_ROWS,
753            "selection must span the full row group, not only the prefix evaluated before the limit"
754        );
755    }
756
757    #[test]
758    fn with_predicate_options_preserves_mask_selection() {
759        use crate::arrow::ProjectionMask;
760        use crate::arrow::array_reader::StructArrayReader;
761        use crate::arrow::array_reader::test_util::make_int32_page_reader;
762        use crate::arrow::arrow_reader::ArrowPredicateFn;
763        use arrow_schema::{DataType as ArrowType, Field, Fields};
764
765        let data: Vec<i32> = (0..6).collect();
766        let levels = vec![0; data.len()];
767        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
768        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
769            "c0",
770            ArrowType::Int32,
771            false,
772        )]));
773        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
774
775        let prior = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
776            true, false, true, true, false, true,
777        ]));
778        let mut filters = vec![BooleanArray::from(vec![true, false, true, false])];
779        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), move |batch| {
780            assert_eq!(batch.num_rows(), 4);
781            Ok(filters.remove(0))
782        });
783
784        let builder = ReadPlanBuilder::new(16)
785            .with_selection(Some(prior))
786            .with_predicate_options(PredicateOptions::new(
787                Box::new(struct_reader),
788                &mut predicate,
789            ))
790            .unwrap();
791
792        let selection = builder.selection().unwrap();
793        assert!(selection.as_mask().is_some());
794
795        let expected = RowSelection::from_boolean_buffer(BooleanBuffer::from(vec![
796            true, false, false, true, false, false,
797        ]));
798        assert_eq!(selection, &expected);
799    }
800
801    #[test]
802    fn with_predicate_options_limit_handles_null_filters() {
803        use crate::arrow::ProjectionMask;
804        use crate::arrow::array_reader::StructArrayReader;
805        use crate::arrow::array_reader::test_util::make_int32_page_reader;
806        use crate::arrow::arrow_reader::ArrowPredicateFn;
807        use arrow_schema::{DataType as ArrowType, Field, Fields};
808
809        const TOTAL_ROWS: usize = 100;
810        const LIMIT: usize = 10;
811
812        let data: Vec<i32> = (0..TOTAL_ROWS as i32).collect();
813        let levels = vec![0; TOTAL_ROWS];
814        let leaf = make_int32_page_reader(&data, &levels, &levels, 0, 0, None);
815        let struct_type = ArrowType::Struct(Fields::from(vec![Field::new(
816            "c0",
817            ArrowType::Int32,
818            false,
819        )]));
820        let struct_reader = StructArrayReader::new(struct_type, vec![leaf], 0, 0, false, None);
821
822        let mut predicate = ArrowPredicateFn::new(ProjectionMask::all(), |batch| {
823            Ok((0..batch.num_rows())
824                .map(|i| match i % 4 {
825                    0 | 2 => Some(true),
826                    1 => None,
827                    _ => Some(false),
828                })
829                .collect::<BooleanArray>())
830        });
831
832        let builder = ReadPlanBuilder::new(16)
833            .with_predicate_options(
834                PredicateOptions::new(Box::new(struct_reader), &mut predicate)
835                    .with_limit(LIMIT, TOTAL_ROWS),
836            )
837            .unwrap();
838
839        let selection = builder
840            .selection()
841            .expect("limit-driven early break must produce a selection");
842
843        assert_eq!(selection.row_count(), LIMIT);
844
845        assert_eq!(selection.total_row_count(), TOTAL_ROWS);
846    }
847}