Skip to main content

vortex_layout/scan/
layout.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::any::Any;
5use std::ops::Range;
6use std::pin::Pin;
7use std::sync::Arc;
8use std::task::Context;
9use std::task::Poll;
10
11use async_trait::async_trait;
12use futures::FutureExt;
13use futures::Stream;
14use futures::stream;
15use futures::stream::StreamExt;
16use vortex_array::IntoArray;
17use vortex_array::arrays::ConstantArray;
18use vortex_array::dtype::DType;
19use vortex_array::dtype::FieldPath;
20use vortex_array::dtype::Nullability;
21use vortex_array::expr::BoundExpression;
22use vortex_array::expr::stats::Precision;
23use vortex_array::scalar::Scalar;
24use vortex_array::stats::StatsSet;
25use vortex_array::stream::ArrayStreamAdapter;
26use vortex_array::stream::ArrayStreamExt;
27use vortex_array::stream::SendableArrayStream;
28use vortex_error::VortexResult;
29use vortex_error::vortex_bail;
30use vortex_mask::Mask;
31use vortex_metrics::MetricsRegistry;
32use vortex_scan::DataSource;
33use vortex_scan::DataSourceScan;
34use vortex_scan::DataSourceScanRef;
35use vortex_scan::Partition;
36use vortex_scan::PartitionRef;
37use vortex_scan::PartitionStream;
38use vortex_scan::ScanRequest;
39use vortex_scan::selection::Selection;
40use vortex_session::VortexSession;
41
42use crate::LayoutReaderRef;
43use crate::scan::scan_builder::ScanBuilder;
44
45/// An implementation of a [`DataSource`] that reads data from a [`LayoutReaderRef`].
46pub struct LayoutReaderDataSource {
47    reader: LayoutReaderRef,
48    session: VortexSession,
49    split_max_row_count: u64,
50    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
51}
52
53impl LayoutReaderDataSource {
54    /// Creates a new [`LayoutReaderDataSource`].
55    ///
56    /// By default, the entire scan is returned as a single split. This best preserves V1
57    /// `ScanBuilder` behavior where one scan covers the full row range, allowing the internal
58    /// I/O pipeline and `SplitBy::Layout` chunking to operate without per-split overhead from
59    /// redundant expression resolution and layout tree traversal.
60    pub fn new(reader: LayoutReaderRef, session: VortexSession) -> Self {
61        Self {
62            reader,
63            session,
64            split_max_row_count: u64::MAX,
65            metrics_registry: None,
66        }
67    }
68
69    /// Sets the maximum number of rows per Scan API split.
70    ///
71    /// Each split drives a [`ScanBuilder`] over its row range, which internally handles
72    /// physical layout alignment and I/O pipelining. This controls the engine-level
73    /// parallelism granularity, not the I/O granularity.
74    pub fn with_split_max_row_count(mut self, row_count: u64) -> Self {
75        self.split_max_row_count = row_count;
76        self
77    }
78
79    /// Sets the metrics registry for tracking scan performance.
80    pub fn with_metrics_registry(mut self, metrics: Arc<dyn MetricsRegistry>) -> Self {
81        self.metrics_registry = Some(metrics);
82        self
83    }
84
85    /// Optionally sets the metrics registry for tracking scan performance.
86    pub fn with_some_metrics_registry(mut self, metrics: Option<Arc<dyn MetricsRegistry>>) -> Self {
87        self.metrics_registry = metrics;
88        self
89    }
90}
91
92#[async_trait]
93impl DataSource for LayoutReaderDataSource {
94    fn dtype(&self) -> &DType {
95        self.reader.dtype()
96    }
97
98    fn row_count(&self) -> Precision<u64> {
99        Precision::exact(self.reader.row_count())
100    }
101
102    fn byte_size(&self) -> Precision<u64> {
103        Precision::Absent
104    }
105
106    fn deserialize_partition(
107        &self,
108        _data: &[u8],
109        _session: &VortexSession,
110    ) -> VortexResult<PartitionRef> {
111        vortex_bail!("LayoutReader splits are not yet serializable");
112    }
113
114    async fn scan(&self, scan_request: ScanRequest) -> VortexResult<DataSourceScanRef> {
115        let total_rows = self.reader.row_count();
116        let row_range = scan_request.row_range.unwrap_or(0..total_rows);
117
118        let projection = scan_request
119            .projection
120            .optimize_recursive(self.reader.dtype())?
121            .bind(self.reader.dtype())?;
122        let filter = scan_request
123            .filter
124            .map(|expr| {
125                expr.optimize_recursive(self.reader.dtype())?
126                    .bind(self.reader.dtype())
127            })
128            .transpose()?;
129        let dtype = projection.dtype().clone();
130
131        // If the dtype is an empty struct, and there is no filter, we can return a special
132        // length-only scan.
133        if let DType::Struct(fields, Nullability::NonNullable) = &dtype
134            && fields.nfields() == 0
135            && filter.is_none()
136        {
137            // FIXME(ngates): extract out maybe?
138            let row_count = row_range.end - row_range.start;
139            let row_count = scan_request.selection.row_count(row_count);
140
141            // Apply the limit.
142            let row_count = if let Some(limit) = scan_request.limit {
143                row_count.min(limit)
144            } else {
145                row_count
146            };
147
148            return Ok(Box::new(Empty { dtype, row_count }));
149        }
150
151        // Check file-level pruning: if the filter can be proven false for the entire row range
152        // using file-level statistics (e.g. via FileStatsLayoutReader), skip the scan entirely.
153        if let Some(filter) = &filter {
154            let mask = Mask::new_true(
155                usize::try_from(row_range.end - row_range.start).unwrap_or(usize::MAX),
156            );
157            let pruning_result = self
158                .reader
159                .pruning_evaluation(&row_range, filter, mask)?
160                .now_or_never();
161            if let Some(Ok(result_mask)) = pruning_result
162                && result_mask.all_false()
163            {
164                return Ok(Box::new(Empty {
165                    dtype,
166                    row_count: 0,
167                }));
168            }
169        }
170
171        Ok(Box::new(LayoutReaderScan {
172            reader: Arc::clone(&self.reader),
173            session: self.session.clone(),
174            dtype,
175            projection,
176            filter,
177            limit: scan_request.limit,
178            selection: scan_request.selection,
179            ordered: scan_request.ordered,
180            metrics_registry: self.metrics_registry.clone(),
181            next_row: row_range.start,
182            end_row: row_range.end,
183            split_size: self.split_max_row_count,
184        }))
185    }
186
187    async fn field_statistics(&self, _field_path: &FieldPath) -> VortexResult<StatsSet> {
188        Ok(StatsSet::default())
189    }
190}
191
192struct LayoutReaderScan {
193    reader: LayoutReaderRef,
194    session: VortexSession,
195    dtype: DType,
196    projection: BoundExpression,
197    filter: Option<BoundExpression>,
198    limit: Option<u64>,
199    ordered: bool,
200    selection: Selection,
201    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
202    next_row: u64,
203    end_row: u64,
204    split_size: u64,
205}
206
207impl DataSourceScan for LayoutReaderScan {
208    fn dtype(&self) -> &DType {
209        &self.dtype
210    }
211
212    fn partition_count(&self) -> Precision<usize> {
213        let (lower, upper) = self.size_hint();
214        match upper {
215            Some(u) if u == lower => Precision::exact(lower),
216            Some(u) => Precision::inexact(u),
217            None => Precision::inexact(lower),
218        }
219    }
220
221    fn partitions(self: Box<Self>) -> PartitionStream {
222        (*self).boxed()
223    }
224}
225
226impl Stream for LayoutReaderScan {
227    type Item = VortexResult<PartitionRef>;
228
229    fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
230        let this = self.get_mut();
231
232        if this.next_row >= this.end_row {
233            return Poll::Ready(None);
234        }
235
236        if this.limit.is_some_and(|limit| limit == 0) {
237            return Poll::Ready(None);
238        }
239
240        let split_end = this
241            .next_row
242            .saturating_add(this.split_size)
243            .min(this.end_row);
244        let row_range = this.next_row..split_end;
245        let split_rows = split_end - this.next_row;
246
247        let split_limit = this.limit;
248        // Only decrement the remaining limit when there is no filter. With a filter,
249        // the actual output row count is unknown (could be anywhere from 0 to split_rows),
250        // so decrementing by split_rows would be too aggressive and could stop producing
251        // splits before the limit is reached. Instead, pass the full remaining limit to
252        // each split and let the engine enforce the exact limit at the stream level.
253        if this.filter.is_none()
254            && let Some(ref mut limit) = this.limit
255        {
256            *limit = limit.saturating_sub(split_rows);
257        }
258
259        let split = Box::new(LayoutReaderSplit {
260            reader: Arc::clone(&this.reader),
261            session: this.session.clone(),
262            projection: this.projection.clone(),
263            filter: this.filter.clone(),
264            limit: split_limit,
265            ordered: this.ordered,
266            row_range,
267            selection: this.selection.clone(),
268            metrics_registry: this.metrics_registry.clone(),
269        }) as PartitionRef;
270
271        this.next_row = split_end;
272
273        Poll::Ready(Some(Ok(split)))
274    }
275
276    fn size_hint(&self) -> (usize, Option<usize>) {
277        if self.next_row >= self.end_row {
278            return (0, Some(0));
279        }
280        let remaining_rows = self.end_row - self.next_row;
281        let splits = remaining_rows.div_ceil(self.split_size);
282        (0, Some(usize::try_from(splits).unwrap_or(usize::MAX)))
283    }
284}
285
286struct LayoutReaderSplit {
287    reader: LayoutReaderRef,
288    session: VortexSession,
289    projection: BoundExpression,
290    filter: Option<BoundExpression>,
291    limit: Option<u64>,
292    ordered: bool,
293    row_range: Range<u64>,
294    selection: Selection,
295    metrics_registry: Option<Arc<dyn MetricsRegistry>>,
296}
297
298impl Partition for LayoutReaderSplit {
299    fn as_any(&self) -> &dyn Any {
300        self
301    }
302
303    #[expect(clippy::cast_possible_truncation)]
304    fn index(&self) -> usize {
305        // Row range is unique per split
306        self.row_range.start as usize
307    }
308
309    fn row_count(&self) -> Precision<u64> {
310        let row_count = self.row_range.end - self.row_range.start;
311        let row_count = self.selection.row_count(row_count);
312        let row_count = self.limit.map_or(row_count, |limit| row_count.min(limit));
313
314        if self.filter.is_some() {
315            Precision::inexact(row_count)
316        } else {
317            Precision::exact(row_count)
318        }
319    }
320
321    fn byte_size(&self) -> Precision<u64> {
322        Precision::Absent
323    }
324
325    fn execute(self: Box<Self>) -> VortexResult<SendableArrayStream> {
326        let builder = ScanBuilder::new(self.session, self.reader)
327            .with_row_range(self.row_range)
328            .with_selection(self.selection)
329            .with_projection(self.projection)
330            .with_some_filter(self.filter)
331            .with_some_limit(self.limit)
332            .with_some_metrics_registry(self.metrics_registry)
333            .with_ordered(self.ordered);
334
335        let dtype = builder.dtype()?;
336        // Use into_stream() which creates a LazyScanStream that spawns individual I/O
337        // tasks onto the runtime, enabling parallel execution across executor threads.
338        let stream = builder.into_stream()?;
339
340        Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
341            dtype, stream,
342        )))
343    }
344}
345
346/// A scan that produces no data, only empty arrays with the correct row count.
347struct Empty {
348    dtype: DType,
349    row_count: u64,
350}
351
352impl DataSourceScan for Empty {
353    fn dtype(&self) -> &DType {
354        &self.dtype
355    }
356
357    fn partition_count(&self) -> Precision<usize> {
358        Precision::exact(1usize)
359    }
360
361    fn partitions(self: Box<Self>) -> PartitionStream {
362        stream::iter([Ok(self as _)]).boxed()
363    }
364}
365
366impl Partition for Empty {
367    fn as_any(&self) -> &dyn Any {
368        self
369    }
370
371    fn index(&self) -> usize {
372        0
373    }
374
375    fn row_count(&self) -> Precision<u64> {
376        Precision::exact(self.row_count)
377    }
378
379    fn byte_size(&self) -> Precision<u64> {
380        Precision::exact(0u64)
381    }
382
383    fn execute(mut self: Box<Self>) -> VortexResult<SendableArrayStream> {
384        let scalar = Scalar::default_value(&self.dtype);
385        let dtype = self.dtype.clone();
386
387        // Create an iterator of arrays with the correct row count, respecting u64::MAX limits.
388        let iter = std::iter::from_fn(move || {
389            if self.row_count == 0 {
390                return None;
391            }
392            let chunk_size = usize::try_from(self.row_count).unwrap_or(usize::MAX);
393            self.row_count -= chunk_size as u64;
394            Some(VortexResult::Ok(
395                ConstantArray::new(scalar.clone(), chunk_size).into_array(),
396            ))
397        });
398
399        Ok(ArrayStreamExt::boxed(ArrayStreamAdapter::new(
400            dtype,
401            stream::iter(iter),
402        )))
403    }
404}