This is an automated email from the ASF dual-hosted git repository.
github-bot pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion.git
The following commit(s) were added to refs/heads/main by this push:
new a910b039dc fix(unparser): Fix BigQuery timestamp literal format in SQL
unparsing (#21103)
a910b039dc is described below
commit a910b039dc95dc2cf0d4c6a96a001b310bf0eded
Author: Sergei Grebnov <[email protected]>
AuthorDate: Fri Mar 27 20:48:07 2026 +0300
fix(unparser): Fix BigQuery timestamp literal format in SQL unparsing
(#21103)
## Which issue does this PR close?
The default `Dialect::timestamp_with_tz_to_string` uses `dt.to_string()`
which produces timestamps with a space before the TimeZone offset. This
causes filter pushdown to fail when unparsing timestamp predicates for
BigQuery.
>2016-08-06 20:05:00 +00:00 <- invalid for BigQuery:
invalid timestamp: '2016-08-06 20:05:00 +00:00'; while executing the
filter on column 'startTime' (query) (sqlstate: [0, 0, 0, 0, 0],
vendor_code: -2147483648)
BigQuery rejects this format. Per the [BigQuery timestamp
docs](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type),
the offset must be attached directly to the time:
>2016-08-06 20:05:00+00:00 <- valid
## What changes are included in this PR?
Following similar to [DuckDB pattern/fix
](https://github.com/apache/datafusion/pull/17653) override
`timestamp_with_tz_to_string` for `BigQueryDialect` to produce valid
timestamp format
**Before (default `dt.to_string()`):**
```sql
CAST('2016-08-06 20:05:00 +00:00' AS TIMESTAMP) -- BigQuery error
```
After (%:z format):
```sql
CAST('2016-08-06 20:05:00+00:00' AS TIMESTAMP) -- valid BigQuery timestamp
```
## Are these changes tested?
Added unit test and manual e2e test with Google BigQuery instance.
## Are there any user-facing changes?
No
---
datafusion/sql/src/unparser/dialect.rs | 12 ++++++++++++
datafusion/sql/src/unparser/expr.rs | 36 ++++++++++++++++++++++++++++++++--
2 files changed, 46 insertions(+), 2 deletions(-)
diff --git a/datafusion/sql/src/unparser/dialect.rs
b/datafusion/sql/src/unparser/dialect.rs
index fe278a0e1e..a3367dd96c 100644
--- a/datafusion/sql/src/unparser/dialect.rs
+++ b/datafusion/sql/src/unparser/dialect.rs
@@ -635,6 +635,18 @@ 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 20db6bc0cd..686650b1c8 100644
--- a/datafusion/sql/src/unparser/expr.rs
+++ b/datafusion/sql/src/unparser/expr.rs
@@ -1860,8 +1860,9 @@ mod tests {
use sqlparser::ast::ExactNumberInfo;
use crate::unparser::dialect::{
- CharacterLengthStyle, CustomDialect, CustomDialectBuilder,
DateFieldExtractStyle,
- DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect,
ScalarFnToSqlHandler,
+ BigQueryDialect, CharacterLengthStyle, CustomDialect,
CustomDialectBuilder,
+ DateFieldExtractStyle, DefaultDialect, Dialect, DuckDBDialect,
PostgreSqlDialect,
+ ScalarFnToSqlHandler,
};
use super::*;
@@ -3349,6 +3350,7 @@ mod tests {
Arc::new(CustomDialectBuilder::new().build());
let duckdb_dialect: Arc<dyn Dialect> = Arc::new(DuckDBDialect::new());
+ let bigquery_dialect: Arc<dyn Dialect> =
Arc::new(BigQueryDialect::new());
for (dialect, scalar, expected) in [
(
@@ -3409,6 +3411,36 @@ mod tests {
),
"CAST('2025-09-15 11: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)",
+ ),
+ (
+ Arc::clone(&bigquery_dialect),
+ ScalarValue::TimestampMillisecond(
+ Some(1757934000123),
+ Some("+01:00".into()),
+ ),
+ "CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
+ ),
+ (
+ Arc::clone(&bigquery_dialect),
+ ScalarValue::TimestampMicrosecond(
+ Some(1757934000123456),
+ Some("-01:00".into()),
+ ),
+ "CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
+ ),
+ (
+ Arc::clone(&bigquery_dialect),
+ ScalarValue::TimestampNanosecond(
+ Some(1757934000123456789),
+ Some("+00:00".into()),
+ ),
+ "CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
+ ),
] {
let unparser = Unparser::new(dialect.as_ref());
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]