1use 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
34pub 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 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 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#[derive(Clone, Debug)]
82pub struct ReadPlanBuilder {
83 batch_size: usize,
84 selection: Option<RowSelection>,
86 row_selection_policy: RowSelectionPolicy,
88 loaded_row_ranges: Option<Arc<LoadedRowRanges>>,
90}
91
92impl ReadPlanBuilder {
93 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 pub fn with_selection(mut self, selection: Option<RowSelection>) -> Self {
105 self.selection = selection;
106 self
107 }
108
109 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 pub fn row_selection_policy(&self) -> &RowSelectionPolicy {
124 &self.row_selection_policy
125 }
126
127 pub fn selection(&self) -> Option<&RowSelection> {
129 self.selection.as_ref()
130 }
131
132 pub(crate) fn limited(self, row_count: usize) -> LimitedReadPlanBuilder {
140 LimitedReadPlanBuilder::new(self, row_count)
141 }
142
143 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 pub fn num_rows_selected(&self) -> Option<usize> {
153 self.selection.as_ref().map(|s| s.row_count())
154 }
155
156 pub(crate) fn resolve_selection_strategy(&self) -> RowSelectionStrategy {
160 self.row_selection_policy.resolve(self.selection.as_ref())
161 }
162
163 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 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 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 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 _ => 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 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 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 pub fn build(mut self) -> ReadPlan {
283 if !self.selects_any() {
285 self.selection = Some(RowSelection::from(vec![]));
286 }
287
288 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
309pub(crate) struct LimitedReadPlanBuilder {
313 inner: ReadPlanBuilder,
315 row_count: usize,
318 offset: Option<usize>,
320 limit: Option<usize>,
322}
323
324impl LimitedReadPlanBuilder {
325 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 pub(crate) fn with_offset(mut self, offset: Option<usize>) -> Self {
337 self.offset = offset;
338 self
339 }
340
341 pub(crate) fn with_limit(mut self, limit: Option<usize>) -> Self {
343 self.limit = limit;
344 self
345 }
346
347 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 !inner.selects_any() {
359 inner.selection = Some(RowSelection::from(vec![]));
360 }
361
362 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 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#[derive(Debug)]
398pub struct ReadPlan {
399 batch_size: usize,
401 row_selection_cursor: RowSelectionCursor,
403}
404
405impl ReadPlan {
406 pub fn row_selection_cursor_mut(&mut self) -> &mut RowSelectionCursor {
408 &mut self.row_selection_cursor
409 }
410
411 #[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 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)] 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 let mut bits = vec![false; 5];
652 bits.push(true); bits.extend(std::iter::repeat_n(false, 10)); bits.push(true); bits.extend([true, false, true]); 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 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 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 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 assert_eq!(selection.row_count(), LIMIT);
747
748 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}