Skip to main content

vortex_layout/scan/
repeated_scan.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::cmp;
5use std::iter;
6use std::ops::Range;
7use std::sync::Arc;
8use std::task::Poll;
9
10use futures::Future;
11use futures::FutureExt;
12use futures::Stream;
13use futures::StreamExt;
14use futures::future;
15use futures::future::BoxFuture;
16use futures::ready;
17use futures::stream;
18use futures::stream::BoxStream;
19use itertools::Either;
20use itertools::Itertools;
21use vortex_array::ArrayRef;
22use vortex_array::dtype::DType;
23use vortex_array::expr::BoundExpression;
24use vortex_array::iter::ArrayIterator;
25use vortex_array::iter::ArrayIteratorAdapter;
26use vortex_array::stream::ArrayStream;
27use vortex_array::stream::ArrayStreamAdapter;
28use vortex_error::VortexExpect;
29use vortex_error::VortexResult;
30use vortex_error::vortex_bail;
31use vortex_io::runtime::BlockingRuntime;
32use vortex_io::session::RuntimeSessionExt;
33use vortex_scan::row_mask::RowMask;
34use vortex_scan::selection::Selection;
35use vortex_session::VortexSession;
36use vortex_utils::parallelism::get_available_parallelism;
37
38use crate::LayoutReaderRef;
39use crate::scan::filter::FilterExpr;
40use crate::scan::limit::RowLimit;
41use crate::scan::limit::ScanLimit;
42use crate::scan::splits::Splits;
43use crate::scan::tasks::SplitFuture;
44use crate::scan::tasks::TaskContext;
45use crate::scan::tasks::split_exec;
46use crate::scan::tasks::split_projection;
47
48/// A projected subset (by indices, range, and filter) of rows from a Vortex data source.
49///
50/// The method of this struct enable, possibly concurrent, scanning of multiple row ranges of this
51/// data source.
52pub struct RepeatedScan<A> {
53    session: VortexSession,
54    layout_reader: LayoutReaderRef,
55    projection: BoundExpression,
56    filter: Option<BoundExpression>,
57    ordered: bool,
58    /// Optionally read a subset of the rows in the file.
59    row_range: Option<Range<u64>>,
60    /// The selection mask to apply to the selected row range.
61    selection: Selection,
62    /// The natural splits of the file.
63    splits: Splits,
64    /// The number of splits to make progress on concurrently **per-thread**.
65    concurrency: usize,
66    /// Function to apply to each [`ArrayRef`] within the spawned split tasks.
67    map_fn: MapFn<A>,
68    /// Maximal number of rows to read (after filtering).
69    limit: Option<ScanLimit>,
70    /// The dtype of the projected arrays.
71    dtype: DType,
72}
73
74/// A function applied to each projected split array.
75type MapFn<A> = Arc<dyn Fn(ArrayRef) -> VortexResult<A> + Send + Sync>;
76
77impl RepeatedScan<ArrayRef> {
78    pub fn execute_array_iter<B: BlockingRuntime>(
79        &self,
80        row_range: Option<Range<u64>>,
81        runtime: &B,
82    ) -> VortexResult<impl ArrayIterator + 'static> {
83        let dtype = self.dtype.clone();
84        let stream = self.execute_stream(row_range)?;
85        let iter = runtime.block_on_stream(stream);
86        Ok(ArrayIteratorAdapter::new(dtype, iter))
87    }
88
89    pub fn execute_array_stream(
90        &self,
91        row_range: Option<Range<u64>>,
92    ) -> VortexResult<impl ArrayStream + Send + 'static> {
93        let dtype = self.dtype.clone();
94        let stream = self.execute_stream(row_range)?;
95        Ok(ArrayStreamAdapter::new(dtype, stream))
96    }
97}
98
99impl<A: 'static + Send> RepeatedScan<A> {
100    /// Create split futures for an executor that schedules its own scan work.
101    ///
102    /// No tasks are spawned. Each future returns the mapped split, or `None` when its split is
103    /// filtered out. The futures may be polled independently in any order.
104    ///
105    /// Scans with row limits must use [`Self::execute_stream`] so the scan can coordinate the
106    /// limit across splits.
107    pub fn execute(
108        &self,
109        row_range: Option<Range<u64>>,
110    ) -> VortexResult<Vec<BoxFuture<'static, VortexResult<Option<A>>>>> {
111        if self.limit.is_some() {
112            vortex_bail!("Split futures do not support row limits; use a scan stream or iterator");
113        }
114
115        let ctx = self.task_context();
116        self.split_masks(row_range)
117            .map(|row_mask| Ok(map_split(split_exec(&ctx, row_mask)?, &self.map_fn)))
118            .collect()
119    }
120
121    pub fn dtype(&self) -> &DType {
122        &self.dtype
123    }
124
125    /// Constructor just to allow `scan_builder` to create a `RepeatedScan`.
126    #[expect(
127        clippy::too_many_arguments,
128        reason = "all arguments are needed for scan construction"
129    )]
130    pub(crate) fn new(
131        session: VortexSession,
132        layout_reader: LayoutReaderRef,
133        projection: BoundExpression,
134        filter: Option<BoundExpression>,
135        ordered: bool,
136        row_range: Option<Range<u64>>,
137        selection: Selection,
138        splits: Splits,
139        concurrency: usize,
140        map_fn: MapFn<A>,
141        limit: Option<ScanLimit>,
142        dtype: DType,
143    ) -> Self {
144        Self {
145            session,
146            layout_reader,
147            projection,
148            filter,
149            ordered,
150            row_range,
151            selection,
152            splits,
153            concurrency,
154            map_fn,
155            limit,
156            dtype,
157        }
158    }
159
160    fn task_context(&self) -> Arc<TaskContext> {
161        Arc::new(TaskContext {
162            filter: self
163                .filter
164                .clone()
165                .map(|filter| Arc::new(FilterExpr::new(filter))),
166            reader: Arc::clone(&self.layout_reader),
167            projection: self.projection.clone(),
168        })
169    }
170
171    fn split_ranges(&self, row_range: Option<Range<u64>>) -> Vec<Range<u64>> {
172        let selection_range: Option<Range<u64>> = match &self.selection {
173            Selection::IncludeByIndex(buf) if !buf.is_empty() => {
174                Some(buf[0]..buf[buf.len() - 1] + 1)
175            }
176            Selection::IncludeRoaring(roaring) if !roaring.is_empty() => {
177                Some(roaring.min().vortex_expect("empty")..roaring.max().vortex_expect("empty") + 1)
178            }
179            _ => None,
180        };
181        let row_range = intersect_ranges(self.row_range.as_ref(), row_range);
182        let row_range = intersect_ranges(row_range.as_ref(), selection_range);
183
184        match &self.splits {
185            Splits::Natural(vec) => {
186                debug_assert!(vec.is_sorted());
187                let splits_iter = match row_range {
188                    None => Either::Left(vec.iter().copied()),
189                    Some(range) => {
190                        if range.is_empty() {
191                            return Vec::new();
192                        }
193                        let lo = vec.partition_point(|&x| x <= range.start);
194                        let hi = vec.partition_point(|&x| x < range.end);
195                        Either::Right(
196                            iter::once(range.start)
197                                .chain(vec[lo..hi].iter().copied())
198                                .chain(iter::once(range.end)),
199                        )
200                    }
201                };
202
203                splits_iter
204                    .tuple_windows()
205                    .map(|(start, end)| start..end)
206                    .collect()
207            }
208            Splits::Ranges(ranges) => match row_range {
209                None => ranges.to_vec(),
210                Some(range) => {
211                    if range.is_empty() {
212                        return Vec::new();
213                    }
214                    ranges
215                        .iter()
216                        .filter_map(move |r| {
217                            let start = cmp::max(r.start, range.start);
218                            let end = cmp::min(r.end, range.end);
219                            (start < end).then_some(start..end)
220                        })
221                        .collect()
222                }
223            },
224        }
225    }
226
227    /// The non-empty row masks of the splits within `row_range`, in split order.
228    fn split_masks(&self, row_range: Option<Range<u64>>) -> impl Iterator<Item = RowMask> + '_ {
229        self.split_ranges(row_range)
230            .into_iter()
231            .filter(|range| range.start < range.end)
232            .map(|range| self.selection.row_mask(&range))
233            .filter(|row_mask| !row_mask.mask().all_false())
234    }
235
236    /// Execute the scan over `row_range` as a stream of the mapped split arrays.
237    ///
238    /// The stream ends after yielding its first error.
239    pub fn execute_stream(
240        &self,
241        row_range: Option<Range<u64>>,
242    ) -> VortexResult<BoxStream<'static, VortexResult<A>>> {
243        let concurrency = self.concurrency * get_available_parallelism().unwrap_or(1);
244        let handle = self.session.handle();
245        let ordered = self.ordered;
246        let ctx = self.task_context();
247
248        let map_fn = &self.map_fn;
249
250        let arrays = match self.limit.as_ref().map(ScanLimit::budget) {
251            // With a filter, a split's output row count is unknown until its filter has run.
252            // Splits therefore filter and project ahead of the consumer, and the rows they
253            // return are taken from the limit as they are emitted, discarding the excess. Split
254            // tasks are built lazily so that no new splits start once the budget is spent. The
255            // map function needs the trimmed arrays, so it runs in a second stage of CPU tasks.
256            Some(limit) if ctx.filter.is_some() => {
257                let gate = limit.clone();
258                let selection = self.selection.clone();
259                let split_handle = handle.clone();
260                let tasks = stream::iter(self.split_ranges(row_range))
261                    .take_while(move |_| future::ready(!gate.is_exhausted()))
262                    .map(move |row_range| {
263                        let ctx = Arc::clone(&ctx);
264                        let selection = selection.clone();
265                        split_handle.spawn_nested(move |handle| async move {
266                            let task = handle
267                                .spawn_cpu(move || {
268                                    let row_mask = selection.row_mask(&row_range);
269                                    if row_mask.mask().all_false() {
270                                        return Ok(None);
271                                    }
272                                    split_exec(&ctx, row_mask).map(Some)
273                                })
274                                .await?;
275                            match task {
276                                Some(task) => task.await,
277                                None => Ok(None),
278                            }
279                        })
280                    });
281                let map_fn = Arc::clone(map_fn);
282                let arrays = buffer(tasks, ordered, concurrency)
283                    .filter_map(|array| future::ready(array.transpose()))
284                    .boxed();
285                let mapped = limit_rows(arrays, limit).map(move |array| {
286                    let map_fn = Arc::clone(&map_fn);
287                    handle.spawn_cpu(move || map_fn(array?))
288                });
289                buffer(mapped, ordered, concurrency)
290            }
291            limit => {
292                // Build eagerly to register I/O up front. Without a filter, selected row counts
293                // are exact, so a limit can also be reserved here in split order.
294                let mut tasks = Vec::new();
295                for row_mask in self.split_masks(row_range) {
296                    let task = if let Some(limit) = &limit {
297                        if limit.is_exhausted() {
298                            break;
299                        }
300                        let mask = limit.limit(row_mask.mask().clone());
301                        split_projection(&ctx, &row_mask.row_range(), mask)?
302                    } else {
303                        split_exec(&ctx, row_mask)?
304                    };
305                    tasks.push(map_split(task, map_fn));
306                }
307                buffer(
308                    stream::iter(tasks).map(move |task| handle.spawn(task)),
309                    ordered,
310                    concurrency,
311                )
312                .filter_map(|array| future::ready(array.transpose()))
313                .boxed()
314            }
315        };
316
317        Ok(stop_on_error(arrays))
318    }
319}
320
321/// Apply `map_fn` to a split's array within the split's task.
322fn map_split<A: 'static + Send>(
323    task: SplitFuture,
324    map_fn: &MapFn<A>,
325) -> BoxFuture<'static, VortexResult<Option<A>>> {
326    let map_fn = Arc::clone(map_fn);
327    task.map(move |array| array?.map(|array| map_fn(array)).transpose())
328        .boxed()
329}
330
331/// Trim arrays to the budget and drop upstream work as soon as it is exhausted.
332fn limit_rows(
333    arrays: BoxStream<'static, VortexResult<ArrayRef>>,
334    limit: RowLimit,
335) -> BoxStream<'static, VortexResult<ArrayRef>> {
336    let mut arrays = Some(arrays);
337    stream::poll_fn(move |cx| {
338        loop {
339            if limit.is_exhausted() {
340                arrays = None;
341            }
342            let Some(stream) = arrays.as_mut() else {
343                return Poll::Ready(None);
344            };
345            let array = match ready!(stream.poll_next_unpin(cx)) {
346                Some(Ok(array)) => array,
347                end => {
348                    arrays = None;
349                    return Poll::Ready(end);
350                }
351            };
352            let granted = limit.take(array.len());
353            if limit.is_exhausted() {
354                arrays = None;
355            }
356            if granted != 0 {
357                return Poll::Ready(Some(if granted < array.len() {
358                    array.slice(0..granted)
359                } else {
360                    Ok(array)
361                }));
362            }
363        }
364    })
365    .boxed()
366}
367
368/// Yield the first error and drop upstream without polling it again.
369fn stop_on_error<T: Send + 'static>(
370    stream: BoxStream<'static, VortexResult<T>>,
371) -> BoxStream<'static, VortexResult<T>> {
372    let mut stream = Some(stream);
373    stream::poll_fn(move |cx| {
374        let Some(input) = stream.as_mut() else {
375            return Poll::Ready(None);
376        };
377        let item = ready!(input.poll_next_unpin(cx));
378        if !matches!(item, Some(Ok(_))) {
379            stream = None;
380        }
381        Poll::Ready(item)
382    })
383    .boxed()
384}
385
386/// Run up to `concurrency` futures of `tasks` at once, in order when `ordered` is set.
387fn buffer<T: Send + 'static>(
388    tasks: impl Stream<Item = impl Future<Output = T> + Send + 'static> + Send + 'static,
389    ordered: bool,
390    concurrency: usize,
391) -> BoxStream<'static, T> {
392    if ordered {
393        tasks.buffered(concurrency).boxed()
394    } else {
395        tasks.buffer_unordered(concurrency).boxed()
396    }
397}
398
399fn intersect_ranges(left: Option<&Range<u64>>, right: Option<Range<u64>>) -> Option<Range<u64>> {
400    match (left, right) {
401        (None, None) => None,
402        (None, Some(r)) => Some(r),
403        (Some(l), None) => Some(l.clone()),
404        (Some(l), Some(r)) => Some(cmp::max(l.start, r.start)..cmp::min(l.end, r.end)),
405    }
406}
407
408#[cfg(test)]
409mod tests {
410    use std::sync::Arc;
411    use std::task::Poll;
412
413    use futures::FutureExt;
414    use futures::StreamExt;
415    use futures::stream;
416    use rstest::rstest;
417    use vortex_array::IntoArray;
418    use vortex_array::arrays::PrimitiveArray;
419    use vortex_error::VortexResult;
420    use vortex_error::vortex_err;
421
422    use super::limit_rows;
423    use super::stop_on_error;
424    use crate::scan::limit::RowLimit;
425
426    #[rstest]
427    #[case::whole_array(4)]
428    #[case::trimmed_array(2)]
429    fn limit_ends_before_pending_work(#[case] rows: usize) -> VortexResult<()> {
430        let array = PrimitiveArray::from_iter(0..4i32).into_array();
431        let alive = Arc::new(());
432        let dropped = Arc::downgrade(&alive);
433        let pending = stream::poll_fn(move |_| {
434            let _ = &alive;
435            Poll::Pending
436        });
437        let arrays = stream::iter([Ok(array)]).chain(pending).boxed();
438        let mut limited = limit_rows(arrays, RowLimit::new(rows as u64));
439
440        let array = limited
441            .next()
442            .now_or_never()
443            .expect("ready")
444            .expect("array")?;
445        assert_eq!(array.len(), rows);
446        assert!(dropped.upgrade().is_none());
447        assert!(matches!(limited.next().now_or_never(), Some(None)));
448        Ok(())
449    }
450
451    #[test]
452    fn exhausted_shared_limit_ends_before_pending_work() {
453        let limit = RowLimit::new(1);
454        let mut limited = limit_rows(stream::pending().boxed(), limit.clone());
455        assert!(limited.next().now_or_never().is_none());
456
457        assert_eq!(limit.take(1), 1);
458        assert!(matches!(limited.next().now_or_never(), Some(None)));
459    }
460
461    #[test]
462    fn zero_limit_does_not_poll_input() {
463        let arrays = stream::poll_fn(|_| panic!("zero limit must not poll input")).boxed();
464        let mut limited = limit_rows(arrays, RowLimit::new(0));
465        assert!(matches!(limited.next().now_or_never(), Some(None)));
466    }
467
468    #[test]
469    fn error_ends_before_pending_work() {
470        let arrays = stream::iter([Err::<(), _>(vortex_err!("scan failed"))])
471            .chain(stream::pending())
472            .boxed();
473        let mut stream = stop_on_error(arrays);
474
475        assert!(matches!(stream.next().now_or_never(), Some(Some(Err(_)))));
476        assert!(matches!(stream.next().now_or_never(), Some(None)));
477    }
478}