mirror of
https://github.com/apache/superset.git
synced 2026-04-29 04:54:21 +00:00
Compare commits
5 Commits
enxdev/fix
...
fix/postgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac864cc94b | ||
|
|
0a2b837c89 | ||
|
|
d3c562657a | ||
|
|
be90e08f83 | ||
|
|
4e74dc0250 |
@@ -21,10 +21,11 @@ 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 import types
|
||||
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 +527,37 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
|
||||
ENUM(),
|
||||
GenericDataType.STRING,
|
||||
),
|
||||
(
|
||||
re.compile(r"^interval", re.IGNORECASE),
|
||||
INTERVAL(),
|
||||
GenericDataType.NUMERIC,
|
||||
),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _normalize_interval(v: Any) -> Any:
|
||||
"""Convert PostgreSQL INTERVAL values to milliseconds.
|
||||
|
||||
psycopg2 returns timedelta objects which we convert to milliseconds for
|
||||
numeric operations in bar/pie charts. Using milliseconds allows users to
|
||||
apply the built-in "DURATION" number format for human-readable display
|
||||
(e.g., "1d 2h 30m 45s").
|
||||
|
||||
Returns None for values that cannot be converted to preserve NULL semantics
|
||||
and avoid mixed-type columns.
|
||||
"""
|
||||
if v is None:
|
||||
return None
|
||||
if hasattr(v, "total_seconds"):
|
||||
return v.total_seconds() * 1000
|
||||
if isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
return float(v) * 1000
|
||||
return None # Can't convert to numeric — treat as missing
|
||||
|
||||
column_type_mutators: dict[types.TypeEngine, Callable[[Any], Any]] = {
|
||||
INTERVAL: _normalize_interval.__func__, # type: ignore[attr-defined]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_schema_from_engine_params(
|
||||
cls,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -87,6 +87,8 @@ def test_convert_dttm(
|
||||
("TIME", types.Time, None, GenericDataType.TEMPORAL, True),
|
||||
# Boolean
|
||||
("BOOLEAN", types.Boolean, None, GenericDataType.BOOLEAN, False),
|
||||
# Interval (mapped to NUMERIC for chart rendering)
|
||||
("INTERVAL", INTERVAL, None, GenericDataType.NUMERIC, False),
|
||||
],
|
||||
)
|
||||
def test_get_column_spec(
|
||||
@@ -363,3 +365,41 @@ 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
|
||||
|
||||
INTERVAL values are converted to milliseconds so users can apply
|
||||
the built-in "DURATION" number format for human-readable display.
|
||||
"""
|
||||
mutator = spec.column_type_mutators[INTERVAL]
|
||||
|
||||
# Test timedelta conversion (most common case from psycopg2)
|
||||
# Result is in milliseconds for compatibility with DURATION formatter
|
||||
td = timedelta(days=1, hours=2, minutes=30, seconds=45)
|
||||
assert mutator(td) == 95445000.0 # Total ms: (1*86400 + 2*3600 + 30*60 + 45) * 1000
|
||||
|
||||
# Test zero duration
|
||||
assert mutator(timedelta(0)) == 0.0
|
||||
|
||||
# Test negative interval
|
||||
assert mutator(timedelta(days=-1)) == -86400000.0
|
||||
|
||||
# Test numeric values (assumed to be seconds) are converted to milliseconds
|
||||
assert mutator(12345) == 12345000.0
|
||||
assert mutator(123.45) == 123450.0
|
||||
|
||||
# Test None preserves NULL semantics (not converted to 0)
|
||||
assert mutator(None) is None
|
||||
|
||||
# Test bool is not treated as numeric (bool is subclass of int in Python)
|
||||
assert mutator(True) is None
|
||||
assert mutator(False) is None
|
||||
|
||||
# Test unconvertible types return None to avoid mixed-type columns
|
||||
assert mutator("1 day 02:30:45") is None
|
||||
assert mutator("P1DT2H30M45S") is None
|
||||
assert mutator([1, 2, 3]) is None
|
||||
assert mutator({"days": 1}) is None
|
||||
|
||||
Reference in New Issue
Block a user