Skip to main content

vortex_layout/
reader.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::sync::Arc;
7
8use futures::future::BoxFuture;
9use futures::try_join;
10use once_cell::sync::OnceCell;
11use vortex_array::ArrayRef;
12use vortex_array::IntoArray;
13use vortex_array::MaskFuture;
14use vortex_array::builtins::ArrayBuiltins;
15use vortex_array::dtype::DType;
16use vortex_array::dtype::FieldMask;
17use vortex_array::expr::BoundExpression;
18use vortex_error::VortexResult;
19use vortex_error::vortex_bail;
20use vortex_mask::Mask;
21use vortex_session::VortexSession;
22
23use crate::LayoutReaderContext;
24use crate::children::LayoutChildren;
25use crate::segments::SegmentSource;
26
27/// Shared handle to a stateful layout reader.
28pub type LayoutReaderRef = Arc<dyn LayoutReader>;
29
30/// A row range used when registering natural scan splits.
31///
32/// Row range is relative to the reader that receives it. Offset is the offset
33/// that the local row range needs to be shifted by to get the global row range.
34#[derive(Clone, Debug, Eq, PartialEq)]
35pub struct SplitRange {
36    row_offset: u64,
37    row_range: Range<u64>,
38}
39
40impl SplitRange {
41    /// Constructs a split range, returning an error if the local row range is invalid.
42    pub fn try_new(row_offset: u64, row_range: Range<u64>) -> VortexResult<Self> {
43        if row_range.start > row_range.end {
44            vortex_bail!("Invalid split range {:?}", row_range);
45        }
46
47        Ok(Self {
48            row_offset,
49            row_range,
50        })
51    }
52
53    /// Constructs a split range for the root layout.
54    pub fn root(row_range: Range<u64>) -> VortexResult<Self> {
55        Self::try_new(0, row_range)
56    }
57
58    /// The root-layout row offset of this reader's local row zero.
59    pub fn row_offset(&self) -> u64 {
60        self.row_offset
61    }
62
63    /// The local row range within this reader.
64    pub fn row_range(&self) -> &Range<u64> {
65        &self.row_range
66    }
67
68    /// The length of the local row range.
69    pub fn len(&self) -> u64 {
70        self.row_range.end - self.row_range.start
71    }
72
73    /// Returns `true` if the local row range is empty.
74    pub fn is_empty(&self) -> bool {
75        self.row_range.is_empty()
76    }
77
78    /// Returns the equivalent row range in the root layout's coordinate space.
79    pub fn root_row_range(&self) -> Range<u64> {
80        self.row_offset + self.row_range.start..self.row_offset + self.row_range.end
81    }
82
83    /// Returns an error if the local row range is outside the given row count.
84    pub fn check_bounds(&self, row_count: u64) -> VortexResult<()> {
85        if self.row_range.end > row_count {
86            vortex_bail!(
87                "Split range {:?} is out of bounds for row count {}",
88                self.row_range,
89                row_count
90            );
91        }
92
93        Ok(())
94    }
95}
96
97/// A collection of root-coordinate row split points.
98///
99/// Boundaries arrive as non-descending runs, one per layout subtree walked; a descending push
100/// starts a new run. A run that exactly repeats the previous surviving run — common when sibling
101/// columns share chunk boundaries — is dropped as it completes, and the final sort is skipped
102/// when only a single run survives (the boundaries are then already ascending).
103pub struct RowSplits {
104    splits: Vec<u64>,
105    /// Start index of the run currently being appended.
106    run_start: usize,
107    /// Start index of the surviving run immediately before `run_start`.
108    prev_run_start: usize,
109}
110
111impl RowSplits {
112    /// Add a row boundary to the split set.
113    pub fn push(&mut self, row: u64) {
114        if let Some(&last) = self.splits.last()
115            && row < last
116        {
117            self.close_run();
118        }
119        self.splits.push(row);
120    }
121
122    /// Close the current run: drop it if it exactly repeats the previous surviving run,
123    /// otherwise keep it and start a new run.
124    fn close_run(&mut self) {
125        if !self.drop_repeated_run() {
126            self.prev_run_start = self.run_start;
127            self.run_start = self.splits.len();
128        }
129    }
130
131    /// Drop the current run if it exactly repeats the run before it. Repeated runs contribute
132    /// nothing to the sorted-deduped boundary set.
133    fn drop_repeated_run(&mut self) -> bool {
134        if self.run_start > 0
135            && self.splits[self.prev_run_start..self.run_start] == self.splits[self.run_start..]
136        {
137            self.splits.truncate(self.run_start);
138            return true;
139        }
140        false
141    }
142
143    /// Extend with a batch of non-descending row boundaries.
144    ///
145    /// Only the first element is checked for a descent against the current run, so the caller
146    /// must ensure the batch itself is non-descending.
147    pub fn extend_ascending(&mut self, rows: impl IntoIterator<Item = u64>) {
148        let mut rows = rows.into_iter();
149        let Some(first) = rows.next() else {
150            return;
151        };
152        self.push(first);
153        self.splits.extend(rows);
154        debug_assert!(
155            self.splits[self.run_start..].is_sorted(),
156            "extend_ascending batch must be non-descending"
157        );
158    }
159
160    /// Reserve space for additional row boundaries.
161    pub fn reserve(&mut self, additional: usize) {
162        self.splits.reserve(additional);
163    }
164
165    /// Create a new RowSplits with preallocated "capacity"
166    pub(crate) fn new_capacity(capacity: usize) -> Self {
167        Self {
168            splits: Vec::with_capacity(capacity),
169            run_start: 0,
170            prev_run_start: 0,
171        }
172    }
173
174    pub(crate) fn into_sorted_deduped(mut self) -> Vec<u64> {
175        let final_run_dropped = self.drop_repeated_run();
176        // Surviving runs always have a descent between them, so the boundaries are ascending
177        // iff a single run survived: no run before the final one (`prev_run_start == 0`) and
178        // the final one either is the first (`run_start == 0`) or was dropped.
179        let sorted = self.prev_run_start == 0 && (self.run_start == 0 || final_run_dropped);
180        if !sorted {
181            self.splits.sort_unstable();
182        }
183        self.splits.dedup();
184        self.splits.shrink_to_fit();
185        self.splits
186    }
187}
188
189/// Stateful reader for a [`crate::Layout`].
190///
191/// A reader owns or references any state needed to evaluate many scan operations over the same
192/// layout, such as child readers, decoded metadata, or segment caches. Scan planning calls
193/// [`register_splits`](Self::register_splits); execution calls pruning, filter, and projection
194/// evaluation for each selected row range.
195pub trait LayoutReader: 'static + Send + Sync {
196    /// Returns the name of the layout reader for debugging.
197    fn name(&self) -> &Arc<str>;
198
199    /// Returns this reader as [`Any`] for downcasting by specialized wrappers.
200    fn as_any(&self) -> &dyn Any;
201
202    /// Returns the un-projected dtype of the layout reader.
203    fn dtype(&self) -> &DType;
204
205    /// Returns the number of rows in the layout.
206    fn row_count(&self) -> u64;
207
208    /// Register natural split boundaries for this reader.
209    ///
210    /// `field_mask` contains the projected and filtered field paths needed by the scan.
211    /// Implementations should add root-coordinate split boundaries to `splits`, constrained to
212    /// `split_range`.
213    // TODO(ngates): this is a temporary API until we make layout readers stream based.
214    fn register_splits(
215        &self,
216        field_mask: &[FieldMask],
217        split_range: &SplitRange,
218        splits: &mut RowSplits,
219    ) -> VortexResult<()>;
220
221    /// Returns a mask where all false values are proven to be false in the given expression.
222    ///
223    /// The returned mask **does not** need to have been intersected with the input mask.
224    fn pruning_evaluation(
225        &self,
226        row_range: &Range<u64>,
227        expr: &BoundExpression,
228        mask: Mask,
229    ) -> VortexResult<MaskFuture>;
230
231    /// Refines the given mask, returning a mask equal in length to the input mask.
232    ///
233    /// It is recommended to defer awaiting the input mask for as long as possible (ideally, after
234    /// all I/O is complete). This allows other conjuncts the opportunity to refine the mask as much
235    /// as possible before it is used.
236    ///
237    /// ## Post-conditions
238    ///
239    /// The returned mask **MUST** have been intersected with the input mask.
240    fn filter_evaluation(
241        &self,
242        row_range: &Range<u64>,
243        expr: &BoundExpression,
244        mask: MaskFuture,
245    ) -> VortexResult<MaskFuture>;
246
247    /// Evaluates an expression against an array.
248    ///
249    /// It is recommended to defer awaiting the input mask for as long as possible (ideally, after
250    /// all I/O is complete). This allows other conjuncts the opportunity to refine the mask as much
251    /// as possible before it is used.
252    ///
253    /// ## Post-conditions
254    ///
255    /// The returned array **MUST** have length equal to the true count of the input mask.
256    fn projection_evaluation(
257        &self,
258        row_range: &Range<u64>,
259        expr: &BoundExpression,
260        mask: MaskFuture,
261    ) -> VortexResult<ArrayFuture>;
262}
263
264/// Future resolving to a projected Vortex array.
265pub type ArrayFuture = BoxFuture<'static, VortexResult<ArrayRef>>;
266
267/// Helpers for futures that resolve to arrays.
268pub trait ArrayFutureExt {
269    /// Apply a row mask to the resolved array.
270    fn masked(self, mask: MaskFuture) -> Self;
271}
272
273impl ArrayFutureExt for ArrayFuture {
274    /// Returns a new `ArrayFuture` that masks the output with a mask
275    fn masked(self, mask: MaskFuture) -> Self {
276        Box::pin(async move {
277            let (array, mask) = try_join!(self, mask)?;
278            array.mask(mask.into_array())
279        })
280    }
281}
282
283/// Per-child metadata for [`LazyReaderChildren`].
284enum ChildMeta {
285    /// Every child shares one dtype and one debug name (e.g. chunked layouts), avoiding a
286    /// clone per child at construction.
287    Uniform { dtype: DType, name: Arc<str> },
288    /// Distinct dtype and name per child.
289    PerChild {
290        dtypes: Vec<DType>,
291        names: Vec<Arc<str>>,
292    },
293}
294
295/// Lazily constructs and caches child readers while preserving reader context.
296pub struct LazyReaderChildren {
297    children: Arc<dyn LayoutChildren>,
298    meta: ChildMeta,
299    segment_source: Arc<dyn SegmentSource>,
300    session: VortexSession,
301    ctx: LayoutReaderContext,
302    // TODO(ngates): we may want a hash map of some sort here?
303    cache: Vec<OnceCell<LayoutReaderRef>>,
304}
305
306impl LazyReaderChildren {
307    /// Create a lazy child-reader cache.
308    ///
309    /// `dtypes` and `names` must be aligned with the child indices exposed by `children`.
310    pub fn new(
311        children: Arc<dyn LayoutChildren>,
312        dtypes: Vec<DType>,
313        names: Vec<Arc<str>>,
314        segment_source: Arc<dyn SegmentSource>,
315        session: VortexSession,
316        ctx: LayoutReaderContext,
317    ) -> Self {
318        Self::with_meta(
319            children,
320            ChildMeta::PerChild { dtypes, names },
321            segment_source,
322            session,
323            ctx,
324        )
325    }
326
327    /// Create a lazy child-reader cache where every child shares `dtype` and `name`.
328    pub fn new_uniform(
329        children: Arc<dyn LayoutChildren>,
330        dtype: DType,
331        name: Arc<str>,
332        segment_source: Arc<dyn SegmentSource>,
333        session: VortexSession,
334        ctx: LayoutReaderContext,
335    ) -> Self {
336        Self::with_meta(
337            children,
338            ChildMeta::Uniform { dtype, name },
339            segment_source,
340            session,
341            ctx,
342        )
343    }
344
345    fn with_meta(
346        children: Arc<dyn LayoutChildren>,
347        meta: ChildMeta,
348        segment_source: Arc<dyn SegmentSource>,
349        session: VortexSession,
350        ctx: LayoutReaderContext,
351    ) -> Self {
352        let nchildren = children.nchildren();
353        let cache = (0..nchildren).map(|_| OnceCell::new()).collect();
354        Self {
355            children,
356            meta,
357            segment_source,
358            session,
359            ctx,
360            cache,
361        }
362    }
363
364    /// Return the child reader at `idx`, constructing it on first access.
365    pub fn get(&self, idx: usize) -> VortexResult<&LayoutReaderRef> {
366        if idx >= self.cache.len() {
367            vortex_bail!("Child index out of bounds: {} of {}", idx, self.cache.len());
368        }
369
370        self.cache[idx].get_or_try_init(|| {
371            let (dtype, name) = match &self.meta {
372                ChildMeta::Uniform { dtype, name } => (dtype, name),
373                ChildMeta::PerChild { dtypes, names } => (&dtypes[idx], &names[idx]),
374            };
375            let child = self.children.child(idx, dtype)?;
376            child.new_reader(
377                Arc::clone(name),
378                Arc::clone(&self.segment_source),
379                &self.session,
380                &self.ctx,
381            )
382        })
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use rstest::rstest;
389
390    use super::RowSplits;
391
392    /// The result must always equal the plain sort+dedup of every pushed value, regardless of
393    /// how pushes group into runs or which runs get dropped early.
394    #[rstest]
395    // Identical runs collapse (aligned columns).
396    #[case(vec![vec![0, 5, 10], vec![0, 5, 10], vec![0, 5, 10]])]
397    // Misaligned runs merge through the sort fallback.
398    #[case(vec![vec![0, 5, 10], vec![0, 3, 10]])]
399    // A run that is a strict prefix of its predecessor is not dropped.
400    #[case(vec![vec![0, 5, 10], vec![0, 5]])]
401    // A run extending its predecessor survives.
402    #[case(vec![vec![10, 20, 30], vec![10, 20, 30, 40]])]
403    // Repeated runs after a distinct run still collapse.
404    #[case(vec![vec![0, 5], vec![0, 3, 5], vec![0, 3, 5]])]
405    // Identical runs re-diverging.
406    #[case(vec![vec![0, 5], vec![0, 5], vec![0, 3]])]
407    // Single-element descending runs.
408    #[case(vec![vec![5], vec![3], vec![1]])]
409    // Adjacent duplicates within a run.
410    #[case(vec![vec![0, 5, 5, 10]])]
411    // Single run stays untouched.
412    #[case(vec![vec![0, 5, 10]])]
413    // No pushes at all.
414    #[case(vec![])]
415    fn into_sorted_deduped_matches_model(#[case] runs: Vec<Vec<u64>>) {
416        let mut splits = RowSplits::new_capacity(16);
417        let mut model = Vec::new();
418        for run in &runs {
419            for &row in run {
420                splits.push(row);
421                model.push(row);
422            }
423        }
424        model.sort_unstable();
425        model.dedup();
426        assert_eq!(splits.into_sorted_deduped(), model);
427    }
428}