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