Skip to content

Commit 5b0938d

Browse files
authored
fix(unparser): use to_rfc3339 for default TIMESTAMPTZ formatting (#21295)
## Which issue does this PR close? - Closes #21294. ## Rationale for this change The default `timestamp_with_tz_to_string` uses `DateTime<Tz>.to_string()`, which produces literals like `2025-09-15 11:00:00 +00:00` that are not supported by multiple dialects (e.g. DuckDB, BigQuery). Switching to `to_rfc3339()` produces valid ISO 8601 / RFC 3339 strings (e.g. `2025-09-15T11:00:00+00:00`) that are broadly compatible. ## What changes are included in this PR? - Changed the default `Dialect::timestamp_with_tz_to_string` implementation from `dt.to_string()` to `dt.to_rfc3339()` - Removed now-redundant `timestamp_with_tz_to_string` overrides from `DuckDBDialect` and `BigQueryDialect` (they produced equivalent RFC 3339-compatible output via custom format strings) - Updated test expectations to match the new default format ## Are these changes tested? Yes, existing tests in `unparser::expr::tests` have been updated to reflect the new format. Manually verifying updated format supported by `PostgreSQL`, `MySQL`, `SQLite`, `DuckDB` and `BigQuery`. ## Are there any user-facing changes? Yes — timestamp-with-timezone literals are now formatted as RFC 3339 (`2025-09-15T11:00:00+00:00`) instead of the previous `Display` format (`2025-09-15 11:00:00 +00:00`). Dialects that need a different format can still override `timestamp_with_tz_to_string`.
1 parent 1e93a67 commit 5b0938d

2 files changed

Lines changed: 17 additions & 41 deletions

File tree

datafusion/sql/src/unparser/dialect.rs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ pub trait Dialect: Send + Sync {
216216

217217
/// Allows the dialect to override logic of formatting datetime with tz into string.
218218
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, _unit: TimeUnit) -> String {
219-
dt.to_string()
219+
dt.to_rfc3339()
220220
}
221221

222222
/// Whether the dialect supports an empty select list such as `SELECT FROM table`.
@@ -489,17 +489,6 @@ impl Dialect for DuckDBDialect {
489489

490490
Ok(None)
491491
}
492-
493-
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
494-
let format = match unit {
495-
TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
496-
TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
497-
TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
498-
TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
499-
};
500-
501-
dt.format(format).to_string()
502-
}
503492
}
504493

505494
pub struct MySqlDialect {}
@@ -656,18 +645,6 @@ impl Dialect for BigQueryDialect {
656645
fn unnest_as_table_factor(&self) -> bool {
657646
true
658647
}
659-
660-
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
661-
// https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type
662-
let format = match unit {
663-
TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
664-
TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
665-
TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
666-
TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
667-
};
668-
669-
dt.format(format).to_string()
670-
}
671648
}
672649

673650
impl BigQueryDialect {

datafusion/sql/src/unparser/expr.rs

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,7 @@ mod tests {
20472047
ScalarValue::TimestampSecond(Some(10001), Some("+08:00".into())),
20482048
None,
20492049
),
2050-
r#"CAST('1970-01-01 10:46:41 +08:00' AS TIMESTAMP)"#,
2050+
r#"CAST('1970-01-01T10:46:41+08:00' AS TIMESTAMP)"#,
20512051
),
20522052
(
20532053
Expr::Literal(ScalarValue::TimestampMillisecond(Some(10001), None), None),
@@ -2058,7 +2058,7 @@ mod tests {
20582058
ScalarValue::TimestampMillisecond(Some(10001), Some("+08:00".into())),
20592059
None,
20602060
),
2061-
r#"CAST('1970-01-01 08:00:10.001 +08:00' AS TIMESTAMP)"#,
2061+
r#"CAST('1970-01-01T08:00:10.001+08:00' AS TIMESTAMP)"#,
20622062
),
20632063
(
20642064
Expr::Literal(ScalarValue::TimestampMicrosecond(Some(10001), None), None),
@@ -2069,7 +2069,7 @@ mod tests {
20692069
ScalarValue::TimestampMicrosecond(Some(10001), Some("+08:00".into())),
20702070
None,
20712071
),
2072-
r#"CAST('1970-01-01 08:00:00.010001 +08:00' AS TIMESTAMP)"#,
2072+
r#"CAST('1970-01-01T08:00:00.010001+08:00' AS TIMESTAMP)"#,
20732073
),
20742074
(
20752075
Expr::Literal(ScalarValue::TimestampNanosecond(Some(10001), None), None),
@@ -2080,7 +2080,7 @@ mod tests {
20802080
ScalarValue::TimestampNanosecond(Some(10001), Some("+08:00".into())),
20812081
None,
20822082
),
2083-
r#"CAST('1970-01-01 08:00:00.000010001 +08:00' AS TIMESTAMP)"#,
2083+
r#"CAST('1970-01-01T08:00:00.000010001+08:00' AS TIMESTAMP)"#,
20842084
),
20852085
(
20862086
Expr::Literal(ScalarValue::Time32Second(Some(10001)), None),
@@ -3374,90 +3374,89 @@ mod tests {
33743374
(
33753375
Arc::clone(&default_dialect),
33763376
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3377-
"CAST('2025-09-15 11:00:00 +00:00' AS TIMESTAMP)",
3377+
"CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
33783378
),
33793379
(
33803380
Arc::clone(&default_dialect),
33813381
ScalarValue::TimestampMillisecond(
33823382
Some(1757934000123),
33833383
Some("+01:00".into()),
33843384
),
3385-
"CAST('2025-09-15 12:00:00.123 +01:00' AS TIMESTAMP)",
3385+
"CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
33863386
),
33873387
(
33883388
Arc::clone(&default_dialect),
33893389
ScalarValue::TimestampMicrosecond(
33903390
Some(1757934000123456),
33913391
Some("-01:00".into()),
33923392
),
3393-
"CAST('2025-09-15 10:00:00.123456 -01:00' AS TIMESTAMP)",
3393+
"CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
33943394
),
33953395
(
33963396
Arc::clone(&default_dialect),
33973397
ScalarValue::TimestampNanosecond(
33983398
Some(1757934000123456789),
33993399
Some("+00:00".into()),
34003400
),
3401-
"CAST('2025-09-15 11:00:00.123456789 +00:00' AS TIMESTAMP)",
3401+
"CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
34023402
),
34033403
(
34043404
Arc::clone(&duckdb_dialect),
34053405
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3406-
"CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
3406+
"CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
34073407
),
34083408
(
34093409
Arc::clone(&duckdb_dialect),
34103410
ScalarValue::TimestampMillisecond(
34113411
Some(1757934000123),
34123412
Some("+01:00".into()),
34133413
),
3414-
"CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
3414+
"CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
34153415
),
34163416
(
34173417
Arc::clone(&duckdb_dialect),
34183418
ScalarValue::TimestampMicrosecond(
34193419
Some(1757934000123456),
34203420
Some("-01:00".into()),
34213421
),
3422-
"CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
3422+
"CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
34233423
),
34243424
(
34253425
Arc::clone(&duckdb_dialect),
34263426
ScalarValue::TimestampNanosecond(
34273427
Some(1757934000123456789),
34283428
Some("+00:00".into()),
34293429
),
3430-
"CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
3430+
"CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
34313431
),
3432-
// BigQuery: should be no space between timestamp and timezone
34333432
(
34343433
Arc::clone(&bigquery_dialect),
34353434
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3436-
"CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
3435+
"CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
34373436
),
34383437
(
34393438
Arc::clone(&bigquery_dialect),
34403439
ScalarValue::TimestampMillisecond(
34413440
Some(1757934000123),
34423441
Some("+01:00".into()),
34433442
),
3444-
"CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
3443+
"CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
34453444
),
34463445
(
34473446
Arc::clone(&bigquery_dialect),
34483447
ScalarValue::TimestampMicrosecond(
34493448
Some(1757934000123456),
34503449
Some("-01:00".into()),
34513450
),
3452-
"CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
3451+
"CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
34533452
),
34543453
(
34553454
Arc::clone(&bigquery_dialect),
34563455
ScalarValue::TimestampNanosecond(
34573456
Some(1757934000123456789),
34583457
Some("+00:00".into()),
34593458
),
3460-
"CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
3459+
"CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
34613460
),
34623461
] {
34633462
let unparser = Unparser::new(dialect.as_ref());

0 commit comments

Comments
 (0)