From 4e74dc0250f4503d74189aeb25ed745ec1c0f3c7 Mon Sep 17 00:00:00 2001 From: Evan Rusackas Date: Fri, 1 Aug 2025 14:52:24 -0700 Subject: [PATCH] fix(charts): handle PostgreSQL INTERVAL type in bar and pie charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PostgreSQL INTERVAL types were causing bar and pie charts to fail rendering when used as metrics. This fix converts INTERVAL values (timedelta objects) to numeric seconds so they can be properly displayed in charts. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- superset/db_engine_specs/postgres.py | 13 +++++++++++-- .../db_engine_specs/test_postgres.py | 19 +++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/superset/db_engine_specs/postgres.py b/superset/db_engine_specs/postgres.py index b1b265c78eb..8398f7b0e68 100644 --- a/superset/db_engine_specs/postgres.py +++ b/superset/db_engine_specs/postgres.py @@ -21,10 +21,10 @@ import logging import re from datetime import datetime from re import Pattern -from typing import Any, Optional, TYPE_CHECKING +from typing import Any, Callable, Optional, TYPE_CHECKING from flask_babel import gettext as __ -from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, JSON +from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON from sqlalchemy.dialects.postgresql.base import PGInspector from sqlalchemy.engine.reflection import Inspector from sqlalchemy.engine.url import URL @@ -526,8 +526,17 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec): ENUM(), GenericDataType.STRING, ), + ( + re.compile(r"^interval", re.IGNORECASE), + INTERVAL(), + GenericDataType.NUMERIC, + ), ) + column_type_mutators: dict[Any, Callable[[Any], Any]] = { + INTERVAL: lambda v: v.total_seconds() if hasattr(v, "total_seconds") else v, + } + @classmethod def get_schema_from_engine_params( cls, diff --git a/tests/unit_tests/db_engine_specs/test_postgres.py b/tests/unit_tests/db_engine_specs/test_postgres.py index 88ce789e131..c4d3e109dc9 100644 --- a/tests/unit_tests/db_engine_specs/test_postgres.py +++ b/tests/unit_tests/db_engine_specs/test_postgres.py @@ -15,14 +15,14 @@ # specific language governing permissions and limitations # under the License. -from datetime import datetime +from datetime import datetime, timedelta from typing import Any, Optional from unittest.mock import MagicMock import pytest from pytest_mock import MockerFixture from sqlalchemy import column, types -from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, JSON +from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION, ENUM, INTERVAL, JSON from sqlalchemy.engine.interfaces import Dialect from sqlalchemy.engine.url import make_url @@ -363,3 +363,18 @@ class TestRedshiftDetection: spec.update_params_from_encrypted_extra(database, params) assert "pool_events" not in params + + +def test_interval_type_mutator() -> None: + """ + DB Eng Specs (postgres): Test INTERVAL type mutator + """ + # Test timedelta conversion + td = timedelta(days=1, hours=2, minutes=30, seconds=45) + mutator = spec.column_type_mutators[INTERVAL] + assert mutator(td) == 95445.0 # Total seconds: 1*86400 + 2*3600 + 30*60 + 45 + + # Test that non-timedelta values pass through unchanged + assert mutator("not a timedelta") == "not a timedelta" + assert mutator(12345) == 12345 + assert mutator(None) is None