mirror of
https://github.com/apache/superset.git
synced 2026-05-02 06:24:37 +00:00
Compare commits
5 Commits
docs/db-ca
...
fix/postgr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac864cc94b | ||
|
|
0a2b837c89 | ||
|
|
d3c562657a | ||
|
|
be90e08f83 | ||
|
|
4e74dc0250 |
@@ -21,10 +21,11 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from re import Pattern
|
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 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.dialects.postgresql.base import PGInspector
|
||||||
from sqlalchemy.engine.reflection import Inspector
|
from sqlalchemy.engine.reflection import Inspector
|
||||||
from sqlalchemy.engine.url import URL
|
from sqlalchemy.engine.url import URL
|
||||||
@@ -526,8 +527,37 @@ class PostgresEngineSpec(BasicParametersMixin, PostgresBaseEngineSpec):
|
|||||||
ENUM(),
|
ENUM(),
|
||||||
GenericDataType.STRING,
|
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
|
@classmethod
|
||||||
def get_schema_from_engine_params(
|
def get_schema_from_engine_params(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -15,14 +15,14 @@
|
|||||||
# specific language governing permissions and limitations
|
# specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
from sqlalchemy import column, types
|
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.interfaces import Dialect
|
||||||
from sqlalchemy.engine.url import make_url
|
from sqlalchemy.engine.url import make_url
|
||||||
|
|
||||||
@@ -87,6 +87,8 @@ def test_convert_dttm(
|
|||||||
("TIME", types.Time, None, GenericDataType.TEMPORAL, True),
|
("TIME", types.Time, None, GenericDataType.TEMPORAL, True),
|
||||||
# Boolean
|
# Boolean
|
||||||
("BOOLEAN", types.Boolean, None, GenericDataType.BOOLEAN, False),
|
("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(
|
def test_get_column_spec(
|
||||||
@@ -363,3 +365,41 @@ class TestRedshiftDetection:
|
|||||||
spec.update_params_from_encrypted_extra(database, params)
|
spec.update_params_from_encrypted_extra(database, params)
|
||||||
|
|
||||||
assert "pool_events" not in 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