blob: a4a43ca9aeab3c743fef6785981212dc831c06a0 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! A generic stream over file format readers that can be used by
//! any file format that read its files from start to end.
//!
//! Note: Most traits here need to be marked `Sync + Send` to be
//! compliant with the `SendableRecordBatchStream` trait.
use std::collections::VecDeque;
use std::mem;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use crate::file_scan_config::{FileScanConfig, PartitionColumnProjector};
use crate::PartitionedFile;
use arrow::datatypes::SchemaRef;
use datafusion_common::error::Result;
use datafusion_execution::RecordBatchStream;
use datafusion_physical_plan::metrics::{
BaselineMetrics, Count, ExecutionPlanMetricsSet, MetricBuilder, Time,
};
use arrow::record_batch::RecordBatch;
use datafusion_common::instant::Instant;
use datafusion_common::ScalarValue;
use futures::future::BoxFuture;
use futures::stream::BoxStream;
use futures::{ready, FutureExt as _, Stream, StreamExt as _};
/// A stream that iterates record batch by record batch, file over file.
pub struct FileStream {
/// An iterator over input files.
file_iter: VecDeque<PartitionedFile>,
/// The stream schema (file schema including partition columns and after
/// projection).
projected_schema: SchemaRef,
/// The remaining number of records to parse, None if no limit
remain: Option<usize>,
/// A dynamic [`FileOpener`]. Calling `open()` returns a [`FileOpenFuture`],
/// which can be resolved to a stream of `RecordBatch`.
file_opener: Arc<dyn FileOpener>,
/// The partition column projector
pc_projector: PartitionColumnProjector,
/// The stream state
state: FileStreamState,
/// File stream specific metrics
file_stream_metrics: FileStreamMetrics,
/// runtime baseline metrics
baseline_metrics: BaselineMetrics,
/// Describes the behavior of the `FileStream` if file opening or scanning fails
on_error: OnError,
}
impl FileStream {
/// Create a new `FileStream` using the give `FileOpener` to scan underlying files
pub fn new(
config: &FileScanConfig,
partition: usize,
file_opener: Arc<dyn FileOpener>,
metrics: &ExecutionPlanMetricsSet,
) -> Result<Self> {
let projected_schema = config.projected_schema();
let pc_projector = PartitionColumnProjector::new(
Arc::clone(&projected_schema),
&config
.table_partition_cols()
.iter()
.map(|x| x.name().clone())
.collect::<Vec<_>>(),
);
let file_group = config.file_groups[partition].clone();
Ok(Self {
file_iter: file_group.into_inner().into_iter().collect(),
projected_schema,
remain: config.limit,
file_opener,
pc_projector,
state: FileStreamState::Idle,
file_stream_metrics: FileStreamMetrics::new(metrics, partition),
baseline_metrics: BaselineMetrics::new(metrics, partition),
on_error: OnError::Fail,
})
}
/// Specify the behavior when an error occurs opening or scanning a file
///
/// If `OnError::Skip` the stream will skip files which encounter an error and continue
/// If `OnError:Fail` (default) the stream will fail and stop processing when an error occurs
pub fn with_on_error(mut self, on_error: OnError) -> Self {
self.on_error = on_error;
self
}
/// Begin opening the next file in parallel while decoding the current file in FileStream.
///
/// Since file opening is mostly IO (and may involve a
/// bunch of sequential IO), it can be parallelized with decoding.
fn start_next_file(&mut self) -> Option<Result<(FileOpenFuture, Vec<ScalarValue>)>> {
let part_file = self.file_iter.pop_front()?;
let partition_values = part_file.partition_values.clone();
Some(
self.file_opener
.open(part_file)
.map(|future| (future, partition_values)),
)
}
fn poll_inner(&mut self, cx: &mut Context<'_>) -> Poll<Option<Result<RecordBatch>>> {
loop {
match &mut self.state {
FileStreamState::Idle => {
self.file_stream_metrics.time_opening.start();
match self.start_next_file().transpose() {
Ok(Some((future, partition_values))) => {
self.state = FileStreamState::Open {
future,
partition_values,
}
}
Ok(None) => return Poll::Ready(None),
Err(e) => {
self.state = FileStreamState::Error;
return Poll::Ready(Some(Err(e)));
}
}
}
FileStreamState::Open {
future,
partition_values,
} => match ready!(future.poll_unpin(cx)) {
Ok(reader) => {
let partition_values = mem::take(partition_values);
// include time needed to start opening in `start_next_file`
self.file_stream_metrics.time_opening.stop();
let next = self.start_next_file().transpose();
self.file_stream_metrics.time_scanning_until_data.start();
self.file_stream_metrics.time_scanning_total.start();
match next {
Ok(Some((next_future, next_partition_values))) => {
self.state = FileStreamState::Scan {
partition_values,
reader,
next: Some((
NextOpen::Pending(next_future),
next_partition_values,
)),
};
}
Ok(None) => {
self.state = FileStreamState::Scan {
reader,
partition_values,
next: None,
};
}
Err(e) => {
self.state = FileStreamState::Error;
return Poll::Ready(Some(Err(e)));
}
}
}
Err(e) => {
self.file_stream_metrics.file_open_errors.add(1);
match self.on_error {
OnError::Skip => {
self.file_stream_metrics.time_opening.stop();
self.state = FileStreamState::Idle
}
OnError::Fail => {
self.state = FileStreamState::Error;
return Poll::Ready(Some(Err(e)));
}
}
}
},
FileStreamState::Scan {
reader,
partition_values,
next,
} => {
// We need to poll the next `FileOpenFuture` here to drive it forward
if let Some((next_open_future, _)) = next {
if let NextOpen::Pending(f) = next_open_future {
if let Poll::Ready(reader) = f.as_mut().poll(cx) {
*next_open_future = NextOpen::Ready(reader);
}
}
}
match ready!(reader.poll_next_unpin(cx)) {
Some(Ok(batch)) => {
self.file_stream_metrics.time_scanning_until_data.stop();
self.file_stream_metrics.time_scanning_total.stop();
let result = self
.pc_projector
.project(batch, partition_values)
.map(|batch| match &mut self.remain {
Some(remain) => {
if *remain > batch.num_rows() {
*remain -= batch.num_rows();
batch
} else {
let batch = batch.slice(0, *remain);
self.state = FileStreamState::Limit;
*remain = 0;
batch
}
}
None => batch,
});
if result.is_err() {
// If the partition value projection fails, this is not governed by
// the `OnError` behavior
self.state = FileStreamState::Error
}
self.file_stream_metrics.time_scanning_total.start();
return Poll::Ready(Some(result));
}
Some(Err(err)) => {
self.file_stream_metrics.file_scan_errors.add(1);
self.file_stream_metrics.time_scanning_until_data.stop();
self.file_stream_metrics.time_scanning_total.stop();
match self.on_error {
// If `OnError::Skip` we skip the file as soon as we hit the first error
OnError::Skip => match mem::take(next) {
Some((future, partition_values)) => {
self.file_stream_metrics.time_opening.start();
match future {
NextOpen::Pending(future) => {
self.state = FileStreamState::Open {
future,
partition_values,
}
}
NextOpen::Ready(reader) => {
self.state = FileStreamState::Open {
future: Box::pin(std::future::ready(
reader,
)),
partition_values,
}
}
}
}
None => return Poll::Ready(None),
},
OnError::Fail => {
self.state = FileStreamState::Error;
return Poll::Ready(Some(Err(err)));
}
}
}
None => {
self.file_stream_metrics.time_scanning_until_data.stop();
self.file_stream_metrics.time_scanning_total.stop();
match mem::take(next) {
Some((future, partition_values)) => {
self.file_stream_metrics.time_opening.start();
match future {
NextOpen::Pending(future) => {
self.state = FileStreamState::Open {
future,
partition_values,
}
}
NextOpen::Ready(reader) => {
self.state = FileStreamState::Open {
future: Box::pin(std::future::ready(
reader,
)),
partition_values,
}
}
}
}
None => return Poll::Ready(None),
}
}
}
}
FileStreamState::Error | FileStreamState::Limit => {
return Poll::Ready(None)
}
}
}
}
}
impl Stream for FileStream {
type Item = Result<RecordBatch>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
self.file_stream_metrics.time_processing.start();
let result = self.poll_inner(cx);
self.file_stream_metrics.time_processing.stop();
self.baseline_metrics.record_poll(result)
}
}
impl RecordBatchStream for FileStream {
fn schema(&self) -> SchemaRef {
Arc::clone(&self.projected_schema)
}
}
/// A fallible future that resolves to a stream of [`RecordBatch`]
pub type FileOpenFuture =
BoxFuture<'static, Result<BoxStream<'static, Result<RecordBatch>>>>;
/// Describes the behavior of the `FileStream` if file opening or scanning fails
#[derive(Default)]
pub enum OnError {
/// Fail the entire stream and return the underlying error
#[default]
Fail,
/// Continue scanning, ignoring the failed file
Skip,
}
/// Generic API for opening a file using an [`ObjectStore`] and resolving to a
/// stream of [`RecordBatch`]
///
/// [`ObjectStore`]: object_store::ObjectStore
pub trait FileOpener: Unpin + Send + Sync {
/// Asynchronously open the specified file and return a stream
/// of [`RecordBatch`]
fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture>;
}
/// Represents the state of the next `FileOpenFuture`. Since we need to poll
/// this future while scanning the current file, we need to store the result if it
/// is ready
pub enum NextOpen {
Pending(FileOpenFuture),
Ready(Result<BoxStream<'static, Result<RecordBatch>>>),
}
pub enum FileStreamState {
/// The idle state, no file is currently being read
Idle,
/// Currently performing asynchronous IO to obtain a stream of RecordBatch
/// for a given file
Open {
/// A [`FileOpenFuture`] returned by [`FileOpener::open`]
future: FileOpenFuture,
/// The partition values for this file
partition_values: Vec<ScalarValue>,
},
/// Scanning the [`BoxStream`] returned by the completion of a [`FileOpenFuture`]
/// returned by [`FileOpener::open`]
Scan {
/// Partitioning column values for the current batch_iter
partition_values: Vec<ScalarValue>,
/// The reader instance
reader: BoxStream<'static, Result<RecordBatch>>,
/// A [`FileOpenFuture`] for the next file to be processed,
/// and its corresponding partition column values, if any.
/// This allows the next file to be opened in parallel while the
/// current file is read.
next: Option<(NextOpen, Vec<ScalarValue>)>,
},
/// Encountered an error
Error,
/// Reached the row limit
Limit,
}
/// A timer that can be started and stopped.
pub struct StartableTime {
pub metrics: Time,
// use for record each part cost time, will eventually add into 'metrics'.
pub start: Option<Instant>,
}
impl StartableTime {
pub fn start(&mut self) {
assert!(self.start.is_none());
self.start = Some(Instant::now());
}
pub fn stop(&mut self) {
if let Some(start) = self.start.take() {
self.metrics.add_elapsed(start);
}
}
}
#[allow(rustdoc::broken_intra_doc_links)]
/// Metrics for [`FileStream`]
///
/// Note that all of these metrics are in terms of wall clock time
/// (not cpu time) so they include time spent waiting on I/O as well
/// as other operators.
///
/// [`FileStream`]: <https://github.com/apache/datafusion/blob/main/datafusion/datasource/src/file_stream.rs>
pub struct FileStreamMetrics {
/// Wall clock time elapsed for file opening.
///
/// Time between when [`FileOpener::open`] is called and when the
/// [`FileStream`] receives a stream for reading.
///
/// If there are multiple files being scanned, the stream
/// will open the next file in the background while scanning the
/// current file. This metric will only capture time spent opening
/// while not also scanning.
/// [`FileStream`]: <https://github.com/apache/datafusion/blob/main/datafusion/datasource/src/file_stream.rs>
pub time_opening: StartableTime,
/// Wall clock time elapsed for file scanning + first record batch of decompression + decoding
///
/// Time between when the [`FileStream`] requests data from the
/// stream and when the first [`RecordBatch`] is produced.
/// [`FileStream`]: <https://github.com/apache/datafusion/blob/main/datafusion/datasource/src/file_stream.rs>
pub time_scanning_until_data: StartableTime,
/// Total elapsed wall clock time for scanning + record batch decompression / decoding
///
/// Sum of time between when the [`FileStream`] requests data from
/// the stream and when a [`RecordBatch`] is produced for all
/// record batches in the stream. Note that this metric also
/// includes the time of the parent operator's execution.
pub time_scanning_total: StartableTime,
/// Wall clock time elapsed for data decompression + decoding
///
/// Time spent waiting for the FileStream's input.
pub time_processing: StartableTime,
/// Count of errors opening file.
///
/// If using `OnError::Skip` this will provide a count of the number of files
/// which were skipped and will not be included in the scan results.
pub file_open_errors: Count,
/// Count of errors scanning file
///
/// If using `OnError::Skip` this will provide a count of the number of files
/// which were skipped and will not be included in the scan results.
pub file_scan_errors: Count,
}
impl FileStreamMetrics {
pub fn new(metrics: &ExecutionPlanMetricsSet, partition: usize) -> Self {
let time_opening = StartableTime {
metrics: MetricBuilder::new(metrics)
.subset_time("time_elapsed_opening", partition),
start: None,
};
let time_scanning_until_data = StartableTime {
metrics: MetricBuilder::new(metrics)
.subset_time("time_elapsed_scanning_until_data", partition),
start: None,
};
let time_scanning_total = StartableTime {
metrics: MetricBuilder::new(metrics)
.subset_time("time_elapsed_scanning_total", partition),
start: None,
};
let time_processing = StartableTime {
metrics: MetricBuilder::new(metrics)
.subset_time("time_elapsed_processing", partition),
start: None,
};
let file_open_errors =
MetricBuilder::new(metrics).counter("file_open_errors", partition);
let file_scan_errors =
MetricBuilder::new(metrics).counter("file_scan_errors", partition);
Self {
time_opening,
time_scanning_until_data,
time_scanning_total,
time_processing,
file_open_errors,
file_scan_errors,
}
}
}
#[cfg(test)]
mod tests {
use crate::file_scan_config::FileScanConfigBuilder;
use crate::tests::make_partition;
use crate::PartitionedFile;
use datafusion_common::error::Result;
use datafusion_execution::object_store::ObjectStoreUrl;
use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
use futures::{FutureExt as _, StreamExt as _};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use crate::file_stream::{FileOpenFuture, FileOpener, FileStream, OnError};
use crate::test_util::MockSource;
use arrow::array::RecordBatch;
use arrow::datatypes::Schema;
use datafusion_common::{assert_batches_eq, exec_err, internal_err};
/// Test `FileOpener` which will simulate errors during file opening or scanning
#[derive(Default)]
struct TestOpener {
/// Index in stream of files which should throw an error while opening
error_opening_idx: Vec<usize>,
/// Index in stream of files which should throw an error while scanning
error_scanning_idx: Vec<usize>,
/// Index of last file in stream
current_idx: AtomicUsize,
/// `RecordBatch` to return
records: Vec<RecordBatch>,
}
impl FileOpener for TestOpener {
fn open(&self, _partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
let idx = self.current_idx.fetch_add(1, Ordering::SeqCst);
if self.error_opening_idx.contains(&idx) {
Ok(futures::future::ready(internal_err!("error opening")).boxed())
} else if self.error_scanning_idx.contains(&idx) {
let error = futures::future::ready(exec_err!("error scanning"));
let stream = futures::stream::once(error).boxed();
Ok(futures::future::ready(Ok(stream)).boxed())
} else {
let iterator = self.records.clone().into_iter().map(Ok);
let stream = futures::stream::iter(iterator).boxed();
Ok(futures::future::ready(Ok(stream)).boxed())
}
}
}
#[derive(Default)]
struct FileStreamTest {
/// Number of files in the stream
num_files: usize,
/// Global limit of records emitted by the stream
limit: Option<usize>,
/// Error-handling behavior of the stream
on_error: OnError,
/// Mock `FileOpener`
opener: TestOpener,
}
impl FileStreamTest {
pub fn new() -> Self {
Self::default()
}
/// Specify the number of files in the stream
pub fn with_num_files(mut self, num_files: usize) -> Self {
self.num_files = num_files;
self
}
/// Specify the limit
pub fn with_limit(mut self, limit: Option<usize>) -> Self {
self.limit = limit;
self
}
/// Specify the index of files in the stream which should
/// throw an error when opening
pub fn with_open_errors(mut self, idx: Vec<usize>) -> Self {
self.opener.error_opening_idx = idx;
self
}
/// Specify the index of files in the stream which should
/// throw an error when scanning
pub fn with_scan_errors(mut self, idx: Vec<usize>) -> Self {
self.opener.error_scanning_idx = idx;
self
}
/// Specify the behavior of the stream when an error occurs
pub fn with_on_error(mut self, on_error: OnError) -> Self {
self.on_error = on_error;
self
}
/// Specify the record batches that should be returned from each
/// file that is successfully scanned
pub fn with_records(mut self, records: Vec<RecordBatch>) -> Self {
self.opener.records = records;
self
}
/// Collect the results of the `FileStream`
pub async fn result(self) -> Result<Vec<RecordBatch>> {
let file_schema = self
.opener
.records
.first()
.map(|batch| batch.schema())
.unwrap_or_else(|| Arc::new(Schema::empty()));
// let ctx = SessionContext::new();
let mock_files: Vec<(String, u64)> = (0..self.num_files)
.map(|idx| (format!("mock_file{idx}"), 10_u64))
.collect();
// let mock_files_ref: Vec<(&str, u64)> = mock_files
// .iter()
// .map(|(name, size)| (name.as_str(), *size))
// .collect();
let file_group = mock_files
.into_iter()
.map(|(name, size)| PartitionedFile::new(name, size))
.collect();
let on_error = self.on_error;
let config = FileScanConfigBuilder::new(
ObjectStoreUrl::parse("test:///").unwrap(),
file_schema,
Arc::new(MockSource::default()),
)
.with_file_group(file_group)
.with_limit(self.limit)
.build();
let metrics_set = ExecutionPlanMetricsSet::new();
let file_stream =
FileStream::new(&config, 0, Arc::new(self.opener), &metrics_set)
.unwrap()
.with_on_error(on_error);
file_stream
.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>>>()
}
}
/// helper that creates a stream of 2 files with the same pair of batches in each ([0,1,2] and [0,1])
async fn create_and_collect(limit: Option<usize>) -> Vec<RecordBatch> {
FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_limit(limit)
.result()
.await
.expect("error executing stream")
}
#[tokio::test]
async fn on_error_opening() -> Result<()> {
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_open_errors(vec![0])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_open_errors(vec![1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_open_errors(vec![0, 1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"++",
"++",
], &batches);
Ok(())
}
#[tokio::test]
async fn on_error_scanning_fail() -> Result<()> {
let result = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Fail)
.with_scan_errors(vec![1])
.result()
.await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn on_error_opening_fail() -> Result<()> {
let result = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Fail)
.with_open_errors(vec![1])
.result()
.await;
assert!(result.is_err());
Ok(())
}
#[tokio::test]
async fn on_error_scanning() -> Result<()> {
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_scan_errors(vec![0])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_scan_errors(vec![1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(2)
.with_on_error(OnError::Skip)
.with_scan_errors(vec![0, 1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"++",
"++",
], &batches);
Ok(())
}
#[tokio::test]
async fn on_error_mixed() -> Result<()> {
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(3)
.with_on_error(OnError::Skip)
.with_open_errors(vec![1])
.with_scan_errors(vec![0])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(3)
.with_on_error(OnError::Skip)
.with_open_errors(vec![0])
.with_scan_errors(vec![1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(3)
.with_on_error(OnError::Skip)
.with_open_errors(vec![2])
.with_scan_errors(vec![0, 1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"++",
"++",
], &batches);
let batches = FileStreamTest::new()
.with_records(vec![make_partition(3), make_partition(2)])
.with_num_files(3)
.with_on_error(OnError::Skip)
.with_open_errors(vec![0, 2])
.with_scan_errors(vec![1])
.result()
.await?;
#[rustfmt::skip]
assert_batches_eq!(&[
"++",
"++",
], &batches);
Ok(())
}
#[tokio::test]
async fn without_limit() -> Result<()> {
let batches = create_and_collect(None).await;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
Ok(())
}
#[tokio::test]
async fn with_limit_between_files() -> Result<()> {
let batches = create_and_collect(Some(5)).await;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"+---+",
], &batches);
Ok(())
}
#[tokio::test]
async fn with_limit_at_middle_of_batch() -> Result<()> {
let batches = create_and_collect(Some(6)).await;
#[rustfmt::skip]
assert_batches_eq!(&[
"+---+",
"| i |",
"+---+",
"| 0 |",
"| 1 |",
"| 2 |",
"| 0 |",
"| 1 |",
"| 0 |",
"+---+",
], &batches);
Ok(())
}
}