1use arrow_array::RecordBatch;
7use arrow_schema::{ArrowError, SchemaRef};
8use futures::stream::{self, Stream, StreamExt};
9use std::pin::Pin;
10
11use crate::deepcopy::deep_copy_batch_sliced;
12
13pub fn rechunk_stream_by_size<S, E>(
21 input: S,
22 input_schema: SchemaRef,
23 min_bytes: usize,
24 max_bytes: usize,
25) -> impl Stream<Item = Result<RecordBatch, E>>
26where
27 S: Stream<Item = Result<RecordBatch, E>>,
28 E: From<ArrowError>,
29{
30 rechunk_stream_by_size_inner(input, input_schema, min_bytes, max_bytes, false)
31}
32
33pub fn rechunk_stream_by_size_deep_copy<S, E>(
49 input: S,
50 input_schema: SchemaRef,
51 min_bytes: usize,
52 max_bytes: usize,
53) -> impl Stream<Item = Result<RecordBatch, E>>
54where
55 S: Stream<Item = Result<RecordBatch, E>>,
56 E: From<ArrowError>,
57{
58 rechunk_stream_by_size_inner(input, input_schema, min_bytes, max_bytes, true)
59}
60
61fn rechunk_stream_by_size_inner<S, E>(
62 input: S,
63 input_schema: SchemaRef,
64 min_bytes: usize,
65 max_bytes: usize,
66 deep_copy: bool,
67) -> impl Stream<Item = Result<RecordBatch, E>>
68where
69 S: Stream<Item = Result<RecordBatch, E>>,
70 E: From<ArrowError>,
71{
72 stream::try_unfold(
73 RechunkState {
74 input: Box::pin(input),
75 accumulated: Vec::new(),
76 acc_bytes: 0,
77 done: false,
78 input_schema,
79 min_bytes,
80 max_bytes,
81 deep_copy,
82 },
83 |mut state| async move {
84 if state.done && state.accumulated.is_empty() {
85 return Ok(None);
86 }
87
88 while !state.done && (state.accumulated.is_empty() || state.acc_bytes < state.min_bytes)
91 {
92 match state.input.next().await {
93 Some(Ok(batch)) => {
94 state.acc_bytes += batch.get_array_memory_size();
95 state.accumulated.push(batch);
96 }
97 Some(Err(e)) => return Err(e),
98 None => {
99 state.done = true;
100 }
101 }
102 }
103
104 if state.accumulated.is_empty() {
105 return Ok(None);
106 }
107
108 if state.accumulated.len() > 1
112 && state.accumulated[0].get_array_memory_size() >= state.min_bytes
113 {
114 let b = state.accumulated.remove(0);
115 state.acc_bytes -= b.get_array_memory_size();
116 return Ok(Some((b, state)));
117 }
118
119 let batch = if state.accumulated.len() == 1 {
120 state.accumulated.pop().unwrap()
121 } else {
122 let b =
123 arrow_select::concat::concat_batches(&state.input_schema, &state.accumulated)
124 .map_err(E::from)?;
125 state.accumulated.clear();
126 b
127 };
128 state.acc_bytes = 0;
129
130 let mut slices =
132 slice_batch(batch, state.max_bytes, state.deep_copy).map_err(E::from)?;
133
134 if slices.len() == 1 {
135 Ok(Some((slices.pop().unwrap(), state)))
136 } else {
137 let first = slices.remove(0);
138
139 for a in &slices {
141 state.acc_bytes += a.get_array_memory_size();
142 }
143 state.accumulated = slices;
144
145 Ok(Some((first, state)))
146 }
147 },
148 )
149}
150
151fn slice_batch(
163 batch: RecordBatch,
164 max_bytes: usize,
165 deep_copy: bool,
166) -> Result<Vec<RecordBatch>, ArrowError> {
167 let batch_bytes = batch.get_array_memory_size();
168 let num_rows = batch.num_rows();
169
170 if batch_bytes <= max_bytes {
171 return Ok(vec![batch]);
172 }
173
174 if num_rows <= 1 {
175 if deep_copy {
182 return Ok(vec![deep_copy_batch_sliced(&batch)?]);
183 }
184 return Ok(vec![batch]);
185 }
186
187 let rows_per_chunk = (max_bytes as u64 * num_rows as u64 / batch_bytes as u64).max(1) as usize;
188
189 let mut result = Vec::new();
190 let mut offset = 0;
191 while offset < num_rows {
192 let len = rows_per_chunk.min(num_rows - offset);
193 let slice = batch.slice(offset, len);
194 if deep_copy {
195 let copied = deep_copy_batch_sliced(&slice)?;
196 result.extend(slice_batch(copied, max_bytes, true)?);
199 } else {
200 result.push(slice);
201 }
202 offset += len;
203 }
204
205 Ok(result)
206}
207
208struct RechunkState<S> {
212 input: Pin<Box<S>>,
213 accumulated: Vec<RecordBatch>,
214 acc_bytes: usize,
215 done: bool,
216 input_schema: SchemaRef,
217 min_bytes: usize,
218 max_bytes: usize,
219 deep_copy: bool,
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 use std::sync::Arc;
227
228 use arrow_array::Int32Array;
229 use arrow_schema::{DataType, Field, Schema};
230 use futures::executor::block_on;
231
232 #[test]
236 fn a_single_row_slice_reports_its_own_size() {
237 use arrow_array::LargeStringArray;
238
239 let row = "x".repeat(100 * 1024);
240 let values: Vec<&str> = (0..2000).map(|_| row.as_str()).collect();
241 let schema = Arc::new(Schema::new(vec![Field::new(
242 "text",
243 DataType::LargeUtf8,
244 false,
245 )]));
246 let batch =
247 RecordBatch::try_new(schema, vec![Arc::new(LargeStringArray::from(values))]).unwrap();
248 let whole = batch.get_array_memory_size();
249
250 let one_row = batch.slice(7, 1);
251 assert_eq!(
252 one_row.get_array_memory_size(),
253 whole,
254 "a slice shares its source buffer, which is the reason this test exists"
255 );
256
257 let chunks = slice_batch(one_row, 1024, true).unwrap();
259 assert_eq!(chunks.len(), 1);
260 let measured = chunks[0].get_array_memory_size();
261 assert_eq!(chunks[0].num_rows(), 1);
262 assert!(
263 measured < whole / 100,
264 "single row still measured as {measured} bytes against a {whole} byte source"
265 );
266 }
267
268 fn make_batch(num_rows: usize) -> RecordBatch {
269 let schema = test_schema();
270 let values: Vec<i32> = (0..num_rows as i32).collect();
271 RecordBatch::try_new(schema, vec![Arc::new(Int32Array::from(values))]).unwrap()
272 }
273
274 fn test_schema() -> SchemaRef {
275 Arc::new(Schema::new(vec![Field::new("a", DataType::Int32, false)]))
276 }
277
278 fn collect_rechunked(
279 batches: Vec<RecordBatch>,
280 min_bytes: usize,
281 max_bytes: usize,
282 ) -> Vec<RecordBatch> {
283 let input = stream::iter(batches.into_iter().map(Ok::<_, ArrowError>));
284 let rechunked = rechunk_stream_by_size(input, test_schema(), min_bytes, max_bytes);
285 block_on(rechunked.collect::<Vec<_>>())
286 .into_iter()
287 .map(|r| r.unwrap())
288 .collect()
289 }
290
291 fn total_rows(batches: &[RecordBatch]) -> usize {
292 batches.iter().map(|b| b.num_rows()).sum()
293 }
294
295 #[test]
296 fn test_empty_stream() {
297 let result = collect_rechunked(vec![], 100, 200);
298 assert!(result.is_empty());
299 }
300
301 #[test]
302 fn test_single_batch_passthrough() {
303 let batch = make_batch(100);
304 let bytes = batch.get_array_memory_size();
305 let result = collect_rechunked(vec![batch], bytes / 2, bytes * 2);
307 assert_eq!(result.len(), 1);
308 assert_eq!(result[0].num_rows(), 100);
309 }
310
311 #[test]
312 fn test_small_batches_concatenated() {
313 let one_batch_bytes = make_batch(10).get_array_memory_size();
314 let batches: Vec<_> = (0..8).map(|_| make_batch(10)).collect();
315 let result = collect_rechunked(batches, one_batch_bytes * 5, one_batch_bytes * 10);
317 assert_eq!(total_rows(&result), 80);
318 assert!(
320 result.len() < 8,
321 "expected fewer output batches, got {}",
322 result.len()
323 );
324 }
325
326 #[test]
327 fn test_large_batch_sliced() {
328 let batch = make_batch(1000);
329 let bytes = batch.get_array_memory_size();
330 let result = collect_rechunked(vec![batch], bytes / 8, bytes / 4);
331 assert_eq!(total_rows(&result), 1000);
332 assert!(
333 result.len() >= 4,
334 "expected at least 4 slices, got {}",
335 result.len()
336 );
337 }
338
339 #[test]
340 fn test_sliced_leftovers_are_not_recombined() {
341 let batch = make_batch(1000);
347 let bytes = batch.get_array_memory_size();
348 let orig_data = batch.column(0).to_data();
349 let orig_buf = &orig_data.buffers()[0];
350 let orig_start = orig_buf.as_ptr() as usize;
351 let orig_end = orig_start + orig_buf.len();
352
353 let result = collect_rechunked(vec![batch], bytes / 8, bytes / 4);
354
355 assert_eq!(total_rows(&result), 1000);
356 assert!(result.len() >= 4);
357
358 for (i, b) in result.iter().enumerate() {
359 let ptr = b.column(0).to_data().buffers()[0].as_ptr() as usize;
360 assert!(
361 ptr >= orig_start && ptr < orig_end,
362 "slice {i} buffer at {ptr:#x} is outside the original allocation \
363 [{orig_start:#x}, {orig_end:#x}) — it was re-concatenated"
364 );
365 }
366 }
367
368 #[test]
369 fn test_flush_remainder_on_stream_end() {
370 let batch = make_batch(10);
372 let bytes = batch.get_array_memory_size();
373 let result = collect_rechunked(vec![batch], bytes * 100, bytes * 200);
374 assert_eq!(result.len(), 1);
375 assert_eq!(result[0].num_rows(), 10);
376 }
377
378 #[test]
379 fn test_large_then_small_batches() {
380 let large = make_batch(1000);
383 let small_bytes = make_batch(10).get_array_memory_size();
384 let batches = vec![
385 large,
386 make_batch(10),
387 make_batch(10),
388 make_batch(10),
389 make_batch(10),
390 make_batch(10),
391 ];
392 let result = collect_rechunked(batches, small_bytes * 3, small_bytes * 100);
393 assert_eq!(total_rows(&result), 1050);
394 assert!(result.len() < 6);
398 }
399
400 #[test]
401 fn test_row_preservation_across_slicing() {
402 let batch = make_batch(237); let bytes = batch.get_array_memory_size();
406 let result = collect_rechunked(vec![batch], bytes / 8, bytes / 5);
407
408 assert_eq!(total_rows(&result), 237);
409
410 let values: Vec<i32> = result
411 .iter()
412 .flat_map(|b| {
413 b.column(0)
414 .as_any()
415 .downcast_ref::<Int32Array>()
416 .unwrap()
417 .values()
418 .iter()
419 .copied()
420 })
421 .collect();
422 let expected: Vec<i32> = (0..237).collect();
423 assert_eq!(values, expected);
424 }
425
426 #[test]
427 fn test_min_bytes_zero_still_yields_all_rows() {
428 let batches: Vec<_> = (0..5).map(|_| make_batch(100)).collect();
431 let batch_bytes = batches[0].get_array_memory_size();
432 let result = collect_rechunked(batches, 0, batch_bytes * 2);
433 assert_eq!(total_rows(&result), 500);
434 }
435
436 #[test]
437 fn test_min_bytes_zero_slices_oversized() {
438 let batch = make_batch(1000);
440 let bytes = batch.get_array_memory_size();
441 let result = collect_rechunked(vec![batch], 0, bytes / 4);
442 assert_eq!(total_rows(&result), 1000);
443 assert!(
444 result.len() >= 4,
445 "expected at least 4 slices, got {}",
446 result.len()
447 );
448 }
449
450 fn make_variable_batch(
454 num_rows: usize,
455 small_size: usize,
456 big_row_idx: usize,
457 big_size: usize,
458 ) -> RecordBatch {
459 let schema = Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]));
460 let values: Vec<String> = (0..num_rows)
461 .map(|i| {
462 if i == big_row_idx {
463 "X".repeat(big_size)
464 } else {
465 "x".repeat(small_size)
466 }
467 })
468 .collect();
469 let array = arrow_array::StringArray::from(values);
470 RecordBatch::try_new(schema, vec![Arc::new(array)]).unwrap()
471 }
472
473 fn variable_schema() -> SchemaRef {
474 Arc::new(Schema::new(vec![Field::new("s", DataType::Utf8, false)]))
475 }
476
477 fn collect_rechunked_variable(
478 batches: Vec<RecordBatch>,
479 min_bytes: usize,
480 max_bytes: usize,
481 ) -> Vec<RecordBatch> {
482 let input = stream::iter(batches.into_iter().map(Ok::<_, ArrowError>));
483 let rechunked =
484 rechunk_stream_by_size_deep_copy(input, variable_schema(), min_bytes, max_bytes);
485 block_on(rechunked.collect::<Vec<_>>())
486 .into_iter()
487 .map(|r| r.unwrap())
488 .collect()
489 }
490
491 #[test]
492 fn test_oversized_row_at_end() {
493 let batch = make_variable_batch(100, 64, 99, 100 * 1024);
495 let max_bytes = 64 * 1024;
496 let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
497 assert_eq!(total_rows(&result), 100);
498 for (i, b) in result.iter().enumerate() {
499 let size = b.get_array_memory_size();
500 assert!(
501 size <= max_bytes || b.num_rows() == 1,
502 "batch {i} has {size} bytes (max {max_bytes}) and {} rows",
503 b.num_rows()
504 );
505 }
506 }
507
508 #[test]
509 fn test_oversized_row_at_start() {
510 let batch = make_variable_batch(100, 64, 0, 100 * 1024);
512 let max_bytes = 64 * 1024;
513 let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
514 assert_eq!(total_rows(&result), 100);
515 for (i, b) in result.iter().enumerate() {
516 let size = b.get_array_memory_size();
517 assert!(
518 size <= max_bytes || b.num_rows() == 1,
519 "batch {i} has {size} bytes (max {max_bytes}) and {} rows",
520 b.num_rows()
521 );
522 }
523 }
524
525 #[test]
526 fn test_oversized_row_in_middle() {
527 let batch = make_variable_batch(100, 64, 50, 100 * 1024);
529 let max_bytes = 64 * 1024;
530 let result = collect_rechunked_variable(vec![batch], 0, max_bytes);
531 assert_eq!(total_rows(&result), 100);
532 for (i, b) in result.iter().enumerate() {
533 let size = b.get_array_memory_size();
534 assert!(
535 size <= max_bytes || b.num_rows() == 1,
536 "batch {i} has {size} bytes (max {max_bytes}) and {} rows",
537 b.num_rows()
538 );
539 }
540 }
541
542 #[test]
543 fn test_error_propagation() {
544 let input = stream::iter(vec![
545 Ok(make_batch(10)),
546 Err(ArrowError::ComputeError("boom".into())),
547 Ok(make_batch(10)),
548 ]);
549 let rechunked = rechunk_stream_by_size(input, test_schema(), 1, usize::MAX);
550 let results: Vec<Result<RecordBatch, ArrowError>> = block_on(rechunked.collect());
551 assert!(results.iter().any(|r| r.is_err()));
552 }
553}