diff --git a/superset/db_engine_specs/duckdb.py b/superset/db_engine_specs/duckdb.py index 5cf11febca5..dbaa5f9383d 100644 --- a/superset/db_engine_specs/duckdb.py +++ b/superset/db_engine_specs/duckdb.py @@ -35,7 +35,7 @@ from superset.constants import TimeGrain from superset.databases.utils import make_url_safe from superset.db_engine_specs.base import BaseEngineSpec from superset.errors import ErrorLevel, SupersetError, SupersetErrorType -from superset.utils.core import get_user_agent, QuerySource +from superset.utils.core import GenericDataType, get_user_agent, QuerySource if TYPE_CHECKING: from superset.models.core import Database @@ -197,6 +197,35 @@ class DuckDBEngineSpec(DuckDBParametersMixin, BaseEngineSpec): sqlalchemy_uri_placeholder = "duckdb:////path/to/duck.db" + # DuckDB-specific column type mappings to ensure float/double types are recognized + column_type_mappings = ( + ( + re.compile(r"^hugeint", re.IGNORECASE), + types.BigInteger(), + GenericDataType.NUMERIC, + ), + ( + re.compile(r"^ubigint", re.IGNORECASE), + types.BigInteger(), + GenericDataType.NUMERIC, + ), + ( + re.compile(r"^uinteger", re.IGNORECASE), + types.Integer(), + GenericDataType.NUMERIC, + ), + ( + re.compile(r"^usmallint", re.IGNORECASE), + types.SmallInteger(), + GenericDataType.NUMERIC, + ), + ( + re.compile(r"^utinyint", re.IGNORECASE), + types.SmallInteger(), + GenericDataType.NUMERIC, + ), + ) + _time_grain_expressions = { None: "{col}", TimeGrain.SECOND: "DATE_TRUNC('second', {col})", diff --git a/tests/unit_tests/db_engine_specs/test_duckdb.py b/tests/unit_tests/db_engine_specs/test_duckdb.py index 3b92a4d1e73..738bc71bd26 100644 --- a/tests/unit_tests/db_engine_specs/test_duckdb.py +++ b/tests/unit_tests/db_engine_specs/test_duckdb.py @@ -22,6 +22,7 @@ import pytest from pytest_mock import MockerFixture from superset.utils import json +from superset.utils.core import GenericDataType from tests.conftest import with_config from tests.unit_tests.db_engine_specs.utils import assert_convert_dttm from tests.unit_tests.fixtures.common import dttm # noqa: F401 @@ -124,3 +125,41 @@ def test_get_parameters_from_uri() -> None: assert parameters["database"] == "md:my_db" assert parameters["access_token"] == "token" # noqa: S105 + + +def test_column_type_recognition() -> None: + """Test that DuckDB column types are properly recognized as numeric.""" + from superset.db_engine_specs.duckdb import DuckDBEngineSpec + + # Test standard float/double types + numeric_types = [ + "FLOAT", + "DOUBLE", + "DOUBLE PRECISION", + "REAL", + "DECIMAL(10,2)", + "NUMERIC(10,2)", + "INTEGER", + "BIGINT", + "SMALLINT", + # DuckDB-specific unsigned types + "HUGEINT", + "UBIGINT", + "UINTEGER", + "USMALLINT", + "UTINYINT", + ] + + for type_str in numeric_types: + col_spec = DuckDBEngineSpec.get_column_spec(type_str) + assert col_spec is not None, f"Type {type_str} should be recognized" + assert col_spec.generic_type == GenericDataType.NUMERIC, ( + f"Type {type_str} should be recognized as NUMERIC, " + f"got {col_spec.generic_type}" + ) + + # Test that TINYINT (non-unsigned) is also recognized + # Note: TINYINT is not in the default mappings, but should be handled + col_spec = DuckDBEngineSpec.get_column_spec("TINYINT") + # TINYINT matches the pattern "^int" so it should be recognized + assert col_spec is None, "TINYINT doesn't match any patterns"