Skip to main content

vortex_layout/scan/
scan_builder.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::ops::Range;
5use std::pin::Pin;
6use std::sync::Arc;
7use std::task::Context;
8use std::task::Poll;
9use std::task::ready;
10
11use futures::Stream;
12use futures::future::BoxFuture;
13use futures::stream::BoxStream;
14use itertools::Itertools;
15use vortex_array::ArrayRef;
16use vortex_array::dtype::DType;
17use vortex_array::dtype::FieldMask;
18use vortex_array::expr::BoundExpression;
19use vortex_array::expr::analysis::referenced_field_paths;
20use vortex_array::expr::transform::find_between;
21use vortex_array::iter::ArrayIterator;
22use vortex_array::iter::ArrayIteratorAdapter;
23use vortex_array::stream::ArrayStream;
24use vortex_array::stream::ArrayStreamAdapter;
25use vortex_error::VortexExpect;
26use vortex_error::VortexResult;
27use vortex_io::runtime::BlockingRuntime;
28use vortex_io::runtime::Task;
29use vortex_io::session::RuntimeSessionExt;
30use vortex_metrics::MetricsRegistry;
31use vortex_scan::selection::Selection;
32use vortex_scan::strict_sorted_buffer::StrictSortedBuffer;
33use vortex_session::VortexSession;
34
35use crate::LayoutReader;
36use crate::LayoutReaderRef;
37use crate::layouts::row_idx::RowIdx;
38use crate::layouts::row_idx::RowIdxLayoutReader;
39use crate::scan::limit::RowLimit;
40use crate::scan::limit::ScanLimit;
41use crate::scan::repeated_scan::RepeatedScan;
42use crate::scan::split_by::SplitBy;
43use crate::scan::splits::Splits;
44use crate::scan::splits::attempt_split_ranges;
45
46/// Builder for scanning a [`LayoutReader`] into arrays, streams, iterators, or mapped outputs.
47///
48/// A scan has three independent row restriction mechanisms:
49///
50/// - [`with_row_range`](Self::with_row_range) selects a contiguous range before scanning.
51/// - [`with_selection`](Self::with_selection) applies a [`Selection`] inside that range.
52/// - [`with_filter`](Self::with_filter) evaluates an expression predicate during execution.
53///
54/// Projection and filter expressions must be bound against the reader dtype. Work is divided by
55/// the configured [`SplitBy`] strategy or by explicit selection ranges.
56pub struct ScanBuilder<A> {
57    session: VortexSession,
58    layout_reader: LayoutReaderRef,
59    projection: BoundExpression,
60    filter: Option<BoundExpression>,
61    /// Whether the scan needs to return splits in the order they appear in the file.
62    ordered: bool,
63    /// Optionally read a subset of the rows in the file.
64    row_range: Option<Range<u64>>,
65    /// The selection mask to apply to the selected row range.
66    // TODO(joe): replace this is usage of row_id selection, see
67    selection: Selection,
68    /// How to split the file for concurrent processing.
69    split_by: SplitBy,
70    /// Precomputed full-file natural split boundaries; when set, [`prepare`](Self::prepare)
71    /// uses them instead of walking the layout.
72    natural_splits: Option<Arc<[u64]>>,
73    /// The number of splits to make progress on concurrently **per-thread**.
74    concurrency: usize,
75    /// Function to apply to each [`ArrayRef`] within the spawned split tasks.
76    map_fn: Arc<dyn Fn(ArrayRef) -> VortexResult<A> + Send + Sync>,
77    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
78    /// Maximal number of rows to read after filtering.
79    limit: Option<ScanLimit>,
80    /// The row-offset assigned to the first row of the file. Used by the `row_idx` expression,
81    /// but not by the scan [`Selection`] which remains relative.
82    row_offset: u64,
83}
84
85impl ScanBuilder<ArrayRef> {
86    /// Create a scan builder over `layout_reader` using `session` for runtime and execution state.
87    pub fn new(session: VortexSession, layout_reader: Arc<dyn LayoutReader>) -> Self {
88        let projection = BoundExpression::new_root(layout_reader.dtype().clone());
89        Self {
90            session,
91            layout_reader,
92            projection,
93            filter: None,
94            ordered: true,
95            row_range: None,
96            selection: Default::default(),
97            split_by: SplitBy::default(),
98            natural_splits: None,
99            // We default to four tasks per worker thread, which allows for some I/O lookahead
100            // without too much impact on work-stealing.
101            concurrency: 4,
102            map_fn: Arc::new(Ok),
103            metrics_registry: None,
104            limit: None,
105            row_offset: 0,
106        }
107    }
108
109    /// Returns an [`ArrayStream`] with tasks spawned onto the session's runtime handle.
110    ///
111    /// See [`ScanBuilder::into_stream`] for more details.
112    pub fn into_array_stream(self) -> VortexResult<impl ArrayStream + Send + 'static> {
113        let dtype = self.dtype()?;
114        let stream = self.into_stream()?;
115        Ok(ArrayStreamAdapter::new(dtype, stream))
116    }
117
118    /// Returns an [`ArrayIterator`] using the given blocking runtime.
119    pub fn into_array_iter<B: BlockingRuntime>(
120        self,
121        runtime: &B,
122    ) -> VortexResult<impl ArrayIterator + 'static> {
123        let stream = self.into_array_stream()?;
124        let dtype = stream.dtype().clone();
125        Ok(ArrayIteratorAdapter::new(
126            dtype,
127            runtime.block_on_stream(stream),
128        ))
129    }
130}
131
132impl<A: 'static + Send> ScanBuilder<A> {
133    /// Add a filter expression bound against the reader dtype.
134    pub fn with_filter(mut self, filter: BoundExpression) -> Self {
135        self.filter = Some(filter);
136        self
137    }
138
139    /// Add or clear a filter expression bound against the reader dtype.
140    pub fn with_some_filter(mut self, filter: Option<BoundExpression>) -> Self {
141        self.filter = filter;
142        self
143    }
144
145    /// Set a projection expression bound against the reader dtype.
146    pub fn with_projection(mut self, projection: BoundExpression) -> Self {
147        self.projection = projection;
148        self
149    }
150
151    /// Returns whether output chunks are yielded in file order.
152    pub fn ordered(&self) -> bool {
153        self.ordered
154    }
155
156    /// Configure whether output chunks must be yielded in file order.
157    pub fn with_ordered(mut self, ordered: bool) -> Self {
158        self.ordered = ordered;
159        self
160    }
161
162    /// Restrict scanning to a contiguous row range.
163    pub fn with_row_range(mut self, row_range: Range<u64>) -> Self {
164        self.row_range = Some(row_range);
165        self
166    }
167
168    /// Apply a row selection to the selected row range.
169    pub fn with_selection(mut self, selection: Selection) -> Self {
170        self.selection = selection;
171        self
172    }
173
174    /// Select rows by strictly sorted absolute indices relative to the scan input.
175    pub fn with_row_indices(mut self, row_indices: StrictSortedBuffer<u64>) -> Self {
176        self.selection = Selection::IncludeByIndex(row_indices);
177        self
178    }
179
180    /// Set the root row offset used by row-index expressions.
181    pub fn with_row_offset(mut self, row_offset: u64) -> Self {
182        self.row_offset = row_offset;
183        self
184    }
185
186    /// Configure how natural scan work is split for concurrency.
187    pub fn with_split_by(mut self, split_by: SplitBy) -> Self {
188        self.split_by = split_by;
189        self
190    }
191
192    /// Supply precomputed full-file natural split boundaries (see
193    /// [`full_file_splits`](Self::full_file_splits)) so [`prepare`](Self::prepare) reuses them
194    /// instead of walking the layout. Callers translating external partitions into row ranges
195    /// can compute the boundaries once per file and share them across partitions.
196    ///
197    /// Takes precedence over [`with_split_by`](Self::with_split_by); boundaries outside the
198    /// scan's row range are clamped during execution. Boundaries must be strictly increasing.
199    pub fn with_natural_splits(mut self, boundaries: Arc<[u64]>) -> Self {
200        debug_assert!(
201            boundaries.windows(2).all(|w| w[0] < w[1]),
202            "natural split boundaries must be strictly increasing"
203        );
204        self.natural_splits = Some(boundaries);
205        self
206    }
207
208    /// Compute the full-file natural split boundaries for the fields referenced by this scan's
209    /// projection and filter, ignoring any configured row range.
210    ///
211    /// These are the boundaries [`prepare`](Self::prepare) derives for a whole-file scan; hand
212    /// them back via [`with_natural_splits`](Self::with_natural_splits) to skip the layout walk
213    /// in `prepare`.
214    pub fn full_file_splits(&self) -> VortexResult<Vec<u64>> {
215        let field_mask = referenced_field_masks(&self.projection, self.filter.as_ref())?;
216        self.split_by.splits(
217            self.layout_reader.as_ref(),
218            &(0..self.layout_reader.row_count()),
219            &field_mask,
220        )
221    }
222
223    /// Returns the per-worker row-split concurrency.
224    pub fn concurrency(&self) -> usize {
225        self.concurrency
226    }
227
228    /// The number of row splits to make progress on concurrently per-thread, must
229    /// be greater than 0.
230    pub fn with_concurrency(mut self, concurrency: usize) -> Self {
231        assert!(concurrency > 0);
232        self.concurrency = concurrency;
233        self
234    }
235
236    /// Add or clear the metrics registry used by scan execution.
237    pub fn with_some_metrics_registry(mut self, metrics: Option<Arc<dyn MetricsRegistry>>) -> Self {
238        self.metrics_registry = metrics;
239        self
240    }
241
242    /// Set the metrics registry used by scan execution.
243    pub fn with_metrics_registry(mut self, metrics: Arc<dyn MetricsRegistry>) -> Self {
244        self.metrics_registry = Some(metrics);
245        self
246    }
247
248    /// Add or clear the maximum number of rows returned after filtering.
249    ///
250    /// A prepared [`RepeatedScan`] applies the limit to each of its executions independently.
251    pub fn with_some_limit(mut self, limit: Option<u64>) -> Self {
252        self.limit = limit.map(ScanLimit::PerExecution);
253        self
254    }
255
256    /// Set the maximum number of rows returned after filtering.
257    ///
258    /// A prepared [`RepeatedScan`] applies the limit to each of its executions independently.
259    pub fn with_limit(self, limit: u64) -> Self {
260        self.with_some_limit(Some(limit))
261    }
262
263    /// Draw rows from a budget shared with sibling partitions instead of a limit of its own.
264    pub(crate) fn with_shared_limit(mut self, limit: RowLimit) -> Self {
265        self.limit = Some(ScanLimit::Shared(limit));
266        self
267    }
268
269    /// The [`DType`] returned by the scan, after applying the projection.
270    pub fn dtype(&self) -> VortexResult<DType> {
271        Ok(self.projection.dtype().clone())
272    }
273
274    /// The session used by the scan.
275    pub fn session(&self) -> &VortexSession {
276        &self.session
277    }
278
279    /// Map each split of the scan. The function will be run on the spawned task.
280    pub fn map<B: 'static>(
281        self,
282        map_fn: impl Fn(A) -> VortexResult<B> + 'static + Send + Sync,
283    ) -> ScanBuilder<B> {
284        let old_map_fn = self.map_fn;
285        ScanBuilder {
286            session: self.session,
287            layout_reader: self.layout_reader,
288            projection: self.projection,
289            filter: self.filter,
290            ordered: self.ordered,
291            row_range: self.row_range,
292            selection: self.selection,
293            split_by: self.split_by,
294            natural_splits: self.natural_splits,
295            concurrency: self.concurrency,
296            metrics_registry: self.metrics_registry,
297            limit: self.limit,
298            row_offset: self.row_offset,
299            map_fn: Arc::new(move |a| old_map_fn(a).and_then(&map_fn)),
300        }
301    }
302
303    /// Rewrite range comparisons in the filter into betweens, compute split ranges, and return an
304    /// executable repeated scan.
305    pub fn prepare(self) -> VortexResult<RepeatedScan<A>> {
306        let dtype = self.dtype()?;
307
308        // Spin up the root layout reader, and wrap it in a FilterLayoutReader to perform
309        // conjunction splitting if a filter is provided.
310        let mut layout_reader = self.layout_reader;
311
312        // Enrich the layout reader to support RowIdx expressions if scan uses #row_idx.
313        // Note that this is applied below the filter layout reader since it can perform
314        // better over individual conjunctions.
315        let mut found_row_idx = self.projection.contains::<RowIdx>()?;
316        if !found_row_idx && let Some(filter) = self.filter.as_ref() {
317            found_row_idx = filter.contains::<RowIdx>()?;
318        }
319        if found_row_idx {
320            layout_reader = Arc::new(RowIdxLayoutReader::new(
321                self.row_offset,
322                layout_reader,
323                self.session.clone(),
324            ));
325        }
326
327        let bound_projection = self.projection;
328        let bound_filter = self.filter.map(find_between).transpose()?;
329
330        // Compute the row splits of the scan.
331        let splits =
332            if let Some(ranges) = attempt_split_ranges(&self.selection, self.row_range.as_ref()) {
333                Splits::Ranges(ranges)
334            } else if let Some(boundaries) = self.natural_splits {
335                // Caller-supplied full-file boundaries; execution clamps them to the row range.
336                Splits::Natural(boundaries)
337            } else {
338                let field_mask = referenced_field_masks(&bound_projection, bound_filter.as_ref())?;
339                let split_range = self
340                    .row_range
341                    .clone()
342                    .unwrap_or_else(|| 0..layout_reader.row_count());
343                Splits::Natural(
344                    self.split_by
345                        .splits(layout_reader.as_ref(), &split_range, &field_mask)?
346                        .into(),
347                )
348            };
349
350        Ok(RepeatedScan::new(
351            self.session.clone(),
352            layout_reader,
353            bound_projection,
354            bound_filter,
355            self.ordered,
356            self.row_range,
357            self.selection,
358            splits,
359            self.concurrency,
360            self.map_fn,
361            self.limit,
362            dtype,
363        ))
364    }
365
366    /// Constructs a task per row split of the scan, returned as a vector of futures.
367    ///
368    /// Scans with row limits must use [`Self::into_stream`] or [`Self::into_iter`] so the scan
369    /// can coordinate the limit across splits.
370    pub fn build(self) -> VortexResult<Vec<BoxFuture<'static, VortexResult<Option<A>>>>> {
371        self.prepare()?.execute(None)
372    }
373
374    /// Returns a [`Stream`] with tasks spawned onto the session's runtime handle.
375    ///
376    /// Preparation and initial stream construction begin on the first poll. Errors from either
377    /// step are returned as the stream's next item.
378    pub fn into_stream(
379        self,
380    ) -> VortexResult<impl Stream<Item = VortexResult<A>> + Send + 'static + use<A>> {
381        Ok(LazyScanStream::new(self))
382    }
383
384    /// Returns an [`Iterator`] using the session's runtime.
385    pub fn into_iter<B: BlockingRuntime>(
386        self,
387        runtime: &B,
388    ) -> VortexResult<impl Iterator<Item = VortexResult<A>> + 'static> {
389        let stream = self.into_stream()?;
390        Ok(runtime.block_on_stream(stream))
391    }
392}
393
394enum LazyScanState<A: 'static + Send> {
395    Builder(Option<Box<ScanBuilder<A>>>),
396    Preparing(Task<VortexResult<BoxStream<'static, VortexResult<A>>>>),
397    Stream(BoxStream<'static, VortexResult<A>>),
398    Error(Option<vortex_error::VortexError>),
399}
400
401struct LazyScanStream<A: 'static + Send> {
402    state: LazyScanState<A>,
403}
404
405impl<A: 'static + Send> LazyScanStream<A> {
406    fn new(builder: ScanBuilder<A>) -> Self {
407        Self {
408            state: LazyScanState::Builder(Some(Box::new(builder))),
409        }
410    }
411}
412
413impl<A: 'static + Send> Unpin for LazyScanStream<A> {}
414
415impl<A: 'static + Send> Stream for LazyScanStream<A> {
416    type Item = VortexResult<A>;
417
418    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
419        loop {
420            match &mut self.state {
421                LazyScanState::Builder(builder) => {
422                    let builder = builder.take().vortex_expect("polled after completion");
423                    let handle = builder.session.handle();
424                    // IMPORTANT: Building the stream can synchronously walk the layout and
425                    // register I/O for every split. Keep it with preparation in this CPU
426                    // task: poll_next must only wait for and poll an already-constructed stream.
427                    // This also keeps construction errors on the Preparing -> Error path rather
428                    // than running construction on the caller's executor.
429                    let task = handle.spawn_cpu(move || builder.prepare()?.execute_stream(None));
430                    self.state = LazyScanState::Preparing(task);
431                }
432                LazyScanState::Preparing(task) => match ready!(Pin::new(task).poll(cx)) {
433                    Ok(stream) => self.state = LazyScanState::Stream(stream),
434                    Err(err) => self.state = LazyScanState::Error(Some(err)),
435                },
436                LazyScanState::Stream(stream) => return stream.as_mut().poll_next(cx),
437                LazyScanState::Error(err) => return Poll::Ready(err.take().map(Err)),
438            }
439        }
440    }
441}
442
443/// Compute masks of field paths referenced by the projection and filter in the scan.
444///
445/// Projection and filter must be pre-simplified and bound against the scan dtype.
446pub fn referenced_field_masks(
447    projection: &BoundExpression,
448    filter: Option<&BoundExpression>,
449) -> VortexResult<Vec<FieldMask>> {
450    let mut field_paths = referenced_field_paths(projection)?;
451    if let Some(filter) = filter {
452        field_paths.extend(referenced_field_paths(filter)?);
453    }
454    Ok(field_paths
455        .into_iter()
456        .map(|path| {
457            if path.is_root() {
458                FieldMask::All
459            } else {
460                FieldMask::Prefix(path)
461            }
462        })
463        .collect_vec())
464}
465
466#[cfg(test)]
467mod test {
468    use std::ops::Range;
469    use std::pin::Pin;
470    use std::sync::Arc;
471    use std::sync::atomic::AtomicUsize;
472    use std::sync::atomic::Ordering;
473    use std::sync::mpsc;
474    use std::task::Context;
475    use std::task::Poll;
476    use std::time::Duration;
477
478    use futures::Stream;
479    use futures::StreamExt;
480    use futures::task::noop_waker_ref;
481    use parking_lot::Mutex;
482    use rstest::rstest;
483    use vortex_array::IntoArray;
484    use vortex_array::MaskFuture;
485    use vortex_array::VortexSessionExecute;
486    use vortex_array::array_session;
487    use vortex_array::arrays::PrimitiveArray;
488    use vortex_array::dtype::DType;
489    use vortex_array::dtype::FieldMask;
490    use vortex_array::dtype::FieldPath;
491    use vortex_array::dtype::Nullability;
492    use vortex_array::dtype::PType;
493    use vortex_array::dtype::StructFields;
494    use vortex_array::expr::BoundExpression;
495    use vortex_array::expr::ExactBoundExpr;
496    use vortex_array::expr::and;
497    use vortex_array::expr::between;
498    use vortex_array::expr::col;
499    use vortex_array::expr::eq;
500    use vortex_array::expr::get_item;
501    use vortex_array::expr::gt_eq;
502    use vortex_array::expr::is_not_null;
503    use vortex_array::expr::lit;
504    use vortex_array::expr::lt;
505    use vortex_array::expr::root;
506    use vortex_array::scalar_fn::fns::between::BetweenOptions;
507    use vortex_array::scalar_fn::fns::between::StrictComparison;
508    use vortex_error::VortexResult;
509    use vortex_error::vortex_bail;
510    use vortex_error::vortex_err;
511    use vortex_io::runtime::BlockingRuntime;
512    use vortex_io::runtime::single::SingleThreadRuntime;
513    use vortex_io::runtime::tokio::TokioRuntime;
514    use vortex_mask::Mask;
515
516    use super::ScanBuilder;
517    use super::referenced_field_masks;
518    use crate::ArrayFuture;
519    use crate::LayoutReader;
520    use crate::RowSplits;
521    use crate::SplitRange;
522    use crate::scan::limit::RowLimit;
523    use crate::scan::test::SCAN_SESSION;
524    use crate::scan::test::TestLayoutReader;
525    use crate::scan::test::collect_scan_values;
526    use crate::scan::test::drain_runtime;
527    use crate::scan::test::keep_all;
528    use crate::scan::test::keep_odd;
529    use crate::scan::test::session_with_handle;
530
531    fn nested_dtype() -> DType {
532        DType::Struct(
533            StructFields::from_iter([
534                (
535                    "a",
536                    DType::Struct(
537                        StructFields::from_iter([
538                            ("1", DType::Primitive(PType::I32, Nullability::NonNullable)),
539                            ("2", DType::Primitive(PType::I32, Nullability::NonNullable)),
540                        ]),
541                        Nullability::NonNullable,
542                    ),
543                ),
544                ("b", DType::Primitive(PType::I32, Nullability::NonNullable)),
545            ]),
546            Nullability::NonNullable,
547        )
548    }
549
550    #[test]
551    fn bound_setters_preserve_identity() -> VortexResult<()> {
552        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
553        let projection = eq(root(), lit(1_i32)).bind(&dtype)?;
554        let filter = eq(root(), lit(2_i32)).bind(&dtype)?;
555        let expected_projection = ExactBoundExpr(projection.clone());
556        let expected_filter = ExactBoundExpr(filter.clone());
557        let reader = Arc::new(CountingLayoutReader::new(Arc::new(AtomicUsize::new(0))));
558
559        let builder = ScanBuilder::new(SCAN_SESSION.clone(), reader)
560            .with_projection(projection)
561            .with_filter(filter);
562
563        assert_eq!(ExactBoundExpr(builder.projection), expected_projection);
564        assert_eq!(builder.filter.map(ExactBoundExpr), Some(expected_filter));
565        Ok(())
566    }
567
568    #[test]
569    fn root_projection_produces_all_mask() -> VortexResult<()> {
570        let dtype = DType::Primitive(PType::I32, Nullability::NonNullable);
571        let projection = root().bind(&dtype)?;
572
573        assert_eq!(referenced_field_masks(&projection, None)?, [FieldMask::All]);
574        Ok(())
575    }
576
577    #[test]
578    fn nested_projection_preserves_field_path_in_split_mask() -> VortexResult<()> {
579        let dtype = nested_dtype();
580        let projection = get_item("1", get_item("a", root())).bind(&dtype)?;
581        let filter = eq(get_item("2", get_item("a", root())), lit(0_i32)).bind(&dtype)?;
582
583        let field_masks = referenced_field_masks(&projection, Some(&filter))?;
584
585        assert_eq!(field_masks.len(), 2);
586        assert!(field_masks.contains(&FieldMask::Prefix(FieldPath::from_name("a").push("1"))));
587        assert!(field_masks.contains(&FieldMask::Prefix(FieldPath::from_name("a").push("2"))));
588        Ok(())
589    }
590
591    #[test]
592    fn filter_path_covers_nested_projection_path() -> VortexResult<()> {
593        let dtype = nested_dtype();
594        let projection = get_item("1", get_item("a", root())).bind(&dtype)?;
595        let filter = is_not_null(get_item("a", root())).bind(&dtype)?;
596
597        let field_masks = referenced_field_masks(&projection, Some(&filter))?;
598
599        assert_eq!(field_masks, [FieldMask::Prefix(FieldPath::from_name("a"))]);
600        Ok(())
601    }
602
603    #[test]
604    fn parent_projection_path_covers_nested_filter_path() -> VortexResult<()> {
605        let dtype = nested_dtype();
606        let projection = get_item("a", root()).bind(&dtype)?;
607        let filter = is_not_null(get_item("1", get_item("a", root()))).bind(&dtype)?;
608
609        let field_masks = referenced_field_masks(&projection, Some(&filter))?;
610
611        assert_eq!(field_masks, [FieldMask::Prefix(FieldPath::from_name("a"))]);
612        Ok(())
613    }
614
615    #[derive(Debug)]
616    struct CountingLayoutReader {
617        name: Arc<str>,
618        dtype: DType,
619        row_count: u64,
620        register_splits_calls: Arc<AtomicUsize>,
621    }
622
623    impl CountingLayoutReader {
624        fn new(register_splits_calls: Arc<AtomicUsize>) -> Self {
625            Self {
626                name: Arc::from("counting"),
627                dtype: DType::Primitive(PType::I32, Nullability::NonNullable),
628                row_count: 1,
629                register_splits_calls,
630            }
631        }
632    }
633
634    impl LayoutReader for CountingLayoutReader {
635        fn name(&self) -> &Arc<str> {
636            &self.name
637        }
638
639        fn dtype(&self) -> &DType {
640            &self.dtype
641        }
642
643        fn row_count(&self) -> u64 {
644            self.row_count
645        }
646
647        fn register_splits(
648            &self,
649            _field_mask: &[FieldMask],
650            split_range: &SplitRange,
651            splits: &mut RowSplits,
652        ) -> VortexResult<()> {
653            self.register_splits_calls.fetch_add(1, Ordering::Relaxed);
654            splits.push(split_range.root_row_range().end);
655            Ok(())
656        }
657
658        fn pruning_evaluation(
659            &self,
660            _row_range: &Range<u64>,
661            _expr: &BoundExpression,
662            _mask: Mask,
663        ) -> VortexResult<MaskFuture> {
664            vortex_bail!("not needed for this test");
665        }
666
667        fn filter_evaluation(
668            &self,
669            _row_range: &Range<u64>,
670            _expr: &BoundExpression,
671            _mask: MaskFuture,
672        ) -> VortexResult<MaskFuture> {
673            vortex_bail!("not needed for this test");
674        }
675
676        fn projection_evaluation(
677            &self,
678            _row_range: &Range<u64>,
679            _expr: &BoundExpression,
680            _mask: MaskFuture,
681        ) -> VortexResult<ArrayFuture> {
682            Ok(Box::pin(async move {
683                unreachable!("scan should not be polled in this test")
684            }))
685        }
686
687        fn as_any(&self) -> &dyn std::any::Any {
688            self
689        }
690    }
691
692    #[test]
693    fn into_stream_is_lazy() {
694        let calls = Arc::new(AtomicUsize::new(0));
695        let reader = Arc::new(CountingLayoutReader::new(Arc::clone(&calls)));
696
697        let session = SCAN_SESSION.clone();
698
699        let _stream = ScanBuilder::new(session, reader).into_stream().unwrap();
700
701        assert_eq!(calls.load(Ordering::Relaxed), 0);
702    }
703
704    #[derive(Debug)]
705    struct SplittingLayoutReader {
706        name: Arc<str>,
707        dtype: DType,
708        row_count: u64,
709        register_splits_calls: Arc<AtomicUsize>,
710        blocking_projection: Option<BlockingProjection>,
711    }
712
713    #[derive(Debug)]
714    struct BlockingProjection {
715        started: mpsc::Sender<()>,
716        gate: Arc<Mutex<()>>,
717    }
718
719    impl SplittingLayoutReader {
720        fn new(register_splits_calls: Arc<AtomicUsize>) -> Self {
721            Self {
722                name: Arc::from("splitting"),
723                dtype: DType::Primitive(PType::I32, Nullability::NonNullable),
724                row_count: 4,
725                register_splits_calls,
726                blocking_projection: None,
727            }
728        }
729
730        fn with_blocking_projection(
731            register_splits_calls: Arc<AtomicUsize>,
732            gate: Arc<Mutex<()>>,
733            started: mpsc::Sender<()>,
734        ) -> Self {
735            let mut reader = Self::new(register_splits_calls);
736            reader.blocking_projection = Some(BlockingProjection { started, gate });
737            reader
738        }
739    }
740
741    impl LayoutReader for SplittingLayoutReader {
742        fn name(&self) -> &Arc<str> {
743            &self.name
744        }
745
746        fn dtype(&self) -> &DType {
747            &self.dtype
748        }
749
750        fn row_count(&self) -> u64 {
751            self.row_count
752        }
753
754        fn register_splits(
755            &self,
756            _field_mask: &[FieldMask],
757            split_range: &SplitRange,
758            splits: &mut RowSplits,
759        ) -> VortexResult<()> {
760            self.register_splits_calls.fetch_add(1, Ordering::Relaxed);
761            for split in (split_range.row_range().start + 1)..=split_range.row_range().end {
762                splits.push(split_range.row_offset() + split);
763            }
764            Ok(())
765        }
766
767        fn pruning_evaluation(
768            &self,
769            _row_range: &Range<u64>,
770            _expr: &BoundExpression,
771            mask: Mask,
772        ) -> VortexResult<MaskFuture> {
773            Ok(MaskFuture::ready(mask))
774        }
775
776        fn filter_evaluation(
777            &self,
778            _row_range: &Range<u64>,
779            _expr: &BoundExpression,
780            mask: MaskFuture,
781        ) -> VortexResult<MaskFuture> {
782            Ok(mask)
783        }
784
785        fn projection_evaluation(
786            &self,
787            row_range: &Range<u64>,
788            _expr: &BoundExpression,
789            _mask: MaskFuture,
790        ) -> VortexResult<ArrayFuture> {
791            if let Some(blocking_projection) = &self.blocking_projection {
792                blocking_projection
793                    .started
794                    .send(())
795                    .map_err(|_| vortex_err!("test projection-start receiver dropped"))?;
796                let _guard = blocking_projection.gate.lock();
797            }
798
799            let start = usize::try_from(row_range.start)
800                .map_err(|_| vortex_err!("row_range.start must fit in usize"))?;
801            let end = usize::try_from(row_range.end)
802                .map_err(|_| vortex_err!("row_range.end must fit in usize"))?;
803
804            let values: VortexResult<Vec<i32>> = (start..end)
805                .map(|v| i32::try_from(v).map_err(|_| vortex_err!("split value must fit in i32")))
806                .collect();
807
808            let array = PrimitiveArray::from_iter(values?).into_array();
809            Ok(Box::pin(async move { Ok(array) }))
810        }
811
812        fn as_any(&self) -> &dyn std::any::Any {
813            self
814        }
815    }
816
817    #[test]
818    fn into_stream_executes_after_prepare() -> VortexResult<()> {
819        let mut ctx = array_session().create_execution_ctx();
820        let calls = Arc::new(AtomicUsize::new(0));
821        let reader = Arc::new(SplittingLayoutReader::new(Arc::clone(&calls)));
822
823        let runtime = SingleThreadRuntime::default();
824        let session = session_with_handle(runtime.handle());
825
826        let stream = ScanBuilder::new(session, reader).into_stream()?;
827        let mut iter = runtime.block_on_stream(stream);
828
829        let mut values = Vec::new();
830        for chunk in &mut iter {
831            let prim = chunk?.execute::<PrimitiveArray>(&mut ctx)?;
832            values.extend(prim.into_buffer::<i32>().iter().copied());
833        }
834
835        assert_eq!(calls.load(Ordering::Relaxed), 1);
836        assert_eq!(values.as_ref(), [0, 1, 2, 3]);
837
838        Ok(())
839    }
840
841    #[test]
842    fn supplied_natural_splits_skip_layout_walk() -> VortexResult<()> {
843        let mut ctx = array_session().create_execution_ctx();
844        let calls = Arc::new(AtomicUsize::new(0));
845        let reader = Arc::new(SplittingLayoutReader::new(Arc::clone(&calls)));
846
847        let runtime = SingleThreadRuntime::default();
848        let session = session_with_handle(runtime.handle());
849
850        let stream = ScanBuilder::new(session, reader)
851            .with_natural_splits(vec![0u64, 2, 4].into())
852            .with_row_range(1..4)
853            .into_stream()?;
854        let mut iter = runtime.block_on_stream(stream);
855
856        let mut chunks = Vec::new();
857        for chunk in &mut iter {
858            let prim = chunk?.execute::<PrimitiveArray>(&mut ctx)?;
859            chunks.push(prim.into_buffer::<i32>().to_vec());
860        }
861
862        assert_eq!(calls.load(Ordering::Relaxed), 0);
863        // Supplied full-file boundaries [0, 2, 4] clamped to rows 1..4.
864        assert_eq!(chunks, [vec![1], vec![2, 3]]);
865
866        Ok(())
867    }
868
869    #[rstest]
870    #[case::eager(false)]
871    #[case::filtered_limit(true)]
872    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
873    async fn scan_constructs_tasks_off_the_poller(#[case] filtered: bool) -> VortexResult<()> {
874        let gate = Arc::new(Mutex::new(()));
875        let guard = gate.lock();
876        let calls = Arc::new(AtomicUsize::new(0));
877        let (started_send, started_recv) = mpsc::channel();
878        let reader = Arc::new(SplittingLayoutReader::with_blocking_projection(
879            Arc::clone(&calls),
880            Arc::clone(&gate),
881            started_send,
882        ));
883
884        let runtime = TokioRuntime::new(tokio::runtime::Handle::current());
885        let session = session_with_handle(runtime.handle());
886        let filter = is_not_null(root()).bind(reader.dtype())?;
887        let builder = ScanBuilder::new(session, reader);
888        let mut stream = if filtered {
889            // Drive the prepared stream directly so its first poll must construct a split.
890            builder
891                .with_filter(filter)
892                .with_limit(1)
893                .prepare()?
894                .execute_stream(None)?
895        } else {
896            builder.into_stream()?.boxed()
897        };
898
899        let (poll_send, poll_recv) = mpsc::channel();
900        let (release_send, release_recv) = mpsc::channel();
901        let join = std::thread::spawn(move || {
902            let waker = noop_waker_ref();
903            let mut cx = Context::from_waker(waker);
904            let poll = Pin::new(&mut stream).poll_next(&mut cx);
905            let _ = poll_send.send(matches!(poll, Poll::Pending));
906            let _ = release_recv.recv();
907        });
908
909        let poll_result = poll_recv.recv_timeout(Duration::from_secs(1));
910        let projection_started = started_recv.recv_timeout(Duration::from_secs(1));
911
912        // Release the task and join its caller before reporting a failed assertion.
913        drop(guard);
914        let _ = release_send.send(());
915        drop(join.join());
916
917        assert!(
918            poll_result.is_ok_and(|poll_pending| poll_pending),
919            "first poll must return while scan task construction is blocked"
920        );
921        projection_started
922            .map_err(|_| vortex_err!("stream construction did not begin in the background"))?;
923        assert_eq!(calls.load(Ordering::Relaxed), 1);
924
925        Ok(())
926    }
927
928    #[tokio::test]
929    async fn into_stream_reports_stream_construction_errors() -> VortexResult<()> {
930        let range_start = i32::MAX as u64 + 1;
931        let reader = Arc::new(SplittingLayoutReader::new(Arc::new(AtomicUsize::new(0))));
932        let session = session_with_handle(TokioRuntime::current());
933        let mut stream = ScanBuilder::new(session, reader)
934            .with_row_range(range_start..range_start + 1)
935            .into_stream()?;
936
937        assert!(matches!(stream.next().await, Some(Err(_))));
938        assert!(stream.next().await.is_none());
939
940        Ok(())
941    }
942
943    #[rstest]
944    #[case::limit_below_matches(8, keep_all, 3, &[0, 1, 2])]
945    #[case::limit_zero(8, keep_all, 0, &[])]
946    #[case::limit_exceeds_matches(8, keep_odd, 100, &[1, 3, 5, 7])]
947    #[case::empty_input(0, keep_all, 3, &[])]
948    fn filtered_limit_yields_expected_rows(
949        #[case] row_count: u64,
950        #[case] keep_row: fn(u64) -> bool,
951        #[case] limit: u64,
952        #[case] expected: &[i32],
953    ) -> VortexResult<()> {
954        let runtime = SingleThreadRuntime::default();
955        let session = session_with_handle(runtime.handle());
956        let reader = Arc::new(
957            TestLayoutReader::new(row_count)
958                .with_split_size(2)
959                .with_keep_row(keep_row),
960        );
961        let filter = root().bind(reader.dtype())?;
962
963        let stream = ScanBuilder::new(session, reader)
964            .with_filter(filter)
965            .with_limit(limit)
966            .into_stream()?;
967        let values = collect_scan_values(runtime.block_on_stream(stream))?;
968        drain_runtime(&runtime);
969
970        assert_eq!(values.as_slice(), expected);
971        Ok(())
972    }
973
974    /// An ordered filtered limit takes rows in split order, so it returns the earliest matching
975    /// rows even when later splits finish first.
976    #[test]
977    fn ordered_filtered_limit_returns_the_earliest_rows() -> VortexResult<()> {
978        let runtime = SingleThreadRuntime::default();
979        let session = session_with_handle(runtime.handle());
980        let reader = Arc::new(TestLayoutReader::new(12).with_split_size(4));
981        let filter = root().bind(reader.dtype())?;
982
983        let stream = ScanBuilder::new(session, reader)
984            .with_filter(filter)
985            .with_limit(6)
986            .into_stream()?;
987        let values = collect_scan_values(runtime.block_on_stream(stream))?;
988        drain_runtime(&runtime);
989
990        assert_eq!(values, [0, 1, 2, 3, 4, 5]);
991        Ok(())
992    }
993
994    /// `map` applies to every emitted split, including the trimmed output of a filtered limit.
995    #[rstest]
996    #[case::unlimited(None, 8)]
997    #[case::limited(Some(3), 3)]
998    fn map_applies_to_every_emitted_split(
999        #[case] limit: Option<u64>,
1000        #[case] expected_rows: usize,
1001    ) -> VortexResult<()> {
1002        let runtime = SingleThreadRuntime::default();
1003        let session = session_with_handle(runtime.handle());
1004        let reader = Arc::new(TestLayoutReader::new(8).with_split_size(2));
1005        let filter = root().bind(reader.dtype())?;
1006
1007        let rows = ScanBuilder::new(session, reader)
1008            .with_filter(filter)
1009            .with_some_limit(limit)
1010            .map(|array| Ok(array.len()))
1011            .into_iter(&runtime)?
1012            .sum::<VortexResult<usize>>()?;
1013        drain_runtime(&runtime);
1014
1015        assert_eq!(rows, expected_rows);
1016        Ok(())
1017    }
1018
1019    #[rstest]
1020    #[case::filter_error(true, None)]
1021    #[case::filter_error_with_limit(true, Some(2))]
1022    #[case::projection_error(false, None)]
1023    #[case::projection_error_with_limit(false, Some(2))]
1024    fn errors_end_the_scan(
1025        #[case] fail_filter: bool,
1026        #[case] limit: Option<u64>,
1027    ) -> VortexResult<()> {
1028        let runtime = SingleThreadRuntime::default();
1029        let session = session_with_handle(runtime.handle());
1030        let reader = TestLayoutReader::new(2).with_split_size(1);
1031        let reader = Arc::new(if fail_filter {
1032            reader.with_fail_first_filter()
1033        } else {
1034            reader.with_fail_first_projection()
1035        });
1036        let filter = root().bind(reader.dtype())?;
1037        let stream = ScanBuilder::new(session, reader)
1038            .with_filter(filter)
1039            .with_some_limit(limit)
1040            .into_stream()?;
1041        let mut iter = runtime.block_on_stream(stream);
1042
1043        assert!(matches!(iter.next(), Some(Err(_))));
1044        assert!(iter.next().is_none());
1045        drop(iter);
1046        drain_runtime(&runtime);
1047        Ok(())
1048    }
1049
1050    #[test]
1051    fn full_file_splits_ignore_row_range() -> VortexResult<()> {
1052        let calls = Arc::new(AtomicUsize::new(0));
1053        let reader = Arc::new(SplittingLayoutReader::new(Arc::clone(&calls)));
1054
1055        let splits = ScanBuilder::new(SCAN_SESSION.clone(), reader)
1056            .with_row_range(1..3)
1057            .full_file_splits()?;
1058
1059        assert_eq!(splits, [0, 1, 2, 3, 4]);
1060
1061        Ok(())
1062    }
1063
1064    #[test]
1065    fn projection_errors_are_stream_items() -> VortexResult<()> {
1066        let runtime = SingleThreadRuntime::default();
1067        let session = session_with_handle(runtime.handle());
1068        let projection_masks = Arc::new(Mutex::new(Vec::new()));
1069        let reader = Arc::new(
1070            TestLayoutReader::new(1)
1071                .with_projection_masks(Arc::clone(&projection_masks))
1072                .with_projection_error(),
1073        );
1074        let filter = root().bind(reader.dtype())?;
1075        let stream = ScanBuilder::new(session, reader)
1076            .with_filter(filter)
1077            .with_limit(1)
1078            .into_stream()?;
1079        let mut iter = runtime.block_on_stream(stream);
1080
1081        assert!(matches!(iter.next(), Some(Err(_))));
1082        assert!(iter.next().is_none());
1083        assert_eq!(projection_masks.lock().as_slice(), [1]);
1084        Ok(())
1085    }
1086
1087    #[test]
1088    fn prepared_scan_limits_filtered_results() -> VortexResult<()> {
1089        let runtime = SingleThreadRuntime::default();
1090        let session = session_with_handle(runtime.handle());
1091        let reader = Arc::new(
1092            TestLayoutReader::new(8)
1093                .with_split_size(2)
1094                .with_keep_row(keep_odd),
1095        );
1096        let filter = root().bind(reader.dtype())?;
1097
1098        let scan = ScanBuilder::new(session, reader)
1099            .with_filter(filter)
1100            .with_limit(3)
1101            .prepare()?;
1102        let values = collect_scan_values(scan.execute_array_iter(None, &runtime)?)?;
1103        drain_runtime(&runtime);
1104
1105        assert_eq!(values, [1, 3, 5]);
1106        Ok(())
1107    }
1108
1109    #[test]
1110    fn scan_evaluates_filter_ranges_as_between() -> VortexResult<()> {
1111        let runtime = SingleThreadRuntime::default();
1112        let session = session_with_handle(runtime.handle());
1113        let filter_exprs = Arc::new(Mutex::new(Vec::new()));
1114        let dtype = nested_dtype();
1115        let reader = Arc::new(
1116            TestLayoutReader::new(4)
1117                .with_dtype(dtype.clone())
1118                .with_filter_exprs(Arc::clone(&filter_exprs)),
1119        );
1120        let filter = and(gt_eq(col("b"), lit(2_i32)), lt(col("b"), lit(5_i32))).bind(&dtype)?;
1121
1122        let values = collect_scan_values(
1123            ScanBuilder::new(session, reader)
1124                .with_projection(col("b").bind(&dtype)?)
1125                .with_filter(filter)
1126                .into_array_iter(&runtime)?,
1127        )?;
1128        drain_runtime(&runtime);
1129
1130        assert_eq!(values, [0, 1, 2, 3]);
1131        let expected = between(
1132            col("b"),
1133            lit(2_i32),
1134            lit(5_i32),
1135            BetweenOptions {
1136                lower_strict: StrictComparison::NonStrict,
1137                upper_strict: StrictComparison::Strict,
1138            },
1139        )
1140        .bind(&dtype)?;
1141        assert_eq!(filter_exprs.lock().as_slice(), [expected]);
1142        Ok(())
1143    }
1144
1145    #[test]
1146    fn prepared_scan_split_futures_can_be_polled_in_reverse() -> VortexResult<()> {
1147        let runtime = SingleThreadRuntime::default();
1148        let session = session_with_handle(runtime.handle());
1149        let projection_masks = Arc::new(Mutex::new(Vec::new()));
1150        let reader = Arc::new(
1151            TestLayoutReader::new(8)
1152                .with_split_size(2)
1153                .with_keep_row(keep_odd)
1154                .with_projection_masks(Arc::clone(&projection_masks)),
1155        );
1156        let filter = root().bind(reader.dtype())?;
1157        let scan = ScanBuilder::new(session, reader)
1158            .with_filter(filter)
1159            .prepare()?;
1160        let tasks = scan.execute(Some(1..7))?;
1161        assert!(projection_masks.lock().is_empty());
1162
1163        let mut arrays = Vec::new();
1164        for task in tasks.into_iter().rev() {
1165            if let Some(array) = runtime.block_on(task)? {
1166                arrays.push(Ok(array));
1167            }
1168        }
1169        assert_eq!(collect_scan_values(arrays)?, [5, 3, 1]);
1170        Ok(())
1171    }
1172
1173    #[rstest]
1174    #[case(false)]
1175    #[case(true)]
1176    fn prepared_scan_split_futures_reject_limits(#[case] shared: bool) -> VortexResult<()> {
1177        let reader = Arc::new(TestLayoutReader::new(8).with_split_size(2));
1178        let builder = ScanBuilder::new(SCAN_SESSION.clone(), reader);
1179        let builder = if shared {
1180            builder.with_shared_limit(RowLimit::new(1))
1181        } else {
1182            builder.with_limit(1)
1183        };
1184
1185        assert!(builder.prepare()?.execute(None).is_err());
1186        Ok(())
1187    }
1188
1189    #[derive(Debug)]
1190    struct BlockingSplitsLayoutReader {
1191        name: Arc<str>,
1192        dtype: DType,
1193        row_count: u64,
1194        register_splits_calls: Arc<AtomicUsize>,
1195        gate: Arc<Mutex<()>>,
1196    }
1197
1198    impl BlockingSplitsLayoutReader {
1199        fn new(gate: Arc<Mutex<()>>, register_splits_calls: Arc<AtomicUsize>) -> Self {
1200            Self {
1201                name: Arc::from("blocking-splits"),
1202                dtype: DType::Primitive(PType::I32, Nullability::NonNullable),
1203                row_count: 1,
1204                register_splits_calls,
1205                gate,
1206            }
1207        }
1208    }
1209
1210    impl LayoutReader for BlockingSplitsLayoutReader {
1211        fn name(&self) -> &Arc<str> {
1212            &self.name
1213        }
1214
1215        fn dtype(&self) -> &DType {
1216            &self.dtype
1217        }
1218
1219        fn row_count(&self) -> u64 {
1220            self.row_count
1221        }
1222
1223        fn register_splits(
1224            &self,
1225            _field_mask: &[FieldMask],
1226            split_range: &SplitRange,
1227            splits: &mut RowSplits,
1228        ) -> VortexResult<()> {
1229            self.register_splits_calls.fetch_add(1, Ordering::Relaxed);
1230            let _guard = self.gate.lock();
1231            splits.push(split_range.root_row_range().end);
1232            Ok(())
1233        }
1234
1235        fn pruning_evaluation(
1236            &self,
1237            _row_range: &Range<u64>,
1238            _expr: &BoundExpression,
1239            _mask: Mask,
1240        ) -> VortexResult<MaskFuture> {
1241            vortex_bail!("not needed for this test");
1242        }
1243
1244        fn filter_evaluation(
1245            &self,
1246            _row_range: &Range<u64>,
1247            _expr: &BoundExpression,
1248            _mask: MaskFuture,
1249        ) -> VortexResult<MaskFuture> {
1250            vortex_bail!("not needed for this test");
1251        }
1252
1253        fn projection_evaluation(
1254            &self,
1255            _row_range: &Range<u64>,
1256            _expr: &BoundExpression,
1257            _mask: MaskFuture,
1258        ) -> VortexResult<ArrayFuture> {
1259            Ok(Box::pin(async move {
1260                unreachable!("scan should not be polled in this test")
1261            }))
1262        }
1263
1264        fn as_any(&self) -> &dyn std::any::Any {
1265            self
1266        }
1267    }
1268
1269    #[test]
1270    fn into_stream_first_poll_does_not_block() {
1271        let gate = Arc::new(Mutex::new(()));
1272        let guard = gate.lock();
1273
1274        let calls = Arc::new(AtomicUsize::new(0));
1275        let reader = Arc::new(BlockingSplitsLayoutReader::new(
1276            Arc::clone(&gate),
1277            Arc::clone(&calls),
1278        ));
1279
1280        let runtime = SingleThreadRuntime::default();
1281        let session = session_with_handle(runtime.handle());
1282
1283        let mut stream = ScanBuilder::new(session, reader).into_stream().unwrap();
1284
1285        let (send, recv) = mpsc::channel::<bool>();
1286        let join = std::thread::spawn(move || {
1287            let waker = noop_waker_ref();
1288            let mut cx = Context::from_waker(waker);
1289            let poll = Pin::new(&mut stream).poll_next(&mut cx);
1290            let _ = send.send(matches!(poll, Poll::Pending));
1291        });
1292
1293        let polled_pending = recv.recv_timeout(Duration::from_secs(1)).ok();
1294
1295        // Always release the gate and join the thread so failures don't hang the test process.
1296        drop(guard);
1297        drop(join.join());
1298
1299        let polled_pending = polled_pending.expect("poll_next blocked; expected quick return");
1300        assert!(
1301            polled_pending,
1302            "expected Poll::Pending while prepare is blocked"
1303        );
1304        assert_eq!(calls.load(Ordering::Relaxed), 0);
1305
1306        drop(runtime);
1307    }
1308
1309    #[test]
1310    fn into_stream_with_row_range() -> VortexResult<()> {
1311        let mut ctx = array_session().create_execution_ctx();
1312        let calls = Arc::new(AtomicUsize::new(0));
1313        let reader = Arc::new(SplittingLayoutReader::new(Arc::clone(&calls)));
1314
1315        let runtime = SingleThreadRuntime::default();
1316        let session = session_with_handle(runtime.handle());
1317
1318        let stream = ScanBuilder::new(session, reader)
1319            .with_row_range(1..3)
1320            .into_stream()?;
1321        let mut iter = runtime.block_on_stream(stream);
1322
1323        let mut values = Vec::new();
1324        for chunk in &mut iter {
1325            let prim = chunk?.execute::<PrimitiveArray>(&mut ctx)?;
1326            values.extend(prim.into_buffer::<i32>().iter().copied());
1327        }
1328
1329        assert_eq!(calls.load(Ordering::Relaxed), 1);
1330        assert_eq!(values.as_ref(), [1, 2]);
1331
1332        Ok(())
1333    }
1334}