From 8c217845639f8f5db6abcc74dc5c9c4fc7507bd7 Mon Sep 17 00:00:00 2001 From: Sergei Grebnov Date: Wed, 1 Apr 2026 12:51:59 +0300 Subject: [PATCH] fix(unparser): use to_rfc3339 for default TIMESTAMPTZ formatting --- 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 a3367dd96ce22..ca48706724cb8 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, _unit: TimeUnit) -> String { - dt.to_string() + dt.to_rfc3339() } /// Whether the dialect supports an empty select list such as `SELECT FROM table`. @@ -468,17 +468,6 @@ impl Dialect for DuckDBDialect { Ok(None) } - - fn timestamp_with_tz_to_string(&self, dt: DateTime, 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 {} @@ -635,18 +624,6 @@ impl Dialect for BigQueryDialect { fn unnest_as_table_factor(&self) -> bool { true } - - fn timestamp_with_tz_to_string(&self, dt: DateTime, 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 686650b1c8c58..803f8f3e2a820 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), @@ -3356,7 +3356,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), @@ -3364,7 +3364,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), @@ -3372,7 +3372,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), @@ -3380,12 +3380,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), @@ -3393,7 +3393,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), @@ -3401,7 +3401,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), @@ -3409,13 +3409,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), @@ -3423,7 +3422,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), @@ -3431,7 +3430,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), @@ -3439,7 +3438,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());