This is an automated email from the ASF dual-hosted git repository.

aminghadersohi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 3e36b242a61 fix(datafusion): render temporal SQL that DataFusion 
accepts (#44700)
3e36b242a61 is described below

commit 3e36b242a61337a40877a8f545e077f1b1879287
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Thu Oct 1 00:01:53 2026 +1000

    fix(datafusion): render temporal SQL that DataFusion accepts (#44700)
---
 superset/db_engine_specs/datafusion.py             | 34 ++++++++++++++++------
 .../unit_tests/db_engine_specs/test_datafusion.py  | 32 +++++++-------------
 2 files changed, 36 insertions(+), 30 deletions(-)

diff --git a/superset/db_engine_specs/datafusion.py 
b/superset/db_engine_specs/datafusion.py
index ec7a9ac4f64..5e344b97965 100644
--- a/superset/db_engine_specs/datafusion.py
+++ b/superset/db_engine_specs/datafusion.py
@@ -25,6 +25,20 @@ from superset.db_engine_specs import BaseEngineSpec
 from superset.db_engine_specs.base import DatabaseCategory
 
 
+def _date_bin(interval: str) -> str:
+    """
+    Bucket from the Unix epoch without unsupported interval/Float64 arithmetic.
+
+    The UTC-epoch origin aligns buckets to UTC, while ``DATE_TRUNC`` aligns to
+    the column's local time. On timezone-aware columns in zones with a :45
+    offset (e.g. Asia/Kathmandu), sub-hour grains therefore land on different
+    boundaries than the hour grain: PT30M maps 13:32:33 to 13:15, PT1H to 
13:00.
+    A per-zone origin is not possible because DataFusion only accepts a literal
+    origin.
+    """
+    return f"DATE_BIN(INTERVAL '{interval}', {{col}}, TIMESTAMP '1970-01-01 
00:00:00')"
+
+
 class DataFusionEngineSpec(BaseEngineSpec):
     engine_name = "Apache DataFusion"
     engine = "datafusion"
@@ -59,13 +73,13 @@ class DataFusionEngineSpec(BaseEngineSpec):
     _time_grain_expressions = {
         None: "{col}",
         TimeGrain.SECOND: "DATE_TRUNC('second', {col})",
-        TimeGrain.FIVE_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '5 
seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 5)",  # noqa: E501
-        TimeGrain.THIRTY_SECONDS: "DATE_TRUNC('minute', {col}) + INTERVAL '30 
seconds' * FLOOR(EXTRACT(SECOND FROM {col}) / 30)",  # noqa: E501
+        TimeGrain.FIVE_SECONDS: _date_bin("5 seconds"),
+        TimeGrain.THIRTY_SECONDS: _date_bin("30 seconds"),
         TimeGrain.MINUTE: "DATE_TRUNC('minute', {col})",
-        TimeGrain.FIVE_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '5 
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 5)",  # noqa: E501
-        TimeGrain.TEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '10 
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 10)",  # noqa: E501
-        TimeGrain.FIFTEEN_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '15 
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 15)",  # noqa: E501
-        TimeGrain.THIRTY_MINUTES: "DATE_TRUNC('hour', {col}) + INTERVAL '30 
minutes' * FLOOR(EXTRACT(MINUTE FROM {col}) / 30)",  # noqa: E501
+        TimeGrain.FIVE_MINUTES: _date_bin("5 minutes"),
+        TimeGrain.TEN_MINUTES: _date_bin("10 minutes"),
+        TimeGrain.FIFTEEN_MINUTES: _date_bin("15 minutes"),
+        TimeGrain.THIRTY_MINUTES: _date_bin("30 minutes"),
         TimeGrain.HOUR: "DATE_TRUNC('hour', {col})",
         TimeGrain.DAY: "DATE_TRUNC('day', {col})",
         TimeGrain.WEEK: "DATE_TRUNC('week', {col})",
@@ -84,9 +98,11 @@ class DataFusionEngineSpec(BaseEngineSpec):
     ) -> str | None:
         sqla_type = cls.get_sqla_column_type(target_type)
 
+        # DataFusion's to_timestamp()/to_date() take chrono format strings, not
+        # Oracle/PostgreSQL ones, so render ANSI typed literals instead.
         if isinstance(sqla_type, types.Date):
-            return f"TO_DATE('{dttm.date().isoformat()}', 'YYYY-MM-DD')"
+            return f"DATE '{dttm.date().isoformat()}'"
         if isinstance(sqla_type, types.DateTime):
-            dttm_formatted = dttm.isoformat(sep=" ", timespec="milliseconds")
-            return f"""TO_TIMESTAMP('{dttm_formatted}', 'YYYY-MM-DD 
HH24:MI:SS.FFF')"""
+            dttm_formatted = dttm.isoformat(sep=" ", timespec="microseconds")
+            return f"TIMESTAMP '{dttm_formatted}'"
         return None
diff --git a/tests/unit_tests/db_engine_specs/test_datafusion.py 
b/tests/unit_tests/db_engine_specs/test_datafusion.py
index 0595f885cfd..a369cf79ebc 100644
--- a/tests/unit_tests/db_engine_specs/test_datafusion.py
+++ b/tests/unit_tests/db_engine_specs/test_datafusion.py
@@ -26,15 +26,11 @@ from tests.unit_tests.fixtures.common import dttm  # noqa: 
F401
 @pytest.mark.parametrize(
     "target_type,expected_result",
     [
-        ("Date", "TO_DATE('2019-01-02', 'YYYY-MM-DD')"),
-        (
-            "DateTime",
-            "TO_TIMESTAMP('2019-01-02 03:04:05.678', 'YYYY-MM-DD 
HH24:MI:SS.FFF')",
-        ),
-        (
-            "TimeStamp",
-            "TO_TIMESTAMP('2019-01-02 03:04:05.678', 'YYYY-MM-DD 
HH24:MI:SS.FFF')",
-        ),
+        ("Date", "DATE '2019-01-02'"),
+        ("DATE", "DATE '2019-01-02'"),
+        ("DateTime", "TIMESTAMP '2019-01-02 03:04:05.678900'"),
+        ("TimeStamp", "TIMESTAMP '2019-01-02 03:04:05.678900'"),
+        ("TIMESTAMP", "TIMESTAMP '2019-01-02 03:04:05.678900'"),
         ("UnknownType", None),
     ],
 )
@@ -57,34 +53,28 @@ def test_epoch_to_dttm() -> None:
         ("PT1S", "DATE_TRUNC('second', ts)"),
         (
             "PT5S",
-            "DATE_TRUNC('minute', ts) + INTERVAL '5 seconds' * "
-            "FLOOR(EXTRACT(SECOND FROM ts) / 5)",
+            "DATE_BIN(INTERVAL '5 seconds', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         (
             "PT30S",
-            "DATE_TRUNC('minute', ts) + INTERVAL '30 seconds' * "
-            "FLOOR(EXTRACT(SECOND FROM ts) / 30)",
+            "DATE_BIN(INTERVAL '30 seconds', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         ("PT1M", "DATE_TRUNC('minute', ts)"),
         (
             "PT5M",
-            "DATE_TRUNC('hour', ts) + INTERVAL '5 minutes' * "
-            "FLOOR(EXTRACT(MINUTE FROM ts) / 5)",
+            "DATE_BIN(INTERVAL '5 minutes', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         (
             "PT10M",
-            "DATE_TRUNC('hour', ts) + INTERVAL '10 minutes' * "
-            "FLOOR(EXTRACT(MINUTE FROM ts) / 10)",
+            "DATE_BIN(INTERVAL '10 minutes', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         (
             "PT15M",
-            "DATE_TRUNC('hour', ts) + INTERVAL '15 minutes' * "
-            "FLOOR(EXTRACT(MINUTE FROM ts) / 15)",
+            "DATE_BIN(INTERVAL '15 minutes', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         (
             "PT30M",
-            "DATE_TRUNC('hour', ts) + INTERVAL '30 minutes' * "
-            "FLOOR(EXTRACT(MINUTE FROM ts) / 30)",
+            "DATE_BIN(INTERVAL '30 minutes', ts, TIMESTAMP '1970-01-01 
00:00:00')",
         ),
         ("PT1H", "DATE_TRUNC('hour', ts)"),
         ("P1D", "DATE_TRUNC('day', ts)"),

Reply via email to