1use 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
17pub const DEFAULT_MAX_SPLIT_ROWS: u64 = IDEAL_SPLIT_SIZE;
21
22#[derive(Copy, Clone, Debug)]
26pub enum SplitBy {
27 Layout,
31 LayoutSubSplitting { max_rows: u64 },
38 RowCount(usize),
40 }
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 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
81fn layout_boundaries(
84 layout_reader: &dyn LayoutReader,
85 row_range: &Range<u64>,
86 field_mask: &[FieldMask],
87) -> VortexResult<Vec<u64>> {
88 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
99fn 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 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 out.push(lo);
125
126 let span = hi - lo;
127 if span > max_span {
128 let sub_count = span.div_ceil(max_span);
131 let sub_size = span.div_ceil(sub_count);
134 let mut point = lo + sub_size;
135 while point < hi {
136 out.push(point);
137 point = point.saturating_add(sub_size);
139 }
140 }
141 }
142 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 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 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 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 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 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 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 #[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 assert_eq!(out.first(), boundaries.first());
407 assert_eq!(out.last(), boundaries.last());
408 assert!(
410 out.windows(2).all(|w| w[0] < w[1]),
411 "not strictly increasing: {out:?}"
412 );
413 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}