This is an automated email from the ASF dual-hosted git repository.
alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git
The following commit(s) were added to refs/heads/main by this push:
new 008c94210c Support the custom terminator for the CSV file format
(#12263)
008c94210c is described below
commit 008c94210c1342d6183ca3d4936a68623759980b
Author: Jax Liu <[email protected]>
AuthorDate: Thu Sep 5 19:20:30 2024 +0800
Support the custom terminator for the CSV file format (#12263)
* add terminator config to CsvConfig
* add test and fix missing builder
* remove the debug message and fix the doc
* support EscapedStringLiteral
* add create external table tests
* refactor the error assertion
* add issue reference
---
datafusion-examples/examples/csv_opener.rs | 1 +
datafusion/common/src/config.rs | 13 ++
datafusion/core/src/datasource/file_format/csv.rs | 8 ++
.../core/src/datasource/file_format/options.rs | 10 ++
.../core/src/datasource/physical_plan/csv.rs | 139 ++++++++++++++++++++-
datafusion/core/tests/data/cr_terminator.csv | 1 +
.../data/newlines_in_values_cr_terminator.csv | 1 +
.../proto-common/proto/datafusion_common.proto | 1 +
datafusion/proto-common/src/from_proto/mod.rs | 1 +
datafusion/proto-common/src/generated/pbjson.rs | 20 +++
datafusion/proto-common/src/generated/prost.rs | 3 +
datafusion/proto-common/src/to_proto/mod.rs | 1 +
.../proto/src/generated/datafusion_proto_common.rs | 3 +
datafusion/proto/src/logical_plan/file_formats.rs | 6 +
datafusion/sql/src/utils.rs | 2 +-
datafusion/sqllogictest/test_files/csv_files.slt | 21 ++++
16 files changed, 228 insertions(+), 3 deletions(-)
diff --git a/datafusion-examples/examples/csv_opener.rs
b/datafusion-examples/examples/csv_opener.rs
index 1f45026a21..e7b7ead109 100644
--- a/datafusion-examples/examples/csv_opener.rs
+++ b/datafusion-examples/examples/csv_opener.rs
@@ -47,6 +47,7 @@ async fn main() -> Result<()> {
true,
b',',
b'"',
+ None,
object_store,
Some(b'#'),
);
diff --git a/datafusion/common/src/config.rs b/datafusion/common/src/config.rs
index 7c247103f6..19978e102c 100644
--- a/datafusion/common/src/config.rs
+++ b/datafusion/common/src/config.rs
@@ -1604,6 +1604,7 @@ config_namespace! {
pub has_header: Option<bool>, default = None
pub delimiter: u8, default = b','
pub quote: u8, default = b'"'
+ pub terminator: Option<u8>, default = None
pub escape: Option<u8>, default = None
pub double_quote: Option<bool>, default = None
/// Specifies whether newlines in (quoted) values are supported.
@@ -1672,6 +1673,13 @@ impl CsvOptions {
self
}
+ /// The character that terminates a row.
+ /// - default to None (CRLF)
+ pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
+ self.terminator = terminator;
+ self
+ }
+
/// The escape character in a row.
/// - default is None
pub fn with_escape(mut self, escape: Option<u8>) -> Self {
@@ -1718,6 +1726,11 @@ impl CsvOptions {
self.quote
}
+ /// The terminator character.
+ pub fn terminator(&self) -> Option<u8> {
+ self.terminator
+ }
+
/// The escape character.
pub fn escape(&self) -> Option<u8> {
self.escape
diff --git a/datafusion/core/src/datasource/file_format/csv.rs
b/datafusion/core/src/datasource/file_format/csv.rs
index e43f6ab29a..99e8f13776 100644
--- a/datafusion/core/src/datasource/file_format/csv.rs
+++ b/datafusion/core/src/datasource/file_format/csv.rs
@@ -247,6 +247,13 @@ impl CsvFormat {
self
}
+ /// The character used to indicate the end of a row.
+ /// - default to None (CRLF)
+ pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
+ self.options.terminator = terminator;
+ self
+ }
+
/// Specifies whether newlines in (quoted) values are supported.
///
/// Parsing newlines in quoted values may be affected by execution
behaviour such as
@@ -359,6 +366,7 @@ impl FileFormat for CsvFormat {
.with_has_header(has_header)
.with_delimeter(self.options.delimiter)
.with_quote(self.options.quote)
+ .with_terminator(self.options.terminator)
.with_escape(self.options.escape)
.with_comment(self.options.comment)
.with_newlines_in_values(newlines_in_values)
diff --git a/datafusion/core/src/datasource/file_format/options.rs
b/datafusion/core/src/datasource/file_format/options.rs
index db90262edb..7ef5a2182d 100644
--- a/datafusion/core/src/datasource/file_format/options.rs
+++ b/datafusion/core/src/datasource/file_format/options.rs
@@ -59,6 +59,8 @@ pub struct CsvReadOptions<'a> {
pub delimiter: u8,
/// An optional quote character. Defaults to `b'"'`.
pub quote: u8,
+ /// An optional terminator character. Defaults to None (CRLF).
+ pub terminator: Option<u8>,
/// An optional escape character. Defaults to None.
pub escape: Option<u8>,
/// If enabled, lines beginning with this byte are ignored.
@@ -102,6 +104,7 @@ impl<'a> CsvReadOptions<'a> {
schema_infer_max_records: DEFAULT_SCHEMA_INFER_MAX_RECORD,
delimiter: b',',
quote: b'"',
+ terminator: None,
escape: None,
newlines_in_values: false,
file_extension: DEFAULT_CSV_EXTENSION,
@@ -136,6 +139,12 @@ impl<'a> CsvReadOptions<'a> {
self
}
+ /// Specify terminator to use for CSV read
+ pub fn terminator(mut self, terminator: Option<u8>) -> Self {
+ self.terminator = terminator;
+ self
+ }
+
/// Specify delimiter to use for CSV read
pub fn escape(mut self, escape: u8) -> Self {
self.escape = Some(escape);
@@ -511,6 +520,7 @@ impl ReadOptions<'_> for CsvReadOptions<'_> {
.with_delimiter(self.delimiter)
.with_quote(self.quote)
.with_escape(self.escape)
+ .with_terminator(self.terminator)
.with_newlines_in_values(self.newlines_in_values)
.with_schema_infer_max_rec(self.schema_infer_max_records)
.with_file_compression_type(self.file_compression_type.to_owned());
diff --git a/datafusion/core/src/datasource/physical_plan/csv.rs
b/datafusion/core/src/datasource/physical_plan/csv.rs
index 5ab32ed36e..6cd1864deb 100644
--- a/datafusion/core/src/datasource/physical_plan/csv.rs
+++ b/datafusion/core/src/datasource/physical_plan/csv.rs
@@ -77,6 +77,7 @@ pub struct CsvExec {
has_header: bool,
delimiter: u8,
quote: u8,
+ terminator: Option<u8>,
escape: Option<u8>,
comment: Option<u8>,
newlines_in_values: bool,
@@ -98,6 +99,7 @@ pub struct CsvExecBuilder {
has_header: bool,
delimiter: u8,
quote: u8,
+ terminator: Option<u8>,
escape: Option<u8>,
comment: Option<u8>,
newlines_in_values: bool,
@@ -112,6 +114,7 @@ impl CsvExecBuilder {
has_header: false,
delimiter: b',',
quote: b'"',
+ terminator: None,
escape: None,
comment: None,
newlines_in_values: false,
@@ -143,6 +146,14 @@ impl CsvExecBuilder {
self
}
+ /// Set the line terminator. If not set, the default is CRLF.
+ ///
+ /// The default is None.
+ pub fn with_terminator(mut self, terminator: Option<u8>) -> Self {
+ self.terminator = terminator;
+ self
+ }
+
/// Set the escape character.
///
/// The default is `None` (i.e. quotes cannot be escaped).
@@ -191,6 +202,7 @@ impl CsvExecBuilder {
has_header,
delimiter,
quote,
+ terminator,
escape,
comment,
newlines_in_values,
@@ -210,6 +222,7 @@ impl CsvExecBuilder {
has_header,
delimiter,
quote,
+ terminator,
escape,
newlines_in_values,
metrics: ExecutionPlanMetricsSet::new(),
@@ -229,6 +242,7 @@ impl CsvExec {
has_header: bool,
delimiter: u8,
quote: u8,
+ terminator: Option<u8>,
escape: Option<u8>,
comment: Option<u8>,
newlines_in_values: bool,
@@ -238,6 +252,7 @@ impl CsvExec {
.with_has_header(has_header)
.with_delimeter(delimiter)
.with_quote(quote)
+ .with_terminator(terminator)
.with_escape(escape)
.with_comment(comment)
.with_newlines_in_values(newlines_in_values)
@@ -270,6 +285,11 @@ impl CsvExec {
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
@@ -406,10 +426,10 @@ impl ExecutionPlan for CsvExec {
delimiter: self.delimiter,
quote: self.quote,
escape: self.escape,
+ terminator: self.terminator,
object_store,
comment: self.comment,
});
-
let opener = CsvOpener {
config,
file_compression_type: self.file_compression_type.to_owned(),
@@ -441,6 +461,7 @@ impl ExecutionPlan for CsvExec {
delimiter: self.delimiter,
quote: self.quote,
escape: self.escape,
+ terminator: self.terminator,
comment: self.comment,
newlines_in_values: self.newlines_in_values,
metrics: self.metrics.clone(),
@@ -459,6 +480,7 @@ pub struct CsvConfig {
has_header: bool,
delimiter: u8,
quote: u8,
+ terminator: Option<u8>,
escape: Option<u8>,
object_store: Arc<dyn ObjectStore>,
comment: Option<u8>,
@@ -474,6 +496,7 @@ impl CsvConfig {
has_header: bool,
delimiter: u8,
quote: u8,
+ terminator: Option<u8>,
object_store: Arc<dyn ObjectStore>,
comment: Option<u8>,
) -> Self {
@@ -484,6 +507,7 @@ impl CsvConfig {
has_header,
delimiter,
quote,
+ terminator,
escape: None,
object_store,
comment,
@@ -502,7 +526,9 @@ impl CsvConfig {
.with_batch_size(self.batch_size)
.with_header(self.has_header)
.with_quote(self.quote);
-
+ if let Some(terminator) = self.terminator {
+ builder = builder.with_terminator(terminator);
+ }
if let Some(proj) = &self.file_projection {
builder = builder.with_projection(proj.clone());
}
@@ -775,6 +801,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -844,6 +871,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -913,6 +941,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -979,6 +1008,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -1044,6 +1074,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -1139,6 +1170,7 @@ mod tests {
.with_has_header(true)
.with_delimeter(b',')
.with_quote(b'"')
+ .with_terminator(None)
.with_escape(None)
.with_comment(None)
.with_newlines_in_values(false)
@@ -1210,6 +1242,107 @@ mod tests {
crate::assert_batches_eq!(expected, &result);
}
+ #[tokio::test]
+ async fn test_terminator() {
+ let session_ctx = SessionContext::new();
+ let store = object_store::memory::InMemory::new();
+
+ let data = bytes::Bytes::from("a,b\r1,2\r3,4");
+ let path = object_store::path::Path::from("a.csv");
+ store.put(&path, data.into()).await.unwrap();
+
+ let url = Url::parse("memory://").unwrap();
+ session_ctx.register_object_store(&url, Arc::new(store));
+
+ let df = session_ctx
+ .read_csv("memory:///",
CsvReadOptions::new().terminator(Some(b'\r')))
+ .await
+ .unwrap();
+
+ let result = df.collect().await.unwrap();
+
+ let expected = [
+ "+---+---+",
+ "| a | b |",
+ "+---+---+",
+ "| 1 | 2 |",
+ "| 3 | 4 |",
+ "+---+---+",
+ ];
+
+ crate::assert_batches_eq!(expected, &result);
+
+ let e = session_ctx
+ .read_csv("memory:///",
CsvReadOptions::new().terminator(Some(b'\n')))
+ .await
+ .unwrap()
+ .collect()
+ .await
+ .unwrap_err();
+ assert_eq!(e.strip_backtrace(), "Arrow error: Csv error: incorrect
number of fields for line 1, expected 2 got more than 2")
+ }
+
+ #[tokio::test]
+ async fn test_create_external_table_with_terminator() -> Result<()> {
+ let ctx = SessionContext::new();
+ ctx.sql(
+ r#"
+ CREATE EXTERNAL TABLE t1 (
+ col1 TEXT,
+ col2 TEXT
+ ) STORED AS CSV
+ LOCATION 'tests/data/cr_terminator.csv'
+ OPTIONS ('format.terminator' E'\r', 'format.has_header' 'true');
+ "#,
+ )
+ .await?
+ .collect()
+ .await?;
+
+ let df = ctx.sql(r#"select * from t1"#).await?.collect().await?;
+ let expected = [
+ "+------+--------+",
+ "| col1 | col2 |",
+ "+------+--------+",
+ "| id0 | value0 |",
+ "| id1 | value1 |",
+ "| id2 | value2 |",
+ "| id3 | value3 |",
+ "+------+--------+",
+ ];
+ crate::assert_batches_eq!(expected, &df);
+ Ok(())
+ }
+
+ #[tokio::test]
+ async fn
test_create_external_table_with_terminator_with_newlines_in_values(
+ ) -> Result<()> {
+ let ctx = SessionContext::new();
+ ctx.sql(r#"
+ CREATE EXTERNAL TABLE t1 (
+ col1 TEXT,
+ col2 TEXT
+ ) STORED AS CSV
+ LOCATION 'tests/data/newlines_in_values_cr_terminator.csv'
+ OPTIONS ('format.terminator' E'\r', 'format.has_header' 'true',
'format.newlines_in_values' 'true');
+ "#).await?.collect().await?;
+
+ let df = ctx.sql(r#"select * from t1"#).await?.collect().await?;
+ let expected = [
+ "+-------+-----------------------------+",
+ "| col1 | col2 |",
+ "+-------+-----------------------------+",
+ "| 1 | hello\rworld |",
+ "| 2 | something\relse |",
+ "| 3 | \rmany\rlines\rmake\rgood test\r |",
+ "| 4 | unquoted |",
+ "| value | end |",
+ "+-------+-----------------------------+",
+ ];
+ crate::assert_batches_eq!(expected, &df);
+ Ok(())
+ }
+
#[tokio::test]
async fn write_csv_results_error_handling() -> Result<()> {
let ctx = SessionContext::new();
@@ -1365,6 +1498,7 @@ mod tests {
has_header,
delimiter,
quote,
+ terminator,
escape,
comment,
newlines_in_values,
@@ -1374,6 +1508,7 @@ mod tests {
assert_eq!(has_header, default_options.has_header.unwrap_or(false));
assert_eq!(delimiter, default_options.delimiter);
assert_eq!(quote, default_options.quote);
+ assert_eq!(terminator, default_options.terminator);
assert_eq!(escape, default_options.escape);
assert_eq!(comment, default_options.comment);
assert_eq!(
diff --git a/datafusion/core/tests/data/cr_terminator.csv
b/datafusion/core/tests/data/cr_terminator.csv
new file mode 100644
index 0000000000..f2a5d09a4c
--- /dev/null
+++ b/datafusion/core/tests/data/cr_terminator.csv
@@ -0,0 +1 @@
+c1,c2 id0,value0 id1,value1 id2,value2 id3,value3
\ No newline at end of file
diff --git a/datafusion/core/tests/data/newlines_in_values_cr_terminator.csv
b/datafusion/core/tests/data/newlines_in_values_cr_terminator.csv
new file mode 100644
index 0000000000..2f6557d60e
--- /dev/null
+++ b/datafusion/core/tests/data/newlines_in_values_cr_terminator.csv
@@ -0,0 +1 @@
+id,message 1,"hello world" 2,"something else" 3," many lines make good test "
4,unquoted value,end
\ No newline at end of file
diff --git a/datafusion/proto-common/proto/datafusion_common.proto
b/datafusion/proto-common/proto/datafusion_common.proto
index 9268ccca0b..51e94d2caa 100644
--- a/datafusion/proto-common/proto/datafusion_common.proto
+++ b/datafusion/proto-common/proto/datafusion_common.proto
@@ -423,6 +423,7 @@ message CsvOptions {
bytes comment = 13; // Optional comment character as a byte
bytes double_quote = 14; // Indicates if quotes are doubled
bytes newlines_in_values = 15; // Indicates if newlines are supported in
values
+ bytes terminator = 16; // Optional terminator character as a byte
}
// Options controlling CSV format
diff --git a/datafusion/proto-common/src/from_proto/mod.rs
b/datafusion/proto-common/src/from_proto/mod.rs
index feb4c11aa8..45d275fb48 100644
--- a/datafusion/proto-common/src/from_proto/mod.rs
+++ b/datafusion/proto-common/src/from_proto/mod.rs
@@ -863,6 +863,7 @@ impl TryFrom<&protobuf::CsvOptions> for CsvOptions {
has_header: proto_opts.has_header.first().map(|h| *h != 0),
delimiter: proto_opts.delimiter[0],
quote: proto_opts.quote[0],
+ terminator: proto_opts.terminator.first().copied(),
escape: proto_opts.escape.first().copied(),
double_quote: proto_opts.has_header.first().map(|h| *h != 0),
newlines_in_values: proto_opts.newlines_in_values.first().map(|h|
*h != 0),
diff --git a/datafusion/proto-common/src/generated/pbjson.rs
b/datafusion/proto-common/src/generated/pbjson.rs
index 05e57f5585..78ba829f8c 100644
--- a/datafusion/proto-common/src/generated/pbjson.rs
+++ b/datafusion/proto-common/src/generated/pbjson.rs
@@ -1542,6 +1542,9 @@ impl serde::Serialize for CsvOptions {
if !self.newlines_in_values.is_empty() {
len += 1;
}
+ if !self.terminator.is_empty() {
+ len += 1;
+ }
let mut struct_ser =
serializer.serialize_struct("datafusion_common.CsvOptions", len)?;
if !self.has_header.is_empty() {
#[allow(clippy::needless_borrow)]
@@ -1598,6 +1601,10 @@ impl serde::Serialize for CsvOptions {
#[allow(clippy::needless_borrow)]
struct_ser.serialize_field("newlinesInValues",
pbjson::private::base64::encode(&self.newlines_in_values).as_str())?;
}
+ if !self.terminator.is_empty() {
+ #[allow(clippy::needless_borrow)]
+ struct_ser.serialize_field("terminator",
pbjson::private::base64::encode(&self.terminator).as_str())?;
+ }
struct_ser.end()
}
}
@@ -1633,6 +1640,7 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
"doubleQuote",
"newlines_in_values",
"newlinesInValues",
+ "terminator",
];
#[allow(clippy::enum_variant_names)]
@@ -1652,6 +1660,7 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
Comment,
DoubleQuote,
NewlinesInValues,
+ Terminator,
}
impl<'de> serde::Deserialize<'de> for GeneratedField {
fn deserialize<D>(deserializer: D) ->
std::result::Result<GeneratedField, D::Error>
@@ -1688,6 +1697,7 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
"comment" => Ok(GeneratedField::Comment),
"doubleQuote" | "double_quote" =>
Ok(GeneratedField::DoubleQuote),
"newlinesInValues" | "newlines_in_values" =>
Ok(GeneratedField::NewlinesInValues),
+ "terminator" => Ok(GeneratedField::Terminator),
_ => Err(serde::de::Error::unknown_field(value,
FIELDS)),
}
}
@@ -1722,6 +1732,7 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
let mut comment__ = None;
let mut double_quote__ = None;
let mut newlines_in_values__ = None;
+ let mut terminator__ = None;
while let Some(k) = map_.next_key()? {
match k {
GeneratedField::HasHeader => {
@@ -1830,6 +1841,14 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0)
;
}
+ GeneratedField::Terminator => {
+ if terminator__.is_some() {
+ return
Err(serde::de::Error::duplicate_field("terminator"));
+ }
+ terminator__ =
+
Some(map_.next_value::<::pbjson::private::BytesDeserialize<_>>()?.0)
+ ;
+ }
}
}
Ok(CsvOptions {
@@ -1848,6 +1867,7 @@ impl<'de> serde::Deserialize<'de> for CsvOptions {
comment: comment__.unwrap_or_default(),
double_quote: double_quote__.unwrap_or_default(),
newlines_in_values:
newlines_in_values__.unwrap_or_default(),
+ terminator: terminator__.unwrap_or_default(),
})
}
}
diff --git a/datafusion/proto-common/src/generated/prost.rs
b/datafusion/proto-common/src/generated/prost.rs
index ebc05718a4..cb8f86a022 100644
--- a/datafusion/proto-common/src/generated/prost.rs
+++ b/datafusion/proto-common/src/generated/prost.rs
@@ -652,6 +652,9 @@ pub struct CsvOptions {
/// Indicates if newlines are supported in values
#[prost(bytes = "vec", tag = "15")]
pub newlines_in_values: ::prost::alloc::vec::Vec<u8>,
+ /// Optional terminator character as a byte
+ #[prost(bytes = "vec", tag = "16")]
+ pub terminator: ::prost::alloc::vec::Vec<u8>,
}
/// Options controlling CSV format
#[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/datafusion/proto-common/src/to_proto/mod.rs
b/datafusion/proto-common/src/to_proto/mod.rs
index 4cf7e73ac9..3718ccbb0f 100644
--- a/datafusion/proto-common/src/to_proto/mod.rs
+++ b/datafusion/proto-common/src/to_proto/mod.rs
@@ -910,6 +910,7 @@ impl TryFrom<&CsvOptions> for protobuf::CsvOptions {
has_header: opts.has_header.map_or_else(Vec::new, |h| vec![h as
u8]),
delimiter: vec![opts.delimiter],
quote: vec![opts.quote],
+ terminator: opts.terminator.map_or_else(Vec::new, |e| vec![e]),
escape: opts.escape.map_or_else(Vec::new, |e| vec![e]),
double_quote: opts.double_quote.map_or_else(Vec::new, |h| vec![h
as u8]),
newlines_in_values: opts
diff --git a/datafusion/proto/src/generated/datafusion_proto_common.rs
b/datafusion/proto/src/generated/datafusion_proto_common.rs
index 3d7b1007b0..dc8d0017d3 100644
--- a/datafusion/proto/src/generated/datafusion_proto_common.rs
+++ b/datafusion/proto/src/generated/datafusion_proto_common.rs
@@ -652,6 +652,9 @@ pub struct CsvOptions {
/// Indicates if newlines are supported in values
#[prost(bytes = "vec", tag = "15")]
pub newlines_in_values: ::prost::alloc::vec::Vec<u8>,
+ /// Optional terminator character as a byte
+ #[prost(bytes = "vec", tag = "16")]
+ pub terminator: ::prost::alloc::vec::Vec<u8>,
}
/// Options controlling CSV format
#[allow(clippy::derive_partial_eq_without_eq)]
diff --git a/datafusion/proto/src/logical_plan/file_formats.rs
b/datafusion/proto/src/logical_plan/file_formats.rs
index 607a3d8642..2e3476da6a 100644
--- a/datafusion/proto/src/logical_plan/file_formats.rs
+++ b/datafusion/proto/src/logical_plan/file_formats.rs
@@ -53,6 +53,7 @@ impl CsvOptionsProto {
has_header: options.has_header.map_or(vec![], |v| vec![v as
u8]),
delimiter: vec![options.delimiter],
quote: vec![options.quote],
+ terminator: options.terminator.map_or(vec![], |v| vec![v]),
escape: options.escape.map_or(vec![], |v| vec![v]),
double_quote: options.double_quote.map_or(vec![], |v| vec![v
as u8]),
compression: options.compression as i32,
@@ -87,6 +88,11 @@ impl From<&CsvOptionsProto> for CsvOptions {
},
delimiter: proto.delimiter.first().copied().unwrap_or(b','),
quote: proto.quote.first().copied().unwrap_or(b'"'),
+ terminator: if !proto.terminator.is_empty() {
+ Some(proto.terminator[0])
+ } else {
+ None
+ },
escape: if !proto.escape.is_empty() {
Some(proto.escape[0])
} else {
diff --git a/datafusion/sql/src/utils.rs b/datafusion/sql/src/utils.rs
index c32acecaae..2531795a16 100644
--- a/datafusion/sql/src/utils.rs
+++ b/datafusion/sql/src/utils.rs
@@ -268,8 +268,8 @@ pub(crate) fn value_to_string(value: &Value) ->
Option<String> {
Value::DollarQuotedString(s) => Some(s.to_string()),
Value::Number(_, _) | Value::Boolean(_) => Some(value.to_string()),
Value::UnicodeStringLiteral(s) => Some(s.to_string()),
+ Value::EscapedStringLiteral(s) => Some(s.to_string()),
Value::DoubleQuotedString(_)
- | Value::EscapedStringLiteral(_)
| Value::NationalStringLiteral(_)
| Value::SingleQuotedByteStringLiteral(_)
| Value::DoubleQuotedByteStringLiteral(_)
diff --git a/datafusion/sqllogictest/test_files/csv_files.slt
b/datafusion/sqllogictest/test_files/csv_files.slt
index 7cb21abdba..d6600e06dc 100644
--- a/datafusion/sqllogictest/test_files/csv_files.slt
+++ b/datafusion/sqllogictest/test_files/csv_files.slt
@@ -336,3 +336,24 @@ id message
05)good test
4 unquoted
value end
+
+statement ok
+CREATE EXTERNAL TABLE stored_table_with_cr_terminator (
+col1 TEXT,
+col2 TEXT
+) STORED AS CSV
+LOCATION '../core/tests/data/cr_terminator.csv'
+OPTIONS ('format.terminator' E'\r', 'format.has_header' 'true');
+
+# TODO: It should be passed but got the error: External error: query failed:
DataFusion error: Object Store error: Generic LocalFileSystem error: Requested
range was invalid
+# See the issue: https://github.com/apache/datafusion/issues/12328
+# query TT
+# select * from stored_table_with_cr_terminator;
+# ----
+# id0 value0
+# id1 value1
+# id2 value2
+# id3 value3
+
+statement ok
+drop table stored_table_with_cr_terminator;
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]