Skip to content

Commit 51dd135

Browse files
authored
Merge branch 'main' into feat_migrate_ffi_to_stabby
2 parents 9bca5bd + 65f337d commit 51dd135

29 files changed

Lines changed: 2384 additions & 274 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,11 @@ arrow-data = { version = "58.1.0", default-features = false }
104104
arrow-flight = { version = "58.1.0", features = [
105105
"flight-sql-experimental",
106106
] }
107+
# Both codecs are required here to make sure that code paths like
108+
# file-spilling have access to all compression codecs.
107109
arrow-ipc = { version = "58.1.0", default-features = false, features = [
108110
"lz4",
111+
"zstd",
109112
] }
110113
arrow-ord = { version = "58.1.0", default-features = false }
111114
arrow-schema = { version = "58.1.0", default-features = false }

datafusion/common/src/config.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use crate::encryption::{FileDecryptionProperties, FileEncryptionProperties};
2424
use crate::error::_config_err;
2525
use crate::format::{ExplainAnalyzeCategories, ExplainFormat, MetricType};
2626
use crate::parquet_config::DFParquetWriterVersion;
27-
use crate::parsers::CompressionTypeVariant;
27+
use crate::parsers::{CompressionTypeVariant, CsvQuoteStyle};
2828
use crate::utils::get_available_parallelism;
2929
use crate::{DataFusionError, Result};
3030
#[cfg(feature = "parquet_encryption")]
@@ -2042,6 +2042,17 @@ impl ConfigField for CompressionTypeVariant {
20422042
}
20432043
}
20442044

2045+
impl ConfigField for CsvQuoteStyle {
2046+
fn visit<V: Visit>(&self, v: &mut V, key: &str, description: &'static str) {
2047+
v.some(key, self, description)
2048+
}
2049+
2050+
fn set(&mut self, _: &str, value: &str) -> Result<()> {
2051+
*self = CsvQuoteStyle::from_str(value)?;
2052+
Ok(())
2053+
}
2054+
}
2055+
20452056
/// An implementation trait used to recursively walk configuration
20462057
pub trait Visit {
20472058
fn some<V: Display>(&mut self, key: &str, value: V, description: &'static str);
@@ -3114,6 +3125,15 @@ config_namespace! {
31143125
pub terminator: Option<u8>, default = None
31153126
pub escape: Option<u8>, default = None
31163127
pub double_quote: Option<bool>, default = None
3128+
/// Quote style for CSV writing.
3129+
/// One of: "Always", "Necessary", "NonNumeric", "Never"
3130+
pub quote_style: CsvQuoteStyle, default = CsvQuoteStyle::Necessary
3131+
/// Whether to ignore leading whitespace in string values when writing CSV.
3132+
/// Defaults to `false` when `None`.
3133+
pub ignore_leading_whitespace: Option<bool>, default = None
3134+
/// Whether to ignore trailing whitespace in string values when writing CSV.
3135+
/// Defaults to `false` when `None`.
3136+
pub ignore_trailing_whitespace: Option<bool>, default = None
31173137
/// Specifies whether newlines in (quoted) values are supported.
31183138
///
31193139
/// Parsing newlines in quoted values may be affected by execution behaviour such as
@@ -3222,6 +3242,30 @@ impl CsvOptions {
32223242
self
32233243
}
32243244

3245+
/// Set the quote style for CSV writing.
3246+
pub fn with_quote_style(mut self, quote_style: CsvQuoteStyle) -> Self {
3247+
self.quote_style = quote_style;
3248+
self
3249+
}
3250+
3251+
/// Set whether to ignore leading whitespace in string values when writing CSV.
3252+
pub fn with_ignore_leading_whitespace(
3253+
mut self,
3254+
ignore_leading_whitespace: bool,
3255+
) -> Self {
3256+
self.ignore_leading_whitespace = Some(ignore_leading_whitespace);
3257+
self
3258+
}
3259+
3260+
/// Set whether to ignore trailing whitespace in string values when writing CSV.
3261+
pub fn with_ignore_trailing_whitespace(
3262+
mut self,
3263+
ignore_trailing_whitespace: bool,
3264+
) -> Self {
3265+
self.ignore_trailing_whitespace = Some(ignore_trailing_whitespace);
3266+
self
3267+
}
3268+
32253269
/// Specifies whether newlines in (quoted) values are supported.
32263270
///
32273271
/// Parsing newlines in quoted values may be affected by execution behaviour such as

datafusion/common/src/file_options/csv_writer.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,13 @@ impl TryFrom<&CsvOptions> for CsvWriterOptions {
9494
if let Some(v) = &value.double_quote {
9595
builder = builder.with_double_quote(*v)
9696
}
97+
builder = builder.with_quote_style(value.quote_style.into());
98+
if let Some(v) = &value.ignore_leading_whitespace {
99+
builder = builder.with_ignore_leading_whitespace(*v)
100+
}
101+
if let Some(v) = &value.ignore_trailing_whitespace {
102+
builder = builder.with_ignore_trailing_whitespace(*v)
103+
}
97104
Ok(CsvWriterOptions {
98105
writer_options: builder,
99106
compression: value.compression,

datafusion/common/src/parsers.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,59 @@ impl CompressionTypeVariant {
7373
!matches!(self, &Self::UNCOMPRESSED)
7474
}
7575
}
76+
77+
/// CSV quote style
78+
///
79+
/// Controls when fields are quoted when writing CSV files.
80+
/// Corresponds to [`arrow::csv::QuoteStyle`].
81+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
82+
pub enum CsvQuoteStyle {
83+
/// Quote all fields
84+
Always,
85+
/// Only quote fields when necessary (default)
86+
#[default]
87+
Necessary,
88+
/// Quote all non-numeric fields
89+
NonNumeric,
90+
/// Never quote fields
91+
Never,
92+
}
93+
94+
impl FromStr for CsvQuoteStyle {
95+
type Err = DataFusionError;
96+
97+
fn from_str(s: &str) -> Result<Self, Self::Err> {
98+
match s.to_lowercase().as_str() {
99+
"always" => Ok(Self::Always),
100+
"necessary" => Ok(Self::Necessary),
101+
"non_numeric" | "nonnumeric" => Ok(Self::NonNumeric),
102+
"never" => Ok(Self::Never),
103+
_ => Err(DataFusionError::NotImplemented(format!(
104+
"Unsupported CSV quote style {s}"
105+
))),
106+
}
107+
}
108+
}
109+
110+
impl From<CsvQuoteStyle> for arrow::csv::QuoteStyle {
111+
fn from(style: CsvQuoteStyle) -> Self {
112+
match style {
113+
CsvQuoteStyle::Always => Self::Always,
114+
CsvQuoteStyle::NonNumeric => Self::NonNumeric,
115+
CsvQuoteStyle::Never => Self::Never,
116+
CsvQuoteStyle::Necessary => Self::Necessary,
117+
}
118+
}
119+
}
120+
121+
impl Display for CsvQuoteStyle {
122+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123+
let str = match self {
124+
Self::Always => "Always",
125+
Self::Necessary => "Necessary",
126+
Self::NonNumeric => "NonNumeric",
127+
Self::Never => "Never",
128+
};
129+
write!(f, "{str}")
130+
}
131+
}

datafusion/core/tests/physical_optimizer/pushdown_sort.rs

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@ use datafusion_physical_optimizer::pushdown_sort::PushdownSort;
3232
use std::sync::Arc;
3333

3434
use crate::physical_optimizer::test_utils::{
35-
OptimizationTest, coalesce_partitions_exec, parquet_exec, parquet_exec_with_sort,
36-
projection_exec, projection_exec_with_alias, repartition_exec, schema,
37-
simple_projection_exec, sort_exec, sort_exec_with_fetch, sort_expr, sort_expr_named,
38-
test_scan_with_ordering,
35+
OptimizationTest, TestScan, coalesce_partitions_exec, parquet_exec,
36+
parquet_exec_with_sort, projection_exec, projection_exec_with_alias,
37+
repartition_exec, schema, simple_projection_exec, sort_exec, sort_exec_with_fetch,
38+
sort_expr, sort_expr_named, test_scan_with_ordering,
3939
};
4040

4141
#[test]
@@ -996,3 +996,91 @@ fn test_sort_pushdown_with_test_scan_arbitrary_ordering() {
996996
"
997997
);
998998
}
999+
1000+
// ============================================================================
1001+
// EXACT PUSHDOWN TESTS (source guarantees ordering, SortExec removed)
1002+
// ============================================================================
1003+
1004+
#[test]
1005+
fn test_sort_pushdown_exact_no_fetch_no_limit() {
1006+
// When a source returns Exact (without fetch), the SortExec should be
1007+
// removed entirely with no GlobalLimitExec wrapper.
1008+
let schema = schema();
1009+
let a = sort_expr("a", &schema);
1010+
let b = sort_expr("b", &schema);
1011+
let source =
1012+
Arc::new(TestScan::new(schema.clone(), vec![]).with_exact_pushdown(true));
1013+
1014+
let ordering = LexOrdering::new(vec![a, b.reverse()]).unwrap();
1015+
let plan = sort_exec(ordering, source);
1016+
1017+
insta::assert_snapshot!(
1018+
OptimizationTest::new(plan, PushdownSort::new(), true),
1019+
@r"
1020+
OptimizationTest:
1021+
input:
1022+
- SortExec: expr=[a@0 ASC, b@1 DESC NULLS LAST], preserve_partitioning=[false]
1023+
- TestScan
1024+
output:
1025+
Ok:
1026+
- TestScan: requested_ordering=[a@0 ASC, b@1 DESC NULLS LAST]
1027+
"
1028+
);
1029+
}
1030+
1031+
#[test]
1032+
fn test_sort_pushdown_exact_preserves_fetch_with_global_limit() {
1033+
// When a source returns Exact but does NOT support with_fetch(),
1034+
// the optimizer must wrap the result with GlobalLimitExec to preserve
1035+
// the LIMIT from the eliminated SortExec.
1036+
let schema = schema();
1037+
let a = sort_expr("a", &schema);
1038+
let source =
1039+
Arc::new(TestScan::new(schema.clone(), vec![]).with_exact_pushdown(true));
1040+
1041+
let ordering = LexOrdering::new(vec![a]).unwrap();
1042+
let plan = sort_exec_with_fetch(ordering, Some(10), source);
1043+
1044+
insta::assert_snapshot!(
1045+
OptimizationTest::new(plan, PushdownSort::new(), true),
1046+
@r"
1047+
OptimizationTest:
1048+
input:
1049+
- SortExec: TopK(fetch=10), expr=[a@0 ASC], preserve_partitioning=[false]
1050+
- TestScan
1051+
output:
1052+
Ok:
1053+
- GlobalLimitExec: skip=0, fetch=10
1054+
- TestScan: requested_ordering=[a@0 ASC]
1055+
"
1056+
);
1057+
}
1058+
1059+
#[test]
1060+
fn test_sort_pushdown_exact_preserves_fetch_with_source_support() {
1061+
// When a source returns Exact AND supports with_fetch(),
1062+
// the limit should be pushed into the source directly (no GlobalLimitExec).
1063+
let schema = schema();
1064+
let a = sort_expr("a", &schema);
1065+
let source = Arc::new(
1066+
TestScan::new(schema.clone(), vec![])
1067+
.with_exact_pushdown(true)
1068+
.with_supports_fetch(true),
1069+
);
1070+
1071+
let ordering = LexOrdering::new(vec![a]).unwrap();
1072+
let plan = sort_exec_with_fetch(ordering, Some(10), source);
1073+
1074+
insta::assert_snapshot!(
1075+
OptimizationTest::new(plan, PushdownSort::new(), true),
1076+
@r"
1077+
OptimizationTest:
1078+
input:
1079+
- SortExec: TopK(fetch=10), expr=[a@0 ASC], preserve_partitioning=[false]
1080+
- TestScan
1081+
output:
1082+
Ok:
1083+
- TestScan: requested_ordering=[a@0 ASC], fetch=10
1084+
"
1085+
);
1086+
}

0 commit comments

Comments
 (0)