Skip to content

Commit a910b03

Browse files
authored
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 ](#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
1 parent 9f893a4 commit a910b03

File tree

2 files changed

+46
-2
lines changed

2 files changed

+46
-2
lines changed

datafusion/sql/src/unparser/dialect.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,18 @@ impl Dialect for BigQueryDialect {
635635
fn unnest_as_table_factor(&self) -> bool {
636636
true
637637
}
638+
639+
fn timestamp_with_tz_to_string(&self, dt: DateTime<Tz>, unit: TimeUnit) -> String {
640+
// https://docs.cloud.google.com/bigquery/docs/reference/standard-sql/data-types#timestamp_type
641+
let format = match unit {
642+
TimeUnit::Second => "%Y-%m-%d %H:%M:%S%:z",
643+
TimeUnit::Millisecond => "%Y-%m-%d %H:%M:%S%.3f%:z",
644+
TimeUnit::Microsecond => "%Y-%m-%d %H:%M:%S%.6f%:z",
645+
TimeUnit::Nanosecond => "%Y-%m-%d %H:%M:%S%.9f%:z",
646+
};
647+
648+
dt.format(format).to_string()
649+
}
638650
}
639651

640652
impl BigQueryDialect {

datafusion/sql/src/unparser/expr.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1860,8 +1860,9 @@ mod tests {
18601860
use sqlparser::ast::ExactNumberInfo;
18611861

18621862
use crate::unparser::dialect::{
1863-
CharacterLengthStyle, CustomDialect, CustomDialectBuilder, DateFieldExtractStyle,
1864-
DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect, ScalarFnToSqlHandler,
1863+
BigQueryDialect, CharacterLengthStyle, CustomDialect, CustomDialectBuilder,
1864+
DateFieldExtractStyle, DefaultDialect, Dialect, DuckDBDialect, PostgreSqlDialect,
1865+
ScalarFnToSqlHandler,
18651866
};
18661867

18671868
use super::*;
@@ -3349,6 +3350,7 @@ mod tests {
33493350
Arc::new(CustomDialectBuilder::new().build());
33503351

33513352
let duckdb_dialect: Arc<dyn Dialect> = Arc::new(DuckDBDialect::new());
3353+
let bigquery_dialect: Arc<dyn Dialect> = Arc::new(BigQueryDialect::new());
33523354

33533355
for (dialect, scalar, expected) in [
33543356
(
@@ -3409,6 +3411,36 @@ mod tests {
34093411
),
34103412
"CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
34113413
),
3414+
// BigQuery: should be no space between timestamp and timezone
3415+
(
3416+
Arc::clone(&bigquery_dialect),
3417+
ScalarValue::TimestampSecond(Some(1757934000), Some("+00:00".into())),
3418+
"CAST('2025-09-15 11:00:00+00:00' AS TIMESTAMP)",
3419+
),
3420+
(
3421+
Arc::clone(&bigquery_dialect),
3422+
ScalarValue::TimestampMillisecond(
3423+
Some(1757934000123),
3424+
Some("+01:00".into()),
3425+
),
3426+
"CAST('2025-09-15 12:00:00.123+01:00' AS TIMESTAMP)",
3427+
),
3428+
(
3429+
Arc::clone(&bigquery_dialect),
3430+
ScalarValue::TimestampMicrosecond(
3431+
Some(1757934000123456),
3432+
Some("-01:00".into()),
3433+
),
3434+
"CAST('2025-09-15 10:00:00.123456-01:00' AS TIMESTAMP)",
3435+
),
3436+
(
3437+
Arc::clone(&bigquery_dialect),
3438+
ScalarValue::TimestampNanosecond(
3439+
Some(1757934000123456789),
3440+
Some("+00:00".into()),
3441+
),
3442+
"CAST('2025-09-15 11:00:00.123456789+00:00' AS TIMESTAMP)",
3443+
),
34123444
] {
34133445
let unparser = Unparser::new(dialect.as_ref());
34143446

0 commit comments

Comments
 (0)