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::ExecutionCtx;
14use vortex_array::IntoArray;
15use vortex_array::VortexSessionExecute;
16use vortex_array::aggregate_fn::fns::uncompressed_size_in_bytes::UncompressedSizeInBytes;
17use vortex_array::aggregate_fn::fns::uncompressed_size_in_bytes::UncompressedSizeOpts;
18use vortex_array::aggregate_fn::fns::uncompressed_size_in_bytes::uncompressed_size_in_bytes;
19use vortex_array::arrays::ChunkedArray;
20use vortex_array::dtype::DType;
21use vortex_error::VortexExpect;
22use vortex_error::VortexResult;
23use vortex_error::vortex_err;
24use vortex_session::VortexSession;
25
26use crate::LayoutRef;
27use crate::LayoutStrategy;
28use crate::LayoutWriterContext;
29use crate::segments::SegmentSinkRef;
30use crate::sequence::SendableSequentialStream;
31use crate::sequence::SequencePointer;
32use crate::sequence::SequentialStreamAdapter;
33use crate::sequence::SequentialStreamExt;
34
35#[derive(Clone)]
36pub struct RepartitionWriterOptions {
37 pub block_size_minimum: u64,
39 pub block_len_multiple: usize,
41 pub block_size_target: Option<u64>,
54 pub canonicalize: bool,
55}
56
57impl RepartitionWriterOptions {
58 fn effective_block_len(&self, dtype: &DType) -> usize {
64 let Some(block_size_target) = self.block_size_target else {
65 return self.block_len_multiple;
66 };
67 match dtype.element_size() {
68 Some(elem_size) if elem_size > 0 => {
69 let max_rows = usize::try_from(block_size_target.div_ceil(elem_size as u64))
72 .unwrap_or(usize::MAX);
73 self.block_len_multiple.min(max_rows).max(1)
74 }
75 _ => self.block_len_multiple,
76 }
77 }
78}
79
80#[derive(Clone)]
85pub struct RepartitionStrategy {
86 child: Arc<dyn LayoutStrategy>,
87 options: RepartitionWriterOptions,
88}
89
90impl RepartitionStrategy {
91 pub fn new<S: LayoutStrategy>(child: S, options: RepartitionWriterOptions) -> Self {
92 Self {
93 child: Arc::new(child),
94 options,
95 }
96 }
97}
98
99#[async_trait]
100impl LayoutStrategy for RepartitionStrategy {
101 async fn write_stream(
102 &self,
103 ctx: LayoutWriterContext,
104 segment_sink: SegmentSinkRef,
105 stream: SendableSequentialStream,
106 eof: SequencePointer,
107 session: &VortexSession,
108 ) -> VortexResult<LayoutRef> {
109 let dtype = stream.dtype().clone();
112 let stream = if self.options.canonicalize {
113 let canonicalize_session = session.clone();
114 SequentialStreamAdapter::new(
115 dtype.clone(),
116 stream.map(move |chunk| {
117 let (sequence_id, chunk) = chunk?;
118 let mut ctx = canonicalize_session.create_execution_ctx();
119 let canonical = chunk.execute::<Canonical>(&mut ctx)?.into_array();
120 VortexResult::Ok((sequence_id, canonical))
121 }),
122 )
123 .sendable()
124 } else {
125 stream
126 };
127
128 let dtype_clone = dtype.clone();
129 let options = self.options.clone();
130
131 let block_len = options.effective_block_len(&dtype);
135 let block_size_minimum = options.block_size_minimum;
136 let repartition_session = session.clone();
137
138 let repartitioned_stream = try_stream! {
139 let canonical_stream = stream.peekable();
140 pin_mut!(canonical_stream);
141
142 let mut ctx = repartition_session.create_execution_ctx();
143 let mut chunks = ChunksBuffer::new(block_size_minimum, block_len);
144 while let Some(chunk) = canonical_stream.as_mut().next().await {
145 let (sequence_id, chunk) = chunk?;
146 let mut sequence_pointer = sequence_id.descend();
147 let mut offset = 0;
148 while offset < chunk.len() {
149 let end = (offset + block_len).min(chunk.len());
150 let sliced = chunk.slice(offset..end)?;
151 chunks.push_back(sliced, &mut ctx)?;
152 offset = end;
153
154 if chunks.have_enough() {
155 let output_chunks = chunks.collect_exact_blocks(&mut ctx)?;
156 assert!(!output_chunks.is_empty());
157 let chunked =
158 ChunkedArray::try_new(output_chunks, dtype_clone.clone())?;
159 if !chunked.is_empty() {
160 let canonical = chunked.into_array().execute::<Canonical>(&mut ctx)?.into_array();
161 yield (
162 sequence_pointer.advance(),
163 canonical,
164 )
165 }
166 }
167 }
168 if canonical_stream.as_mut().peek().await.is_none() {
169 let to_flush = ChunkedArray::try_new(
170 chunks.data.drain(..).map(|(arr, _)| arr),
171 dtype_clone.clone(),
172 )?;
173 if !to_flush.is_empty() {
174 let canonical = to_flush.into_array().execute::<Canonical>(&mut ctx)?.into_array();
175 yield (
176 sequence_pointer.advance(),
177 canonical,
178 )
179 }
180 }
181 }
182 };
183
184 self.child
185 .write_stream(
186 ctx,
187 segment_sink,
188 SequentialStreamAdapter::new(dtype, repartitioned_stream).sendable(),
189 eof,
190 session,
191 )
192 .await
193 }
194}
195
196struct ChunksBuffer {
201 data: VecDeque<(ArrayRef, u64)>,
205 row_count: usize,
206 nbytes: u64,
207 block_size_minimum: u64,
208 block_len_multiple: usize,
209}
210
211impl ChunksBuffer {
212 fn new(block_size_minimum: u64, block_len_multiple: usize) -> Self {
213 Self {
214 data: Default::default(),
215 row_count: 0,
216 nbytes: 0,
217 block_size_minimum,
218 block_len_multiple,
219 }
220 }
221
222 fn have_enough(&self) -> bool {
223 self.nbytes >= self.block_size_minimum && self.row_count >= self.block_len_multiple
224 }
225
226 fn collect_exact_blocks(&mut self, ctx: &mut ExecutionCtx) -> VortexResult<Vec<ArrayRef>> {
227 let nblocks = self.row_count / self.block_len_multiple;
228 let mut res = Vec::with_capacity(self.data.len());
229 let mut remaining = nblocks * self.block_len_multiple;
230 while remaining > 0 {
231 let (chunk, _) = self
232 .pop_front()
233 .vortex_expect("must have at least one chunk");
234 let len = chunk.len();
235
236 if len > remaining {
237 let left = chunk.slice(0..remaining)?;
238 let right = chunk.slice(remaining..len)?;
239 self.push_front(right, ctx)?;
240 res.push(left);
241 remaining = 0;
242 } else {
243 res.push(chunk);
244 remaining -= len;
245 }
246 }
247 Ok(res)
248 }
249
250 fn push_back(&mut self, chunk: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
251 let nb = exact_size(&chunk, ctx)?;
252 self.row_count += chunk.len();
253 self.nbytes += nb;
254 self.data.push_back((chunk, nb));
255 Ok(())
256 }
257
258 fn push_front(&mut self, chunk: ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<()> {
259 let nb = exact_size(&chunk, ctx)?;
260 self.row_count += chunk.len();
261 self.nbytes += nb;
262 self.data.push_front((chunk, nb));
263 Ok(())
264 }
265
266 fn pop_front(&mut self) -> Option<(ArrayRef, u64)> {
267 let res = self.data.pop_front();
268 if let Some((chunk, nb)) = res.as_ref() {
269 self.row_count -= chunk.len();
270 self.nbytes -= nb;
271 }
272 res
273 }
274}
275
276fn exact_size(chunk: &ArrayRef, ctx: &mut ExecutionCtx) -> VortexResult<u64> {
279 if !UncompressedSizeInBytes::supports_dtype(chunk.dtype()) {
280 return Ok(chunk.nbytes());
281 }
282 let size = uncompressed_size_in_bytes(chunk, UncompressedSizeOpts::exact(), ctx)?;
283 u64::try_from(size).map_err(|e| vortex_err!("block size does not fit in u64: {e}"))
284}
285
286#[cfg(test)]
287mod tests {
288 use std::sync::Arc;
289
290 use vortex_array::ArrayContext;
291 use vortex_array::IntoArray;
292 use vortex_array::VortexSessionExecute;
293 use vortex_array::array_session;
294 use vortex_array::arrays::ChunkedArray;
295 use vortex_array::arrays::ConstantArray;
296 use vortex_array::arrays::FixedSizeListArray;
297 use vortex_array::arrays::PrimitiveArray;
298 use vortex_array::arrays::VarBinViewArray;
299 use vortex_array::arrays::varbinview::BinaryView;
300 use vortex_array::dtype::DType;
301 use vortex_array::dtype::Nullability::NonNullable;
302 use vortex_array::dtype::PType;
303 use vortex_array::validity::Validity;
304 use vortex_buffer::Buffer;
305 use vortex_buffer::ByteBuffer;
306 use vortex_error::VortexResult;
307 use vortex_io::runtime::single::block_on;
308 use vortex_io::session::RuntimeSessionExt;
309
310 use super::*;
311 use crate::LayoutStrategy;
312 use crate::arrays::shared::Shared;
313 use crate::layouts::chunked::writer::ChunkedLayoutStrategy;
314 use crate::layouts::flat::writer::FlatLayoutStrategy;
315 use crate::segments::TestSegments;
316 use crate::sequence::SequenceId;
317 use crate::sequence::SequentialArrayStreamExt;
318 use crate::test::new_session;
319
320 const ONE_MEG: u64 = 1 << 20;
321
322 #[test]
323 fn effective_block_len_variable_width() {
324 let dtype = DType::Utf8(NonNullable);
326 let options = RepartitionWriterOptions {
327 block_size_minimum: 0,
328 block_len_multiple: 8192,
329 block_size_target: Some(ONE_MEG),
330 canonicalize: false,
331 };
332 assert_eq!(options.effective_block_len(&dtype), 8192);
333 }
334
335 #[test]
336 fn effective_block_len_very_large_elements() {
337 let dtype = DType::FixedSizeList(
340 Arc::new(DType::Primitive(PType::F64, NonNullable)),
341 1_000_000,
342 NonNullable,
343 );
344 let options = RepartitionWriterOptions {
345 block_size_minimum: 0,
346 block_len_multiple: 8192,
347 block_size_target: Some(ONE_MEG),
348 canonicalize: false,
349 };
350 assert_eq!(options.effective_block_len(&dtype), 1);
351 }
352
353 #[test]
354 fn repartition_large_element_type_produces_small_blocks() -> VortexResult<()> {
355 let list_size: u32 = 1000;
360 let num_lists: usize = 1000;
361 let total_elements = list_size as usize * num_lists;
362
363 let elements = PrimitiveArray::from_iter((0..total_elements).map(|i| i as f64));
364 let fsl = FixedSizeListArray::new(
365 elements.into_array(),
366 list_size,
367 Validity::NonNullable,
368 num_lists,
369 );
370
371 let ctx = ArrayContext::empty();
372 let segments = Arc::new(TestSegments::default());
373 let (ptr, eof) = SequenceId::root().split();
374
375 let child = ChunkedLayoutStrategy::new(FlatLayoutStrategy::default());
376 let strategy = RepartitionStrategy::new(
377 child,
378 RepartitionWriterOptions {
379 block_size_minimum: 0,
380 block_len_multiple: 8192,
381 block_size_target: Some(ONE_MEG),
382 canonicalize: false,
383 },
384 );
385
386 let stream = fsl.into_array().to_array_stream().sequenced(ptr);
387 let layout = block_on(|handle| async move {
388 let session = new_session().with_handle(handle);
389 strategy
390 .write_stream(
391 ctx.into(),
392 Arc::<TestSegments>::clone(&segments),
393 stream,
394 eof,
395 &session,
396 )
397 .await
398 })?;
399
400 assert_eq!(layout.row_count(), num_lists as u64);
406
407 let nchildren = layout.nchildren();
409 assert!(nchildren > 1, "expected multiple chunks, got {nchildren}");
410
411 for i in 0..nchildren - 1 {
412 let child = layout.slot(i)?.vortex_expect("chunk slot present");
413 assert_eq!(
414 child.row_count(),
415 132,
416 "chunk {i} has {} rows, expected 132",
417 child.row_count()
418 );
419 }
420
421 let last = layout
423 .slot(nchildren - 1)?
424 .vortex_expect("chunk slot present");
425 assert_eq!(last.row_count(), 1000 - 132 * (nchildren as u64 - 1));
426
427 Ok(())
428 }
429
430 #[test]
431 fn repartition_small_element_type_unchanged() -> VortexResult<()> {
432 let num_elements: usize = 10000;
436 let elements = PrimitiveArray::from_iter((0..num_elements).map(|i| i as f64));
437
438 let ctx = ArrayContext::empty();
439 let segments = Arc::new(TestSegments::default());
440 let (ptr, eof) = SequenceId::root().split();
441
442 let child = ChunkedLayoutStrategy::new(FlatLayoutStrategy::default());
443 let strategy = RepartitionStrategy::new(
444 child,
445 RepartitionWriterOptions {
446 block_size_minimum: 0,
447 block_len_multiple: 8192,
448 block_size_target: Some(ONE_MEG),
449 canonicalize: false,
450 },
451 );
452
453 let stream = elements.into_array().to_array_stream().sequenced(ptr);
454 let layout = block_on(|handle| async move {
455 let session = new_session().with_handle(handle);
456 strategy
457 .write_stream(
458 ctx.into(),
459 Arc::<TestSegments>::clone(&segments),
460 stream,
461 eof,
462 &session,
463 )
464 .await
465 })?;
466
467 assert_eq!(layout.row_count(), num_elements as u64);
468 assert_eq!(layout.nchildren(), 2);
469 assert_eq!(
470 layout
471 .slot(0)?
472 .vortex_expect("chunk slot present")
473 .row_count(),
474 8192
475 );
476 assert_eq!(
477 layout
478 .slot(1)?
479 .vortex_expect("chunk slot present")
480 .row_count(),
481 1808
482 );
483
484 Ok(())
485 }
486
487 #[test]
490 fn repartition_coalesces_view_blocks_sharing_a_large_buffer() -> VortexResult<()> {
491 const VALUE_LEN: usize = 27;
492 const ROWS_PER_CHUNK: usize = 8192;
493 const NUM_CHUNKS: usize = 10;
494
495 let page = ByteBuffer::from(vec![b'x'; 2 << 20]);
497 let value = [b'x'; VALUE_LEN];
498 let dtype = DType::Utf8(NonNullable);
499 let chunks = (0..NUM_CHUNKS)
500 .map(|_| {
501 let views = (0..ROWS_PER_CHUNK)
502 .map(|row| {
503 BinaryView::make_view(&value, 0, u32::try_from(row * VALUE_LEN).unwrap())
504 })
505 .collect::<Buffer<BinaryView>>();
506 unsafe {
508 VarBinViewArray::new_unchecked(
509 views,
510 Arc::from([page.clone()]),
511 dtype.clone(),
512 Validity::NonNullable,
513 )
514 }
515 .into_array()
516 })
517 .collect::<Vec<_>>();
518 let chunked = ChunkedArray::try_new(chunks, dtype)?.into_array();
519
520 let ctx = ArrayContext::empty();
521 let segments = Arc::new(TestSegments::default());
522 let (ptr, eof) = SequenceId::root().split();
523 let strategy = RepartitionStrategy::new(
524 ChunkedLayoutStrategy::new(FlatLayoutStrategy::default()),
525 RepartitionWriterOptions {
526 block_size_minimum: ONE_MEG,
527 block_len_multiple: ROWS_PER_CHUNK,
528 block_size_target: None,
529 canonicalize: false,
530 },
531 );
532
533 let stream = chunked.to_array_stream().sequenced(ptr);
534 let layout = block_on(|handle| async move {
535 let session = new_session().with_handle(handle);
536 strategy
537 .write_stream(
538 ctx.into(),
539 Arc::<TestSegments>::clone(&segments),
540 stream,
541 eof,
542 &session,
543 )
544 .await
545 })?;
546
547 assert_eq!(layout.row_count(), (NUM_CHUNKS * ROWS_PER_CHUNK) as u64);
550 assert_eq!(layout.nchildren(), 4);
551 for i in 0..3 {
552 let child = layout.slot(i)?.vortex_expect("chunk slot present");
553 assert_eq!(child.row_count(), (3 * ROWS_PER_CHUNK) as u64);
554 }
555 Ok(())
556 }
557
558 #[test]
564 fn chunks_buffer_pop_front_no_panic_after_shared_execution() -> VortexResult<()> {
565 let mut ctx = array_session().create_execution_ctx();
566 let n = 20_000usize;
567 let block_len = 10_000usize;
568
569 let constant = ConstantArray::new(42i64, n);
570 let shared = Shared::new(constant.into_array());
571 let shared_handle = shared.clone();
572 let arr = shared.into_array();
573
574 let s1 = arr.slice(0..block_len)?;
575 let s2 = arr.slice(block_len..n)?;
576
577 let mut buf = ChunksBuffer::new(0, block_len);
578 buf.push_back(s1, &mut ctx)?;
579 buf.push_back(s2, &mut ctx)?;
580
581 let _output = buf.pop_front().unwrap();
582
583 use crate::arrays::shared::SharedArrayExt;
585 let _canonical =
586 shared_handle.get_or_compute(|source| source.clone().execute::<Canonical>(&mut ctx))?;
587
588 let _s2 = buf.pop_front().unwrap();
590 assert_eq!(buf.nbytes, 0);
591 assert_eq!(buf.row_count, 0);
592
593 Ok(())
594 }
595}