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}