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 .map_err(DataFusionError::from)
162 .left_stream(),
163 Err(e) => {
164 futures::stream::once(futures::future::ready(Err(e))).right_stream()
165 }
166 };
167 stream.boxed()
168 }
169
170 pub fn read_to_delimited_chunks_from_stream<'a>(
173 &self,
174 stream: BoxStream<'a, Result<Bytes>>,
175 ) -> BoxStream<'a, Result<Bytes>> {
176 let file_compression_type: FileCompressionType = self.options.compression.into();
177 let decoder = file_compression_type.convert_stream(stream);
178 let stream = match decoder {
179 Ok(decoded_stream) => {
180 newline_delimited_stream(decoded_stream.map_err(|e| match e {
181 DataFusionError::ObjectStore(e) => *e,
182 err => object_store::Error::Generic {
183 store: "read to delimited chunks failed",
184 source: Box::new(err),
185 },
186 }))
187 .map_err(DataFusionError::from)
188 .left_stream()
189 }
190 Err(e) => {
191 futures::stream::once(futures::future::ready(Err(e))).right_stream()
192 }
193 };
194 stream.boxed()
195 }
196
197 pub fn with_options(mut self, options: CsvOptions) -> Self {
199 self.options = options;
200 self
201 }
202
203 pub fn options(&self) -> &CsvOptions {
205 &self.options
206 }
207
208 pub fn with_schema_infer_max_rec(mut self, max_rec: usize) -> Self {
216 self.options.schema_infer_max_rec = Some(max_rec);
217 self
218 }
219
220 pub fn with_has_header(mut self, has_header: bool) -> Self {
223 self.options.has_header = Some(has_header);
224 self
225 }
226
227 pub fn with_truncated_rows(mut self, truncated_rows: bool) -> Self {
228 self.options.truncated_rows = Some(truncated_rows);
229 self
230 }
231
232 pub fn with_null_regex(mut self, null_regex: Option<String>) -> Self {
235 self.options.null_regex = null_regex;
236 self
237 }
238
239 pub fn has_header(&self) -> Option<bool> {
242 self.options.has_header
243 }
244
245 pub fn with_comment(mut self, comment: Option<u8>) -> Self {
247 self.options.comment = comment;
248 self
249 }
250
251 pub fn with_delimiter(mut self, delimiter: u8) -> Self {
254 self.options.delimiter = delimiter;
255 self
256 }
257
258 pub fn with_quote(mut self, quote: u8) -> Self {
261 self.options.quote = quote;
262 self
263 }
264
265 pub fn with_escape(mut self, escape: Option<u8>) -> Self {
268 self.options.escape = escape;
269 self
270 }
271
272 pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
275 self.options.terminator = terminator;
276 self
277 }
278
279 pub fn with_newlines_in_values(mut self, newlines_in_values: bool) -> Self {
287 self.options.newlines_in_values = Some(newlines_in_values);
288 self
289 }
290
291 pub fn with_file_compression_type(
294 mut self,
295 file_compression_type: FileCompressionType,
296 ) -> Self {
297 self.options.compression = file_compression_type.into();
298 self
299 }
300
301 pub fn with_truncate_rows(mut self, truncate_rows: bool) -> Self {
304 self.options.truncated_rows = Some(truncate_rows);
305 self
306 }
307
308 pub fn delimiter(&self) -> u8 {
310 self.options.delimiter
311 }
312
313 pub fn quote(&self) -> u8 {
315 self.options.quote
316 }
317
318 pub fn escape(&self) -> Option<u8> {
320 self.options.escape
321 }
322}
323
324#[derive(Debug)]
325pub struct CsvDecoder {
326 inner: arrow::csv::reader::Decoder,
327}
328
329impl CsvDecoder {
330 pub fn new(decoder: arrow::csv::reader::Decoder) -> Self {
331 Self { inner: decoder }
332 }
333}
334
335impl Decoder for CsvDecoder {
336 fn decode(&mut self, buf: &[u8]) -> Result<usize, ArrowError> {
337 self.inner.decode(buf)
338 }
339
340 fn flush(&mut self) -> Result<Option<RecordBatch>, ArrowError> {
341 self.inner.flush()
342 }
343
344 fn can_flush_early(&self) -> bool {
345 self.inner.capacity() == 0
346 }
347}
348
349impl Debug for CsvSerializer {
350 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
351 f.debug_struct("CsvSerializer")
352 .field("header", &self.header)
353 .finish()
354 }
355}
356
357#[async_trait]
358impl FileFormat for CsvFormat {
359 fn get_ext(&self) -> String {
360 CsvFormatFactory::new().get_ext()
361 }
362
363 fn get_ext_with_compression(
364 &self,
365 file_compression_type: &FileCompressionType,
366 ) -> Result<String> {
367 let ext = self.get_ext();
368 Ok(format!("{}{}", ext, file_compression_type.get_ext()))
369 }
370
371 fn compression_type(&self) -> Option<FileCompressionType> {
372 Some(self.options.compression.into())
373 }
374
375 async fn infer_schema(
376 &self,
377 state: &dyn Session,
378 store: &Arc<dyn ObjectStore>,
379 objects: &[ObjectMeta],
380 ) -> Result<SchemaRef> {
381 let mut schemas = vec![];
382
383 let mut records_to_read = self
384 .options
385 .schema_infer_max_rec
386 .unwrap_or(DEFAULT_SCHEMA_INFER_MAX_RECORD);
387
388 for object in objects {
389 let stream = self.read_to_delimited_chunks(store, object).await;
390 let (schema, records_read) = self
391 .infer_schema_from_stream(state, records_to_read, stream)
392 .await
393 .map_err(|err| {
394 DataFusionError::Context(
395 format!("Error when processing CSV file {}", object.location),
396 Box::new(err),
397 )
398 })?;
399 records_to_read -= records_read;
400 schemas.push(schema);
401 if records_to_read == 0 {
402 break;
403 }
404 }
405
406 let merged_schema = Schema::try_merge(schemas)?;
407 Ok(Arc::new(merged_schema))
408 }
409
410 async fn infer_stats(
411 &self,
412 _state: &dyn Session,
413 _store: &Arc<dyn ObjectStore>,
414 table_schema: SchemaRef,
415 _object: &ObjectMeta,
416 ) -> Result<Statistics> {
417 Ok(Statistics::new_unknown(&table_schema))
418 }
419
420 async fn create_physical_plan(
421 &self,
422 state: &dyn Session,
423 conf: FileScanConfig,
424 ) -> Result<Arc<dyn ExecutionPlan>> {
425 let has_header = self
427 .options
428 .has_header
429 .unwrap_or_else(|| state.config_options().catalog.has_header);
430 let newlines_in_values = self
431 .options
432 .newlines_in_values
433 .unwrap_or_else(|| state.config_options().catalog.newlines_in_values);
434
435 let mut csv_options = self.options.clone();
436 csv_options.has_header = Some(has_header);
437 csv_options.newlines_in_values = Some(newlines_in_values);
438
439 let csv_source = conf
442 .file_source
443 .downcast_ref::<CsvSource>()
444 .expect("file_source should be a CsvSource");
445 let source = Arc::new(csv_source.clone().with_csv_options(csv_options));
446
447 let config = FileScanConfigBuilder::from(conf)
448 .with_file_compression_type(self.options.compression.into())
449 .with_source(source)
450 .build();
451
452 Ok(DataSourceExec::from_data_source(config))
453 }
454
455 async fn create_writer_physical_plan(
456 &self,
457 input: Arc<dyn ExecutionPlan>,
458 state: &dyn Session,
459 conf: FileSinkConfig,
460 order_requirements: Option<LexRequirement>,
461 ) -> Result<Arc<dyn ExecutionPlan>> {
462 if conf.insert_op != InsertOp::Append {
463 return not_impl_err!("Overwrites are not implemented yet for CSV");
464 }
465
466 let has_header = self
471 .options()
472 .has_header
473 .unwrap_or_else(|| state.config_options().catalog.has_header);
474 let newlines_in_values = self
475 .options()
476 .newlines_in_values
477 .unwrap_or_else(|| state.config_options().catalog.newlines_in_values);
478
479 let options = self
480 .options()
481 .clone()
482 .with_has_header(has_header)
483 .with_newlines_in_values(newlines_in_values);
484
485 let writer_options = CsvWriterOptions::try_from(&options)?;
486
487 let sink = Arc::new(CsvSink::new(conf, writer_options));
488
489 Ok(Arc::new(DataSinkExec::new(input, sink, order_requirements)) as _)
490 }
491
492 fn file_source(&self, table_schema: TableSchema) -> Arc<dyn FileSource> {
493 let mut csv_options = self.options.clone();
494 if csv_options.has_header.is_none() {
495 csv_options.has_header = Some(true);
496 }
497 Arc::new(CsvSource::new(table_schema).with_csv_options(csv_options))
498 }
499}
500
501impl CsvFormat {
502 pub async fn infer_schema_from_stream(
519 &self,
520 state: &dyn Session,
521 mut records_to_read: usize,
522 stream: impl Stream<Item = Result<Bytes>>,
523 ) -> Result<(Schema, usize)> {
524 let mut total_records_read = 0;
525 let mut column_names = vec![];
526 let mut column_type_possibilities = vec![];
527 let mut record_number = -1;
528 let initial_records_to_read = records_to_read;
529
530 pin_mut!(stream);
531
532 while let Some(chunk) = stream.next().await.transpose()? {
533 record_number += 1;
534 let first_chunk = record_number == 0;
535 let mut format = arrow::csv::reader::Format::default()
536 .with_header(
537 first_chunk
538 && self
539 .options
540 .has_header
541 .unwrap_or_else(|| state.config_options().catalog.has_header),
542 )
543 .with_delimiter(self.options.delimiter)
544 .with_quote(self.options.quote)
545 .with_truncated_rows(self.options.truncated_rows.unwrap_or(false));
546
547 if let Some(null_regex) = &self.options.null_regex {
548 let regex = Regex::new(null_regex.as_str())
549 .expect("Unable to parse CSV null regex.");
550 format = format.with_null_regex(regex);
551 }
552
553 if let Some(escape) = self.options.escape {
554 format = format.with_escape(escape);
555 }
556
557 if let Some(comment) = self.options.comment {
558 format = format.with_comment(comment);
559 }
560
561 let (Schema { fields, .. }, records_read) =
562 format.infer_schema(chunk.reader(), Some(records_to_read))?;
563
564 records_to_read -= records_read;
565 total_records_read += records_read;
566
567 if first_chunk {
568 (column_names, column_type_possibilities) = fields
570 .into_iter()
571 .map(|field| {
572 let mut possibilities = HashSet::new();
573 if records_read > 0 {
574 possibilities.insert(field.data_type().clone());
576 }
577 (field.name().clone(), possibilities)
578 })
579 .unzip();
580 } else {
581 if fields.len() != column_type_possibilities.len()
582 && !self.options.truncated_rows.unwrap_or(false)
583 {
584 return exec_err!(
585 "Encountered unequal lengths between records on CSV file whilst inferring schema. \
586 Expected {} fields, found {} fields at record {}",
587 column_type_possibilities.len(),
588 fields.len(),
589 record_number + 1
590 );
591 }
592
593 column_type_possibilities.iter_mut().zip(&fields).for_each(
595 |(possibilities, field)| {
596 possibilities.insert(field.data_type().clone());
597 },
598 );
599
600 if fields.len() > column_type_possibilities.len() {
602 for field in fields.iter().skip(column_type_possibilities.len()) {
604 column_names.push(field.name().clone());
605 let mut possibilities = HashSet::new();
606 if records_read > 0 {
607 possibilities.insert(field.data_type().clone());
608 }
609 column_type_possibilities.push(possibilities);
610 }
611 }
612 }
613
614 if records_to_read == 0 {
615 break;
616 }
617 }
618
619 let schema = build_schema_helper(
620 column_names,
621 column_type_possibilities,
622 initial_records_to_read == 0,
623 );
624 Ok((schema, total_records_read))
625 }
626}
627
628fn build_schema_helper(
640 names: Vec<String>,
641 types: Vec<HashSet<DataType>>,
642 disable_inference: bool,
643) -> Schema {
644 let fields = names
645 .into_iter()
646 .zip(types)
647 .map(|(field_name, mut data_type_possibilities)| {
648 data_type_possibilities.remove(&DataType::Null);
654
655 match data_type_possibilities.len() {
656 0 => {
661 if disable_inference {
662 Field::new(field_name, DataType::Utf8, true)
663 } else {
664 Field::new(field_name, DataType::Null, true)
665 }
666 }
667 1 => Field::new(
668 field_name,
669 data_type_possibilities.iter().next().unwrap().clone(),
670 true,
671 ),
672 2 => {
673 if data_type_possibilities.contains(&DataType::Int64)
674 && data_type_possibilities.contains(&DataType::Float64)
675 {
676 Field::new(field_name, DataType::Float64, true)
678 } else {
679 Field::new(field_name, DataType::Utf8, true)
681 }
682 }
683 _ => Field::new(field_name, DataType::Utf8, true),
684 }
685 })
686 .collect::<Fields>();
687 Schema::new(fields)
688}
689
690impl Default for CsvSerializer {
691 fn default() -> Self {
692 Self::new()
693 }
694}
695
696pub struct CsvSerializer {
698 builder: WriterBuilder,
700 header: bool,
702}
703
704impl CsvSerializer {
705 pub fn new() -> Self {
707 Self {
708 builder: WriterBuilder::new(),
709 header: true,
710 }
711 }
712
713 pub fn with_builder(mut self, builder: WriterBuilder) -> Self {
715 self.builder = builder;
716 self
717 }
718
719 pub fn with_header(mut self, header: bool) -> Self {
721 self.header = header;
722 self
723 }
724}
725
726impl BatchSerializer for CsvSerializer {
727 fn serialize(&self, batch: RecordBatch, initial: bool) -> Result<Bytes> {
728 let mut buffer = Vec::with_capacity(4096);
729 let builder = self.builder.clone();
730 let header = self.header && initial;
731 let mut writer = builder.with_header(header).build(&mut buffer);
732 writer.write(&batch)?;
733 drop(writer);
734 Ok(Bytes::from(buffer))
735 }
736}
737
738pub struct CsvSink {
740 config: FileSinkConfig,
742 writer_options: CsvWriterOptions,
743}
744
745impl Debug for CsvSink {
746 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
747 f.debug_struct("CsvSink").finish()
748 }
749}
750
751impl DisplayAs for CsvSink {
752 fn fmt_as(&self, t: DisplayFormatType, f: &mut fmt::Formatter<'_>) -> fmt::Result {
753 match t {
754 DisplayFormatType::Default | DisplayFormatType::Verbose => {
755 write!(f, "CsvSink(file_groups=",)?;
756 FileGroupDisplay(&self.config.file_group).fmt_as(t, f)?;
757 write!(f, ")")
758 }
759 DisplayFormatType::TreeRender => {
760 writeln!(f, "format: csv")?;
761 write!(f, "file={}", self.config.original_url)
762 }
763 }
764 }
765}
766
767impl CsvSink {
768 pub fn new(config: FileSinkConfig, writer_options: CsvWriterOptions) -> Self {
770 Self {
771 config,
772 writer_options,
773 }
774 }
775
776 pub fn writer_options(&self) -> &CsvWriterOptions {
778 &self.writer_options
779 }
780}
781
782#[async_trait]
783impl FileSink for CsvSink {
784 fn config(&self) -> &FileSinkConfig {
785 &self.config
786 }
787
788 async fn spawn_writer_tasks_and_join(
789 &self,
790 context: &Arc<TaskContext>,
791 demux_task: SpawnedTask<Result<()>>,
792 file_stream_rx: DemuxedStreamReceiver,
793 object_store: Arc<dyn ObjectStore>,
794 ) -> Result<u64> {
795 let builder = self.writer_options.writer_options.clone();
796 let header = builder.header();
797 let serializer = Arc::new(
798 CsvSerializer::new()
799 .with_builder(builder)
800 .with_header(header),
801 ) as _;
802 spawn_writer_tasks_and_join(
803 context,
804 serializer,
805 self.writer_options.compression.into(),
806 self.writer_options.compression_level,
807 object_store,
808 demux_task,
809 file_stream_rx,
810 )
811 .await
812 }
813}
814
815#[async_trait]
816impl DataSink for CsvSink {
817 fn schema(&self) -> &SchemaRef {
818 self.config.output_schema()
819 }
820
821 async fn write_all(
822 &self,
823 data: SendableRecordBatchStream,
824 context: &Arc<TaskContext>,
825 ) -> Result<u64> {
826 FileSink::write_all(self, data, context).await
827 }
828
829 #[cfg(feature = "proto")]
830 fn try_to_proto(
831 &self,
832 exec: &DataSinkExec,
833 ctx: &datafusion_physical_plan::proto::ExecutionPlanEncodeCtx<'_>,
834 ) -> Result<Option<datafusion_proto_models::protobuf::PhysicalPlanNode>> {
835 use datafusion_proto_models::protobuf;
836 use protobuf::physical_plan_node::PhysicalPlanType;
837
838 let input = ctx.encode_child(exec.input())?;
839 let sort_order = exec.encode_sort_order(ctx)?;
840 let sink = protobuf::CsvSink::try_from(self)?;
841 let node = protobuf::CsvSinkExecNode {
842 input: Some(Box::new(input)),
843 sink: Some(sink),
844 sink_schema: Some(exec.schema().as_ref().try_into()?),
845 sort_order,
846 };
847 Ok(Some(protobuf::PhysicalPlanNode {
848 physical_plan_type: Some(PhysicalPlanType::CsvSink(Box::new(node))),
849 }))
850 }
851}
852
853#[cfg(feature = "proto")]
854impl TryFrom<&CsvSink> for datafusion_proto_models::protobuf::CsvSink {
855 type Error = DataFusionError;
856
857 fn try_from(value: &CsvSink) -> Result<Self> {
858 Ok(Self {
859 config: Some(value.config().try_into()?),
860 writer_options: Some(value.writer_options().try_into()?),
861 })
862 }
863}
864
865#[cfg(feature = "proto")]
866impl TryFrom<&datafusion_proto_models::protobuf::CsvSink> for CsvSink {
867 type Error = DataFusionError;
868
869 fn try_from(value: &datafusion_proto_models::protobuf::CsvSink) -> Result<Self> {
870 let config =
871 FileSinkConfig::try_from(value.config.as_ref().ok_or_else(|| {
872 datafusion_common::internal_datafusion_err!(
873 "CsvSink is missing required field 'config'"
874 )
875 })?)?;
876 let writer_options = value
877 .writer_options
878 .as_ref()
879 .ok_or_else(|| {
880 datafusion_common::internal_datafusion_err!(
881 "CsvSink is missing required field 'writer_options'"
882 )
883 })?
884 .try_into()?;
885
886 Ok(Self::new(config, writer_options))
887 }
888}
889
890#[cfg(feature = "proto")]
891impl CsvSink {
892 pub fn try_from_proto(
894 node: &datafusion_proto_models::protobuf::PhysicalPlanNode,
895 ctx: &datafusion_physical_plan::proto::ExecutionPlanDecodeCtx<'_>,
896 ) -> Result<Arc<dyn ExecutionPlan>> {
897 use datafusion_proto_models::protobuf;
898
899 let sink_node = datafusion_physical_plan::expect_plan_variant!(
900 node,
901 protobuf::physical_plan_node::PhysicalPlanType::CsvSink,
902 "CsvSink",
903 );
904 let input = ctx.decode_required_child(
905 sink_node.input.as_deref(),
906 "CsvSinkExecNode",
907 "input",
908 )?;
909 let proto_sink = sink_node.sink.as_ref().ok_or_else(|| {
910 datafusion_common::internal_datafusion_err!(
911 "CsvSinkExecNode is missing required field 'sink'"
912 )
913 })?;
914 let data_sink = CsvSink::try_from(proto_sink)?;
915 let sort_order = DataSinkExec::decode_sort_order(
916 sink_node.sort_order.as_ref(),
917 ctx,
918 input.schema().as_ref(),
919 )?;
920
921 Ok(Arc::new(DataSinkExec::new(
922 input,
923 Arc::new(data_sink),
924 sort_order,
925 )))
926 }
927}
928
929#[cfg(feature = "proto")]
935impl From<&CsvFormatFactory> for datafusion_proto_models::protobuf::CsvOptions {
936 fn from(factory: &CsvFormatFactory) -> Self {
937 if let Some(options) = &factory.options {
938 datafusion_proto_models::protobuf::CsvOptions {
939 has_header: options.has_header.map_or(vec![], |v| vec![v as u8]),
940 delimiter: vec![options.delimiter],
941 quote: vec![options.quote],
942 terminator: options.terminator.map_or(vec![], |v| vec![v]),
943 escape: options.escape.map_or(vec![], |v| vec![v]),
944 double_quote: options.double_quote.map_or(vec![], |v| vec![v as u8]),
945 compression: options.compression as i32,
946 schema_infer_max_rec: options.schema_infer_max_rec.map(|v| v as u64),
947 date_format: options.date_format.clone().unwrap_or_default(),
948 datetime_format: options.datetime_format.clone().unwrap_or_default(),
949 timestamp_format: options.timestamp_format.clone().unwrap_or_default(),
950 timestamp_tz_format: options
951 .timestamp_tz_format
952 .clone()
953 .unwrap_or_default(),
954 time_format: options.time_format.clone().unwrap_or_default(),
955 null_value: options.null_value.clone().unwrap_or_default(),
956 null_regex: options.null_regex.clone().unwrap_or_default(),
957 comment: options.comment.map_or(vec![], |v| vec![v]),
958 newlines_in_values: options
959 .newlines_in_values
960 .map_or(vec![], |v| vec![v as u8]),
961 truncated_rows: options.truncated_rows.map_or(vec![], |v| vec![v as u8]),
962 compression_level: options.compression_level,
963 quote_style: options.quote_style as i32,
964 ignore_leading_whitespace: options
965 .ignore_leading_whitespace
966 .map_or(vec![], |v| vec![v as u8]),
967 ignore_trailing_whitespace: options
968 .ignore_trailing_whitespace
969 .map_or(vec![], |v| vec![v as u8]),
970 }
971 } else {
972 datafusion_proto_models::protobuf::CsvOptions::default()
973 }
974 }
975}
976
977#[cfg(test)]
978mod tests {
979 use super::build_schema_helper;
980 use arrow::datatypes::DataType;
981 use std::collections::HashSet;
982
983 #[test]
984 fn test_build_schema_helper_different_column_counts() {
985 let mut column_names =
987 vec!["col1".to_string(), "col2".to_string(), "col3".to_string()];
988
989 column_names.push("col4".to_string());
991 column_names.push("col5".to_string());
992
993 let column_type_possibilities = vec![
994 HashSet::from([DataType::Int64]),
995 HashSet::from([DataType::Utf8]),
996 HashSet::from([DataType::Float64]),
997 HashSet::from([DataType::Utf8]), HashSet::from([DataType::Utf8]), ];
1000
1001 let schema = build_schema_helper(column_names, column_type_possibilities, false);
1002
1003 assert_eq!(schema.fields().len(), 5);
1005 assert_eq!(schema.field(0).name(), "col1");
1006 assert_eq!(schema.field(1).name(), "col2");
1007 assert_eq!(schema.field(2).name(), "col3");
1008 assert_eq!(schema.field(3).name(), "col4");
1009 assert_eq!(schema.field(4).name(), "col5");
1010
1011 for field in schema.fields() {
1013 assert!(
1014 field.is_nullable(),
1015 "Field {} should be nullable",
1016 field.name()
1017 );
1018 }
1019 }
1020
1021 #[test]
1022 fn test_build_schema_helper_type_merging() {
1023 let column_names = vec!["col1".to_string(), "col2".to_string()];
1025
1026 let column_type_possibilities = vec![
1027 HashSet::from([DataType::Int64, DataType::Float64]), HashSet::from([DataType::Utf8]), ];
1030
1031 let schema = build_schema_helper(column_names, column_type_possibilities, false);
1032
1033 assert_eq!(*schema.field(0).data_type(), DataType::Float64);
1035
1036 assert_eq!(*schema.field(1).data_type(), DataType::Utf8);
1038 }
1039
1040 #[test]
1041 fn test_build_schema_helper_conflicting_types() {
1042 let column_names = vec!["col1".to_string()];
1044
1045 let column_type_possibilities = vec![
1046 HashSet::from([DataType::Boolean, DataType::Int64, DataType::Utf8]), ];
1048
1049 let schema = build_schema_helper(column_names, column_type_possibilities, false);
1050
1051 assert_eq!(*schema.field(0).data_type(), DataType::Utf8);
1053 }
1054}