1mod builder;
25mod metrics;
26mod scan_state;
27pub(crate) mod work_source;
28
29use std::pin::Pin;
30use std::sync::Arc;
31use std::task::{Context, Poll};
32
33use crate::PartitionedFile;
34use crate::file_scan_config::FileScanConfig;
35use arrow::datatypes::SchemaRef;
36use datafusion_common::Result;
37use datafusion_execution::RecordBatchStream;
38use datafusion_physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet};
39
40use arrow::record_batch::RecordBatch;
41
42use futures::Stream;
43use futures::future::BoxFuture;
44use futures::stream::BoxStream;
45
46use self::scan_state::{ScanAndReturn, ScanState};
47
48pub use builder::FileStreamBuilder;
49pub use metrics::{FileStreamMetrics, StartableTime};
50
51pub struct FileStream {
53 projected_schema: SchemaRef,
56 state: FileStreamState,
58 baseline_metrics: BaselineMetrics,
60}
61
62impl FileStream {
63 #[deprecated(since = "54.0.0", note = "Use FileStreamBuilder instead")]
65 pub fn new(
66 config: &FileScanConfig,
67 partition: usize,
68 file_opener: Arc<dyn FileOpener>,
69 metrics: &ExecutionPlanMetricsSet,
70 ) -> Result<Self> {
71 FileStreamBuilder::new(config)
72 .with_partition(partition)
73 .with_file_opener(file_opener)
74 .with_metrics(metrics)
75 .build()
76 }
77
78 pub fn with_on_error(mut self, on_error: OnError) -> Self {
83 match &mut self.state {
84 FileStreamState::Scan { scan_state } => scan_state.set_on_error(on_error),
85 FileStreamState::Error | FileStreamState::Done => {
86 }
88 };
89 self
90 }
91
92 fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<RecordBatch>>> {
93 loop {
94 match &mut self.state {
95 FileStreamState::Scan { scan_state: queue } => {
96 let action = queue.poll_scan(cx);
97 match action {
98 ScanAndReturn::Continue => continue,
99 ScanAndReturn::Done(result) => {
100 self.state = FileStreamState::Done;
101 return Poll::Ready(result);
102 }
103 ScanAndReturn::Error(err) => {
104 self.state = FileStreamState::Error;
105 return Poll::Ready(Some(Err(err)));
106 }
107 ScanAndReturn::Return(result) => return result,
108 }
109 }
110 FileStreamState::Error | FileStreamState::Done => {
111 return Poll::Ready(None);
112 }
113 }
114 }
115 }
116}
117
118impl Stream for FileStream {
119 type Item = Result<RecordBatch>;
120
121 fn poll_next(
122 mut self: Pin<&mut Self>,
123 cx: &mut Context<'_>,
124 ) -> Poll<Option<Self::Item>> {
125 let result = self.poll_inner(cx);
126 self.baseline_metrics.record_poll(result)
127 }
128}
129
130impl RecordBatchStream for FileStream {
131 fn schema(&self) -> SchemaRef {
132 Arc::clone(&self.projected_schema)
133 }
134}
135
136pub type FileOpenFuture =
138 BoxFuture<'static, Result<BoxStream<'static, Result<RecordBatch>>>>;
139
140#[derive(Default)]
142pub enum OnError {
143 #[default]
145 Fail,
146 Skip,
148}
149
150pub trait FileOpener: Unpin + Send + Sync {
155 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture>;
158}
159
160enum FileStreamState {
161 Scan {
163 scan_state: Box<ScanState>,
165 },
166 Error,
168 Done,
170}
171
172#[cfg(test)]
173mod tests {
174 use crate::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
175 use crate::morsel::mocks::{
176 IoFutureId, MockMorselizer, MockPlanBuilder, MockPlanner, MorselId,
177 PendingPlannerBuilder, PollsToResolve,
178 };
179 use crate::source::DataSource;
180 use crate::tests::make_partition;
181 use crate::{PartitionedFile, TableSchema};
182 use arrow::array::{AsArray, RecordBatch};
183 use arrow::datatypes::{DataType, Field, Int32Type, Schema};
184 use datafusion_common::DataFusionError;
185 use datafusion_common::config::ConfigOptions;
186 use datafusion_common::error::Result;
187 use datafusion_execution::object_store::ObjectStoreUrl;
188 use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
189 use futures::{FutureExt as _, StreamExt as _};
190 use std::collections::{BTreeMap, VecDeque};
191 use std::sync::Arc;
192 use std::sync::atomic::{AtomicUsize, Ordering};
193
194 use crate::file_stream::{
195 FileOpenFuture, FileOpener, FileStream, FileStreamBuilder, OnError,
196 work_source::SharedWorkSource,
197 };
198 use crate::test_util::MockSource;
199
200 use datafusion_common::{assert_batches_eq, exec_err, internal_err};
201
202 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
204 struct PartitionId(usize);
205
206 #[derive(Default)]
208 struct TestOpener {
209 error_opening_idx: Vec<usize>,
211 error_scanning_idx: Vec<usize>,
213 current_idx: AtomicUsize,
215 records: Vec<RecordBatch>,
217 }
218
219 impl FileOpener for TestOpener {
220 fn open(&self, _partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
221 let idx = self.current_idx.fetch_add(1, Ordering::SeqCst);
222
223 if self.error_opening_idx.contains(&idx) {
224 Ok(futures::future::ready(internal_err!("error opening")).boxed())
225 } else if self.error_scanning_idx.contains(&idx) {
226 let error = futures::future::ready(exec_err!("error scanning"));
227 let stream = futures::stream::once(error).boxed();
228 Ok(futures::future::ready(Ok(stream)).boxed())
229 } else {
230 let iterator = self.records.clone().into_iter().map(Ok);
231 let stream = futures::stream::iter(iterator).boxed();
232 Ok(futures::future::ready(Ok(stream)).boxed())
233 }
234 }
235 }
236
237 #[derive(Default)]
238 struct FileStreamTest {
239 num_files: usize,
241 limit: Option<usize>,
243 on_error: OnError,
245 opener: TestOpener,
247 }
248
249 impl FileStreamTest {
250 pub fn new() -> Self {
251 Self::default()
252 }
253
254 pub fn with_num_files(mut self, num_files: usize) -> Self {
256 self.num_files = num_files;
257 self
258 }
259
260 pub fn with_limit(mut self, limit: Option<usize>) -> Self {
262 self.limit = limit;
263 self
264 }
265
266 pub fn with_open_errors(mut self, idx: Vec<usize>) -> Self {
269 self.opener.error_opening_idx = idx;
270 self
271 }
272
273 pub fn with_scan_errors(mut self, idx: Vec<usize>) -> Self {
276 self.opener.error_scanning_idx = idx;
277 self
278 }
279
280 pub fn with_on_error(mut self, on_error: OnError) -> Self {
282 self.on_error = on_error;
283 self
284 }
285
286 pub fn with_records(mut self, records: Vec<RecordBatch>) -> Self {
289 self.opener.records = records;
290 self
291 }
292
293 pub async fn result(self) -> Result<Vec<RecordBatch>> {
295 let file_schema = self
296 .opener
297 .records
298 .first()
299 .map(|batch| batch.schema())
300 .unwrap_or_else(|| Arc::new(Schema::empty()));
301
302 let mock_files: Vec<(String, u64)> = (0..self.num_files)
304 .map(|idx| (format!("mock_file{idx}"), 10_u64))
305 .collect();
306
307 let file_group = mock_files
313 .into_iter()
314 .map(|(name, size)| PartitionedFile::new(name, size))
315 .collect();
316
317 let on_error = self.on_error;
318
319 let table_schema = TableSchema::new(file_schema, vec![]);
320 let config = FileScanConfigBuilder::new(
321 ObjectStoreUrl::parse("test:///").unwrap(),
322 Arc::new(MockSource::new(table_schema)),
323 )
324 .with_file_group(file_group)
325 .with_limit(self.limit)
326 .build();
327 let metrics_set = ExecutionPlanMetricsSet::new();
328 let file_stream = FileStreamBuilder::new(&config)
329 .with_partition(0)
330 .with_file_opener(Arc::new(self.opener))
331 .with_metrics(&metrics_set)
332 .with_on_error(on_error)
333 .build()?;
334
335 file_stream
336 .collect::<Vec<_>>()
337 .await
338 .into_iter()
339 .collect::<Result<Vec<_>>>()
340 }
341 }
342
343 async fn create_and_collect(limit: Option<usize>) -> Vec<RecordBatch> {
345 FileStreamTest::new()
346 .with_records(vec![make_partition(3), make_partition(2)])
347 .with_num_files(2)
348 .with_limit(limit)
349 .result()
350 .await
351 .expect("error executing stream")
352 }
353
354 fn builder_test_config() -> FileScanConfig {
356 let table_schema = TableSchema::new(Arc::new(Schema::empty()), vec![]);
357 FileScanConfigBuilder::new(
358 ObjectStoreUrl::parse("test:///").unwrap(),
359 Arc::new(MockSource::new(table_schema)),
360 )
361 .with_file(PartitionedFile::new("mock_file", 10))
362 .build()
363 }
364
365 fn builder_error(builder: FileStreamBuilder<'_>) -> String {
368 builder.build().err().unwrap().to_string()
369 }
370
371 #[tokio::test]
372 async fn on_error_opening() -> Result<()> {
373 let batches = FileStreamTest::new()
374 .with_records(vec![make_partition(3), make_partition(2)])
375 .with_num_files(2)
376 .with_on_error(OnError::Skip)
377 .with_open_errors(vec![0])
378 .result()
379 .await?;
380
381 #[rustfmt::skip]
382 assert_batches_eq!(&[
383 "+---+",
384 "| i |",
385 "+---+",
386 "| 0 |",
387 "| 1 |",
388 "| 2 |",
389 "| 0 |",
390 "| 1 |",
391 "+---+",
392 ], &batches);
393
394 let batches = FileStreamTest::new()
395 .with_records(vec![make_partition(3), make_partition(2)])
396 .with_num_files(2)
397 .with_on_error(OnError::Skip)
398 .with_open_errors(vec![1])
399 .result()
400 .await?;
401
402 #[rustfmt::skip]
403 assert_batches_eq!(&[
404 "+---+",
405 "| i |",
406 "+---+",
407 "| 0 |",
408 "| 1 |",
409 "| 2 |",
410 "| 0 |",
411 "| 1 |",
412 "+---+",
413 ], &batches);
414
415 let batches = FileStreamTest::new()
416 .with_records(vec![make_partition(3), make_partition(2)])
417 .with_num_files(2)
418 .with_on_error(OnError::Skip)
419 .with_open_errors(vec![0, 1])
420 .result()
421 .await?;
422
423 #[rustfmt::skip]
424 assert_batches_eq!(&[
425 "++",
426 "++",
427 ], &batches);
428
429 Ok(())
430 }
431
432 #[tokio::test]
433 async fn on_error_scanning_fail() -> Result<()> {
434 let result = FileStreamTest::new()
435 .with_records(vec![make_partition(3), make_partition(2)])
436 .with_num_files(2)
437 .with_on_error(OnError::Fail)
438 .with_scan_errors(vec![1])
439 .result()
440 .await;
441
442 assert!(result.is_err());
443
444 Ok(())
445 }
446
447 #[tokio::test]
448 async fn on_error_opening_fail() -> Result<()> {
449 let result = FileStreamTest::new()
450 .with_records(vec![make_partition(3), make_partition(2)])
451 .with_num_files(2)
452 .with_on_error(OnError::Fail)
453 .with_open_errors(vec![1])
454 .result()
455 .await;
456
457 assert!(result.is_err());
458
459 Ok(())
460 }
461
462 #[tokio::test]
463 async fn on_error_scanning() -> Result<()> {
464 let batches = FileStreamTest::new()
465 .with_records(vec![make_partition(3), make_partition(2)])
466 .with_num_files(2)
467 .with_on_error(OnError::Skip)
468 .with_scan_errors(vec![0])
469 .result()
470 .await?;
471
472 #[rustfmt::skip]
473 assert_batches_eq!(&[
474 "+---+",
475 "| i |",
476 "+---+",
477 "| 0 |",
478 "| 1 |",
479 "| 2 |",
480 "| 0 |",
481 "| 1 |",
482 "+---+",
483 ], &batches);
484
485 let batches = FileStreamTest::new()
486 .with_records(vec![make_partition(3), make_partition(2)])
487 .with_num_files(2)
488 .with_on_error(OnError::Skip)
489 .with_scan_errors(vec![1])
490 .result()
491 .await?;
492
493 #[rustfmt::skip]
494 assert_batches_eq!(&[
495 "+---+",
496 "| i |",
497 "+---+",
498 "| 0 |",
499 "| 1 |",
500 "| 2 |",
501 "| 0 |",
502 "| 1 |",
503 "+---+",
504 ], &batches);
505
506 let batches = FileStreamTest::new()
507 .with_records(vec![make_partition(3), make_partition(2)])
508 .with_num_files(2)
509 .with_on_error(OnError::Skip)
510 .with_scan_errors(vec![0, 1])
511 .result()
512 .await?;
513
514 #[rustfmt::skip]
515 assert_batches_eq!(&[
516 "++",
517 "++",
518 ], &batches);
519
520 Ok(())
521 }
522
523 #[tokio::test]
524 async fn on_error_mixed() -> Result<()> {
525 let batches = FileStreamTest::new()
526 .with_records(vec![make_partition(3), make_partition(2)])
527 .with_num_files(3)
528 .with_on_error(OnError::Skip)
529 .with_open_errors(vec![1])
530 .with_scan_errors(vec![0])
531 .result()
532 .await?;
533
534 #[rustfmt::skip]
535 assert_batches_eq!(&[
536 "+---+",
537 "| i |",
538 "+---+",
539 "| 0 |",
540 "| 1 |",
541 "| 2 |",
542 "| 0 |",
543 "| 1 |",
544 "+---+",
545 ], &batches);
546
547 let batches = FileStreamTest::new()
548 .with_records(vec![make_partition(3), make_partition(2)])
549 .with_num_files(3)
550 .with_on_error(OnError::Skip)
551 .with_open_errors(vec![0])
552 .with_scan_errors(vec![1])
553 .result()
554 .await?;
555
556 #[rustfmt::skip]
557 assert_batches_eq!(&[
558 "+---+",
559 "| i |",
560 "+---+",
561 "| 0 |",
562 "| 1 |",
563 "| 2 |",
564 "| 0 |",
565 "| 1 |",
566 "+---+",
567 ], &batches);
568
569 let batches = FileStreamTest::new()
570 .with_records(vec![make_partition(3), make_partition(2)])
571 .with_num_files(3)
572 .with_on_error(OnError::Skip)
573 .with_open_errors(vec![2])
574 .with_scan_errors(vec![0, 1])
575 .result()
576 .await?;
577
578 #[rustfmt::skip]
579 assert_batches_eq!(&[
580 "++",
581 "++",
582 ], &batches);
583
584 let batches = FileStreamTest::new()
585 .with_records(vec![make_partition(3), make_partition(2)])
586 .with_num_files(3)
587 .with_on_error(OnError::Skip)
588 .with_open_errors(vec![0, 2])
589 .with_scan_errors(vec![1])
590 .result()
591 .await?;
592
593 #[rustfmt::skip]
594 assert_batches_eq!(&[
595 "++",
596 "++",
597 ], &batches);
598
599 Ok(())
600 }
601
602 #[tokio::test]
603 async fn without_limit() -> Result<()> {
604 let batches = create_and_collect(None).await;
605
606 #[rustfmt::skip]
607 assert_batches_eq!(&[
608 "+---+",
609 "| i |",
610 "+---+",
611 "| 0 |",
612 "| 1 |",
613 "| 2 |",
614 "| 0 |",
615 "| 1 |",
616 "| 0 |",
617 "| 1 |",
618 "| 2 |",
619 "| 0 |",
620 "| 1 |",
621 "+---+",
622 ], &batches);
623
624 Ok(())
625 }
626
627 #[tokio::test]
628 async fn with_limit_between_files() -> Result<()> {
629 let batches = create_and_collect(Some(5)).await;
630 #[rustfmt::skip]
631 assert_batches_eq!(&[
632 "+---+",
633 "| i |",
634 "+---+",
635 "| 0 |",
636 "| 1 |",
637 "| 2 |",
638 "| 0 |",
639 "| 1 |",
640 "+---+",
641 ], &batches);
642
643 Ok(())
644 }
645
646 #[tokio::test]
647 async fn with_limit_at_middle_of_batch() -> Result<()> {
648 let batches = create_and_collect(Some(6)).await;
649 #[rustfmt::skip]
650 assert_batches_eq!(&[
651 "+---+",
652 "| i |",
653 "+---+",
654 "| 0 |",
655 "| 1 |",
656 "| 2 |",
657 "| 0 |",
658 "| 1 |",
659 "| 0 |",
660 "+---+",
661 ], &batches);
662
663 Ok(())
664 }
665
666 #[test]
667 fn builder_requires_partition_file_opener_and_metrics() {
668 let config = builder_test_config();
669
670 let err = builder_error(FileStreamBuilder::new(&config));
671 assert!(err.contains("FileStreamBuilder missing required partition"));
672
673 let err = builder_error(FileStreamBuilder::new(&config).with_partition(0));
674 assert!(err.contains("FileStreamBuilder missing required morselizer"));
675
676 let err = builder_error(
677 FileStreamBuilder::new(&config)
678 .with_partition(0)
679 .with_file_opener(Arc::new(TestOpener::default())),
680 );
681 assert!(err.contains("FileStreamBuilder missing required metrics"));
682 }
683
684 #[test]
685 fn builder_errors_on_invalid_partition() {
686 let config = builder_test_config();
687 let metrics = ExecutionPlanMetricsSet::new();
688
689 let err = builder_error(
690 FileStreamBuilder::new(&config)
691 .with_partition(1)
692 .with_file_opener(Arc::new(TestOpener::default()))
693 .with_metrics(&metrics),
694 );
695 assert!(err.contains("FileStreamBuilder invalid partition index: 1"));
696 }
697
698 #[tokio::test]
701 async fn morsel_no_io() -> Result<()> {
702 let test = FileStreamMorselTest::new().with_file(
703 MockPlanner::builder("file1.parquet")
704 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 42))
705 .return_none(),
706 );
707
708 insta::assert_snapshot!(test.run().await.unwrap(), @r"
709 ----- Output Stream -----
710 Batch: 42
711 Done
712 ----- File Stream Events -----
713 morselize_file: file1.parquet
714 planner_created: file1.parquet
715 planner_called: file1.parquet
716 morsel_produced: file1.parquet, MorselId(10)
717 morsel_stream_started: MorselId(10)
718 morsel_stream_batch_produced: MorselId(10), BatchId(42)
719 morsel_stream_finished: MorselId(10)
720 ");
721
722 Ok(())
723 }
724
725 #[tokio::test]
728 async fn morsel_single_io_two_batches() -> Result<()> {
729 let test = FileStreamMorselTest::new().with_file(
730 MockPlanner::builder("file1.parquet")
731 .add_plan(
732 PendingPlannerBuilder::new(IoFutureId(1))
733 .with_polls_to_resolve(PollsToResolve(1)),
734 )
735 .add_plan(
736 MockPlanBuilder::new()
737 .with_morsel_batches(MorselId(10), vec![42, 43]),
738 )
739 .return_none(),
740 );
741
742 insta::assert_snapshot!(test.run().await.unwrap(), @r"
743 ----- Output Stream -----
744 Batch: 42
745 Batch: 43
746 Done
747 ----- File Stream Events -----
748 morselize_file: file1.parquet
749 planner_created: file1.parquet
750 planner_called: file1.parquet
751 io_future_created: file1.parquet, IoFutureId(1)
752 io_future_polled: file1.parquet, IoFutureId(1)
753 io_future_polled: file1.parquet, IoFutureId(1)
754 io_future_resolved: file1.parquet, IoFutureId(1)
755 planner_called: file1.parquet
756 morsel_produced: file1.parquet, MorselId(10)
757 morsel_stream_started: MorselId(10)
758 morsel_stream_batch_produced: MorselId(10), BatchId(42)
759 morsel_stream_batch_produced: MorselId(10), BatchId(43)
760 morsel_stream_finished: MorselId(10)
761 ");
762
763 Ok(())
764 }
765
766 #[tokio::test]
769 async fn morsel_two_ios_one_batch() -> Result<()> {
770 let test = FileStreamMorselTest::new().with_file(
771 MockPlanner::builder("file1.parquet")
772 .add_plan(PendingPlannerBuilder::new(IoFutureId(1)))
773 .add_plan(PendingPlannerBuilder::new(IoFutureId(2)))
774 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 42))
775 .return_none(),
776 );
777
778 insta::assert_snapshot!(test.run().await.unwrap(), @r"
779 ----- Output Stream -----
780 Batch: 42
781 Done
782 ----- File Stream Events -----
783 morselize_file: file1.parquet
784 planner_created: file1.parquet
785 planner_called: file1.parquet
786 io_future_created: file1.parquet, IoFutureId(1)
787 io_future_polled: file1.parquet, IoFutureId(1)
788 io_future_resolved: file1.parquet, IoFutureId(1)
789 planner_called: file1.parquet
790 io_future_created: file1.parquet, IoFutureId(2)
791 io_future_polled: file1.parquet, IoFutureId(2)
792 io_future_resolved: file1.parquet, IoFutureId(2)
793 planner_called: file1.parquet
794 morsel_produced: file1.parquet, MorselId(10)
795 morsel_stream_started: MorselId(10)
796 morsel_stream_batch_produced: MorselId(10), BatchId(42)
797 morsel_stream_finished: MorselId(10)
798 ");
799
800 Ok(())
801 }
802
803 #[tokio::test]
805 async fn morsel_io_error() -> Result<()> {
806 let test = FileStreamMorselTest::new().with_file(
807 MockPlanner::builder("file1.parquet").add_plan(
808 PendingPlannerBuilder::new(IoFutureId(1))
809 .with_error("io failed while opening file"),
810 ),
811 );
812
813 insta::assert_snapshot!(test.run().await.unwrap(), @r"
814 ----- Output Stream -----
815 Error: io failed while opening file
816 Done
817 ----- File Stream Events -----
818 morselize_file: file1.parquet
819 planner_created: file1.parquet
820 planner_called: file1.parquet
821 io_future_created: file1.parquet, IoFutureId(1)
822 io_future_polled: file1.parquet, IoFutureId(1)
823 io_future_errored: file1.parquet, IoFutureId(1), io failed while opening file
824 ");
825
826 Ok(())
827 }
828
829 #[tokio::test]
832 async fn morsel_pending_planner_does_not_block_active_reader() -> Result<()> {
833 let test = FileStreamMorselTest::new().with_file(
834 MockPlanner::builder("file1.parquet")
835 .add_plan(
836 MockPlanBuilder::new()
837 .with_morsel_batches(MorselId(10), vec![41, 42])
838 .with_pending_planner(IoFutureId(1), PollsToResolve(3), Ok(())),
839 )
840 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 43))
841 .return_none(),
842 );
843
844 insta::assert_snapshot!(test.run().await.unwrap(), @r"
849 ----- Output Stream -----
850 Batch: 41
851 Batch: 42
852 Batch: 43
853 Done
854 ----- File Stream Events -----
855 morselize_file: file1.parquet
856 planner_created: file1.parquet
857 planner_called: file1.parquet
858 morsel_produced: file1.parquet, MorselId(10)
859 io_future_created: file1.parquet, IoFutureId(1)
860 io_future_polled: file1.parquet, IoFutureId(1)
861 morsel_stream_started: MorselId(10)
862 io_future_polled: file1.parquet, IoFutureId(1)
863 morsel_stream_batch_produced: MorselId(10), BatchId(41)
864 io_future_polled: file1.parquet, IoFutureId(1)
865 morsel_stream_batch_produced: MorselId(10), BatchId(42)
866 io_future_polled: file1.parquet, IoFutureId(1)
867 io_future_resolved: file1.parquet, IoFutureId(1)
868 morsel_stream_finished: MorselId(10)
869 planner_called: file1.parquet
870 morsel_produced: file1.parquet, MorselId(11)
871 morsel_stream_started: MorselId(11)
872 morsel_stream_batch_produced: MorselId(11), BatchId(43)
873 morsel_stream_finished: MorselId(11)
874 ");
875
876 Ok(())
877 }
878
879 #[tokio::test]
882 async fn morsel_ready_child_planner() -> Result<()> {
883 let child_planner = MockPlanner::builder("child planner")
884 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 42))
885 .return_none();
886
887 let test = FileStreamMorselTest::new().with_file(
888 MockPlanner::builder("file1.parquet")
889 .add_plan(MockPlanBuilder::new().with_ready_planner(child_planner))
890 .return_none(),
891 );
892
893 insta::assert_snapshot!(test.run().await.unwrap(), @r"
894 ----- Output Stream -----
895 Batch: 42
896 Done
897 ----- File Stream Events -----
898 morselize_file: file1.parquet
899 planner_created: file1.parquet
900 planner_called: file1.parquet
901 planner_created: child planner
902 planner_called: child planner
903 morsel_produced: child planner, MorselId(10)
904 morsel_stream_started: MorselId(10)
905 morsel_stream_batch_produced: MorselId(10), BatchId(42)
906 morsel_stream_finished: MorselId(10)
907 ");
908
909 Ok(())
910 }
911
912 #[tokio::test]
914 async fn morsel_plan_error_after_io() -> Result<()> {
915 let test = FileStreamMorselTest::new().with_file(
916 MockPlanner::builder("file1.parquet")
917 .add_plan(PendingPlannerBuilder::new(IoFutureId(1)))
918 .return_error("planner failed after io"),
919 );
920
921 insta::assert_snapshot!(test.run().await.unwrap(), @r"
922 ----- Output Stream -----
923 Error: planner failed after io
924 Done
925 ----- File Stream Events -----
926 morselize_file: file1.parquet
927 planner_created: file1.parquet
928 planner_called: file1.parquet
929 io_future_created: file1.parquet, IoFutureId(1)
930 io_future_polled: file1.parquet, IoFutureId(1)
931 io_future_resolved: file1.parquet, IoFutureId(1)
932 planner_called: file1.parquet
933 ");
934
935 Ok(())
936 }
937
938 #[tokio::test]
940 async fn morsel_multiple_files() -> Result<()> {
941 let test = FileStreamMorselTest::new()
942 .with_file(
943 MockPlanner::builder("file1.parquet")
944 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 41))
945 .return_none(),
946 )
947 .with_file(
948 MockPlanner::builder("file2.parquet")
949 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 42))
950 .return_none(),
951 );
952
953 insta::assert_snapshot!(test.run().await.unwrap(), @r"
954 ----- Output Stream -----
955 Batch: 41
956 Batch: 42
957 Done
958 ----- File Stream Events -----
959 morselize_file: file1.parquet
960 planner_created: file1.parquet
961 planner_called: file1.parquet
962 morsel_produced: file1.parquet, MorselId(10)
963 morsel_stream_started: MorselId(10)
964 morsel_stream_batch_produced: MorselId(10), BatchId(41)
965 morsel_stream_finished: MorselId(10)
966 morselize_file: file2.parquet
967 planner_created: file2.parquet
968 planner_called: file2.parquet
969 morsel_produced: file2.parquet, MorselId(11)
970 morsel_stream_started: MorselId(11)
971 morsel_stream_batch_produced: MorselId(11), BatchId(42)
972 morsel_stream_finished: MorselId(11)
973 ");
974
975 Ok(())
976 }
977
978 #[tokio::test]
980 async fn morsel_limit_prevents_second_file() -> Result<()> {
981 let test = FileStreamMorselTest::new()
982 .with_file(
983 MockPlanner::builder("file1.parquet")
984 .add_plan(
985 MockPlanBuilder::new()
986 .with_morsel_batches(MorselId(10), vec![41, 42]),
987 )
988 .return_none(),
989 )
990 .with_file(
991 MockPlanner::builder("file2.parquet")
992 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 43))
993 .return_none(),
994 )
995 .with_limit(1);
996
997 insta::assert_snapshot!(test.run().await.unwrap(), @r"
999 ----- Output Stream -----
1000 Batch: 41
1001 Done
1002 ----- File Stream Events -----
1003 morselize_file: file1.parquet
1004 planner_created: file1.parquet
1005 planner_called: file1.parquet
1006 morsel_produced: file1.parquet, MorselId(10)
1007 morsel_stream_started: MorselId(10)
1008 morsel_stream_batch_produced: MorselId(10), BatchId(41)
1009 ");
1010
1011 Ok(())
1012 }
1013
1014 fn two_partition_morsel_test() -> FileStreamMorselTest {
1020 FileStreamMorselTest::new()
1021 .with_file_in_partition(
1023 PartitionId(0),
1024 MockPlanner::builder("file1.parquet")
1025 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 101))
1026 .return_none(),
1027 )
1028 .with_file_in_partition(
1029 PartitionId(0),
1030 MockPlanner::builder("file2.parquet")
1031 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 102))
1032 .return_none(),
1033 )
1034 .with_file_in_partition(
1035 PartitionId(0),
1036 MockPlanner::builder("file3.parquet")
1037 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(12), 103))
1038 .return_none(),
1039 )
1040 .with_file_in_partition(
1042 PartitionId(1),
1043 MockPlanner::builder("file4.parquet")
1044 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(13), 201))
1045 .return_none(),
1046 )
1047 .with_reads(vec![
1048 PartitionId(1),
1049 PartitionId(1),
1050 PartitionId(1),
1051 PartitionId(1),
1052 PartitionId(1),
1053 ])
1054 }
1055
1056 #[tokio::test]
1059 async fn morsel_shared_files_can_be_stolen() -> Result<()> {
1060 let test = two_partition_morsel_test().with_file_stream_events(false);
1061
1062 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1066 ----- Partition 0 -----
1067 Done
1068 ----- Partition 1 -----
1069 Batch: 101
1070 Batch: 102
1071 Batch: 103
1072 Batch: 201
1073 Done
1074 ----- File Stream Events -----
1075 (omitted due to with_file_stream_events(false))
1076 ");
1077
1078 Ok(())
1079 }
1080
1081 #[tokio::test]
1084 async fn morsel_preserve_order_keeps_files_local() -> Result<()> {
1085 let test = two_partition_morsel_test()
1088 .with_preserve_order(true)
1089 .with_file_stream_events(false);
1090
1091 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1095 ----- Partition 0 -----
1096 Batch: 101
1097 Batch: 102
1098 Batch: 103
1099 Done
1100 ----- Partition 1 -----
1101 Batch: 201
1102 Done
1103 ----- File Stream Events -----
1104 (omitted due to with_file_stream_events(false))
1105 ");
1106
1107 Ok(())
1108 }
1109
1110 #[tokio::test]
1112 async fn morsel_partitioned_by_file_group_keeps_files_local() -> Result<()> {
1113 let test = two_partition_morsel_test()
1116 .with_partitioned_by_file_group(true)
1117 .with_file_stream_events(false);
1118
1119 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1120 ----- Partition 0 -----
1121 Batch: 101
1122 Batch: 102
1123 Batch: 103
1124 Done
1125 ----- Partition 1 -----
1126 Batch: 201
1127 Done
1128 ----- File Stream Events -----
1129 (omitted due to with_file_stream_events(false))
1130 ");
1131
1132 Ok(())
1133 }
1134
1135 #[tokio::test]
1144 async fn morsel_disabled_work_stealing_keeps_files_local() -> Result<()> {
1145 let test = two_partition_morsel_test()
1148 .with_enable_file_stream_work_stealing(false)
1149 .with_file_stream_events(false);
1150
1151 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1154 ----- Partition 0 -----
1155 Batch: 101
1156 Batch: 102
1157 Batch: 103
1158 Done
1159 ----- Partition 1 -----
1160 Batch: 201
1161 Done
1162 ----- File Stream Events -----
1163 (omitted due to with_file_stream_events(false))
1164 ");
1165
1166 Ok(())
1167 }
1168
1169 #[tokio::test]
1172 async fn morsel_empty_sibling_can_steal() -> Result<()> {
1173 let test = FileStreamMorselTest::new()
1174 .with_file_in_partition(
1175 PartitionId(0),
1176 MockPlanner::builder("file1.parquet")
1177 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 101))
1178 .return_none(),
1179 )
1180 .with_file_in_partition(
1181 PartitionId(0),
1182 MockPlanner::builder("file2.parquet")
1183 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 102))
1184 .return_none(),
1185 )
1186 .with_reads(vec![PartitionId(1), PartitionId(1), PartitionId(1)])
1188 .with_file_stream_events(false);
1189
1190 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1191 ----- Partition 0 -----
1192 Done
1193 ----- Partition 1 -----
1194 Batch: 101
1195 Batch: 102
1196 Done
1197 ----- File Stream Events -----
1198 (omitted due to with_file_stream_events(false))
1199 ");
1200
1201 Ok(())
1202 }
1203
1204 #[tokio::test]
1208 async fn morsel_empty_sibling_can_finish_before_shared_work_exists() -> Result<()> {
1209 let test = FileStreamMorselTest::new()
1210 .with_file_in_partition(
1211 PartitionId(0),
1212 MockPlanner::builder("file1.parquet")
1213 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 101))
1214 .return_none(),
1215 )
1216 .with_file_in_partition(
1217 PartitionId(0),
1218 MockPlanner::builder("file2.parquet")
1219 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 102))
1220 .return_none(),
1221 )
1222 .with_build_streams_on_first_read(true)
1227 .with_reads(vec![PartitionId(1), PartitionId(0), PartitionId(1)])
1228 .with_file_stream_events(false);
1229
1230 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1233 ----- Partition 0 -----
1234 Batch: 102
1235 Done
1236 ----- Partition 1 -----
1237 Batch: 101
1238 Done
1239 ----- File Stream Events -----
1240 (omitted due to with_file_stream_events(false))
1241 ");
1242
1243 Ok(())
1244 }
1245
1246 #[tokio::test]
1249 async fn morsel_shared_limit_does_not_double_count_files_processed() -> Result<()> {
1250 let test = two_partition_morsel_test();
1251 let unlimited_config = test.test_config();
1252 let limited_config = test.clone().with_limit(1).test_config();
1253 let shared_work_source = limited_config
1254 .create_sibling_state(&ConfigOptions::default())
1255 .and_then(|state| state.as_ref().downcast_ref::<SharedWorkSource>().cloned())
1256 .expect("shared work source");
1257 let limited_metrics = ExecutionPlanMetricsSet::new();
1258 let unlimited_metrics = ExecutionPlanMetricsSet::new();
1259
1260 let limited_stream = FileStreamBuilder::new(&limited_config)
1261 .with_partition(1)
1262 .with_shared_work_source(Some(shared_work_source.clone()))
1263 .with_morselizer(Box::new(test.morselizer.clone()))
1264 .with_metrics(&limited_metrics)
1265 .build()?;
1266
1267 let unlimited_stream = FileStreamBuilder::new(&unlimited_config)
1268 .with_partition(0)
1269 .with_shared_work_source(Some(shared_work_source))
1270 .with_morselizer(Box::new(test.morselizer))
1271 .with_metrics(&unlimited_metrics)
1272 .build()?;
1273
1274 let limited_output = drain_stream_output(limited_stream).await?;
1275 let unlimited_output = drain_stream_output(unlimited_stream).await?;
1276
1277 insta::assert_snapshot!(format!(
1278 "----- Limited Stream -----\n{limited_output}\n----- Unlimited Stream -----\n{unlimited_output}"
1279 ), @r"
1280 ----- Limited Stream -----
1281 Batch: 101
1282 ----- Unlimited Stream -----
1283 Batch: 102
1284 Batch: 103
1285 Batch: 201
1286 ");
1287
1288 assert_eq!(
1289 metric_count(&limited_metrics, "files_opened"),
1290 1,
1291 "the limited stream should only open the file that produced its output"
1292 );
1293 assert_eq!(
1294 metric_count(&limited_metrics, "files_processed"),
1295 1,
1296 "the limited stream should only mark its own file as processed"
1297 );
1298 assert_eq!(
1299 metric_count(&unlimited_metrics, "files_opened"),
1300 3,
1301 "the draining stream should open the remaining shared files"
1302 );
1303 assert_eq!(
1304 metric_count(&unlimited_metrics, "files_processed"),
1305 3,
1306 "the draining stream should process exactly the files it opened"
1307 );
1308
1309 Ok(())
1310 }
1311
1312 #[tokio::test]
1315 async fn morsel_one_sibling_can_drain_multiple_siblings() -> Result<()> {
1316 let test = FileStreamMorselTest::new()
1317 .with_file_in_partition(
1318 PartitionId(0),
1319 MockPlanner::builder("file1.parquet")
1320 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(10), 101))
1321 .return_none(),
1322 )
1323 .with_file_in_partition(
1325 PartitionId(1),
1326 MockPlanner::builder("file2.parquet")
1327 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(11), 102))
1328 .return_none(),
1329 )
1330 .with_file_in_partition(
1331 PartitionId(1),
1332 MockPlanner::builder("file3.parquet")
1333 .add_plan(MockPlanBuilder::new().with_morsel(MorselId(12), 103))
1334 .return_none(),
1335 )
1336 .with_reads(vec![
1339 PartitionId(2),
1340 PartitionId(2),
1341 PartitionId(1),
1342 PartitionId(2),
1343 ])
1344 .with_file_stream_events(false);
1345
1346 insta::assert_snapshot!(test.run().await.unwrap(), @r"
1347 ----- Partition 0 -----
1348 Done
1349 ----- Partition 1 -----
1350 Batch: 103
1351 Done
1352 ----- Partition 2 -----
1353 Batch: 101
1354 Batch: 102
1355 Done
1356 ----- File Stream Events -----
1357 (omitted due to with_file_stream_events(false))
1358 ");
1359
1360 Ok(())
1361 }
1362
1363 #[derive(Clone)]
1365 struct FileStreamMorselTest {
1366 morselizer: MockMorselizer,
1367 partition_files: BTreeMap<PartitionId, Vec<String>>,
1368 preserve_order: bool,
1369 partitioned_by_file_group: bool,
1370 enable_file_stream_work_stealing: bool,
1371 file_stream_events: bool,
1372 build_streams_on_first_read: bool,
1373 reads: Vec<PartitionId>,
1374 limit: Option<usize>,
1375 }
1376
1377 impl FileStreamMorselTest {
1378 fn new() -> Self {
1380 Self {
1381 morselizer: MockMorselizer::new(),
1382 partition_files: BTreeMap::new(),
1383 preserve_order: false,
1384 partitioned_by_file_group: false,
1385 enable_file_stream_work_stealing: true,
1386 file_stream_events: true,
1387 build_streams_on_first_read: false,
1388 reads: vec![],
1389 limit: None,
1390 }
1391 }
1392
1393 fn with_file(self, planner: impl Into<MockPlanner>) -> Self {
1395 self.with_file_in_partition(PartitionId(0), planner)
1396 }
1397
1398 fn with_file_in_partition(
1400 mut self,
1401 partition: PartitionId,
1402 planner: impl Into<MockPlanner>,
1403 ) -> Self {
1404 let planner = planner.into();
1405 let file_path = planner.file_path().to_string();
1406 self.morselizer = self.morselizer.with_planner(planner);
1407 self.partition_files
1408 .entry(partition)
1409 .or_default()
1410 .push(file_path);
1411 self
1412 }
1413
1414 fn with_preserve_order(mut self, preserve_order: bool) -> Self {
1417 self.preserve_order = preserve_order;
1418 self
1419 }
1420
1421 fn with_partitioned_by_file_group(
1424 mut self,
1425 partitioned_by_file_group: bool,
1426 ) -> Self {
1427 self.partitioned_by_file_group = partitioned_by_file_group;
1428 self
1429 }
1430
1431 fn with_enable_file_stream_work_stealing(mut self, enable: bool) -> Self {
1435 self.enable_file_stream_work_stealing = enable;
1436 self
1437 }
1438
1439 fn with_file_stream_events(mut self, file_stream_events: bool) -> Self {
1445 self.file_stream_events = file_stream_events;
1446 self
1447 }
1448
1449 fn with_build_streams_on_first_read(
1457 mut self,
1458 build_streams_on_first_read: bool,
1459 ) -> Self {
1460 self.build_streams_on_first_read = build_streams_on_first_read;
1461 self
1462 }
1463
1464 fn with_reads(mut self, reads: Vec<PartitionId>) -> Self {
1474 self.reads = reads;
1475 self
1476 }
1477
1478 fn with_limit(mut self, limit: usize) -> Self {
1480 self.limit = Some(limit);
1481 self
1482 }
1483
1484 async fn run(self) -> Result<String> {
1487 let observer = self.morselizer.observer().clone();
1488 observer.clear();
1489
1490 let metrics_set = ExecutionPlanMetricsSet::new();
1491 let partition_count = self.num_partitions();
1492
1493 let mut partitions = (0..partition_count)
1494 .map(|_| PartitionState::new())
1495 .collect::<Vec<_>>();
1496
1497 let mut build_order = Vec::new();
1498 for partition in self.reads.iter().map(|partition| partition.0) {
1499 if !build_order.contains(&partition) {
1500 build_order.push(partition);
1501 }
1502 }
1503 for partition in 0..partition_count {
1504 if !build_order.contains(&partition) {
1505 build_order.push(partition);
1506 }
1507 }
1508
1509 let config = self.test_config();
1510 let mut options = ConfigOptions::default();
1517 options.execution.enable_file_stream_work_stealing =
1518 self.enable_file_stream_work_stealing;
1519 let shared_work_source =
1520 config.create_sibling_state(&options).and_then(|state| {
1521 state.as_ref().downcast_ref::<SharedWorkSource>().cloned()
1522 });
1523 if !self.build_streams_on_first_read {
1524 for partition in build_order {
1525 let stream = FileStreamBuilder::new(&config)
1526 .with_partition(partition)
1527 .with_shared_work_source(shared_work_source.clone())
1528 .with_morselizer(Box::new(self.morselizer.clone()))
1529 .with_metrics(&metrics_set)
1530 .build()?;
1531 partitions[partition].set_stream(stream);
1532 }
1533 }
1534
1535 let mut initial_reads: VecDeque<_> = self.reads.into();
1536 let mut next_round_robin = 0;
1537
1538 while !initial_reads.is_empty()
1539 || partitions.iter().any(PartitionState::is_active)
1540 {
1541 let partition = if let Some(partition) = initial_reads.pop_front() {
1542 partition.0
1543 } else {
1544 let partition = next_round_robin;
1545 next_round_robin = (next_round_robin + 1) % partition_count.max(1);
1546 partition
1547 };
1548
1549 let partition_state = &mut partitions[partition];
1550
1551 if self.build_streams_on_first_read && !partition_state.built {
1552 let stream = FileStreamBuilder::new(&config)
1553 .with_partition(partition)
1554 .with_shared_work_source(shared_work_source.clone())
1555 .with_morselizer(Box::new(self.morselizer.clone()))
1556 .with_metrics(&metrics_set)
1557 .build()?;
1558 partition_state.set_stream(stream);
1559 }
1560
1561 let Some(stream) = partition_state.stream.as_mut() else {
1562 continue;
1563 };
1564
1565 match stream.next().await {
1566 Some(result) => partition_state.push_output(format_result(result)),
1567 None => partition_state.finish(),
1568 }
1569 }
1570
1571 let output_text = if partition_count == 1 {
1572 format!(
1573 "----- Output Stream -----\n{}",
1574 partitions[0].output.join("\n")
1575 )
1576 } else {
1577 partitions
1578 .into_iter()
1579 .enumerate()
1580 .map(|(partition, state)| {
1581 format!(
1582 "----- Partition {} -----\n{}",
1583 partition,
1584 state.output.join("\n")
1585 )
1586 })
1587 .collect::<Vec<_>>()
1588 .join("\n")
1589 };
1590
1591 let file_stream_events = if self.file_stream_events {
1592 observer.format_events()
1593 } else {
1594 "(omitted due to with_file_stream_events(false))".to_string()
1595 };
1596
1597 Ok(format!(
1598 "{output_text}\n----- File Stream Events -----\n{file_stream_events}",
1599 ))
1600 }
1601
1602 fn num_partitions(&self) -> usize {
1605 self.partition_files
1606 .keys()
1607 .map(|partition| partition.0 + 1)
1608 .chain(self.reads.iter().map(|partition| partition.0 + 1))
1609 .max()
1610 .unwrap_or(1)
1611 }
1612
1613 fn test_config(&self) -> FileScanConfig {
1615 let file_groups = (0..self.num_partitions())
1616 .map(|partition| {
1617 self.partition_files
1618 .get(&PartitionId(partition))
1619 .into_iter()
1620 .flat_map(|files| files.iter())
1621 .map(|name| PartitionedFile::new(name, 10))
1622 .collect::<Vec<_>>()
1623 .into()
1624 })
1625 .collect::<Vec<_>>();
1626
1627 let table_schema = TableSchema::new(
1628 Arc::new(Schema::new(vec![Field::new("i", DataType::Int32, false)])),
1629 vec![],
1630 );
1631 FileScanConfigBuilder::new(
1632 ObjectStoreUrl::parse("test:///").unwrap(),
1633 Arc::new(MockSource::new(table_schema)),
1634 )
1635 .with_file_groups(file_groups)
1636 .with_limit(self.limit)
1637 .with_preserve_order(self.preserve_order)
1638 .with_partitioned_by_file_group(self.partitioned_by_file_group)
1639 .build()
1640 }
1641 }
1642
1643 fn format_result(result: Result<RecordBatch>) -> String {
1645 match result {
1646 Ok(batch) => {
1647 let col = batch.column(0).as_primitive::<Int32Type>();
1648 let batch_id = col.value(0);
1649 format!("Batch: {batch_id}")
1650 }
1651 Err(e) => {
1652 let message = if let DataFusionError::External(generic) = e {
1656 generic.to_string()
1657 } else {
1658 e.to_string()
1659 };
1660 format!("Error: {message}")
1661 }
1662 }
1663 }
1664
1665 async fn drain_stream_output(stream: FileStream) -> Result<String> {
1666 let output = stream
1667 .collect::<Vec<_>>()
1668 .await
1669 .into_iter()
1670 .map(|result| result.map(|batch| format_result(Ok(batch))))
1671 .collect::<Result<Vec<_>>>()?;
1672 Ok(output.join("\n"))
1673 }
1674
1675 fn metric_count(metrics: &ExecutionPlanMetricsSet, name: &str) -> usize {
1676 metrics
1677 .clone_inner()
1678 .sum_by_name(name)
1679 .unwrap_or_else(|| panic!("missing metric: {name}"))
1680 .as_usize()
1681 }
1682
1683 struct PartitionState {
1685 built: bool,
1687 stream: Option<FileStream>,
1689 output: Vec<String>,
1691 }
1692
1693 impl PartitionState {
1694 fn new() -> Self {
1696 Self {
1697 built: false,
1698 stream: None,
1699 output: vec![],
1700 }
1701 }
1702
1703 fn is_active(&self) -> bool {
1705 !self.built || self.stream.is_some()
1706 }
1707
1708 fn set_stream(&mut self, stream: FileStream) {
1710 self.stream = Some(stream);
1711 self.built = true;
1712 }
1713
1714 fn push_output(&mut self, line: String) {
1716 self.output.push(line);
1717 }
1718
1719 fn finish(&mut self) {
1721 self.push_output("Done".to_string());
1722 self.stream = None;
1723 }
1724 }
1725}