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]

Reply via email to