datafusion_datasource_csv/
source.rs1use datafusion_datasource::boundary_stream::AlignedBoundaryStream;
21use datafusion_datasource::projection::{ProjectionOpener, SplitProjection};
22use datafusion_physical_plan::projection::ProjectionExprs;
23use std::fmt;
24use std::io::Read;
25use std::sync::Arc;
26
27use datafusion_datasource::decoder::{DecoderDeserializer, deserialize_stream};
28use datafusion_datasource::file_compression_type::FileCompressionType;
29use datafusion_datasource::file_stream::{FileOpenFuture, FileOpener};
30use datafusion_datasource::{
31 FileRange, ListingTableUrl, PartitionedFile, TableSchema, as_file_source,
32};
33
34use arrow::csv;
35use datafusion_common::config::CsvOptions;
36use datafusion_common::tree_node::TreeNodeRecursion;
37use datafusion_common::{DataFusionError, Result, exec_datafusion_err};
38use datafusion_common_runtime::JoinSet;
39use datafusion_datasource::file::FileSource;
40use datafusion_datasource::file_scan_config::FileScanConfig;
41use datafusion_execution::TaskContext;
42use datafusion_physical_plan::metrics::{BaselineMetrics, ExecutionPlanMetricsSet};
43use datafusion_physical_plan::{
44 DisplayFormatType, ExecutionPlan, ExecutionPlanProperties,
45};
46
47use crate::file_format::CsvDecoder;
48use futures::{StreamExt, TryStreamExt};
49use object_store::buffered::BufWriter;
50use object_store::{GetOptions, GetResultPayload, ObjectStore};
51use tokio::io::AsyncWriteExt;
52
53#[derive(Debug, Clone)]
87pub struct CsvSource {
88 options: CsvOptions,
89 batch_size: Option<usize>,
90 table_schema: TableSchema,
91 projection: SplitProjection,
92 metrics: ExecutionPlanMetricsSet,
93}
94
95impl CsvSource {
96 pub fn new(table_schema: impl Into<TableSchema>) -> Self {
98 let table_schema = table_schema.into();
99 Self {
100 options: CsvOptions::default(),
101 projection: SplitProjection::unprojected(&table_schema),
102 table_schema,
103 batch_size: None,
104 metrics: ExecutionPlanMetricsSet::new(),
105 }
106 }
107
108 pub fn with_csv_options(mut self, options: CsvOptions) -> Self {
110 self.options = options;
111 self
112 }
113
114 pub fn has_header(&self) -> bool {
116 self.options.has_header.unwrap_or(true)
117 }
118
119 pub fn truncate_rows(&self) -> bool {
121 self.options.truncated_rows.unwrap_or(false)
122 }
123 pub fn delimiter(&self) -> u8 {
125 self.options.delimiter
126 }
127
128 pub fn quote(&self) -> u8 {
130 self.options.quote
131 }
132
133 pub fn terminator(&self) -> Option<u8> {
135 self.options.terminator
136 }
137
138 pub fn comment(&self) -> Option<u8> {
140 self.options.comment
141 }
142
143 pub fn escape(&self) -> Option<u8> {
145 self.options.escape
146 }
147
148 pub fn with_escape(&self, escape: Option<u8>) -> Self {
150 let mut conf = self.clone();
151 conf.options.escape = escape;
152 conf
153 }
154
155 pub fn with_terminator(&self, terminator: Option<u8>) -> Self {
157 let mut conf = self.clone();
158 conf.options.terminator = terminator;
159 conf
160 }
161
162 pub fn with_comment(&self, comment: Option<u8>) -> Self {
164 let mut conf = self.clone();
165 conf.options.comment = comment;
166 conf
167 }
168
169 pub fn with_truncate_rows(&self, truncate_rows: bool) -> Self {
171 let mut conf = self.clone();
172 conf.options.truncated_rows = Some(truncate_rows);
173 conf
174 }
175
176 pub fn newlines_in_values(&self) -> bool {
178 self.options.newlines_in_values.unwrap_or(false)
179 }
180}
181
182impl CsvSource {
183 fn open<R: Read>(&self, reader: R) -> Result<csv::Reader<R>> {
184 Ok(self.builder().build(reader)?)
185 }
186
187 fn builder(&self) -> csv::ReaderBuilder {
188 let mut builder =
189 csv::ReaderBuilder::new(Arc::clone(self.table_schema.file_schema()))
190 .with_delimiter(self.delimiter())
191 .with_batch_size(
192 self.batch_size
193 .expect("Batch size must be set before initializing builder"),
194 )
195 .with_header(self.has_header())
196 .with_quote(self.quote())
197 .with_truncated_rows(self.truncate_rows());
198 if let Some(terminator) = self.terminator() {
199 builder = builder.with_terminator(terminator);
200 }
201 builder = builder.with_projection(self.projection.file_indices.clone());
202 if let Some(escape) = self.escape() {
203 builder = builder.with_escape(escape)
204 }
205 if let Some(comment) = self.comment() {
206 builder = builder.with_comment(comment);
207 }
208
209 builder
210 }
211}
212
213pub struct CsvOpener {
215 config: Arc<CsvSource>,
216 file_compression_type: FileCompressionType,
217 object_store: Arc<dyn ObjectStore>,
218 partition_index: usize,
219}
220
221impl CsvOpener {
222 pub fn new(
224 config: Arc<CsvSource>,
225 file_compression_type: FileCompressionType,
226 object_store: Arc<dyn ObjectStore>,
227 ) -> Self {
228 Self {
229 config,
230 file_compression_type,
231 object_store,
232 partition_index: 0,
233 }
234 }
235}
236
237impl From<CsvSource> for Arc<dyn FileSource> {
238 fn from(source: CsvSource) -> Self {
239 as_file_source(source)
240 }
241}
242
243impl FileSource for CsvSource {
244 fn create_file_opener(
245 &self,
246 object_store: Arc<dyn ObjectStore>,
247 base_config: &FileScanConfig,
248 partition_index: usize,
249 ) -> Result<Arc<dyn FileOpener>> {
250 let mut opener = Arc::new(CsvOpener {
251 config: Arc::new(self.clone()),
252 file_compression_type: base_config.file_compression_type,
253 object_store,
254 partition_index,
255 }) as Arc<dyn FileOpener>;
256 opener = ProjectionOpener::try_new(
257 self.projection.clone(),
258 Arc::clone(&opener),
259 self.table_schema.file_schema(),
260 )?;
261 Ok(opener)
262 }
263
264 fn table_schema(&self) -> &TableSchema {
265 &self.table_schema
266 }
267
268 fn with_batch_size(&self, batch_size: usize) -> Arc<dyn FileSource> {
269 let mut conf = self.clone();
270 conf.batch_size = Some(batch_size);
271 Arc::new(conf)
272 }
273
274 fn try_pushdown_projection(
275 &self,
276 projection: &ProjectionExprs,
277 ) -> Result<Option<Arc<dyn FileSource>>> {
278 let mut source = self.clone();
279 let new_projection = self.projection.source.try_merge(projection)?;
280 let split_projection =
281 SplitProjection::new(self.table_schema.file_schema(), &new_projection);
282 source.projection = split_projection;
283 Ok(Some(Arc::new(source)))
284 }
285
286 fn projection(&self) -> Option<&ProjectionExprs> {
287 Some(&self.projection.source)
288 }
289
290 fn metrics(&self) -> &ExecutionPlanMetricsSet {
291 &self.metrics
292 }
293
294 fn file_type(&self) -> &str {
295 "csv"
296 }
297
298 fn supports_repartitioning(&self) -> bool {
299 !self.options.newlines_in_values.unwrap_or(false)
302 }
303
304 fn fmt_extra(&self, t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
305 match t {
306 DisplayFormatType::Default | DisplayFormatType::Verbose => {
307 write!(f, ", has_header={}", self.has_header())
308 }
309 DisplayFormatType::TreeRender => Ok(()),
310 }
311 }
312
313 fn apply_expressions(
314 &self,
315 f: &mut dyn FnMut(
316 &Arc<dyn datafusion_physical_plan::PhysicalExpr>,
317 ) -> Result<TreeNodeRecursion>,
318 ) -> Result<TreeNodeRecursion> {
319 datafusion_physical_plan::apply_expression_roots(self.projection.source.iter(), f)
320 }
321
322 #[cfg(feature = "proto")]
324 fn try_to_proto(
325 &self,
326 base: &FileScanConfig,
327 ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
328 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
329 use datafusion_proto_models::protobuf;
330 use protobuf::physical_plan_node::PhysicalPlanType;
331
332 let node = protobuf::CsvScanExecNode {
333 base_conf: Some(base.try_to_proto(ctx)?),
334 has_header: self.has_header(),
335 delimiter: proto_byte_to_string(self.delimiter(), "delimiter")?,
336 quote: proto_byte_to_string(self.quote(), "quote")?,
337 optional_escape: self
338 .escape()
339 .map(|escape| {
340 Ok::<_, DataFusionError>(
341 protobuf::csv_scan_exec_node::OptionalEscape::Escape(
342 proto_byte_to_string(escape, "escape")?,
343 ),
344 )
345 })
346 .transpose()?,
347 optional_comment: self
348 .comment()
349 .map(|comment| {
350 Ok::<_, DataFusionError>(
351 protobuf::csv_scan_exec_node::OptionalComment::Comment(
352 proto_byte_to_string(comment, "comment")?,
353 ),
354 )
355 })
356 .transpose()?,
357 newlines_in_values: self.newlines_in_values(),
358 truncate_rows: self.truncate_rows(),
359 };
360 Ok(Some(protobuf::PhysicalPlanNode {
361 physical_plan_type: Some(PhysicalPlanType::CsvScan(node)),
362 }))
363 }
364}
365
366impl FileOpener for CsvOpener {
367 fn open(&self, partitioned_file: PartitionedFile) -> Result<FileOpenFuture> {
391 let mut csv_has_header = self.config.has_header();
395 if let Some(FileRange { start, .. }) = partitioned_file.range
396 && start != 0
397 {
398 csv_has_header = false;
399 }
400
401 let mut config = (*self.config).clone();
402 config.options.has_header = Some(csv_has_header);
403 config.options.truncated_rows = Some(config.truncate_rows());
404
405 let file_compression_type = self.file_compression_type.to_owned();
406
407 if partitioned_file.range.is_some() {
408 assert!(
409 !file_compression_type.is_compressed(),
410 "Reading compressed .csv in parallel is not supported"
411 );
412 }
413
414 let store = Arc::clone(&self.object_store);
415 let terminator = self.config.terminator();
416
417 let baseline_metrics =
418 BaselineMetrics::new(&self.config.metrics, self.partition_index);
419
420 Ok(Box::pin(async move {
421 let file_size = partitioned_file.object_meta.size;
423 let location = partitioned_file.object_meta.location;
424
425 if let Some(file_range) = partitioned_file.range.as_ref() {
426 let raw_start: u64 = file_range.start.try_into().map_err(|_| {
427 exec_datafusion_err!(
428 "Expected start range to fit in u64, got {}",
429 file_range.start
430 )
431 })?;
432 let raw_end: u64 = file_range.end.try_into().map_err(|_| {
433 exec_datafusion_err!(
434 "Expected end range to fit in u64, got {}",
435 file_range.end
436 )
437 })?;
438
439 let aligned_stream = AlignedBoundaryStream::new(
440 Arc::clone(&store),
441 location.clone(),
442 raw_start,
443 raw_end,
444 file_size,
445 terminator.unwrap_or(b'\n'),
446 )
447 .await?
448 .map_err(DataFusionError::from);
449
450 let decoder = config.builder().build_decoder();
451 let input = file_compression_type
452 .convert_stream(aligned_stream.boxed())?
453 .fuse();
454 let stream = deserialize_stream(
455 input,
456 DecoderDeserializer::new(CsvDecoder::new(decoder)),
457 );
458 return Ok(stream.map_err(Into::into).boxed());
459 }
460
461 let options = GetOptions::default();
463 let result = store.get_opts(&location, options).await?;
464
465 match result.payload {
466 #[cfg(not(target_arch = "wasm32"))]
467 GetResultPayload::File(file, _) => {
468 let decoder = file_compression_type.convert_read(file)?;
469 let mut reader = config.open(decoder)?;
470
471 let iterator = std::iter::from_fn(move || {
473 let mut timer = baseline_metrics.elapsed_compute().timer();
474 let result = reader.next();
475 timer.stop();
476 result
477 });
478
479 Ok(futures::stream::iter(iterator)
480 .map(|r| r.map_err(Into::into))
481 .boxed())
482 }
483 GetResultPayload::Stream(s) => {
484 let decoder = config.builder().build_decoder();
485 let s = s.map_err(DataFusionError::from);
486 let input = file_compression_type.convert_stream(s.boxed())?.fuse();
487
488 let stream = deserialize_stream(
489 input,
490 DecoderDeserializer::new(CsvDecoder::new(decoder)),
491 );
492 Ok(stream.map_err(Into::into).boxed())
493 }
494 }
495 }))
496 }
497}
498
499pub async fn plan_to_csv(
500 task_ctx: Arc<TaskContext>,
501 plan: Arc<dyn ExecutionPlan>,
502 path: impl AsRef<str>,
503) -> Result<()> {
504 let path = path.as_ref();
505 let parsed = ListingTableUrl::parse(path)?;
506 let object_store_url = parsed.object_store();
507 let store = task_ctx.runtime_env().object_store(&object_store_url)?;
508 let writer_buffer_size = task_ctx
509 .session_config()
510 .options()
511 .execution
512 .objectstore_writer_buffer_size;
513 let mut join_set = JoinSet::new();
514 for i in 0..plan.output_partitioning().partition_count() {
515 let storeref = Arc::clone(&store);
516 let plan: Arc<dyn ExecutionPlan> = Arc::clone(&plan);
517 let filename = format!("{}/part-{i}.csv", parsed.prefix());
518 let file = object_store::path::Path::parse(filename)?;
519
520 let mut stream = plan.execute(i, Arc::clone(&task_ctx))?;
521 join_set.spawn(async move {
522 let mut buf_writer =
523 BufWriter::with_capacity(storeref, file.clone(), writer_buffer_size);
524 let mut buffer = Vec::with_capacity(1024);
525 let mut write_headers = true;
527 while let Some(batch) = stream.next().await.transpose()? {
528 let mut writer = csv::WriterBuilder::new()
529 .with_header(write_headers)
530 .build(buffer);
531 writer.write(&batch)?;
532 buffer = writer.into_inner();
533 buf_writer.write_all(&buffer).await?;
534 buffer.clear();
535 write_headers = false;
537 }
538 buf_writer.shutdown().await.map_err(DataFusionError::from)
539 });
540 }
541
542 while let Some(result) = join_set.join_next().await {
543 match result {
544 Ok(res) => res?, Err(e) => {
546 if e.is_panic() {
547 std::panic::resume_unwind(e.into_panic());
548 } else {
549 unreachable!();
550 }
551 }
552 }
553 }
554
555 Ok(())
556}
557
558#[cfg(feature = "proto")]
559fn proto_byte_to_string(b: u8, description: &str) -> Result<String> {
560 let bytes = &[b];
561 let s = std::str::from_utf8(bytes).map_err(|_| {
562 datafusion_common::internal_datafusion_err!(
563 "Invalid CSV {description}: can not represent {bytes:0x?} as utf8"
564 )
565 })?;
566 Ok(s.to_owned())
567}
568
569#[cfg(feature = "proto")]
570fn proto_str_to_byte(s: &str, description: &str) -> Result<u8> {
571 datafusion_common::assert_eq_or_internal_err!(
572 s.len(),
573 1,
574 "Invalid CSV {description}: expected single character, got {s}"
575 );
576 Ok(s.as_bytes()[0])
577}
578
579#[cfg(feature = "proto")]
580impl CsvSource {
581 pub fn try_from_proto(
585 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
586 ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
587 ) -> Result<Arc<dyn ExecutionPlan>> {
588 use datafusion_common::config::CsvOptions;
589 use datafusion_datasource::file_compression_type::FileCompressionType;
590 use datafusion_datasource::file_scan_config::{
591 FileScanConfig, FileScanConfigBuilder,
592 };
593 use datafusion_datasource::source::DataSourceExec;
594 use datafusion_proto_models::protobuf;
595
596 let scan = match &node.physical_plan_type {
597 Some(protobuf::physical_plan_node::PhysicalPlanType::CsvScan(scan)) => scan,
598 _ => {
599 return datafusion_common::internal_err!(
600 "PhysicalPlanNode is not a CsvScan"
601 );
602 }
603 };
604
605 let base_conf = scan.base_conf.as_ref().ok_or_else(|| {
606 datafusion_common::internal_datafusion_err!(
607 "CsvScanExecNode is missing required field 'base_conf'"
608 )
609 })?;
610
611 let escape = match &scan.optional_escape {
612 Some(protobuf::csv_scan_exec_node::OptionalEscape::Escape(escape)) => {
613 Some(proto_str_to_byte(escape, "escape")?)
614 }
615 None => None,
616 };
617 let comment = match &scan.optional_comment {
618 Some(protobuf::csv_scan_exec_node::OptionalComment::Comment(comment)) => {
619 Some(proto_str_to_byte(comment, "comment")?)
620 }
621 None => None,
622 };
623
624 let table_schema = FileScanConfig::parse_table_schema_from_proto(base_conf)?;
625
626 let csv_options = CsvOptions {
627 has_header: Some(scan.has_header),
628 delimiter: proto_str_to_byte(&scan.delimiter, "delimiter")?,
629 quote: proto_str_to_byte(&scan.quote, "quote")?,
630 newlines_in_values: Some(scan.newlines_in_values),
631 truncated_rows: Some(scan.truncate_rows),
632 ..Default::default()
633 };
634 let source = Arc::new(
635 CsvSource::new(table_schema)
636 .with_csv_options(csv_options)
637 .with_escape(escape)
638 .with_comment(comment),
639 );
640
641 let conf = FileScanConfigBuilder::from(FileScanConfig::try_from_proto(
644 base_conf, ctx, source,
645 )?)
646 .with_file_compression_type(FileCompressionType::UNCOMPRESSED)
647 .build();
648 Ok(DataSourceExec::from_data_source(conf))
649 }
650}