1use std::collections::{HashMap, HashSet};
21use std::fmt::{self, Debug};
22use std::sync::Arc;
23
24use crate::source::CsvSource;
25
26use arrow::array::RecordBatch;
27use arrow::csv::WriterBuilder;
28use arrow::datatypes::{DataType, Field, Fields, Schema, SchemaRef};
29use arrow::error::ArrowError;
30use datafusion_common::config::{ConfigField, ConfigFileType, CsvOptions};
31use datafusion_common::file_options::csv_writer::CsvWriterOptions;
32use datafusion_common::{
33 DEFAULT_CSV_EXTENSION, DataFusionError, GetExt, Result, Statistics, exec_err,
34 not_impl_err,
35};
36use datafusion_common_runtime::SpawnedTask;
37use datafusion_datasource::TableSchema;
38use datafusion_datasource::decoder::Decoder;
39use datafusion_datasource::display::FileGroupDisplay;
40use datafusion_datasource::file::FileSource;
41use datafusion_datasource::file_compression_type::FileCompressionType;
42use datafusion_datasource::file_format::{
43 DEFAULT_SCHEMA_INFER_MAX_RECORD, FileFormat, FileFormatFactory,
44};
45use datafusion_datasource::file_scan_config::{FileScanConfig, FileScanConfigBuilder};
46use datafusion_datasource::file_sink_config::{FileSink, FileSinkConfig};
47use datafusion_datasource::sink::{DataSink, DataSinkExec};
48use datafusion_datasource::write::BatchSerializer;
49use datafusion_datasource::write::demux::DemuxedStreamReceiver;
50use datafusion_datasource::write::orchestration::spawn_writer_tasks_and_join;
51use datafusion_execution::{SendableRecordBatchStream, TaskContext};
52use datafusion_expr::dml::InsertOp;
53use datafusion_physical_expr_common::sort_expr::LexRequirement;
54use datafusion_physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan};
55use datafusion_session::Session;
56
57use async_trait::async_trait;
58use bytes::{Buf, Bytes};
59use datafusion_datasource::source::DataSourceExec;
60use futures::stream::BoxStream;
61use futures::{Stream, StreamExt, TryStreamExt, pin_mut};
62use object_store::{
63 ObjectMeta, ObjectStore, ObjectStoreExt, delimited::newline_delimited_stream,
64};
65use regex::Regex;
66
67#[derive(Default)]
68pub struct CsvFormatFactory {
70 pub options: Option<CsvOptions>,
72}
73
74impl CsvFormatFactory {
75 pub fn new() -> Self {
77 Self { options: None }
78 }
79
80 pub fn new_with_options(options: CsvOptions) -> Self {
82 Self {
83 options: Some(options),
84 }
85 }
86}
87
88impl Debug for CsvFormatFactory {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90 f.debug_struct("CsvFormatFactory")
91 .field("options", &self.options)
92 .finish()
93 }
94}
95
96impl FileFormatFactory for CsvFormatFactory {
97 fn create(
98 &self,
99 state: &dyn Session,
100 format_options: &HashMap<String, String>,
101 ) -> Result<Arc<dyn FileFormat>> {
102 let csv_options = match &self.options {
103 None => {
104 let mut table_options = state.default_table_options();
105 table_options.set_config_format(ConfigFileType::CSV);
106 table_options.alter_with_string_hash_map(format_options)?;
107 table_options.csv
108 }
109 Some(csv_options) => {
110 let mut csv_options = csv_options.clone();
111 for (k, v) in format_options {
112 csv_options.set(k, v)?;
113 }
114 csv_options
115 }
116 };
117
118 Ok(Arc::new(CsvFormat::default().with_options(csv_options)))
119 }
120
121 fn default(&self) -> Arc<dyn FileFormat> {
122 Arc::new(CsvFormat::default())
123 }
124}
125
126impl GetExt for CsvFormatFactory {
127 fn get_ext(&self) -> String {
128 DEFAULT_CSV_EXTENSION[1..].to_string()
130 }
131}
132
133#[derive(Debug, Default)]
135pub struct CsvFormat {
136 options: CsvOptions,
137}
138
139impl CsvFormat {
140 async fn read_to_delimited_chunks<'a>(
144 &self,
145 store: &Arc<dyn ObjectStore>,
146 object: &ObjectMeta,
147 ) -> BoxStream<'a, Result<Bytes>> {
148 let stream = store
150 .get(&object.location)
151 .await
152 .map_err(|e| DataFusionError::ObjectStore(Box::new(e)));
153 let stream = match stream {
154 Ok(stream) => self
155 .read_to_delimited_chunks_from_stream(
156 stream
157 .into_stream()
158 .map_err(|e| DataFusionError::ObjectStore(Box::new(e)))
159 .boxed(),
160 )
161 .await
162 .map_err(DataFusionError::from)
163 .left_stream(),
164 Err(e) => {
165 futures::stream::once(futures::future::ready(Err(e))).right_stream()
166 }
167 };
168 stream.boxed()
169 }
170
171 pub async fn read_to_delimited_chunks_from_stream<'a>(
174 &self,
175 stream: BoxStream<'a, Result<Bytes>>,
176 ) -> BoxStream<'a, Result<Bytes>> {
177 let file_compression_type: FileCompressionType = self.options.compression.into();
178 let decoder = file_compression_type.convert_stream(stream);
179 let stream = match decoder {
180 Ok(decoded_stream) => {
181 newline_delimited_stream(decoded_stream.map_err(|e| match e {
182 DataFusionError::ObjectStore(e) => *e,
183 err => object_store::Error::Generic {
184 store: "read to delimited chunks failed",
185 source: Box::new(err),
186 },
187 }))
188 .map_err(DataFusionError::from)
189 .left_stream()
190 }
191 Err(e) => {
192 futures::stream::once(futures::future::ready(Err(e))).right_stream()
193 }
194 };
195 stream.boxed()
196 }
197
198 pub fn with_options(mut self, options: CsvOptions) -> Self {
200 self.options = options;
201 self
202 }
203
204 pub fn options(&self) -> &CsvOptions {
206 &self.options
207 }
208
209 pub fn with_schema_infer_max_rec(mut self, max_rec: usize) -> Self {
217 self.options.schema_infer_max_rec = Some(max_rec);
218 self
219 }
220
221 pub fn with_has_header(mut self, has_header: bool) -> Self {
224 self.options.has_header = Some(has_header);
225 self
226 }
227
228 pub fn with_truncated_rows(mut self, truncated_rows: bool) -> Self {
229 self.options.truncated_rows = Some(truncated_rows);
230 self
231 }
232
233 pub fn with_null_regex(mut self, null_regex: Option<String>) -> Self {
236 self.options.null_regex = null_regex;
237 self
238 }
239
240 pub fn has_header(&self) -> Option<bool> {
243 self.options.has_header
244 }
245
246 pub fn with_comment(mut self, comment: Option<u8>) -> Self {
248 self.options.comment = comment;
249 self
250 }
251
252 pub fn with_delimiter(mut self, delimiter: u8) -> Self {
255 self.options.delimiter = delimiter;
256 self
257 }
258
259 pub fn with_quote(mut self, quote: u8) -> Self {
262 self.options.quote = quote;
263 self
264 }
265
266 pub fn with_escape(mut self, escape: Option<u8>) -> Self {
269 self.options.escape = escape;
270 self
271 }
272
273 pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
276 self.options.terminator = terminator;
277 self
278 }
279
280 pub fn with_newlines_in_values(mut self, newlines_in_values: bool) -> Self {
288 self.options.newlines_in_values = Some(newlines_in_values);
289 self
290 }
291
292 pub fn with_file_compression_type(
295 mut self,
296 file_compression_type: FileCompressionType,
297 ) -> Self {
298 self.options.compression = file_compression_type.into();
299 self
300 }
301
302 pub fn with_truncate_rows(mut self, truncate_rows: bool) -> Self {
305 self.options.truncated_rows = Some(truncate_rows);
306 self
307 }
308
309 pub fn delimiter(&self) -> u8 {
311 self.options.delimiter
312 }
313
314 pub fn quote(&self) -> u8 {
316 self.options.quote
317 }
318
319 pub fn escape(&self) -> Option<u8> {
321 self.options.escape
322 }
323}
324
325#[derive(Debug)]
326pub struct CsvDecoder {
327 inner: arrow::csv::reader::Decoder,
328}
329
330impl CsvDecoder {
331 pub fn new(decoder: arrow::csv::reader::Decoder) -> Self {
332 Self { inner: decoder }
333 }
334}
335
336impl Decoder for CsvDecoder {
337 fn decode(&mut self, buf: &[u8]) -> Result<usize, ArrowError> {
338 self.inner.decode(buf)
339 }
340
341 fn flush(&mut self) -> Result<Option<RecordBatch>, ArrowError> {
342 self.inner.flush()
343 }
344
345 fn can_flush_early(&self) -> bool {
346 self.inner.capacity() == 0
347 }
348}
349
350impl Debug for CsvSerializer {
351 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
352 f.debug_struct("CsvSerializer")
353 .field("header", &self.header)
354 .finish()
355 }
356}
357
358#[async_trait]
359impl FileFormat for CsvFormat {
360 fn get_ext(&self) -> String {
361 CsvFormatFactory::new().get_ext()
362 }
363
364 fn get_ext_with_compression(
365 &self,
366 file_compression_type: &FileCompressionType,
367 ) -> Result<String> {
368 let ext = self.get_ext();
369 Ok(format!("{}{}", ext, file_compression_type.get_ext()))
370 }
371
372 fn compression_type(&self) -> Option<FileCompressionType> {
373 Some(self.options.compression.into())
374 }
375
376 async fn infer_schema(
377 &self,
378 state: &dyn Session,
379 store: &Arc<dyn ObjectStore>,
380 objects: &[ObjectMeta],
381 ) -> Result<SchemaRef> {
382 let mut schemas = vec![];
383
384 let mut records_to_read = self
385 .options
386 .schema_infer_max_rec
387 .unwrap_or(DEFAULT_SCHEMA_INFER_MAX_RECORD);
388
389 for object in objects {
390 let stream = self.read_to_delimited_chunks(store, object).await;
391 let (schema, records_read) = self
392 .infer_schema_from_stream(state, records_to_read, stream)
393 .await
394 .map_err(|err| {
395 DataFusionError::Context(
396 format!("Error when processing CSV file {}", &object.location),
397 Box::new(err),
398 )
399 })?;
400 records_to_read -= records_read;
401 schemas.push(schema);
402 if records_to_read == 0 {
403 break;
404 }
405 }
406
407 let merged_schema = Schema::try_merge(schemas)?;
408 Ok(Arc::new(merged_schema))
409 }
410
411 async fn infer_stats(
412 &self,
413 _state: &dyn Session,
414 _store: &Arc<dyn ObjectStore>,
415 table_schema: SchemaRef,
416 _object: &ObjectMeta,
417 ) -> Result<Statistics> {
418 Ok(Statistics::new_unknown(&table_schema))
419 }
420
421 async fn create_physical_plan(
422 &self,
423 state: &dyn Session,
424 conf: FileScanConfig,
425 ) -> Result<Arc<dyn ExecutionPlan>> {
426 let has_header = self
428 .options
429 .has_header
430 .unwrap_or_else(|| state.config_options().catalog.has_header);
431 let newlines_in_values = self
432 .options
433 .newlines_in_values
434 .unwrap_or_else(|| state.config_options().catalog.newlines_in_values);
435
436 let mut csv_options = self.options.clone();
437 csv_options.has_header = Some(has_header);
438 csv_options.newlines_in_values = Some(newlines_in_values);
439
440 let csv_source = conf
443 .file_source
444 .downcast_ref::<CsvSource>()
445 .expect("file_source should be a CsvSource");
446 let source = Arc::new(csv_source.clone().with_csv_options(csv_options));
447
448 let config = FileScanConfigBuilder::from(conf)
449 .with_file_compression_type(self.options.compression.into())
450 .with_source(source)
451 .build();
452
453 Ok(DataSourceExec::from_data_source(config))
454 }
455
456 async fn create_writer_physical_plan(
457 &self,
458 input: Arc<dyn ExecutionPlan>,
459 state: &dyn Session,
460 conf: FileSinkConfig,
461 order_requirements: Option<LexRequirement>,
462 ) -> Result<Arc<dyn ExecutionPlan>> {
463 if conf.insert_op != InsertOp::Append {
464 return not_impl_err!("Overwrites are not implemented yet for CSV");
465 }
466
467 let has_header = self
472 .options()
473 .has_header
474 .unwrap_or_else(|| state.config_options().catalog.has_header);
475 let newlines_in_values = self
476 .options()
477 .newlines_in_values
478 .unwrap_or_else(|| state.config_options().catalog.newlines_in_values);
479
480 let options = self
481 .options()
482 .clone()
483 .with_has_header(has_header)
484 .with_newlines_in_values(newlines_in_values);
485
486 let writer_options = CsvWriterOptions::try_from(&options)?;
487
488 let sink = Arc::new(CsvSink::new(conf, writer_options));
489
490 Ok(Arc::new(DataSinkExec::new(input, sink, order_requirements)) as _)
491 }
492
493 fn file_source(&self, table_schema: TableSchema) -> Arc<dyn FileSource> {
494 let mut csv_options = self.options.clone();
495 if csv_options.has_header.is_none() {
496 csv_options.has_header = Some(true);
497 }
498 Arc::new(CsvSource::new(table_schema).with_csv_options(csv_options))
499 }
500}
501
502impl CsvFormat {
503 pub async fn infer_schema_from_stream(
520 &self,
521 state: &dyn Session,
522 mut records_to_read: usize,
523 stream: impl Stream<Item = Result<Bytes>>,
524 ) -> Result<(Schema, usize)> {
525 let mut total_records_read = 0;
526 let mut column_names = vec![];
527 let mut column_type_possibilities = vec![];
528 let mut record_number = -1;
529 let initial_records_to_read = records_to_read;
530
531 pin_mut!(stream);
532
533 while let Some(chunk) = stream.next().await.transpose()? {
534 record_number += 1;
535 let first_chunk = record_number == 0;
536 let mut format = arrow::csv::reader::Format::default()
537 .with_header(
538 first_chunk
539 && self
540 .options
541 .has_header
542 .unwrap_or_else(|| state.config_options().catalog.has_header),
543 )
544 .with_delimiter(self.options.delimiter)
545 .with_quote(self.options.quote)
546 .with_truncated_rows(self.options.truncated_rows.unwrap_or(false));
547
548 if let Some(null_regex) = &self.options.null_regex {
549 let regex = Regex::new(null_regex.as_str())
550 .expect("Unable to parse CSV null regex.");
551 format = format.with_null_regex(regex);
552 }
553
554 if let Some(escape) = self.options.escape {
555 format = format.with_escape(escape);
556 }
557
558 if let Some(comment) = self.options.comment {
559 format = format.with_comment(comment);
560 }
561
562 let (Schema { fields, .. }, records_read) =
563 format.infer_schema(chunk.reader(), Some(records_to_read))?;
564
565 records_to_read -= records_read;
566 total_records_read += records_read;
567
568 if first_chunk {
569 (column_names, column_type_possibilities) = fields
571 .into_iter()
572 .map(|field| {
573 let mut possibilities = HashSet::new();
574 if records_read > 0 {
575 possibilities.insert(field.data_type().clone());
577 }
578 (field.name().clone(), possibilities)
579 })
580 .unzip();
581 } else {
582 if fields.len() != column_type_possibilities.len()
583 && !self.options.truncated_rows.unwrap_or(false)
584 {
585 return exec_err!(
586 "Encountered unequal lengths between records on CSV file whilst inferring schema. \
587 Expected {} fields, found {} fields at record {}",
588 column_type_possibilities.len(),
589 fields.len(),
590 record_number + 1
591 );
592 }
593
594 column_type_possibilities.iter_mut().zip(&fields).for_each(
596 |(possibilities, field)| {
597 possibilities.insert(field.data_type().clone());
598 },
599 );
600
601 if fields.len() > column_type_possibilities.len() {
603 for field in fields.iter().skip(column_type_possibilities.len()) {
605 column_names.push(field.name().clone());
606 let mut possibilities = HashSet::new();
607 if records_read > 0 {
608 possibilities.insert(field.data_type().clone());
609 }
610 column_type_possibilities.push(possibilities);
611 }
612 }
613 }
614
615 if records_to_read == 0 {
616 break;
617 }
618 }
619
620 let schema = build_schema_helper(
621 column_names,
622 column_type_possibilities,
623 initial_records_to_read == 0,
624 );
625 Ok((schema, total_records_read))
626 }
627}
628
629fn build_schema_helper(
641 names: Vec<String>,
642 types: Vec<HashSet<DataType>>,
643 disable_inference: bool,
644) -> Schema {
645 let fields = names
646 .into_iter()
647 .zip(types)
648 .map(|(field_name, mut data_type_possibilities)| {
649 data_type_possibilities.remove(&DataType::Null);
655
656 match data_type_possibilities.len() {
657 0 => {
662 if disable_inference {
663 Field::new(field_name, DataType::Utf8, true)
664 } else {
665 Field::new(field_name, DataType::Null, true)
666 }
667 }
668 1 => Field::new(
669 field_name,
670 data_type_possibilities.iter().next().unwrap().clone(),
671 true,
672 ),
673 2 => {
674 if data_type_possibilities.contains(&DataType::Int64)
675 && data_type_possibilities.contains(&DataType::Float64)
676 {
677 Field::new(field_name, DataType::Float64, true)
679 } else {
680 Field::new(field_name, DataType::Utf8, true)
682 }
683 }
684 _ => Field::new(field_name, DataType::Utf8, true),
685 }
686 })
687 .collect::<Fields>();
688 Schema::new(fields)
689}
690
691impl Default for CsvSerializer {
692 fn default() -> Self {
693 Self::new()
694 }
695}
696
697pub struct CsvSerializer {
699 builder: WriterBuilder,
701 header: bool,
703}
704
705impl CsvSerializer {
706 pub fn new() -> Self {
708 Self {
709 builder: WriterBuilder::new(),
710 header: true,
711 }
712 }
713
714 pub fn with_builder(mut self, builder: WriterBuilder) -> Self {
716 self.builder = builder;
717 self
718 }
719
720 pub fn with_header(mut self, header: bool) -> Self {
722 self.header = header;
723 self
724 }
725}
726
727impl BatchSerializer for CsvSerializer {
728 fn serialize(&self, batch: RecordBatch, initial: bool) -> Result<Bytes> {
729 let mut buffer = Vec::with_capacity(4096);
730 let builder = self.builder.clone();
731 let header = self.header && initial;
732 let mut writer = builder.with_header(header).build(&mut buffer);
733 writer.write(&batch)?;
734 drop(writer);
735 Ok(Bytes::from(buffer))
736 }
737}
738
739pub struct CsvSink {
741 config: FileSinkConfig,
743 writer_options: CsvWriterOptions,
744}
745
746impl Debug for CsvSink {
747 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
748 f.debug_struct("CsvSink").finish()
749 }
750}
751
752impl DisplayAs for CsvSink {
753 fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
754 match t {
755 DisplayFormatType::Default | DisplayFormatType::Verbose => {
756 write!(f, "CsvSink(file_groups=",)?;
757 FileGroupDisplay(&self.config.file_group).fmt_as(t, f)?;
758 write!(f, ")")
759 }
760 DisplayFormatType::TreeRender => {
761 writeln!(f, "format: csv")?;
762 write!(f, "file={}", &self.config.original_url)
763 }
764 }
765 }
766}
767
768impl CsvSink {
769 pub fn new(config: FileSinkConfig, writer_options: CsvWriterOptions) -> Self {
771 Self {
772 config,
773 writer_options,
774 }
775 }
776
777 pub fn writer_options(&self) -> &CsvWriterOptions {
779 &self.writer_options
780 }
781}
782
783#[async_trait]
784impl FileSink for CsvSink {
785 fn config(&self) -> &FileSinkConfig {
786 &self.config
787 }
788
789 async fn spawn_writer_tasks_and_join(
790 &self,
791 context: &Arc<TaskContext>,
792 demux_task: SpawnedTask<Result<()>>,
793 file_stream_rx: DemuxedStreamReceiver,
794 object_store: Arc<dyn ObjectStore>,
795 ) -> Result<u64> {
796 let builder = self.writer_options.writer_options.clone();
797 let header = builder.header();
798 let serializer = Arc::new(
799 CsvSerializer::new()
800 .with_builder(builder)
801 .with_header(header),
802 ) as _;
803 spawn_writer_tasks_and_join(
804 context,
805 serializer,
806 self.writer_options.compression.into(),
807 self.writer_options.compression_level,
808 object_store,
809 demux_task,
810 file_stream_rx,
811 )
812 .await
813 }
814}
815
816#[async_trait]
817impl DataSink for CsvSink {
818 fn schema(&self) -> &SchemaRef {
819 self.config.output_schema()
820 }
821
822 async fn write_all(
823 &self,
824 data: SendableRecordBatchStream,
825 context: &Arc<TaskContext>,
826 ) -> Result<u64> {
827 FileSink::write_all(self, data, context).await
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::build_schema_helper;
834 use arrow::datatypes::DataType;
835 use std::collections::HashSet;
836
837 #[test]
838 fn test_build_schema_helper_different_column_counts() {
839 let mut column_names =
841 vec!["col1".to_string(), "col2".to_string(), "col3".to_string()];
842
843 column_names.push("col4".to_string());
845 column_names.push("col5".to_string());
846
847 let column_type_possibilities = vec![
848 HashSet::from([DataType::Int64]),
849 HashSet::from([DataType::Utf8]),
850 HashSet::from([DataType::Float64]),
851 HashSet::from([DataType::Utf8]), HashSet::from([DataType::Utf8]), ];
854
855 let schema = build_schema_helper(column_names, column_type_possibilities, false);
856
857 assert_eq!(schema.fields().len(), 5);
859 assert_eq!(schema.field(0).name(), "col1");
860 assert_eq!(schema.field(1).name(), "col2");
861 assert_eq!(schema.field(2).name(), "col3");
862 assert_eq!(schema.field(3).name(), "col4");
863 assert_eq!(schema.field(4).name(), "col5");
864
865 for field in schema.fields() {
867 assert!(
868 field.is_nullable(),
869 "Field {} should be nullable",
870 field.name()
871 );
872 }
873 }
874
875 #[test]
876 fn test_build_schema_helper_type_merging() {
877 let column_names = vec!["col1".to_string(), "col2".to_string()];
879
880 let column_type_possibilities = vec![
881 HashSet::from([DataType::Int64, DataType::Float64]), HashSet::from([DataType::Utf8]), ];
884
885 let schema = build_schema_helper(column_names, column_type_possibilities, false);
886
887 assert_eq!(*schema.field(0).data_type(), DataType::Float64);
889
890 assert_eq!(*schema.field(1).data_type(), DataType::Utf8);
892 }
893
894 #[test]
895 fn test_build_schema_helper_conflicting_types() {
896 let column_names = vec!["col1".to_string()];
898
899 let column_type_possibilities = vec![
900 HashSet::from([DataType::Boolean, DataType::Int64, DataType::Utf8]), ];
902
903 let schema = build_schema_helper(column_names, column_type_possibilities, false);
904
905 assert_eq!(*schema.field(0).data_type(), DataType::Utf8);
907 }
908}