You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@datafusion.apache.org by gi...@apache.org on 2026/04/03 12:58:37 UTC
(datafusion) 01/01: fix(unparser): use to_rfc3339 for default TIMESTAMPTZ formatting (#21295)
This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch gh-readonly-queue/main/pr-21295-1e93a67bd0f24aa9c92a1d0c680410051019eece
in repository https://gitbox.apache.org/repos/asf/datafusion.git
commit 5b0938de9131eb38e4af2d4c9442e1ab8929add2
Author: Sergei Grebnov <se...@gmail.com>
AuthorDate: Fri Apr 3 15:58:12 2026 +0300
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`.
---
datafusion/sql/src/unparser/dialect.rs | 25 +------------------------
datafusion/sql/src/unparser/expr.rs | 33 ++++++++++++++++-----------------
2 files changed, 17 insertions(+), 41 deletions(-)
diff --git a/datafusion/sql/src/unparser/dialect.rs b/datafusion/sql/src/unparser/dialect.rs
index fa9d40c6c7..9e0683bdd7 100644
--- a/datafusion/sql/src/unparser/dialect.rs
+++ b/datafusion/sql/src/unparser/dialect.rs
@@ -216,7 +216,7 @@ pub trait Dialect: Send + Sync {
/// Allows the dialect to override logic of formatting datetime with tz into string.
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, _unit: TimeUnit) -> String {
- dt.to_string()
+ dt.to_rfc3339()
}
/// Whether the dialect supports an empty select list such as `SELECT FROM table`.
@@ -489,17 +489,6 @@ impl Dialect for DuckDBDialect {
Ok(None)
}
-
- fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
- let format = match unit {
- TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
- TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
- TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
- TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
- };
-
- dt.format(format).to_string()
- }
}
pub struct MySqlDialect {}
@@ -656,18 +645,6 @@ impl Dialect for BigQueryDialect {
fn unnest_as_table_factor(&self) -> bool {
true
}
-
- fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
- // https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type
- let format = match unit {
- TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
- TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
- TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
- TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
- };
-
- dt.format(format).to_string()
- }
}
impl BigQueryDialect {
diff --git a/datafusion/sql/src/unparser/expr.rs b/datafusion/sql/src/unparser/expr.rs
index a4a231d985..3601febe74 100644
--- a/datafusion/sql/src/unparser/expr.rs
+++ b/datafusion/sql/src/unparser/expr.rs
@@ -2047,7 +2047,7 @@ mod tests {
ScalarValue::TimestampSecond(Some(10001), Some("+08:00".into())),
None,
),
- r#"CAST('1970-01-01 10:46:41 +08:00' AS TIMESTAMP)"#,
+ r#"CAST('1970-01-01T10:46:41+08:00' AS TIMESTAMP)"#,
),
(
Expr::Literal(ScalarValue::TimestampMillisecond(Some(10001), None), None),
@@ -2058,7 +2058,7 @@ mod tests {
ScalarValue::TimestampMillisecond(Some(10001), Some("+08:00".into())),
None,
),
- r#"CAST('1970-01-01 08:00:10.001 +08:00' AS TIMESTAMP)"#,
+ r#"CAST('1970-01-01T08:00:10.001+08:00' AS TIMESTAMP)"#,
),
(
Expr::Literal(ScalarValue::TimestampMicrosecond(Some(10001), None), None),
@@ -2069,7 +2069,7 @@ mod tests {
ScalarValue::TimestampMicrosecond(Some(10001), Some("+08:00".into())),
None,
),
- r#"CAST('1970-01-01 08:00:00.010001 +08:00' AS TIMESTAMP)"#,
+ r#"CAST('1970-01-01T08:00:00.010001+08:00' AS TIMESTAMP)"#,
),
(
Expr::Literal(ScalarValue::TimestampNanosecond(Some(10001), None), None),
@@ -2080,7 +2080,7 @@ mod tests {
ScalarValue::TimestampNanosecond(Some(10001), Some("+08:00".into())),
None,
),
- r#"CAST('1970-01-01 08:00:00.000010001 +08:00' AS TIMESTAMP)"#,
+ r#"CAST('1970-01-01T08:00:00.000010001+08:00' AS TIMESTAMP)"#,
),
(
Expr::Literal(ScalarValue::Time32Second(Some(10001)), None),
@@ -3374,7 +3374,7 @@ mod tests {
(
Arc::clone(&default_dialect),
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
- "CAST('2025-09-15 11:00:00 +00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
),
(
Arc::clone(&default_dialect),
@@ -3382,7 +3382,7 @@ mod tests {
Some(1757934000123),
Some("+01:00".into()),
),
- "CAST('2025-09-15 12:00:00.123 +01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
),
(
Arc::clone(&default_dialect),
@@ -3390,7 +3390,7 @@ mod tests {
Some(1757934000123456),
Some("-01:00".into()),
),
- "CAST('2025-09-15 10:00:00.123456 -01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
),
(
Arc::clone(&default_dialect),
@@ -3398,12 +3398,12 @@ mod tests {
Some(1757934000123456789),
Some("+00:00".into()),
),
- "CAST('2025-09-15 11:00:00.123456789 +00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
),
(
Arc::clone(&duckdb_dialect),
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
- "CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
),
(
Arc::clone(&duckdb_dialect),
@@ -3411,7 +3411,7 @@ mod tests {
Some(1757934000123),
Some("+01:00".into()),
),
- "CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
),
(
Arc::clone(&duckdb_dialect),
@@ -3419,7 +3419,7 @@ mod tests {
Some(1757934000123456),
Some("-01:00".into()),
),
- "CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
),
(
Arc::clone(&duckdb_dialect),
@@ -3427,13 +3427,12 @@ mod tests {
Some(1757934000123456789),
Some("+00:00".into()),
),
- "CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
),
- // BigQuery: should be no space between timestamp and timezone
(
Arc::clone(&bigquery_dialect),
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
- "CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00+00:00' AS TIMESTAMP)",
),
(
Arc::clone(&bigquery_dialect),
@@ -3441,7 +3440,7 @@ mod tests {
Some(1757934000123),
Some("+01:00".into()),
),
- "CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T12:00:00.123+01:00' AS TIMESTAMP)",
),
(
Arc::clone(&bigquery_dialect),
@@ -3449,7 +3448,7 @@ mod tests {
Some(1757934000123456),
Some("-01:00".into()),
),
- "CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T10:00:00.123456-01:00' AS TIMESTAMP)",
),
(
Arc::clone(&bigquery_dialect),
@@ -3457,7 +3456,7 @@ mod tests {
Some(1757934000123456789),
Some("+00:00".into()),
),
- "CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
+ "CAST('2025-09-15T11:00:00.123456789+00:00' AS TIMESTAMP)",
),
] {
let unparser = Unparser::new(dialect.as_ref());
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@datafusion.apache.org
For additional commands, e-mail: commits-help@datafusion.apache.org