1use std::collections::VecDeque;
5use std::sync::Arc;
6
7use async_stream::try_stream;
8use async_trait::async_trait;
9use futures::StreamExt as _;
10use futures::pin_mut;
11use vortex_array::ArrayRef;
12use vortex_array::Canonical;
13use vortex_array::IntoArray;
14use vortex_array::VortexSessionExecute;
15use vortex_array::arrays::ChunkedArray;
16use vortex_array::dtype::DType;
17use vortex_error::VortexExpect;
18use vortex_error::VortexResult;
19use vortex_session::VortexSession;
20
21use crate::LayoutRef;
22use crate::LayoutStrategy;
23use crate::LayoutWriterContext;
24use crate::segments::SegmentSinkRef;
25use crate::sequence::SendableSequentialStream;
26use crate::sequence::SequencePointer;
27use crate::sequence::SequentialStreamAdapter;
28use crate::sequence::SequentialStreamExt;
29
30#[derive(Clone)]
31pub struct RepartitionWriterOptions {
32 pub block_size_minimum: u64,
34 pub block_len_multiple: usize,
36 pub block_size_target: Option<u64>,
49 pub canonicalize: bool,
50}
51
52impl RepartitionWriterOptions {
53 fn effective_block_len(&self, dtype: &DType) -> usize {
59 let Some(block_size_target) = self.block_size_target else {
60 return self.block_len_multiple;
61 };
62 match dtype.element_size() {
63 Some(elem_size) if elem_size > 0 => {
64 let max_rows = usize::try_from(block_size_target.div_ceil(elem_size as u64))
67 .unwrap_or(usize::MAX);
68 self.block_len_multiple.min(max_rows).max(1)
69 }
70 _ => self.block_len_multiple,
71 }
72 }
73}
74
75#[derive(Clone)]
80pub struct RepartitionStrategy {
81 child: Arc<dyn LayoutStrategy>,
82 options: RepartitionWriterOptions,
83}
84
85impl RepartitionStrategy {
86 pub fn new<S: LayoutStrategy>(child: S, options: RepartitionWriterOptions) -> Self {
87 Self {
88 child: Arc::new(child),
89 options,
90 }
91 }
92}
93
94#[async_trait]
95impl LayoutStrategy for RepartitionStrategy {
96 async fn write_stream(
97 &self,
98 ctx: LayoutWriterContext,
99 segment_sink: SegmentSinkRef,
100 stream: SendableSequentialStream,
101 eof: SequencePointer,
102 session: &VortexSession,
103 ) -> VortexResult<LayoutRef> {
104 let dtype = stream.dtype().clone();
107 let stream = if self.options.canonicalize {
108 let canonicalize_session = session.clone();
109 SequentialStreamAdapter::new(
110 dtype.clone(),
111 stream.map(move |chunk| {
112 let (sequence_id, chunk) = chunk?;
113 let mut ctx = canonicalize_session.create_execution_ctx();
114 let canonical = chunk.execute::<Canonical>(&mut ctx)?.into_array();
115 VortexResult::Ok((sequence_id, canonical))
116 }),
117 )
118 .sendable()
119 } else {
120 stream
121 };
122
123 let dtype_clone = dtype.clone();
124 let options = self.options.clone();
125
126 let block_len = options.effective_block_len(&dtype);
130 let block_size_minimum = options.block_size_minimum;
131 let repartition_session = session.clone();
132
133 let repartitioned_stream = try_stream! {
134 let canonical_stream = stream.peekable();
135 pin_mut!(canonical_stream);
136
137 let mut ctx = repartition_session.create_execution_ctx();
138 let mut chunks = ChunksBuffer::new(block_size_minimum, block_len);
139 while let Some(chunk) = canonical_stream.as_mut().next().await {
140 let (sequence_id, chunk) = chunk?;
141 let mut sequence_pointer = sequence_id.descend();
142 let mut offset = 0;
143 while offset < chunk.len() {
144 let end = (offset + block_len).min(chunk.len());
145 let sliced = chunk.slice(offset..end)?;
146 chunks.push_back(sliced);
147 offset = end;
148
149 if chunks.have_enough() {
150 let output_chunks = chunks.collect_exact_blocks()?;
151 assert!(!output_chunks.is_empty());
152 let chunked =
153 ChunkedArray::try_new(output_chunks, dtype_clone.clone())?;
154 if !chunked.is_empty() {
155 let canonical = chunked.into_array().execute::<Canonical>(&mut ctx)?.into_array();
156 yield (
157 sequence_pointer.advance(),
158 canonical,
159 )
160 }
161 }
162 }
163 if canonical_stream.as_mut().peek().await.is_none() {
164 let to_flush = ChunkedArray::try_new(
165 chunks.data.drain(..).map(|(arr, _)| arr),
166 dtype_clone.clone(),
167 )?;
168 if !to_flush.is_empty() {
169 let canonical = to_flush.into_array().execute::<Canonical>(&mut ctx)?.into_array();
170 yield (
171 sequence_pointer.advance(),
172 canonical,
173 )
174 }
175 }
176 }
177 };
178
179 self.child
180 .write_stream(
181 ctx,
182 segment_sink,
183 SequentialStreamAdapter::new(dtype, repartitioned_stream).sendable(),
184 eof,
185 session,
186 )
187 .await
188 }
189}
190
191struct ChunksBuffer {
192 data: VecDeque<(ArrayRef, u64)>,
196 row_count: usize,
197 nbytes: u64,
198 block_size_minimum: u64,
199 block_len_multiple: usize,
200}
201
202impl ChunksBuffer {
203 fn new(block_size_minimum: u64, block_len_multiple: usize) -> Self {
204 Self {
205 data: Default::default(),
206 row_count: 0,
207 nbytes: 0,
208 block_size_minimum,
209 block_len_multiple,
210 }
211 }
212
213 fn have_enough(&self) -> bool {
214 self.nbytes >= self.block_size_minimum && self.row_count >= self.block_len_multiple
215 }
216
217 fn collect_exact_blocks(&mut self) -> VortexResult<Vec<ArrayRef>> {
218 let nblocks = self.row_count / self.block_len_multiple;
219 let mut res = Vec::with_capacity(self.data.len());
220 let mut remaining = nblocks * self.block_len_multiple;
221 while remaining > 0 {
222 let (chunk, _) = self
223 .pop_front()
224 .vortex_expect("must have at least one chunk");
225 let len = chunk.len();
226
227 if len > remaining {
228 let left = chunk.slice(0..remaining)?;
229 let right = chunk.slice(remaining..len)?;
230 self.push_front(right);
231 res.push(left);
232 remaining = 0;
233 } else {
234 res.push(chunk);
235 remaining -= len;
236 }
237 }
238 Ok(res)
239 }
240
241 fn push_back(&mut self, chunk: ArrayRef) {
242 let nb = chunk.nbytes();
243 self.row_count += chunk.len();
244 self.nbytes += nb;
245 self.data.push_back((chunk, nb));
246 }
247
248 fn push_front(&mut self, chunk: ArrayRef) {
249 let nb = chunk.nbytes();
250 self.row_count += chunk.len();
251 self.nbytes += nb;
252 self.data.push_front((chunk, nb));
253 }
254
255 fn pop_front(&mut self) -> Option<(ArrayRef, u64)> {
256 let res = self.data.pop_front();
257 if let Some((chunk, nb)) = res.as_ref() {
258 self.row_count -= chunk.len();
259 self.nbytes -= nb;
260 }
261 res
262 }
263}
264
265#[cfg(test)]
266mod tests {
267 use std::sync::Arc;
268
269 use vortex_array::ArrayContext;
270 use vortex_array::IntoArray;
271 use vortex_array::VortexSessionExecute;
272 use vortex_array::array_session;
273 use vortex_array::arrays::ConstantArray;
274 use vortex_array::arrays::FixedSizeListArray;
275 use vortex_array::arrays::PrimitiveArray;
276 use vortex_array::arrays::SharedArray;
277 use vortex_array::dtype::DType;
278 use vortex_array::dtype::Nullability::NonNullable;
279 use vortex_array::dtype::PType;
280 use vortex_array::validity::Validity;
281 use vortex_error::VortexResult;
282 use vortex_io::runtime::single::block_on;
283 use vortex_io::session::RuntimeSessionExt;
284
285 use super::*;
286 use crate::LayoutStrategy;
287 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
288 use crate::layouts::flat::writer::FlatLayoutStrategy;
289 use crate::segments::TestSegments;
290 use crate::sequence::SequenceId;
291 use crate::sequence::SequentialArrayStreamExt;
292 use crate::test::new_session;
293
294 const ONE_MEG: u64 = 1 << 20;
295
296 #[test]
297 fn effective_block_len_small_elements() {
298 let dtype = DType::Primitive(PType::F64, NonNullable);
300 let options = RepartitionWriterOptions {
301 block_size_minimum: 0,
302 block_len_multiple: 8192,
303 block_size_target: Some(ONE_MEG),
304 canonicalize: false,
305 };
306 assert_eq!(options.effective_block_len(&dtype), 8192);
307 }
308
309 #[test]
310 fn effective_block_len_large_elements() {
311 let dtype = DType::FixedSizeList(
314 Arc::new(DType::Primitive(PType::F64, NonNullable)),
315 1000,
316 NonNullable,
317 );
318 let options = RepartitionWriterOptions {
319 block_size_minimum: 0,
320 block_len_multiple: 8192,
321 block_size_target: Some(ONE_MEG),
322 canonicalize: false,
323 };
324 assert_eq!(options.effective_block_len(&dtype), 132);
325 }
326
327 #[test]
328 fn effective_block_len_variable_width() {
329 let dtype = DType::Utf8(NonNullable);
331 let options = RepartitionWriterOptions {
332 block_size_minimum: 0,
333 block_len_multiple: 8192,
334 block_size_target: Some(ONE_MEG),
335 canonicalize: false,
336 };
337 assert_eq!(options.effective_block_len(&dtype), 8192);
338 }
339
340 #[test]
341 fn effective_block_len_very_large_elements() {
342 let dtype = DType::FixedSizeList(
345 Arc::new(DType::Primitive(PType::F64, NonNullable)),
346 1_000_000,
347 NonNullable,
348 );
349 let options = RepartitionWriterOptions {
350 block_size_minimum: 0,
351 block_len_multiple: 8192,
352 block_size_target: Some(ONE_MEG),
353 canonicalize: false,
354 };
355 assert_eq!(options.effective_block_len(&dtype), 1);
356 }
357
358 #[test]
359 fn repartition_large_element_type_produces_small_blocks() -> VortexResult<()> {
360 let list_size: u32 = 1000;
365 let num_lists: usize = 1000;
366 let total_elements = list_size as usize * num_lists;
367
368 let elements = PrimitiveArray::from_iter((0..total_elements).map(|i| i as f64));
369 let fsl = FixedSizeListArray::new(
370 elements.into_array(),
371 list_size,
372 Validity::NonNullable,
373 num_lists,
374 );
375
376 let ctx = ArrayContext::empty();
377 let segments = Arc::new(TestSegments::default());
378 let (ptr, eof) = SequenceId::root().split();
379
380 let child = ChunkedLayoutStrategy::new(FlatLayoutStrategy::default());
381 let strategy = RepartitionStrategy::new(
382 child,
383 RepartitionWriterOptions {
384 block_size_minimum: 0,
385 block_len_multiple: 8192,
386 block_size_target: Some(ONE_MEG),
387 canonicalize: false,
388 },
389 );
390
391 let stream = fsl.into_array().to_array_stream().sequenced(ptr);
392 let layout = block_on(|handle| async move {
393 let session = new_session().with_handle(handle);
394 strategy
395 .write_stream(
396 ctx.into(),
397 Arc::<TestSegments>::clone(&segments),
398 stream,
399 eof,
400 &session,
401 )
402 .await
403 })?;
404
405 assert_eq!(layout.row_count(), num_lists as u64);
411
412 let nchildren = layout.nchildren();
414 assert!(nchildren > 1, "expected multiple chunks, got {nchildren}");
415
416 for i in 0..nchildren - 1 {
417 let child = layout.slot(i)?.vortex_expect("chunk slot present");
418 assert_eq!(
419 child.row_count(),
420 132,
421 "chunk {i} has {} rows, expected 131",
422 child.row_count()
423 );
424 }
425
426 let last = layout
428 .slot(nchildren - 1)?
429 .vortex_expect("chunk slot present");
430 assert_eq!(last.row_count(), 1000 - 132 * (nchildren as u64 - 1));
431
432 Ok(())
433 }
434
435 #[test]
436 fn repartition_small_element_type_unchanged() -> VortexResult<()> {
437 let num_elements: usize = 10000;
441 let elements = PrimitiveArray::from_iter((0..num_elements).map(|i| i as f64));
442
443 let ctx = ArrayContext::empty();
444 let segments = Arc::new(TestSegments::default());
445 let (ptr, eof) = SequenceId::root().split();
446
447 let child = ChunkedLayoutStrategy::new(FlatLayoutStrategy::default());
448 let strategy = RepartitionStrategy::new(
449 child,
450 RepartitionWriterOptions {
451 block_size_minimum: 0,
452 block_len_multiple: 8192,
453 block_size_target: Some(ONE_MEG),
454 canonicalize: false,
455 },
456 );
457
458 let stream = elements.into_array().to_array_stream().sequenced(ptr);
459 let layout = block_on(|handle| async move {
460 let session = new_session().with_handle(handle);
461 strategy
462 .write_stream(
463 ctx.into(),
464 Arc::<TestSegments>::clone(&segments),
465 stream,
466 eof,
467 &session,
468 )
469 .await
470 })?;
471
472 assert_eq!(layout.row_count(), num_elements as u64);
473 assert_eq!(layout.nchildren(), 2);
474 assert_eq!(
475 layout
476 .slot(0)?
477 .vortex_expect("chunk slot present")
478 .row_count(),
479 8192
480 );
481 assert_eq!(
482 layout
483 .slot(1)?
484 .vortex_expect("chunk slot present")
485 .row_count(),
486 1808
487 );
488
489 Ok(())
490 }
491
492 #[test]
498 fn chunks_buffer_pop_front_no_panic_after_shared_execution() -> VortexResult<()> {
499 let mut ctx = array_session().create_execution_ctx();
500 let n = 20_000usize;
501 let block_len = 10_000usize;
502
503 let constant = ConstantArray::new(42i64, n);
504 let shared = SharedArray::new(constant.into_array());
505 let shared_handle = shared.clone();
506 let arr = shared.into_array();
507
508 let s1 = arr.slice(0..block_len)?;
509 let s2 = arr.slice(block_len..n)?;
510
511 let mut buf = ChunksBuffer::new(0, block_len);
512 buf.push_back(s1);
513 buf.push_back(s2);
514
515 let _output = buf.pop_front().unwrap();
516
517 use vortex_array::arrays::shared::SharedArrayExt;
519 let _canonical =
520 shared_handle.get_or_compute(|source| source.clone().execute::<Canonical>(&mut ctx))?;
521
522 let _s2 = buf.pop_front().unwrap();
524 assert_eq!(buf.nbytes, 0);
525 assert_eq!(buf.row_count, 0);
526
527 Ok(())
528 }
529}