Skip to content

Commit 03b6789

Browse files
authored
Custom timestamp format for DuckDB (#17653)
1 parent 14656f5 commit 03b6789

File tree

4 files changed

+111
-13
lines changed

4 files changed

+111
-13
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

datafusion/sql/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ recursive_protection = ["dep:recursive"]
5050
[dependencies]
5151
arrow = { workspace = true }
5252
bigdecimal = { workspace = true }
53+
chrono = { workspace = true }
5354
datafusion-common = { workspace = true, features = ["sql"] }
5455
datafusion-expr = { workspace = true, features = ["sql"] }
5556
indexmap = { workspace = true }

datafusion/sql/src/unparser/dialect.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ use super::{
2121
utils::character_length_to_sql, utils::date_part_to_sql,
2222
utils::sqlite_date_trunc_to_sql, utils::sqlite_from_unixtime_to_sql, Unparser,
2323
};
24+
use arrow::array::timezone::Tz;
2425
use arrow::datatypes::TimeUnit;
26+
use chrono::DateTime;
2527
use datafusion_common::Result;
2628
use datafusion_expr::Expr;
2729
use regex::Regex;
@@ -204,6 +206,11 @@ pub trait Dialect: Send + Sync {
204206
fn col_alias_overrides(&self, _alias: &str) -> Result<Option<String>> {
205207
Ok(None)
206208
}
209+
210+
/// Allows the dialect to override logic of formatting datetime with tz into string.
211+
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, _unit: TimeUnit) -> String {
212+
dt.to_string()
213+
}
207214
}
208215

209216
/// `IntervalStyle` to use for unparsing
@@ -401,6 +408,17 @@ impl Dialect for DuckDBDialect {
401408

402409
Ok(None)
403410
}
411+
412+
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
413+
let format = match unit {
414+
TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
415+
TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
416+
TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
417+
TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
418+
};
419+
420+
dt.format(format).to_string()
421+
}
404422
}
405423

406424
pub struct MySqlDialect {}

datafusion/sql/src/unparser/expr.rs

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,8 +1064,19 @@ impl Unparser<'_> {
10641064
where
10651065
i64: From<T::Native>,
10661066
{
1067+
let time_unit = match T::DATA_TYPE {
1068+
DataType::Timestamp(unit, _) => unit,
1069+
_ => {
1070+
return Err(internal_datafusion_err!(
1071+
"Expected Timestamp, got {:?}",
1072+
T::DATA_TYPE
1073+
))
1074+
}
1075+
};
1076+
10671077
let ts = if let Some(tz) = tz {
1068-
v.to_array()?
1078+
let dt = v
1079+
.to_array()?
10691080
.as_any()
10701081
.downcast_ref::<PrimitiveArray<T>>()
10711082
.ok_or(internal_datafusion_err!(
@@ -1074,8 +1085,8 @@ impl Unparser<'_> {
10741085
.value_as_datetime_with_tz(0, tz.parse()?)
10751086
.ok_or(internal_datafusion_err!(
10761087
"Unable to convert {v:?} to DateTime"
1077-
))?
1078-
.to_string()
1088+
))?;
1089+
self.dialect.timestamp_with_tz_to_string(dt, time_unit)
10791090
} else {
10801091
v.to_array()?
10811092
.as_any()
@@ -1090,16 +1101,6 @@ impl Unparser<'_> {
10901101
.to_string()
10911102
};
10921103

1093-
let time_unit = match T::DATA_TYPE {
1094-
DataType::Timestamp(unit, _) => unit,
1095-
_ => {
1096-
return Err(internal_datafusion_err!(
1097-
"Expected Timestamp, got {:?}",
1098-
T::DATA_TYPE
1099-
))
1100-
}
1101-
};
1102-
11031104
Ok(ast::Expr::Cast {
11041105
kind: ast::CastKind::Cast,
11051106
expr: Box::new(ast::Expr::value(SingleQuotedString(ts))),
@@ -3219,4 +3220,81 @@ mod tests {
32193220

32203221
Ok(())
32213222
}
3223+
3224+
#[test]
3225+
fn test_timestamp_with_tz_format() -> Result<()> {
3226+
let default_dialect: Arc<dyn Dialect> =
3227+
Arc::new(CustomDialectBuilder::new().build());
3228+
3229+
let duckdb_dialect: Arc<dyn Dialect> = Arc::new(DuckDBDialect::new());
3230+
3231+
for (dialect, scalar, expected) in [
3232+
(
3233+
Arc::clone(&default_dialect),
3234+
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3235+
"CAST('2025-09-15 11:00:00 +00:00' AS TIMESTAMP)",
3236+
),
3237+
(
3238+
Arc::clone(&default_dialect),
3239+
ScalarValue::TimestampMillisecond(
3240+
Some(1757934000123),
3241+
Some("+01:00".into()),
3242+
),
3243+
"CAST('2025-09-15 12:00:00.123 +01:00' AS TIMESTAMP)",
3244+
),
3245+
(
3246+
Arc::clone(&default_dialect),
3247+
ScalarValue::TimestampMicrosecond(
3248+
Some(1757934000123456),
3249+
Some("-01:00".into()),
3250+
),
3251+
"CAST('2025-09-15 10:00:00.123456 -01:00' AS TIMESTAMP)",
3252+
),
3253+
(
3254+
Arc::clone(&default_dialect),
3255+
ScalarValue::TimestampNanosecond(
3256+
Some(1757934000123456789),
3257+
Some("+00:00".into()),
3258+
),
3259+
"CAST('2025-09-15 11:00:00.123456789 +00:00' AS TIMESTAMP)",
3260+
),
3261+
(
3262+
Arc::clone(&duckdb_dialect),
3263+
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3264+
"CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
3265+
),
3266+
(
3267+
Arc::clone(&duckdb_dialect),
3268+
ScalarValue::TimestampMillisecond(
3269+
Some(1757934000123),
3270+
Some("+01:00".into()),
3271+
),
3272+
"CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
3273+
),
3274+
(
3275+
Arc::clone(&duckdb_dialect),
3276+
ScalarValue::TimestampMicrosecond(
3277+
Some(1757934000123456),
3278+
Some("-01:00".into()),
3279+
),
3280+
"CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
3281+
),
3282+
(
3283+
Arc::clone(&duckdb_dialect),
3284+
ScalarValue::TimestampNanosecond(
3285+
Some(1757934000123456789),
3286+
Some("+00:00".into()),
3287+
),
3288+
"CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
3289+
),
3290+
] {
3291+
let unparser = Unparser::new(dialect.as_ref());
3292+
3293+
let expr = Expr::Literal(scalar, None);
3294+
3295+
let actual = format!("{}", unparser.expr_to_sql(&expr)?);
3296+
assert_eq!(actual, expected);
3297+
}
3298+
Ok(())
3299+
}
32223300
}

0 commit comments

Comments
 (0)