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