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

rusackas 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 c792752a58b fix(db_engine_specs): preserve DATE semantics when 
applying Postgres time grains (#42254) (#42329)
c792752a58b is described below

commit c792752a58b1e4d553df95beb70a8a20f8f4bed0
Author: Amogh Atreya <[email protected]>
AuthorDate: Tue Jul 28 06:01:28 2026 +0530

    fix(db_engine_specs): preserve DATE semantics when applying Postgres time 
grains (#42254) (#42329)
    
    Co-authored-by: Amin Ghadersohi <[email protected]>
---
 superset/db_engine_specs/postgres.py              | 27 +++++++++++++
 tests/unit_tests/db_engine_specs/test_postgres.py | 47 +++++++++++++++++++++++
 2 files changed, 74 insertions(+)

diff --git a/superset/db_engine_specs/postgres.py 
b/superset/db_engine_specs/postgres.py
index 4be3006a1ad..1945fa3f303 100644
--- a/superset/db_engine_specs/postgres.py
+++ b/superset/db_engine_specs/postgres.py
@@ -29,6 +29,7 @@ from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, 
ENUM, INTERVAL, JSO
 from sqlalchemy.dialects.postgresql.base import PGInspector
 from sqlalchemy.engine.reflection import Inspector
 from sqlalchemy.engine.url import URL
+from sqlalchemy.sql.expression import ColumnClause
 from sqlalchemy.types import Date, DateTime, String
 
 from superset.constants import TimeGrain
@@ -36,6 +37,7 @@ from superset.db_engine_specs.base import (
     BaseEngineSpec,
     BasicParametersMixin,
     DatabaseCategory,
+    TimestampExpression,
 )
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import SupersetException, SupersetSecurityException
@@ -256,6 +258,31 @@ class PostgresBaseEngineSpec(BaseEngineSpec):
     def epoch_to_dttm(cls) -> str:
         return "(timestamp 'epoch' + {col} * interval '1 second')"
 
+    @classmethod
+    def get_timestamp_expr(
+        cls,
+        col: ColumnClause,
+        pdf: str | None,
+        time_grain: str | None,
+    ) -> TimestampExpression:
+        """
+        Construct a timestamp expression while preserving pure ``DATE`` 
semantics.
+
+        Applying ``DATE_TRUNC`` to a ``DATE`` column implicitly casts the 
value to
+        ``TIMESTAMP``, which can trigger unwanted timezone conversion on the 
client
+        and shift the displayed date by a day. To avoid this, the truncated 
value is
+        cast back to ``DATE`` when the source column is a pure ``DATE`` type.
+
+        See https://github.com/apache/superset/issues/42254.
+        """
+        expr = super().get_timestamp_expr(col, pdf, time_grain)
+        col_type = getattr(col, "type", None)
+        # ``DateTime``/``TIMESTAMP`` are distinct SQLAlchemy types (not 
subclasses
+        # of ``Date``), so this only matches pure ``DATE`` columns.
+        if time_grain and isinstance(col_type, Date):
+            return TimestampExpression(f"CAST({expr.name} AS DATE)", col, 
type_=Date())
+        return expr
+
     @classmethod
     def convert_dttm(
         cls, target_type: str, dttm: datetime, db_extra: dict[str, Any] | None 
= None
diff --git a/tests/unit_tests/db_engine_specs/test_postgres.py 
b/tests/unit_tests/db_engine_specs/test_postgres.py
index 49dc70fe502..287fcec38a3 100644
--- a/tests/unit_tests/db_engine_specs/test_postgres.py
+++ b/tests/unit_tests/db_engine_specs/test_postgres.py
@@ -22,6 +22,7 @@ from unittest.mock import MagicMock
 import pytest
 from pytest_mock import MockerFixture
 from sqlalchemy import column, types
+from sqlalchemy.dialects import postgresql
 from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, 
JSON
 from sqlalchemy.engine.interfaces import Dialect
 from sqlalchemy.engine.url import make_url
@@ -370,6 +371,52 @@ class TestRedshiftDetection:
         assert "pool_events" not in params
 
 
+def _compile(expr: Any) -> str:
+    return str(expr.compile(None, dialect=postgresql.dialect()))
+
+
+def test_get_timestamp_expr_date_column_casts_back_to_date() -> None:
+    """
+    DB Eng Specs (postgres): a time grain on a pure DATE column casts the
+    ``DATE_TRUNC`` result back to DATE to avoid timezone-driven date shifts.
+
+    See https://github.com/apache/superset/issues/42254.
+    """
+    col = column("event_date", type_=types.Date())
+    expr = spec.get_timestamp_expr(col, None, "P1D")
+    assert _compile(expr) == "CAST(DATE_TRUNC('day', event_date) AS DATE)"
+
+
+def test_get_timestamp_expr_datetime_column_not_cast() -> None:
+    """
+    DB Eng Specs (postgres): DATETIME/TIMESTAMP columns keep their timestamp
+    semantics and are not cast back to DATE.
+    """
+    col = column("event_ts", type_=types.DateTime())
+    expr = spec.get_timestamp_expr(col, None, "P1D")
+    assert _compile(expr) == "DATE_TRUNC('day', event_ts)"
+
+
+def test_get_timestamp_expr_date_column_without_grain_not_cast() -> None:
+    """
+    DB Eng Specs (postgres): without a time grain there is no DATE_TRUNC, so 
the
+    column is left untouched.
+    """
+    col = column("event_date", type_=types.Date())
+    expr = spec.get_timestamp_expr(col, None, None)
+    assert _compile(expr) == "event_date"
+
+
+def test_get_timestamp_expr_untyped_column_not_cast() -> None:
+    """
+    DB Eng Specs (postgres): columns without a known type (e.g. raw 
expressions)
+    are not cast to DATE.
+    """
+    col = column("some_expr")
+    expr = spec.get_timestamp_expr(col, None, "P1Y")
+    assert _compile(expr) == "DATE_TRUNC('year', some_expr)"
+
+
 def test_interval_type_mutator() -> None:
     """
     DB Eng Specs (postgres): Test INTERVAL type mutator

Reply via email to