blob: 0b18571e58bd74d4c3204bf670731ad582f2bbfd [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.
//! Execution plan for reading CSV files
use datafusion_datasource::schema_adapter::SchemaAdapterFactory;
use std::any::Any;
use std::fmt;
use std::io::{Read, Seek, SeekFrom};
use std::sync::Arc;
use std::task::Poll;
use datafusion_datasource::decoder::{deserialize_stream, DecoderDeserializer};
use datafusion_datasource::file_compression_type::FileCompressionType;
use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener};
use datafusion_datasource::{
as_file_source, calculate_range, FileRange, ListingTableUrl, PartitionedFile,
RangeCalculation, TableSchema,
};
use arrow::csv;
use arrow::datatypes::SchemaRef;
use datafusion_common::{DataFusionError, Result, Statistics};
use datafusion_common_runtime::JoinSet;
use datafusion_datasource::file::FileSource;
use datafusion_datasource::file_scan_config::FileScanConfig;
use datafusion_execution::TaskContext;
use datafusion_physical_plan::metrics::ExecutionPlanMetricsSet;
use datafusion_physical_plan::{
DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
};
use crate::file_format::CsvDecoder;
use futures::{StreamExt, TryStreamExt};
use object_store::buffered::BufWriter;
use object_store::{GetOptions, GetResultPayload, ObjectStore};
use tokio::io::AsyncWriteExt;
/// A Config for [`CsvOpener`]
///
/// # Example: create a `DataSourceExec` for CSV
/// ```
/// # use std::sync::Arc;
/// # use arrow::datatypes::Schema;
/// # use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
/// # use datafusion_datasource::PartitionedFile;
/// # use datafusion_datasource_csv::source::CsvSource;
/// # use datafusion_execution::object_store::ObjectStoreUrl;
/// # use datafusion_datasource::source::DataSourceExec;
///
/// # let object_store_url = ObjectStoreUrl::local_filesystem();
/// # let file_schema = Arc::new(Schema::empty());
///
/// let source = Arc::new(CsvSource::new(
/// true,
/// b',',
/// b'"',
/// )
/// .with_terminator(Some(b'#')
/// ));
/// // Create a DataSourceExec for reading the first 100MB of `file1.csv`
/// let config = FileScanConfigBuilder::new(object_store_url, file_schema, source)
/// .with_file(PartitionedFile::new("file1.csv", 100*1024*1024))
/// .with_newlines_in_values(true) // The file contains newlines in values;
/// .build();
/// let exec = (DataSourceExec::from_data_source(config));
/// ```
#[derive(Debug, Clone, Default)]
pub struct CsvSource {
batch_size: Option<usize>,
file_schema: Option<SchemaRef>,
file_projection: Option<Vec<usize>>,
pub(crate) has_header: bool,
delimiter: u8,
quote: u8,
terminator: Option<u8>,
escape: Option<u8>,
comment: Option<u8>,
metrics: ExecutionPlanMetricsSet,
projected_statistics: Option<Statistics>,
schema_adapter_factory: Option<Arc<dyn SchemaAdapterFactory>>,
truncate_rows: bool,
}
impl CsvSource {
/// Returns a [`CsvSource`]
pub fn new(has_header: bool, delimiter: u8, quote: u8) -> Self {
Self {
has_header,
delimiter,
quote,
..Self::default()
}
}
/// true if the first line of each file is a header
pub fn has_header(&self) -> bool {
self.has_header
}
// true if rows length support truncate
pub fn truncate_rows(&self) -> bool {
self.truncate_rows
}
/// A column delimiter
pub fn delimiter(&self) -> u8 {
self.delimiter
}
/// The quote character
pub fn quote(&self) -> u8 {
self.quote
}
/// The line terminator
pub fn terminator(&self) -> Option<u8> {
self.terminator
}
/// Lines beginning with this byte are ignored.
pub fn comment(&self) -> Option<u8> {
self.comment
}
/// The escape character
pub fn escape(&self) -> Option<u8> {
self.escape
}
/// Initialize a CsvSource with escape
pub fn with_escape(&self, escape: Option<u8>) -> Self {
let mut conf = self.clone();
conf.escape = escape;
conf
}
/// Initialize a CsvSource with terminator
pub fn with_terminator(&self, terminator: Option<u8>) -> Self {
let mut conf = self.clone();
conf.terminator = terminator;
conf
}
/// Initialize a CsvSource with comment
pub fn with_comment(&self, comment: Option<u8>) -> Self {
let mut conf = self.clone();
conf.comment = comment;
conf
}
/// Whether to support truncate rows when read csv file
pub fn with_truncate_rows(&self, truncate_rows: bool) -> Self {
let mut conf = self.clone();
conf.truncate_rows = truncate_rows;
conf
}
}
impl CsvSource {
fn open<R: Read>(&self, reader: R) -> Result<csv::Reader<R>> {
Ok(self.builder().build(reader)?)
}
fn builder(&self) -> csv::ReaderBuilder {
let mut builder = csv::ReaderBuilder::new(Arc::clone(
self.file_schema
.as_ref()
.expect("Schema must be set before initializing builder"),
))
.with_delimiter(self.delimiter)
.with_batch_size(
self.batch_size
.expect("Batch size must be set before initializing builder"),
)
.with_header(self.has_header)
.with_quote(self.quote)
.with_truncated_rows(self.truncate_rows);
if let Some(terminator) = self.terminator {
builder = builder.with_terminator(terminator);
}
if let Some(proj) = &self.file_projection {
builder = builder.with_projection(proj.clone());
}
if let Some(escape) = self.escape {
builder = builder.with_escape(escape)
}
if let Some(comment) = self.comment {
builder = builder.with_comment(comment);
}
builder
}
}
/// A [`FileOpener`] that opens a CSV file and yields a [`FileOpenFuture`]
pub struct CsvOpener {
config: Arc<CsvSource>,
file_compression_type: FileCompressionType,
object_store: Arc<dyn ObjectStore>,
}
impl CsvOpener {
/// Returns a [`CsvOpener`]
pub fn new(
config: Arc<CsvSource>,
file_compression_type: FileCompressionType,
object_store: Arc<dyn ObjectStore>,
) -> Self {
Self {
config,
file_compression_type,
object_store,
}
}
}
impl From<CsvSource> for Arc<dyn FileSource> {
fn from(source: CsvSource) -> Self {
as_file_source(source)
}
}
impl FileSource for CsvSource {
fn create_file_opener(
&self,
object_store: Arc<dyn ObjectStore>,
base_config: &FileScanConfig,
_partition: usize,
) -> Arc<dyn FileOpener> {
Arc::new(CsvOpener {
config: Arc::new(self.clone()),
file_compression_type: base_config.file_compression_type,
object_store,
})
}
fn as_any(&self) -> &dyn Any {
self
}
fn with_batch_size(&self, batch_size: usize) -> Arc<dyn FileSource> {
let mut conf = self.clone();
conf.batch_size = Some(batch_size);
Arc::new(conf)
}
fn with_schema(&self, schema: TableSchema) -> Arc<dyn FileSource> {
let mut conf = self.clone();
conf.file_schema = Some(Arc::clone(schema.file_schema()));
Arc::new(conf)
}
fn with_statistics(&self, statistics: Statistics) -> Arc<dyn FileSource> {
let mut conf = self.clone();
conf.projected_statistics = Some(statistics);
Arc::new(conf)
}
fn with_projection(&self, config: &FileScanConfig) -> Arc<dyn FileSource> {
let mut conf = self.clone();
conf.file_projection = config.file_column_projection_indices();
Arc::new(conf)
}
fn metrics(&self) -> &ExecutionPlanMetricsSet {
&self.metrics
}
fn statistics(&self) -> Result<Statistics> {
let statistics = &self.projected_statistics;
Ok(statistics
.clone()
.expect("projected_statistics must be set"))
}
fn file_type(&self) -> &str {
"csv"
}
fn fmt_extra(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
match t {
DisplayFormatType::Default | DisplayFormatType::Verbose => {
write!(f, ", has_header={}", self.has_header)
}
DisplayFormatType::TreeRender => Ok(()),
}
}
fn with_schema_adapter_factory(
&self,
schema_adapter_factory: Arc<dyn SchemaAdapterFactory>,
) -> Result<Arc<dyn FileSource>> {
Ok(Arc::new(Self {
schema_adapter_factory: Some(schema_adapter_factory),
..self.clone()
}))
}
fn schema_adapter_factory(&self) -> Option<Arc<dyn SchemaAdapterFactory>> {
self.schema_adapter_factory.clone()
}
}
impl FileOpener for CsvOpener {
/// Open a partitioned CSV file.
///
/// If `file_meta.range` is `None`, the entire file is opened.
/// If `file_meta.range` is `Some(FileRange {start, end})`, this signifies that the partition
/// corresponds to the byte range [start, end) within the file.
///
/// Note: `start` or `end` might be in the middle of some lines. In such cases, the following rules
/// are applied to determine which lines to read:
/// 1. The first line of the partition is the line in which the index of the first character >= `start`.
/// 2. The last line of the partition is the line in which the byte at position `end - 1` resides.
///
/// Examples:
/// Consider the following partitions enclosed by braces `{}`:
///
/// {A,1,2,3,4,5,6,7,8,9\n
/// A,1,2,3,4,5,6,7,8,9\n}
/// A,1,2,3,4,5,6,7,8,9\n
/// The lines read would be: [0, 1]
///
/// A,{1,2,3,4,5,6,7,8,9\n
/// A,1,2,3,4,5,6,7,8,9\n
/// A},1,2,3,4,5,6,7,8,9\n
/// The lines read would be: [1, 2]
fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
// `self.config.has_header` controls whether to skip reading the 1st line header
// If the .csv file is read in parallel and this `CsvOpener` is only reading some middle
// partition, then don't skip first line
let mut csv_has_header = self.config.has_header;
if let Some(FileRange { start, .. }) = partitioned_file.range {
if start != 0 {
csv_has_header = false;
}
}
let config = CsvSource {
has_header: csv_has_header,
truncate_rows: self.config.truncate_rows,
..(*self.config).clone()
};
let file_compression_type = self.file_compression_type.to_owned();
if partitioned_file.range.is_some() {
assert!(
!file_compression_type.is_compressed(),
"Reading compressed .csv in parallel is not supported"
);
}
let store = Arc::clone(&self.object_store);
let terminator = self.config.terminator;
Ok(Box::pin(async move {
// Current partition contains bytes [start_byte, end_byte) (might contain incomplete lines at boundaries)
let calculated_range =
calculate_range(&partitioned_file, &store, terminator).await?;
let range = match calculated_range {
RangeCalculation::Range(None) => None,
RangeCalculation::Range(Some(range)) => Some(range.into()),
RangeCalculation::TerminateEarly => {
return Ok(
futures::stream::poll_fn(move |_| Poll::Ready(None)).boxed()
)
}
};
let options = GetOptions {
range,
..Default::default()
};
let result = store
.get_opts(&partitioned_file.object_meta.location, options)
.await?;
match result.payload {
#[cfg(not(target_arch = "wasm32"))]
GetResultPayload::File(mut file, _) => {
let is_whole_file_scanned = partitioned_file.range.is_none();
let decoder = if is_whole_file_scanned {
// Don't seek if no range as breaks FIFO files
file_compression_type.convert_read(file)?
} else {
file.seek(SeekFrom::Start(result.range.start as _))?;
file_compression_type.convert_read(
file.take((result.range.end - result.range.start) as u64),
)?
};
Ok(futures::stream::iter(config.open(decoder)?)
.map(|r| r.map_err(Into::into))
.boxed())
}
GetResultPayload::Stream(s) => {
let decoder = config.builder().build_decoder();
let s = s.map_err(DataFusionError::from);
let input = file_compression_type.convert_stream(s.boxed())?.fuse();
let stream = deserialize_stream(
input,
DecoderDeserializer::new(CsvDecoder::new(decoder)),
);
Ok(stream.map_err(Into::into).boxed())
}
}
}))
}
}
pub async fn plan_to_csv(
task_ctx: Arc<TaskContext>,
plan: Arc<dyn ExecutionPlan>,
path: impl AsRef<str>,
) -> Result<()> {
let path = path.as_ref();
let parsed = ListingTableUrl::parse(path)?;
let object_store_url = parsed.object_store();
let store = task_ctx.runtime_env().object_store(&object_store_url)?;
let writer_buffer_size = task_ctx
.session_config()
.options()
.execution
.objectstore_writer_buffer_size;
let mut join_set = JoinSet::new();
for i in 0..plan.output_partitioning().partition_count() {
let storeref = Arc::clone(&store);
let plan: Arc<dyn ExecutionPlan> = Arc::clone(&plan);
let filename = format!("{}/part-{i}.csv", parsed.prefix());
let file = object_store::path::Path::parse(filename)?;
let mut stream = plan.execute(i, Arc::clone(&task_ctx))?;
join_set.spawn(async move {
let mut buf_writer =
BufWriter::with_capacity(storeref, file.clone(), writer_buffer_size);
let mut buffer = Vec::with_capacity(1024);
//only write headers on first iteration
let mut write_headers = true;
while let Some(batch) = stream.next().await.transpose()? {
let mut writer = csv::WriterBuilder::new()
.with_header(write_headers)
.build(buffer);
writer.write(&batch)?;
buffer = writer.into_inner();
buf_writer.write_all(&buffer).await?;
buffer.clear();
//prevent writing headers more than once
write_headers = false;
}
buf_writer.shutdown().await.map_err(DataFusionError::from)
});
}
while let Some(result) = join_set.join_next().await {
match result {
Ok(res) => res?, // propagate DataFusion error
Err(e) => {
if e.is_panic() {
std::panic::resume_unwind(e.into_panic());
} else {
unreachable!();
}
}
}
}
Ok(())
}