This is an automated email from the ASF dual-hosted git repository.
agrove pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new ee09cb6 ARROW-8839: [Rust] [DataFusion] support CSV schema inference
in logical plan
ee09cb6 is described below
commit ee09cb6edce31899841afa0c01fb4ef1dd971ef9
Author: Qingping Hou <[email protected]>
AuthorDate: Tue May 19 18:47:52 2020 -0600
ARROW-8839: [Rust] [DataFusion] support CSV schema inference in logical plan
This PR changes schema argument for scan_csv method into `Option<&Schema>`.
Other related changes are needed to make this happen including:
* added delimiter argument to all csv related structs and functions
* fixed a bug in schema field inference function
* made `arrow::csv::reader::infer_file_schema` public so it can be used by
data fusion
Known limitations:
* when provided with a directory of csv files, schema inference code only
reads rows from the first file.
* to avoid adding yet another argument to all csv related functions, i hard
coded number of rows to read for schema inference to 1000
Open questions:
* Should we rename `datasource::csv::CsvFile` struct to `CsvTable` to keep
it consistent with ParquetTable and MemoryTable? The implementation of CsvFile
also supports reading from a directory of files, so `CsvFile` is not an
accurate name.
* csv related function arguments are getting a bit long, should we
introduce a csv option struct to capture the following configs with sensible
defaults?
- schema
- has_header
- delimiter
- infer_max_read_records
Closes #7210 from houqp/csv_schema_infer
Authored-by: Qingping Hou <[email protected]>
Signed-off-by: Andy Grove <[email protected]>
---
rust/arrow/examples/read_csv.rs | 2 +-
rust/arrow/examples/read_csv_infer_schema.rs | 2 +-
rust/arrow/src/csv/mod.rs | 1 +
rust/arrow/src/csv/reader.rs | 88 ++++++++++++++--------
rust/datafusion/benches/aggregate_query_sql.rs | 1 +
rust/datafusion/examples/csv_sql.rs | 1 +
rust/datafusion/src/datasource/csv.rs | 44 ++++++++++-
rust/datafusion/src/execution/context.rs | 54 ++++++++++---
rust/datafusion/src/execution/physical_plan/csv.rs | 35 ++++++++-
.../src/execution/physical_plan/hash_aggregate.rs | 2 +-
.../src/execution/physical_plan/limit.rs | 2 +-
.../src/execution/physical_plan/merge.rs | 2 +-
.../src/execution/physical_plan/projection.rs | 2 +-
.../src/execution/physical_plan/selection.rs | 2 +-
rust/datafusion/src/execution/table_impl.rs | 1 +
rust/datafusion/src/logicalplan.rs | 20 ++++-
.../src/optimizer/projection_push_down.rs | 2 +
rust/datafusion/tests/sql.rs | 4 +-
18 files changed, 209 insertions(+), 56 deletions(-)
diff --git a/rust/arrow/examples/read_csv.rs b/rust/arrow/examples/read_csv.rs
index cde59d7..fb14b4e 100644
--- a/rust/arrow/examples/read_csv.rs
+++ b/rust/arrow/examples/read_csv.rs
@@ -34,7 +34,7 @@ fn main() -> Result<()> {
let file = File::open("test/data/uk_cities.csv").unwrap();
- let mut csv = csv::Reader::new(file, Arc::new(schema), false, 1024, None);
+ let mut csv = csv::Reader::new(file, Arc::new(schema), false, None, 1024,
None);
let batch = csv.next().unwrap().unwrap();
print_batches(&vec![batch])
}
diff --git a/rust/arrow/examples/read_csv_infer_schema.rs
b/rust/arrow/examples/read_csv_infer_schema.rs
index 07c28c7..eff9dc7 100644
--- a/rust/arrow/examples/read_csv_infer_schema.rs
+++ b/rust/arrow/examples/read_csv_infer_schema.rs
@@ -25,7 +25,7 @@ use std::fs::File;
fn main() -> Result<()> {
let file = File::open("test/data/uk_cities_with_headers.csv").unwrap();
let builder = csv::ReaderBuilder::new()
- .has_headers(true)
+ .has_header(true)
.infer_schema(Some(100));
let mut csv = builder.build(file).unwrap();
let batch = csv.next().unwrap().unwrap();
diff --git a/rust/arrow/src/csv/mod.rs b/rust/arrow/src/csv/mod.rs
index 55de4b2..b17b787 100644
--- a/rust/arrow/src/csv/mod.rs
+++ b/rust/arrow/src/csv/mod.rs
@@ -20,6 +20,7 @@
pub mod reader;
pub mod writer;
+pub use self::reader::infer_file_schema;
pub use self::reader::Reader;
pub use self::reader::ReaderBuilder;
pub use self::writer::Writer;
diff --git a/rust/arrow/src/csv/reader.rs b/rust/arrow/src/csv/reader.rs
index 6b99710..0d15570 100644
--- a/rust/arrow/src/csv/reader.rs
+++ b/rust/arrow/src/csv/reader.rs
@@ -36,7 +36,7 @@
//!
//! let file = File::open("test/data/uk_cities.csv").unwrap();
//!
-//! let mut csv = csv::Reader::new(file, Arc::new(schema), false, 1024, None);
+//! let mut csv = csv::Reader::new(file, Arc::new(schema), false, None, 1024,
None);
//! let batch = csv.next().unwrap().unwrap();
//! ```
@@ -57,7 +57,7 @@ use self::csv_crate::{StringRecord, StringRecordsIntoIter};
lazy_static! {
static ref DECIMAL_RE: Regex = Regex::new(r"^-?(\d+\.\d+)$").unwrap();
- static ref INTEGER_RE: Regex = Regex::new(r"^-?(\d*.)$").unwrap();
+ static ref INTEGER_RE: Regex = Regex::new(r"^-?(\d+)$").unwrap();
static ref BOOLEAN_RE: Regex = RegexBuilder::new(r"^(true)$|^(false)$")
.case_insensitive(true)
.build()
@@ -87,19 +87,19 @@ fn infer_field_schema(string: &str) -> DataType {
/// with `max_read_records` controlling the maximum number of records to read.
///
/// If `max_read_records` is not set, the whole file is read to infer its
schema.
-fn infer_file_schema<R: Read + Seek>(
+pub fn infer_file_schema<R: Read + Seek>(
reader: &mut BufReader<R>,
delimiter: u8,
max_read_records: Option<usize>,
- has_headers: bool,
+ has_header: bool,
) -> Result<Schema> {
let mut csv_reader = csv::ReaderBuilder::new()
.delimiter(delimiter)
.from_reader(reader);
// get or create header names
- // when has_headers is false, creates default column names with column_
prefix
- let headers: Vec<String> = if has_headers {
+ // when has_header is false, creates default column names with column_
prefix
+ let headers: Vec<String> = if has_header {
let headers = &csv_reader.headers()?.clone();
headers.iter().map(|s| s.to_string()).collect()
} else {
@@ -198,14 +198,16 @@ impl<R: Read> Reader<R> {
pub fn new(
reader: R,
schema: Arc<Schema>,
- has_headers: bool,
+ has_header: bool,
+ delimiter: Option<u8>,
batch_size: usize,
projection: Option<Vec<usize>>,
) -> Self {
Self::from_buf_reader(
BufReader::new(reader),
schema,
- has_headers,
+ has_header,
+ delimiter,
batch_size,
projection,
)
@@ -233,20 +235,29 @@ impl<R: Read> Reader<R> {
pub fn from_buf_reader(
buf_reader: BufReader<R>,
schema: Arc<Schema>,
- has_headers: bool,
+ has_header: bool,
+ delimiter: Option<u8>,
batch_size: usize,
projection: Option<Vec<usize>>,
) -> Self {
- let csv_reader = csv::ReaderBuilder::new()
- .has_headers(has_headers)
- .from_reader(buf_reader);
+ let mut reader_builder = csv_crate::ReaderBuilder::new();
+ reader_builder.has_headers(has_header);
+
+ match delimiter {
+ Some(c) => {
+ reader_builder.delimiter(c);
+ }
+ _ => (),
+ }
+
+ let csv_reader = reader_builder.from_reader(buf_reader);
let record_iter = csv_reader.into_records();
Self {
schema,
projection,
record_iter,
batch_size,
- line_number: if has_headers { 1 } else { 0 },
+ line_number: if has_header { 1 } else { 0 },
}
}
@@ -369,8 +380,9 @@ impl<R: Read> Reader<R> {
Err(_) => {
// TODO: we should surface the underlying error
here.
return Err(ArrowError::ParseError(format!(
- "Error while parsing value {} at line {}",
+ "Error while parsing value {} for column {} at
line {}",
s,
+ col_idx,
self.line_number + row_index
)));
}
@@ -394,7 +406,7 @@ pub struct ReaderBuilder {
///
/// If schema inference is run on a file with no headers, default column
names
/// are created.
- has_headers: bool,
+ has_header: bool,
/// An optional column delimiter. Defaults to `b','`
delimiter: Option<u8>,
/// Optional maximum number of records to read during schema inference
@@ -413,7 +425,7 @@ impl Default for ReaderBuilder {
fn default() -> ReaderBuilder {
ReaderBuilder {
schema: None,
- has_headers: false,
+ has_header: false,
delimiter: None,
max_records: None,
batch_size: 1024,
@@ -457,8 +469,8 @@ impl ReaderBuilder {
}
/// Set whether the CSV file has headers
- pub fn has_headers(mut self, has_headers: bool) -> Self {
- self.has_headers = has_headers;
+ pub fn has_header(mut self, has_header: bool) -> Self {
+ self.has_header = has_header;
self
}
@@ -492,22 +504,23 @@ impl ReaderBuilder {
pub fn build<R: Read + Seek>(self, reader: R) -> Result<Reader<R>> {
// check if schema should be inferred
let mut buf_reader = BufReader::new(reader);
+ let delimiter = self.delimiter.unwrap_or(b',');
let schema = match self.schema {
Some(schema) => schema,
None => {
let inferred_schema = infer_file_schema(
&mut buf_reader,
- self.delimiter.unwrap_or(b','),
+ delimiter,
self.max_records,
- self.has_headers,
+ self.has_header,
)?;
Arc::new(inferred_schema)
}
};
- let csv_reader = csv::ReaderBuilder::new()
- .delimiter(self.delimiter.unwrap_or(b','))
- .has_headers(self.has_headers)
+ let csv_reader = csv_crate::ReaderBuilder::new()
+ .delimiter(delimiter)
+ .has_headers(self.has_header)
.from_reader(buf_reader);
let record_iter = csv_reader.into_records();
Ok(Reader {
@@ -515,7 +528,7 @@ impl ReaderBuilder {
projection: self.projection.clone(),
record_iter,
batch_size: self.batch_size,
- line_number: if self.has_headers { 1 } else { 0 },
+ line_number: if self.has_header { 1 } else { 0 },
})
}
}
@@ -540,7 +553,8 @@ mod tests {
let file = File::open("test/data/uk_cities.csv").unwrap();
- let mut csv = Reader::new(file, Arc::new(schema.clone()), false, 1024,
None);
+ let mut csv =
+ Reader::new(file, Arc::new(schema.clone()), false, None, 1024,
None);
assert_eq!(Arc::new(schema), csv.schema());
let batch = csv.next().unwrap().unwrap();
assert_eq!(37, batch.num_rows());
@@ -582,6 +596,7 @@ mod tests {
BufReader::new(both_files),
Arc::new(schema),
true,
+ None,
1024,
None,
);
@@ -594,7 +609,7 @@ mod tests {
fn test_csv_with_schema_inference() {
let file = File::open("test/data/uk_cities_with_headers.csv").unwrap();
- let builder =
ReaderBuilder::new().has_headers(true).infer_schema(None);
+ let builder = ReaderBuilder::new().has_header(true).infer_schema(None);
let mut csv = builder.build(file).unwrap();
let expected_schema = Schema::new(vec![
@@ -673,7 +688,8 @@ mod tests {
let file = File::open("test/data/uk_cities.csv").unwrap();
- let mut csv = Reader::new(file, Arc::new(schema), false, 1024,
Some(vec![0, 1]));
+ let mut csv =
+ Reader::new(file, Arc::new(schema), false, None, 1024,
Some(vec![0, 1]));
let projected_schema = Arc::new(Schema::new(vec![
Field::new("city", DataType::Utf8, false),
Field::new("lat", DataType::Float64, false),
@@ -695,7 +711,7 @@ mod tests {
let file = File::open("test/data/null_test.csv").unwrap();
- let mut csv = Reader::new(file, Arc::new(schema), true, 1024, None);
+ let mut csv = Reader::new(file, Arc::new(schema), true, None, 1024,
None);
let batch = csv.next().unwrap().unwrap();
assert_eq!(false, batch.column(1).is_null(0));
@@ -711,7 +727,7 @@ mod tests {
let builder = ReaderBuilder::new()
.infer_schema(None)
- .has_headers(true)
+ .has_header(true)
.with_delimiter(b'|')
.with_batch_size(512)
.with_projection(vec![0, 1, 2, 3]);
@@ -754,7 +770,7 @@ mod tests {
let builder = ReaderBuilder::new()
.with_schema(Arc::new(schema))
- .has_headers(true)
+ .has_header(true)
.with_delimiter(b'|')
.with_batch_size(512)
.with_projection(vec![0, 1, 2, 3]);
@@ -762,10 +778,20 @@ mod tests {
let mut csv = builder.build(file).unwrap();
match csv.next() {
Err(e) => assert_eq!(
- "ParseError(\"Error while parsing value 4.x4 at line 4\")",
+ "ParseError(\"Error while parsing value 4.x4 for column 1 at
line 4\")",
format!("{:?}", e)
),
Ok(_) => panic!("should have failed"),
}
}
+
+ #[test]
+ fn test_infer_field_schema() {
+ assert_eq!(infer_field_schema("A"), DataType::Utf8);
+ assert_eq!(infer_field_schema("\"123\""), DataType::Utf8);
+ assert_eq!(infer_field_schema("10"), DataType::Int64);
+ assert_eq!(infer_field_schema("10.2"), DataType::Float64);
+ assert_eq!(infer_field_schema("true"), DataType::Boolean);
+ assert_eq!(infer_field_schema("false"), DataType::Boolean);
+ }
}
diff --git a/rust/datafusion/benches/aggregate_query_sql.rs
b/rust/datafusion/benches/aggregate_query_sql.rs
index e3b3d56..88b534b 100644
--- a/rust/datafusion/benches/aggregate_query_sql.rs
+++ b/rust/datafusion/benches/aggregate_query_sql.rs
@@ -63,6 +63,7 @@ fn create_context() -> ExecutionContext {
&format!("{}/csv/aggregate_test_100.csv", testdata),
&schema,
true,
+ None,
);
let mem_table = MemTable::load(&csv).unwrap();
diff --git a/rust/datafusion/examples/csv_sql.rs
b/rust/datafusion/examples/csv_sql.rs
index 17106d2..dc538bf 100644
--- a/rust/datafusion/examples/csv_sql.rs
+++ b/rust/datafusion/examples/csv_sql.rs
@@ -52,6 +52,7 @@ fn main() -> Result<()> {
&format!("{}/csv/aggregate_test_100.csv", testdata),
&schema,
true,
+ None,
);
let sql = "SELECT c1, MIN(c12), MAX(c12) FROM aggregate_test_100 WHERE c11
> 0.1 AND c11 < 0.9 GROUP BY c1";
diff --git a/rust/datafusion/src/datasource/csv.rs
b/rust/datafusion/src/datasource/csv.rs
index fb3014e..a6a957d 100644
--- a/rust/datafusion/src/datasource/csv.rs
+++ b/rust/datafusion/src/datasource/csv.rs
@@ -36,17 +36,54 @@ pub struct CsvFile {
filename: String,
schema: Arc<Schema>,
has_header: bool,
+ delimiter: Option<u8>,
}
impl CsvFile {
#[allow(missing_docs)]
- pub fn new(filename: &str, schema: &Schema, has_header: bool) -> Self {
+ pub fn new(
+ filename: &str,
+ schema: &Schema,
+ has_header: bool,
+ delimiter: Option<u8>,
+ ) -> Self {
Self {
filename: String::from(filename),
schema: Arc::new(schema.clone()),
has_header,
+ delimiter,
}
}
+
+ /// Attempt to initialize a new `CsvFile` from a file path
+ pub fn try_new(
+ filename: &str,
+ schema: Option<&Schema>,
+ has_header: bool,
+ delimiter: Option<u8>,
+ ) -> Result<Self> {
+ let schema = match schema {
+ Some(s) => Arc::new(s.clone()),
+ None => {
+ let schema_infer_batch_size = 1024;
+ let csv_exec = CsvExec::try_new(
+ filename,
+ None,
+ has_header,
+ delimiter,
+ None,
+ schema_infer_batch_size,
+ )?;
+ csv_exec.schema()
+ }
+ };
+ Ok(Self {
+ filename: String::from(filename),
+ schema: schema,
+ has_header,
+ delimiter,
+ })
+ }
}
impl TableProvider for CsvFile {
@@ -61,8 +98,9 @@ impl TableProvider for CsvFile {
) -> Result<Vec<ScanResult>> {
let exec = CsvExec::try_new(
&self.filename,
- self.schema.clone(),
+ Some(self.schema.clone()),
self.has_header,
+ self.delimiter,
projection.clone(),
batch_size,
)?;
@@ -88,6 +126,7 @@ impl CsvBatchIterator {
filename: &str,
schema: Arc<Schema>,
has_header: bool,
+ delimiter: Option<u8>,
projection: &Option<Vec<usize>>,
batch_size: usize,
) -> Result<Self> {
@@ -96,6 +135,7 @@ impl CsvBatchIterator {
file,
schema.clone(),
has_header,
+ delimiter,
batch_size,
projection.clone(),
);
diff --git a/rust/datafusion/src/execution/context.rs
b/rust/datafusion/src/execution/context.rs
index 9b8d068..d804ae4 100644
--- a/rust/datafusion/src/execution/context.rs
+++ b/rust/datafusion/src/execution/context.rs
@@ -100,7 +100,7 @@ impl ExecutionContext {
ref header_row,
} => match file_type {
FileType::CSV => {
- self.register_csv(name, location, schema, *header_row);
+ self.register_csv(name, location, schema, *header_row,
None);
Ok(vec![])
}
FileType::Parquet => {
@@ -216,8 +216,12 @@ impl ExecutionContext {
filename: &str,
schema: &Schema,
has_header: bool,
+ delimiter: Option<u8>,
) {
- self.register_table(name, Box::new(CsvFile::new(filename, schema,
has_header)));
+ self.register_table(
+ name,
+ Box::new(CsvFile::new(filename, schema, has_header, delimiter)),
+ );
}
/// Register a Parquet file as a table so that it can be queried from SQL
@@ -314,12 +318,14 @@ impl ExecutionContext {
path,
schema,
has_header,
+ delimiter,
projection,
..
} => Ok(Arc::new(CsvExec::try_new(
path,
- Arc::new(schema.as_ref().to_owned()),
+ Some(Arc::new(schema.as_ref().to_owned())),
*has_header,
+ *delimiter,
projection.to_owned(),
batch_size,
)?)),
@@ -864,11 +870,35 @@ mod tests {
]));
// register each partition as well as the top level dir
- ctx.register_csv("part0", &format!("{}/part-0.csv", out_dir), &schema,
true);
- ctx.register_csv("part1", &format!("{}/part-1.csv", out_dir), &schema,
true);
- ctx.register_csv("part2", &format!("{}/part-2.csv", out_dir), &schema,
true);
- ctx.register_csv("part3", &format!("{}/part-3.csv", out_dir), &schema,
true);
- ctx.register_csv("allparts", &out_dir, &schema, true);
+ ctx.register_csv(
+ "part0",
+ &format!("{}/part-0.csv", out_dir),
+ &schema,
+ true,
+ None,
+ );
+ ctx.register_csv(
+ "part1",
+ &format!("{}/part-1.csv", out_dir),
+ &schema,
+ true,
+ None,
+ );
+ ctx.register_csv(
+ "part2",
+ &format!("{}/part-2.csv", out_dir),
+ &schema,
+ true,
+ None,
+ );
+ ctx.register_csv(
+ "part3",
+ &format!("{}/part-3.csv", out_dir),
+ &schema,
+ true,
+ None,
+ );
+ ctx.register_csv("allparts", &out_dir, &schema, true, None);
let part0 = collect(&mut ctx, "SELECT c1, c2 FROM part0")?;
let part1 = collect(&mut ctx, "SELECT c1, c2 FROM part1")?;
@@ -1031,7 +1061,13 @@ mod tests {
}
// register csv file with the execution context
- ctx.register_csv("test", tmp_dir.path().to_str().unwrap(), &schema,
true);
+ ctx.register_csv(
+ "test",
+ tmp_dir.path().to_str().unwrap(),
+ &schema,
+ true,
+ None,
+ );
Ok(ctx)
}
diff --git a/rust/datafusion/src/execution/physical_plan/csv.rs
b/rust/datafusion/src/execution/physical_plan/csv.rs
index a07417c..14df0d5 100644
--- a/rust/datafusion/src/execution/physical_plan/csv.rs
+++ b/rust/datafusion/src/execution/physical_plan/csv.rs
@@ -18,9 +18,10 @@
//! Execution plan for reading CSV files
use std::fs::File;
+use std::io::BufReader;
use std::sync::{Arc, Mutex};
-use crate::error::Result;
+use crate::error::{ExecutionError, Result};
use crate::execution::physical_plan::common;
use crate::execution::physical_plan::{BatchIterator, ExecutionPlan, Partition};
use arrow::csv;
@@ -35,6 +36,8 @@ pub struct CsvExec {
schema: Arc<Schema>,
/// Does the CSV file have a header?
has_header: bool,
+ /// An optional column delimiter. Defaults to `b','`
+ delimiter: Option<u8>,
/// Optional projection for which columns to load
projection: Option<Vec<usize>>,
/// Batch size
@@ -58,6 +61,7 @@ impl ExecutionPlan for CsvExec {
&filename,
self.schema.clone(),
self.has_header,
+ self.delimiter,
self.projection.clone(),
self.batch_size,
)) as Arc<dyn Partition>
@@ -71,15 +75,35 @@ impl CsvExec {
/// Create a new execution plan for reading a set of CSV files
pub fn try_new(
path: &str,
- schema: Arc<Schema>,
+ schema: Option<Arc<Schema>>,
has_header: bool,
+ delimiter: Option<u8>,
projection: Option<Vec<usize>>,
batch_size: usize,
) -> Result<Self> {
+ let schema = match schema {
+ Some(s) => s,
+ None => {
+ let mut filenames: Vec<String> = vec![];
+ common::build_file_list(path, &mut filenames, ".csv")?;
+ if filenames.is_empty() {
+ return Err(ExecutionError::General("No files
found".to_string()));
+ }
+
+ let f = File::open(&filenames[0])?;
+ Arc::new(csv::infer_file_schema(
+ &mut BufReader::new(f),
+ delimiter.unwrap_or(b','),
+ Some(1000),
+ has_header,
+ )?)
+ }
+ };
Ok(Self {
path: path.to_string(),
schema,
has_header,
+ delimiter,
projection,
batch_size,
})
@@ -94,6 +118,8 @@ struct CsvPartition {
schema: Arc<Schema>,
/// Does the CSV file have a header?
has_header: bool,
+ /// An optional column delimiter. Defaults to `b','`
+ delimiter: Option<u8>,
/// Optional projection for which columns to load
projection: Option<Vec<usize>>,
/// Batch size
@@ -105,6 +131,7 @@ impl CsvPartition {
path: &str,
schema: Arc<Schema>,
has_header: bool,
+ delimiter: Option<u8>,
projection: Option<Vec<usize>>,
batch_size: usize,
) -> Self {
@@ -112,6 +139,7 @@ impl CsvPartition {
path: path.to_string(),
schema,
has_header,
+ delimiter,
projection,
batch_size,
}
@@ -125,6 +153,7 @@ impl Partition for CsvPartition {
&self.path,
self.schema.clone(),
self.has_header,
+ self.delimiter,
&self.projection,
self.batch_size,
)?)))
@@ -143,6 +172,7 @@ impl CsvIterator {
filename: &str,
schema: Arc<Schema>,
has_header: bool,
+ delimiter: Option<u8>,
projection: &Option<Vec<usize>>,
batch_size: usize,
) -> Result<Self> {
@@ -151,6 +181,7 @@ impl CsvIterator {
file,
schema.clone(),
has_header,
+ delimiter,
batch_size,
projection.clone(),
);
diff --git a/rust/datafusion/src/execution/physical_plan/hash_aggregate.rs
b/rust/datafusion/src/execution/physical_plan/hash_aggregate.rs
index 0d1a376..83ffa73 100644
--- a/rust/datafusion/src/execution/physical_plan/hash_aggregate.rs
+++ b/rust/datafusion/src/execution/physical_plan/hash_aggregate.rs
@@ -736,7 +736,7 @@ mod tests {
let partitions = 4;
let path = test::create_partitioned_csv("aggregate_test_100.csv",
partitions)?;
- let csv = CsvExec::try_new(&path, schema.clone(), true, None, 1024)?;
+ let csv = CsvExec::try_new(&path, Some(schema.clone()), true, None,
None, 1024)?;
let group_expr: Vec<Arc<dyn PhysicalExpr>> = vec![col(1,
schema.as_ref())];
diff --git a/rust/datafusion/src/execution/physical_plan/limit.rs
b/rust/datafusion/src/execution/physical_plan/limit.rs
index bdc42df..4aa25f8 100644
--- a/rust/datafusion/src/execution/physical_plan/limit.rs
+++ b/rust/datafusion/src/execution/physical_plan/limit.rs
@@ -183,7 +183,7 @@ mod tests {
let path =
test::create_partitioned_csv("aggregate_test_100.csv",
num_partitions)?;
- let csv = CsvExec::try_new(&path, schema.clone(), true, None, 1024)?;
+ let csv = CsvExec::try_new(&path, Some(schema.clone()), true, None,
None, 1024)?;
// input should have 4 partitions
let input = csv.partitions()?;
diff --git a/rust/datafusion/src/execution/physical_plan/merge.rs
b/rust/datafusion/src/execution/physical_plan/merge.rs
index 0ef8a39..18283a9 100644
--- a/rust/datafusion/src/execution/physical_plan/merge.rs
+++ b/rust/datafusion/src/execution/physical_plan/merge.rs
@@ -111,7 +111,7 @@ mod tests {
let path =
test::create_partitioned_csv("aggregate_test_100.csv",
num_partitions)?;
- let csv = CsvExec::try_new(&path, schema.clone(), true, None, 1024)?;
+ let csv = CsvExec::try_new(&path, Some(schema.clone()), true, None,
None, 1024)?;
// input should have 4 partitions
let input = csv.partitions()?;
diff --git a/rust/datafusion/src/execution/physical_plan/projection.rs
b/rust/datafusion/src/execution/physical_plan/projection.rs
index 3ae8ba0..d90050c 100644
--- a/rust/datafusion/src/execution/physical_plan/projection.rs
+++ b/rust/datafusion/src/execution/physical_plan/projection.rs
@@ -150,7 +150,7 @@ mod tests {
let partitions = 4;
let path = test::create_partitioned_csv("aggregate_test_100.csv",
partitions)?;
- let csv = CsvExec::try_new(&path, schema.clone(), true, None, 1024)?;
+ let csv = CsvExec::try_new(&path, Some(schema.clone()), true, None,
None, 1024)?;
let projection = ProjectionExec::try_new(
vec![Arc::new(Column::new(0, &schema.as_ref().field(0).name()))],
diff --git a/rust/datafusion/src/execution/physical_plan/selection.rs
b/rust/datafusion/src/execution/physical_plan/selection.rs
index ea74a89..e8956e7 100644
--- a/rust/datafusion/src/execution/physical_plan/selection.rs
+++ b/rust/datafusion/src/execution/physical_plan/selection.rs
@@ -159,7 +159,7 @@ mod tests {
let partitions = 4;
let path = test::create_partitioned_csv("aggregate_test_100.csv",
partitions)?;
- let csv = CsvExec::try_new(&path, schema.clone(), true, None, 1024)?;
+ let csv = CsvExec::try_new(&path, Some(schema.clone()), true, None,
None, 1024)?;
let predicate: Arc<dyn PhysicalExpr> = binary(
binary(
diff --git a/rust/datafusion/src/execution/table_impl.rs
b/rust/datafusion/src/execution/table_impl.rs
index 10d65a4..de3bcb6 100644
--- a/rust/datafusion/src/execution/table_impl.rs
+++ b/rust/datafusion/src/execution/table_impl.rs
@@ -267,6 +267,7 @@ mod tests {
&format!("{}/csv/aggregate_test_100.csv", testdata),
&schema,
true,
+ None,
);
}
}
diff --git a/rust/datafusion/src/logicalplan.rs
b/rust/datafusion/src/logicalplan.rs
index fddd60d..ecae04e 100644
--- a/rust/datafusion/src/logicalplan.rs
+++ b/rust/datafusion/src/logicalplan.rs
@@ -25,6 +25,7 @@ use std::fmt;
use arrow::datatypes::{DataType, Field, Schema};
+use crate::datasource::csv::CsvFile;
use crate::datasource::parquet::ParquetTable;
use crate::datasource::TableProvider;
use crate::error::{ExecutionError, Result};
@@ -560,6 +561,8 @@ pub enum LogicalPlan {
schema: Box<Schema>,
/// Whether the CSV file(s) have a header containing column names
has_header: bool,
+ /// An optional column delimiter. Defaults to `b','`
+ delimiter: Option<u8>,
/// Optional column indices to use as a projection
projection: Option<Vec<usize>>,
/// The projected schema
@@ -794,16 +797,27 @@ impl LogicalPlanBuilder {
pub fn scan_csv(
path: &str,
has_header: bool,
- schema: &Schema,
+ schema: Option<&Schema>,
+ delimiter: Option<u8>,
projection: Option<Vec<usize>>,
) -> Result<Self> {
+ let schema: Schema = match schema {
+ Some(s) => s.to_owned(),
+ None => CsvFile::try_new(path, None, has_header, delimiter)?
+ .schema()
+ .as_ref()
+ .to_owned(),
+ };
+
let projected_schema = projection
.clone()
.map(|p| Schema::new(p.iter().map(|i|
schema.field(*i).clone()).collect()));
+
Ok(Self::from(&LogicalPlan::CsvScan {
path: path.to_owned(),
schema: Box::new(schema.to_owned()),
has_header,
+ delimiter,
projection,
projected_schema: Box::new(
projected_schema.or(Some(schema.clone())).unwrap(),
@@ -815,7 +829,6 @@ impl LogicalPlanBuilder {
pub fn scan_parquet(path: &str, projection: Option<Vec<usize>>) ->
Result<Self> {
let p = ParquetTable::try_new(path)?;
let schema = p.schema().as_ref().to_owned();
- println!("{:?}", schema);
let projected_schema = projection
.clone()
.map(|p| Schema::new(p.iter().map(|i|
schema.field(*i).clone()).collect()));
@@ -957,7 +970,8 @@ mod tests {
let plan = LogicalPlanBuilder::scan_csv(
"employee.csv",
true,
- &employee_schema(),
+ Some(&employee_schema()),
+ None,
Some(vec![0, 3]),
)?
.filter(col("state").eq(&lit_str("CO")))?
diff --git a/rust/datafusion/src/optimizer/projection_push_down.rs
b/rust/datafusion/src/optimizer/projection_push_down.rs
index 8f614fc..ea47215 100644
--- a/rust/datafusion/src/optimizer/projection_push_down.rs
+++ b/rust/datafusion/src/optimizer/projection_push_down.rs
@@ -133,6 +133,7 @@ impl ProjectionPushDown {
LogicalPlan::CsvScan {
path,
has_header,
+ delimiter,
schema,
projection,
..
@@ -144,6 +145,7 @@ impl ProjectionPushDown {
path: path.to_owned(),
has_header: *has_header,
schema: schema.clone(),
+ delimiter: *delimiter,
projection: Some(projection),
projected_schema: Box::new(projected_schema),
})
diff --git a/rust/datafusion/tests/sql.rs b/rust/datafusion/tests/sql.rs
index 08aa656..a4bf16e 100644
--- a/rust/datafusion/tests/sql.rs
+++ b/rust/datafusion/tests/sql.rs
@@ -56,7 +56,7 @@ fn nyc() -> Result<()> {
]);
let mut ctx = ExecutionContext::new();
- ctx.register_csv("tripdata", "file.csv", &schema, true);
+ ctx.register_csv("tripdata", "file.csv", &schema, true, None);
let logical_plan = ctx.create_logical_plan(
"SELECT passenger_count, MIN(fare_amount), MAX(fare_amount) \
@@ -442,7 +442,7 @@ fn register_csv(
filename: &str,
schema: &Arc<Schema>,
) {
- ctx.register_csv(name, filename, &schema, true);
+ ctx.register_csv(name, filename, &schema, true, None);
}
fn register_alltypes_parquet(ctx: &mut ExecutionContext) {