1use std::pin::Pin;
5use std::task::Poll;
6use std::{collections::VecDeque, task::Context};
7
8use arrow::compute::kernels;
9use arrow_array::RecordBatch;
10use datafusion::physical_plan::{SendableRecordBatchStream, stream::RecordBatchStreamAdapter};
11use datafusion_common::DataFusionError;
12use futures::{Stream, StreamExt, TryStreamExt, ready};
13
14use lance_core::Result;
15use lance_core::error::DataFusionResult;
16
17struct BatchReaderChunker {
20 inner: SendableRecordBatchStream,
22 buffered: VecDeque<RecordBatch>,
24 output_size: usize,
26 i: usize,
28}
29
30impl BatchReaderChunker {
31 fn new(inner: SendableRecordBatchStream, output_size: usize) -> Self {
32 Self {
33 inner,
34 buffered: VecDeque::new(),
35 output_size,
36 i: 0,
37 }
38 }
39
40 fn buffered_len(&self) -> usize {
41 let buffer_total: usize = self.buffered.iter().map(|batch| batch.num_rows()).sum();
42 buffer_total - self.i
43 }
44
45 async fn fill_buffer(&mut self, output_size: usize) -> Result<()> {
46 while self.buffered_len() < output_size {
47 match self.inner.next().await {
48 Some(Ok(batch)) => self.buffered.push_back(batch),
49 Some(Err(e)) => return Err(e.into()),
50 None => break,
51 }
52 }
53 Ok(())
54 }
55
56 async fn next(&mut self) -> Option<Result<Vec<RecordBatch>>> {
57 self.next_sized(self.output_size).await
58 }
59
60 async fn next_sized(&mut self, output_size: usize) -> Option<Result<Vec<RecordBatch>>> {
61 match self.fill_buffer(output_size).await {
62 Ok(_) => {}
63 Err(e) => return Some(Err(e)),
64 };
65
66 let mut batches = Vec::new();
67
68 let mut rows_collected = 0;
69
70 while rows_collected < output_size {
71 if let Some(batch) = self.buffered.pop_front() {
72 if batch.num_rows() == 0 {
74 continue;
75 }
76
77 let rows_remaining_in_batch = batch.num_rows() - self.i;
78 let rows_to_take =
79 std::cmp::min(rows_remaining_in_batch, output_size - rows_collected);
80
81 if rows_to_take == rows_remaining_in_batch {
82 let batch = if self.i == 0 {
84 batch
85 } else {
86 batch.slice(self.i, rows_to_take)
88 };
89 batches.push(batch);
90 self.i = 0;
91 } else {
92 batches.push(batch.slice(self.i, rows_to_take));
94 self.i += rows_to_take;
96 self.buffered.push_front(batch);
97 }
98
99 rows_collected += rows_to_take;
100 } else {
101 break;
102 }
103 }
104
105 if batches.is_empty() {
106 None
107 } else {
108 Some(Ok(batches))
109 }
110 }
111
112 async fn next_at_most(&mut self, output_size: usize) -> Option<Result<Vec<RecordBatch>>> {
113 loop {
114 let batch = match self.buffered.pop_front() {
115 Some(batch) => batch,
116 None => match self.inner.next().await {
117 Some(Ok(batch)) => batch,
118 Some(Err(error)) => return Some(Err(error.into())),
119 None => return None,
120 },
121 };
122
123 if batch.num_rows() == 0 {
124 continue;
125 }
126
127 let rows_remaining_in_batch = batch.num_rows() - self.i;
128 let rows_to_take = rows_remaining_in_batch.min(output_size);
129 if rows_to_take == rows_remaining_in_batch {
130 let batch = if self.i == 0 {
131 batch
132 } else {
133 batch.slice(self.i, rows_to_take)
134 };
135 self.i = 0;
136 return Some(Ok(vec![batch]));
137 }
138
139 let output = batch.slice(self.i, rows_to_take);
140 self.i += rows_to_take;
141 self.buffered.push_front(batch);
142 return Some(Ok(vec![output]));
143 }
144 }
145}
146
147struct VariableBatchReaderChunker<I> {
148 chunker: BatchReaderChunker,
149 output_sizes: I,
150 is_done: bool,
151}
152
153struct VariableBreakStreamState<I> {
154 chunker: BatchReaderChunker,
155 output_sizes: I,
156 rows_remaining: Option<usize>,
157 is_done: bool,
158}
159
160struct BreakStreamState {
161 max_rows: usize,
162 rows_seen: usize,
163 rows_remaining: usize,
164 batch: Option<RecordBatch>,
165}
166
167impl BreakStreamState {
168 fn next(mut self) -> Option<(Result<RecordBatch>, Self)> {
169 if self.rows_remaining == 0 {
170 return None;
171 }
172 if self.rows_remaining + self.rows_seen <= self.max_rows {
173 self.rows_seen = (self.rows_seen + self.rows_remaining) % self.max_rows;
174 self.rows_remaining = 0;
175 let next = self.batch.take().unwrap();
176 Some((Ok(next), self))
177 } else {
178 let rows_to_emit = self.max_rows - self.rows_seen;
179 self.rows_seen = 0;
180 self.rows_remaining -= rows_to_emit;
181 let batch = self.batch.as_mut().unwrap();
182 let next = batch.slice(0, rows_to_emit);
183 *batch = batch.slice(rows_to_emit, batch.num_rows() - rows_to_emit);
184 Some((Ok(next), self))
185 }
186 }
187}
188
189pub fn break_stream(
197 stream: SendableRecordBatchStream,
198 max_chunk_size: usize,
199) -> Pin<Box<dyn Stream<Item = Result<RecordBatch>> + Send>> {
200 let mut rows_already_seen = 0;
201 stream
202 .map_ok(move |batch| {
203 let state = BreakStreamState {
204 rows_remaining: batch.num_rows(),
205 max_rows: max_chunk_size,
206 rows_seen: rows_already_seen,
207 batch: Some(batch),
208 };
209 rows_already_seen = (state.rows_seen + state.rows_remaining) % state.max_rows;
210
211 futures::stream::unfold(state, move |state| std::future::ready(state.next()))
212 .fuse()
213 .boxed()
214 })
215 .try_flatten()
216 .boxed()
217}
218
219pub fn chunk_stream(
225 stream: SendableRecordBatchStream,
226 chunk_size: usize,
227) -> Pin<Box<dyn Stream<Item = Result<Vec<RecordBatch>>> + Send>> {
228 let chunker = BatchReaderChunker::new(stream, chunk_size);
229 futures::stream::unfold(chunker, |mut chunker| async move {
230 match chunker.next().await {
231 Some(Ok(batches)) => Some((Ok(batches), chunker)),
232 Some(Err(e)) => Some((Err(e), chunker)),
233 None => None,
234 }
235 })
236 .fuse()
237 .boxed()
238}
239
240pub fn break_stream_with_sizes<I>(
257 stream: SendableRecordBatchStream,
258 output_sizes: I,
259) -> Pin<Box<dyn Stream<Item = Result<Vec<RecordBatch>>> + Send>>
260where
261 I: IntoIterator<Item = usize>,
262 I::IntoIter: Send + 'static,
263{
264 let state = VariableBreakStreamState {
265 chunker: BatchReaderChunker::new(stream, 1),
266 output_sizes: output_sizes.into_iter(),
267 rows_remaining: None,
268 is_done: false,
269 };
270 futures::stream::unfold(state, |mut state| async move {
271 if state.is_done {
272 return None;
273 }
274
275 if state.rows_remaining.is_none() {
276 let Some(output_size) = state.output_sizes.next() else {
277 return match state.chunker.next_at_most(1).await {
278 None => None,
279 Some(Ok(_)) => {
280 state.is_done = true;
281 Some((
282 Err(lance_core::Error::invalid_input(
283 "Input contained more rows than the requested chunk sizes",
284 )),
285 state,
286 ))
287 }
288 Some(Err(error)) => {
289 state.is_done = true;
290 Some((Err(error), state))
291 }
292 };
293 };
294 if output_size == 0 {
295 state.is_done = true;
296 return Some((
297 Err(lance_core::Error::invalid_input(
298 "Requested chunk sizes must be greater than zero",
299 )),
300 state,
301 ));
302 }
303 state.rows_remaining = Some(output_size);
304 }
305
306 let Some(rows_remaining) = state.rows_remaining else {
307 state.is_done = true;
308 return Some((
309 Err(lance_core::Error::internal(
310 "Requested chunk boundary was not initialized",
311 )),
312 state,
313 ));
314 };
315 match state.chunker.next_at_most(rows_remaining).await {
316 Some(Ok(batches)) => {
317 let actual_size = batches.iter().map(RecordBatch::num_rows).sum::<usize>();
318 let Some(rows_remaining) = rows_remaining.checked_sub(actual_size) else {
319 state.is_done = true;
320 return Some((
321 Err(lance_core::Error::internal(
322 "A boundary-preserving chunk exceeded its requested row count",
323 )),
324 state,
325 ));
326 };
327 state.rows_remaining = (rows_remaining > 0).then_some(rows_remaining);
328 Some((Ok(batches), state))
329 }
330 Some(Err(error)) => {
331 state.is_done = true;
332 Some((Err(error), state))
333 }
334 None => {
335 state.is_done = true;
336 Some((
337 Err(lance_core::Error::invalid_input(format!(
338 "Input ended with {rows_remaining} rows remaining in a requested chunk"
339 ))),
340 state,
341 ))
342 }
343 }
344 })
345 .boxed()
346}
347
348pub fn chunk_stream_with_sizes<I>(
365 stream: SendableRecordBatchStream,
366 output_sizes: I,
367) -> Pin<Box<dyn Stream<Item = Result<Vec<RecordBatch>>> + Send>>
368where
369 I: IntoIterator<Item = usize>,
370 I::IntoIter: Send + 'static,
371{
372 let state = VariableBatchReaderChunker {
373 chunker: BatchReaderChunker::new(stream, 1),
374 output_sizes: output_sizes.into_iter(),
375 is_done: false,
376 };
377 futures::stream::unfold(state, |mut state| async move {
378 if state.is_done {
379 return None;
380 }
381
382 let Some(output_size) = state.output_sizes.next() else {
383 return match state.chunker.next_sized(1).await {
384 None => None,
385 Some(Ok(_)) => {
386 state.is_done = true;
387 Some((
388 Err(lance_core::Error::invalid_input(
389 "Input contained more rows than the requested chunk sizes",
390 )),
391 state,
392 ))
393 }
394 Some(Err(error)) => {
395 state.is_done = true;
396 Some((Err(error), state))
397 }
398 };
399 };
400
401 if output_size == 0 {
402 state.is_done = true;
403 return Some((
404 Err(lance_core::Error::invalid_input(
405 "Requested chunk sizes must be greater than zero",
406 )),
407 state,
408 ));
409 }
410
411 match state.chunker.next_sized(output_size).await {
412 Some(Ok(batches)) => {
413 let actual_size = batches.iter().map(RecordBatch::num_rows).sum::<usize>();
414 if actual_size == output_size {
415 Some((Ok(batches), state))
416 } else {
417 state.is_done = true;
418 Some((
419 Err(lance_core::Error::invalid_input(format!(
420 "Input ended after {actual_size} rows while filling a requested {output_size}-row chunk"
421 ))),
422 state,
423 ))
424 }
425 }
426 Some(Err(error)) => {
427 state.is_done = true;
428 Some((Err(error), state))
429 }
430 None => {
431 state.is_done = true;
432 Some((
433 Err(lance_core::Error::invalid_input(format!(
434 "Input ended before a requested {output_size}-row chunk could be filled"
435 ))),
436 state,
437 ))
438 }
439 }
440 })
441 .boxed()
442}
443
444pub fn chunk_concat_stream(
450 stream: SendableRecordBatchStream,
451 chunk_size: usize,
452) -> SendableRecordBatchStream {
453 let schema = stream.schema();
454 let schema_copy = schema.clone();
455 let chunked = chunk_stream(stream, chunk_size);
456 let chunk_concat = chunked
457 .and_then(move |batches| {
458 std::future::ready(
459 kernels::concat::concat_batches(&schema, batches.iter()).map_err(|e| e.into()),
462 )
463 })
464 .map_err(DataFusionError::from)
465 .boxed();
466 Box::pin(RecordBatchStreamAdapter::new(schema_copy, chunk_concat))
467}
468
469pub struct StrictBatchSizeStream<S> {
475 inner: S,
476 batch_size: usize,
477 residual: Option<RecordBatch>,
478}
479
480impl<S: Stream<Item = DataFusionResult<RecordBatch>> + Unpin> StrictBatchSizeStream<S> {
481 pub fn new(inner: S, batch_size: usize) -> Self {
482 Self {
483 inner,
484 batch_size,
485 residual: None,
486 }
487 }
488}
489
490impl<S> Stream for StrictBatchSizeStream<S>
506where
507 S: Stream<Item = DataFusionResult<RecordBatch>> + Unpin,
508{
509 type Item = DataFusionResult<RecordBatch>;
510
511 fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
512 loop {
513 if let Some(residual) = self.residual.take() {
515 if residual.num_rows() >= self.batch_size {
516 let split_at = self.batch_size;
517 let chunk = residual.slice(0, split_at);
518 let new_residual = residual.slice(split_at, residual.num_rows() - split_at);
519 self.residual = Some(new_residual);
520 return Poll::Ready(Some(Ok(chunk)));
521 } else {
522 self.residual = Some(residual);
524 }
525 }
526
527 match ready!(Pin::new(&mut self.inner).poll_next(cx)) {
529 Some(Ok(batch)) => {
530 let current_batch = if let Some(residual) = self.residual.take() {
532 arrow::compute::concat_batches(&residual.schema(), &[residual, batch])
533 .map_err(|e| DataFusionError::External(Box::new(e)))?
534 } else {
535 batch
536 };
537
538 if current_batch.num_rows() >= self.batch_size {
539 let split_at = self.batch_size;
540 let chunk = current_batch.slice(0, split_at);
541 let new_residual =
542 current_batch.slice(split_at, current_batch.num_rows() - split_at);
543 if new_residual.num_rows() > 0 {
544 self.residual = Some(new_residual);
545 }
546 return Poll::Ready(Some(Ok(chunk)));
547 } else {
548 self.residual = Some(current_batch);
550 continue;
551 }
552 }
553 Some(Err(e)) => return Poll::Ready(Some(Err(e))),
554 None => {
555 return Poll::Ready(
556 self.residual
557 .take()
558 .filter(|r| r.num_rows() > 0)
559 .map(Ok::<_, DataFusionError>),
560 );
561 }
562 }
563 }
564 }
565}
566
567#[cfg(test)]
568mod tests {
569 use std::sync::{
570 Arc,
571 atomic::{AtomicUsize, Ordering},
572 };
573
574 use arrow::datatypes::{Int32Type, Int64Type};
575 use datafusion::physical_plan::stream::RecordBatchStreamAdapter;
576 use futures::{StreamExt, TryStreamExt};
577 use lance_datagen::{BatchCount, RowCount, array};
578
579 use crate::datagen::DatafusionDatagenExt;
580
581 #[tokio::test]
582 async fn test_chunkers() {
583 let schema = Arc::new(arrow::datatypes::Schema::new(vec![
584 arrow::datatypes::Field::new("", arrow::datatypes::DataType::Int32, false),
585 ]));
586
587 let make_batch = |num_rows: u32| {
588 lance_datagen::gen_batch()
589 .anon_col(lance_datagen::array::step::<Int32Type>())
590 .into_batch_rows(RowCount::from(num_rows as u64))
591 .unwrap()
592 };
593
594 let batches = vec![make_batch(10), make_batch(5), make_batch(13), make_batch(0)];
595
596 let make_stream = || {
597 let stream = futures::stream::iter(
598 batches
599 .clone()
600 .into_iter()
601 .map(datafusion_common::Result::Ok),
602 )
603 .boxed();
604 Box::pin(RecordBatchStreamAdapter::new(schema.clone(), stream))
605 };
606
607 let chunked = super::chunk_stream(make_stream(), 10)
608 .try_collect::<Vec<_>>()
609 .await
610 .unwrap();
611
612 assert_eq!(chunked.len(), 3);
613 assert_eq!(chunked[0].len(), 1);
614 assert_eq!(chunked[0][0].num_rows(), 10);
615 assert_eq!(chunked[1].len(), 2);
616 assert_eq!(chunked[1][0].num_rows(), 5);
617 assert_eq!(chunked[1][1].num_rows(), 5);
618 assert_eq!(chunked[2].len(), 1);
619 assert_eq!(chunked[2][0].num_rows(), 8);
620
621 let sizes_consumed = Arc::new(AtomicUsize::new(0));
622 let requested_sizes = [9, 10, 9].into_iter().inspect({
623 let sizes_consumed = sizes_consumed.clone();
624 move |_| {
625 sizes_consumed.fetch_add(1, Ordering::SeqCst);
626 }
627 });
628 let mut chunked = super::chunk_stream_with_sizes(make_stream(), requested_sizes);
629 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 0);
630 let first_chunk = chunked.next().await.unwrap().unwrap();
631 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 1);
632 let mut chunked = chunked.try_collect::<Vec<_>>().await.unwrap();
633 chunked.insert(0, first_chunk);
634 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 3);
635 assert_eq!(
636 chunked
637 .iter()
638 .map(|batches| batches.iter().map(|batch| batch.num_rows()).sum::<usize>())
639 .collect::<Vec<_>>(),
640 vec![9, 10, 9]
641 );
642
643 let error = super::chunk_stream_with_sizes(make_stream(), vec![10, 17])
644 .try_collect::<Vec<_>>()
645 .await
646 .unwrap_err();
647 assert!(
648 error
649 .to_string()
650 .contains("more rows than the requested chunk sizes")
651 );
652
653 let error = super::chunk_stream_with_sizes(make_stream(), vec![10, 19])
654 .try_collect::<Vec<_>>()
655 .await
656 .unwrap_err();
657 assert!(error.to_string().contains("ended after 18 rows"));
658
659 let sizes_consumed = Arc::new(AtomicUsize::new(0));
660 let requested_sizes = [9, 10, 9].into_iter().inspect({
661 let sizes_consumed = sizes_consumed.clone();
662 move |_| {
663 sizes_consumed.fetch_add(1, Ordering::SeqCst);
664 }
665 });
666 let mut broken = super::break_stream_with_sizes(make_stream(), requested_sizes);
667 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 0);
668 let first_batch = broken.next().await.unwrap().unwrap();
669 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 1);
670 let mut broken = broken.try_collect::<Vec<_>>().await.unwrap();
671 broken.insert(0, first_batch);
672 assert_eq!(sizes_consumed.load(Ordering::SeqCst), 3);
673 assert_eq!(
674 broken
675 .iter()
676 .map(|batches| batches.iter().map(|batch| batch.num_rows()).sum::<usize>())
677 .collect::<Vec<_>>(),
678 vec![9, 1, 5, 4, 9]
679 );
680
681 let error = super::break_stream_with_sizes(make_stream(), vec![27])
682 .try_collect::<Vec<_>>()
683 .await
684 .unwrap_err();
685 assert!(
686 error
687 .to_string()
688 .contains("more rows than the requested chunk sizes")
689 );
690
691 let error = super::break_stream_with_sizes(make_stream(), vec![29])
692 .try_collect::<Vec<_>>()
693 .await
694 .unwrap_err();
695 assert!(error.to_string().contains("1 rows remaining"));
696
697 let chunked = super::chunk_concat_stream(make_stream(), 10)
698 .try_collect::<Vec<_>>()
699 .await
700 .unwrap();
701
702 assert_eq!(chunked.len(), 3);
703 assert_eq!(chunked[0].num_rows(), 10);
704 assert_eq!(chunked[1].num_rows(), 10);
705 assert_eq!(chunked[2].num_rows(), 8);
706
707 let chunked = super::break_stream(make_stream(), 10)
708 .try_collect::<Vec<_>>()
709 .await
710 .unwrap();
711
712 assert_eq!(chunked.len(), 4);
713 assert_eq!(chunked[0].num_rows(), 10);
714 assert_eq!(chunked[1].num_rows(), 5);
715 assert_eq!(chunked[2].num_rows(), 5);
716 assert_eq!(chunked[3].num_rows(), 8);
717 }
718
719 #[tokio::test]
720 async fn test_strict_batch_size_stream() {
721 let batches = lance_datagen::gen_batch()
722 .anon_col(array::step::<Int32Type>())
723 .anon_col(array::step::<Int64Type>())
724 .into_df_stream(RowCount::from(7), BatchCount::from(10));
725
726 let stream = super::StrictBatchSizeStream::new(batches, 10);
727
728 let batches = stream.try_collect::<Vec<_>>().await.unwrap();
729 assert_eq!(batches.len(), 7);
730
731 for batch in batches {
732 assert_eq!(batch.num_rows(), 10);
733 }
734 }
735}