Skip to main content

vortex_layout/scan/
split_by.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4use std::iter::once;
5use std::ops::Range;
6
7use vortex_array::dtype::FieldMask;
8use vortex_error::VortexExpect;
9use vortex_error::VortexResult;
10use vortex_error::vortex_ensure;
11
12use crate::LayoutReader;
13use crate::RowSplits;
14use crate::SplitRange;
15use crate::scan::IDEAL_SPLIT_SIZE;
16
17/// The default `max_rows` for [`SplitBy::LayoutSubSplitting`]: chunk-boundary spans wider than
18/// this are sub-divided into multiple row-range splits so that a file with few, large chunks can
19/// be decoded across multiple cores rather than one.
20pub const DEFAULT_MAX_SPLIT_ROWS: u64 = IDEAL_SPLIT_SIZE;
21
22/// Defines how the Vortex file is split into batches for reading.
23///
24/// Note that each split must fit into the platform's maximum usize.
25#[derive(Copy, Clone, Debug)]
26pub enum SplitBy {
27    /// Splits any time there is a chunk boundary in the file, and nowhere else. This yields
28    /// splits that follow the file's chunking exactly, trading intra-file decode parallelism
29    /// for fewer, larger batches.
30    Layout,
31    /// Splits like [`SplitBy::Layout`], except that spans between adjacent chunk boundaries
32    /// wider than `max_rows` are further sub-divided into evenly sized splits of at most
33    /// `max_rows` rows, so that a file with few, large chunks can still be decoded across
34    /// multiple cores. `max_rows` must be non-zero.
35    ///
36    /// This is the default, with [`DEFAULT_MAX_SPLIT_ROWS`].
37    LayoutSubSplitting { max_rows: u64 },
38    /// Splits every n rows.
39    RowCount(usize),
40    // UncompressedSize(u64),
41}
42
43impl Default for SplitBy {
44    fn default() -> Self {
45        SplitBy::LayoutSubSplitting {
46            max_rows: DEFAULT_MAX_SPLIT_ROWS,
47        }
48    }
49}
50
51impl SplitBy {
52    /// Compute the splits for the given layout.
53    // TODO(ngates): remove this once layout readers are stream based.
54    pub fn splits(
55        &self,
56        layout_reader: &dyn LayoutReader,
57        row_range: &Range<u64>,
58        field_mask: &[FieldMask],
59    ) -> VortexResult<Vec<u64>> {
60        Ok(match *self {
61            SplitBy::Layout => layout_boundaries(layout_reader, row_range, field_mask)?,
62            SplitBy::LayoutSubSplitting { max_rows } => {
63                vortex_ensure!(
64                    max_rows > 0,
65                    "SplitBy::LayoutSubSplitting requires a non-zero max_rows"
66                );
67                subdivide_large_spans(
68                    layout_boundaries(layout_reader, row_range, field_mask)?,
69                    max_rows,
70                )
71            }
72            SplitBy::RowCount(n) => row_range
73                .clone()
74                .step_by(n)
75                .chain(once(row_range.end))
76                .collect(),
77        })
78    }
79}
80
81/// The sorted, deduplicated chunk boundaries the layout registers within `row_range`, bracketed
82/// by the range's start (the layout registers its end).
83fn layout_boundaries(
84    layout_reader: &dyn LayoutReader,
85    row_range: &Range<u64>,
86    field_mask: &[FieldMask],
87) -> VortexResult<Vec<u64>> {
88    // We usually have under 100 splits so reserving upfront saves us some allocations
89    let mut row_splits = RowSplits::new_capacity(128);
90    row_splits.push(row_range.start);
91    layout_reader.register_splits(
92        field_mask,
93        &SplitRange::root(row_range.clone())?,
94        &mut row_splits,
95    )?;
96    Ok(row_splits.into_sorted_deduped())
97}
98
99/// Sub-divide any gap between adjacent split boundaries that is wider than `max_span` into evenly
100/// sized row-range sub-splits.
101///
102/// `boundaries` is the sorted, deduplicated list of split points produced by the layout (chunk
103/// boundaries). Downstream consumers turn this list into half-open ranges by pairing adjacent
104/// entries (`tuple_windows().map(|(s, e)| s..e)`), so the row coverage is fully determined by the
105/// boundary set. This function only *inserts* points that lie strictly between two existing
106/// adjacent boundaries; it never moves or removes a boundary. Splitting `[lo, hi)` at an interior
107/// point `m` (with `lo < m < hi`) yields exactly `[lo, m) + [m, hi)`, so the union of ranges is
108/// unchanged: the rows are still partitioned contiguously, with no gaps and no overlaps, covering
109/// every row exactly once. The output remains sorted and deduplicated.
110fn subdivide_large_spans(boundaries: Vec<u64>, max_span: u64) -> Vec<u64> {
111    debug_assert!(boundaries.is_sorted(), "boundaries must be sorted");
112    debug_assert!(max_span > 0, "max_span must be non-zero");
113
114    // Fast path: nothing to split (also covers the empty / single-boundary cases).
115    if boundaries.len() < 2 || boundaries.windows(2).all(|w| w[1] - w[0] <= max_span) {
116        return boundaries;
117    }
118
119    let mut out = Vec::with_capacity(boundaries.len() * 2);
120    for window in boundaries.windows(2) {
121        let lo = window[0];
122        let hi = window[1];
123        // Always emit the lower boundary; the final `hi` is appended once after the loop.
124        out.push(lo);
125
126        let span = hi - lo;
127        if span > max_span {
128            // Number of sub-ranges so that each is <= max_span. `span > max_span` and
129            // `max_span >= 1` guarantee `sub_count >= 2`.
130            let sub_count = span.div_ceil(max_span);
131            // Even sub-range size (rounded up); the last sub-range absorbs any remainder and is
132            // bounded by `hi`. Inserted points `lo + k*sub_size` are strictly in `(lo, hi)`.
133            let sub_size = span.div_ceil(sub_count);
134            let mut point = lo + sub_size;
135            while point < hi {
136                out.push(point);
137                // Saturating: a sum past u64::MAX can never be < `hi`, so the loop exits.
138                point = point.saturating_add(sub_size);
139            }
140        }
141    }
142    // Append the final boundary (the `hi` of the last window).
143    out.push(*boundaries.last().vortex_expect("len >= 2 checked above"));
144
145    debug_assert!(out.is_sorted(), "subdivided boundaries must stay sorted");
146    debug_assert!(
147        out.windows(2).all(|w| w[0] < w[1]),
148        "subdivided boundaries must stay strictly increasing (deduped)"
149    );
150    out
151}
152
153#[cfg(test)]
154mod test {
155    use std::any::Any;
156    use std::sync::Arc;
157
158    use futures::future::BoxFuture;
159    use vortex_array::ArrayContext;
160    use vortex_array::ArrayRef;
161    use vortex_array::IntoArray;
162    use vortex_array::MaskFuture;
163    use vortex_array::dtype::DType;
164    use vortex_array::dtype::FieldPath;
165    use vortex_array::dtype::Nullability;
166    use vortex_array::dtype::PType;
167    use vortex_array::expr::BoundExpression;
168    use vortex_buffer::buffer;
169    use vortex_error::vortex_bail;
170    use vortex_io::runtime::single::block_on;
171    use vortex_mask::Mask;
172
173    use super::*;
174    use crate::LayoutReaderRef;
175    use crate::LayoutStrategy;
176    use crate::RowSplits;
177    use crate::layouts::flat::writer::FlatLayoutStrategy;
178    use crate::scan::test::SCAN_SESSION;
179    use crate::scan::test::session_with_handle;
180    use crate::segments::TestSegments;
181    use crate::sequence::SequenceId;
182    use crate::sequence::SequentialArrayStreamExt;
183
184    fn reader() -> LayoutReaderRef {
185        let ctx = ArrayContext::empty();
186        let segments = Arc::new(TestSegments::default());
187        let (ptr, eof) = SequenceId::root().split();
188        let layout = block_on(|handle| async {
189            let session = session_with_handle(handle);
190            FlatLayoutStrategy::default()
191                .write_stream(
192                    ctx.into(),
193                    Arc::<TestSegments>::clone(&segments),
194                    buffer![1_i32; 10]
195                        .into_array()
196                        .to_array_stream()
197                        .sequenced(ptr),
198                    eof,
199                    &session,
200                )
201                .await
202        })
203        .unwrap();
204
205        layout
206            .new_reader("".into(), segments, &SCAN_SESSION, &Default::default())
207            .unwrap()
208    }
209
210    #[test]
211    fn test_layout_splits_flat() {
212        let reader = reader();
213
214        let splits = SplitBy::Layout
215            .splits(
216                reader.as_ref(),
217                &(0..10),
218                &[FieldMask::Exact(FieldPath::root())],
219            )
220            .unwrap();
221        assert_eq!(splits, vec![0u64, 10]);
222    }
223
224    #[test]
225    fn test_row_count_splits() {
226        let reader = reader();
227
228        let splits = SplitBy::RowCount(3)
229            .splits(
230                reader.as_ref(),
231                &(0..10),
232                &[FieldMask::Exact(FieldPath::root())],
233            )
234            .unwrap();
235        assert_eq!(splits, vec![0u64, 3, 6, 9, 10]);
236    }
237
238    /// A reader that registers a fixed set of interior split points (which may repeat) plus the
239    /// end of its row range.
240    struct StubReader {
241        name: Arc<str>,
242        dtype: DType,
243        row_count: u64,
244        interior_splits: Vec<u64>,
245    }
246
247    impl StubReader {
248        fn new(row_count: u64, interior_splits: Vec<u64>) -> Self {
249            Self {
250                name: Arc::from("stub"),
251                dtype: DType::Primitive(PType::U8, Nullability::NonNullable),
252                row_count,
253                interior_splits,
254            }
255        }
256    }
257
258    impl LayoutReader for StubReader {
259        fn name(&self) -> &Arc<str> {
260            &self.name
261        }
262
263        fn as_any(&self) -> &dyn Any {
264            self
265        }
266
267        fn dtype(&self) -> &DType {
268            &self.dtype
269        }
270
271        fn row_count(&self) -> u64 {
272            self.row_count
273        }
274
275        fn register_splits(
276            &self,
277            _field_mask: &[FieldMask],
278            split_range: &SplitRange,
279            splits: &mut RowSplits,
280        ) -> VortexResult<()> {
281            for split in &self.interior_splits {
282                splits.push(split_range.row_offset() + split);
283            }
284            splits.push(split_range.root_row_range().end);
285            Ok(())
286        }
287
288        fn pruning_evaluation(
289            &self,
290            _: &Range<u64>,
291            _: &BoundExpression,
292            _: Mask,
293        ) -> VortexResult<MaskFuture> {
294            vortex_bail!("not needed for this test")
295        }
296
297        fn filter_evaluation(
298            &self,
299            _: &Range<u64>,
300            _: &BoundExpression,
301            _: MaskFuture,
302        ) -> VortexResult<MaskFuture> {
303            vortex_bail!("not needed for this test")
304        }
305
306        fn projection_evaluation(
307            &self,
308            _: &Range<u64>,
309            _: &BoundExpression,
310            _: MaskFuture,
311        ) -> VortexResult<BoxFuture<'static, VortexResult<ArrayRef>>> {
312            vortex_bail!("not needed for this test")
313        }
314    }
315
316    #[test]
317    fn test_layout_splits_dedup() -> VortexResult<()> {
318        let reader = StubReader::new(10, vec![5, 5]);
319        let splits = SplitBy::Layout.splits(&reader, &(0..10), &[FieldMask::All])?;
320        assert_eq!(splits, vec![0u64, 5, 10]);
321        Ok(())
322    }
323
324    #[test]
325    fn test_layout_sub_splitting_max_rows() -> VortexResult<()> {
326        // A single 10-row chunk: left whole by SplitBy::Layout, sub-divided into pieces of at
327        // most `max_rows` by SplitBy::LayoutSubSplitting.
328        let reader = StubReader::new(10, vec![]);
329
330        let splits = SplitBy::Layout.splits(&reader, &(0..10), &[FieldMask::All])?;
331        assert_eq!(splits, vec![0, 10]);
332
333        let splits = SplitBy::LayoutSubSplitting { max_rows: 4 }.splits(
334            &reader,
335            &(0..10),
336            &[FieldMask::All],
337        )?;
338        assert_eq!(splits, vec![0, 4, 8, 10]);
339
340        Ok(())
341    }
342
343    #[test]
344    fn test_layout_sub_splitting_rejects_zero_max_rows() {
345        let reader = StubReader::new(10, vec![]);
346        let result = SplitBy::LayoutSubSplitting { max_rows: 0 }.splits(
347            &reader,
348            &(0..10),
349            &[FieldMask::All],
350        );
351        assert!(result.is_err());
352    }
353
354    #[test]
355    fn subdivide_below_threshold_is_noop() {
356        // Gaps all <= max_span: boundaries returned unchanged.
357        assert_eq!(subdivide_large_spans(vec![0, 5, 10], 100), vec![0, 5, 10]);
358        assert_eq!(subdivide_large_spans(vec![0, 100], 100), vec![0, 100]);
359        assert_eq!(
360            subdivide_large_spans(Vec::<u64>::new(), 100),
361            Vec::<u64>::new()
362        );
363        assert_eq!(subdivide_large_spans(vec![7], 100), vec![7]);
364    }
365
366    #[test]
367    fn subdivide_near_u64_max_does_not_overflow() {
368        // The increment past the last interior point would overflow without saturating math.
369        let hi = u64::MAX;
370        let out = subdivide_large_spans(vec![hi - 3, hi], 2);
371        assert_eq!(out, vec![hi - 3, hi - 1, hi]);
372    }
373
374    #[test]
375    fn subdivide_splits_large_single_chunk() {
376        // One large chunk [0, 1000) with max_span 100 -> 10 contiguous sub-splits.
377        let out = subdivide_large_spans(vec![0, 1000], 100);
378        assert_eq!(
379            out,
380            vec![0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]
381        );
382    }
383
384    #[test]
385    fn subdivide_only_large_gaps() {
386        // Mixed: [0,50) stays whole, [50, 350) splits into 100-row pieces, [350, 360) stays whole.
387        let out = subdivide_large_spans(vec![0, 50, 350, 360], 100);
388        assert_eq!(out, vec![0, 50, 150, 250, 350, 360]);
389    }
390
391    /// Property: for any sorted, deduped boundary set, subdivision (a) keeps the first and last
392    /// boundary, (b) stays strictly increasing, and (c) preserves exact row coverage — the union
393    /// of the half-open ranges the consumer derives is identical before and after.
394    #[test]
395    fn subdivide_preserves_exact_coverage() {
396        let cases: Vec<Vec<u64>> = vec![
397            vec![0, 1000],
398            vec![0, 7, 250_001],
399            vec![0, 5, 10, 15, 20, 25, 30],
400            vec![3, 1_000_003],
401            vec![0, 99_999, 100_000, 300_000],
402        ];
403        for boundaries in cases {
404            let out = subdivide_large_spans(boundaries.clone(), DEFAULT_MAX_SPLIT_ROWS);
405            // (a) endpoints preserved
406            assert_eq!(out.first(), boundaries.first());
407            assert_eq!(out.last(), boundaries.last());
408            // (b) strictly increasing (sorted + deduped)
409            assert!(
410                out.windows(2).all(|w| w[0] < w[1]),
411                "not strictly increasing: {out:?}"
412            );
413            // (c) exact coverage: ranges from `out` tile the same span with no gap/overlap, and
414            // every original boundary is still present (so original ranges are sub-divided, never
415            // merged or shifted).
416            let total: u64 = out.windows(2).map(|w| w[1] - w[0]).sum();
417            let expected_total = boundaries.last().unwrap() - boundaries.first().unwrap();
418            assert_eq!(
419                total, expected_total,
420                "coverage span changed for {boundaries:?}"
421            );
422            for b in &boundaries {
423                assert!(
424                    out.contains(b),
425                    "original boundary {b} dropped from {out:?}"
426                );
427            }
428        }
429    }
430}