From f882bc92fb3216c080bc5dfd14c70143eee857dd Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 6 Jul 2026 11:41:14 -0700 Subject: [PATCH] fix(sqla): cast native UUID columns to string for LIKE/ILIKE filters Table chart server-pagination search builds an ILIKE filter against the searched column. Native UUID columns (PostgreSQL, ClickHouse) map to GenericDataType.STRING, so the existing not-a-string guard skipped the cast and the raw ILIKE hit the UUID column type, which the database rejects (e.g. ClickHouse: "Illegal type UUID of argument of function ilike"). Detect UUID native types and force the string cast so searching UUID columns works. Fixes #41795 Co-Authored-By: Claude Opus 4.8 --- superset/models/helpers.py | 24 ++++++++- tests/unit_tests/models/helpers_test.py | 68 +++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index 8fcff4c7ee4..0a9acd1ac31 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -250,6 +250,18 @@ def json_to_dict(json_str: str) -> dict[Any, Any]: return {} +def is_uuid_native_type(native_type: Optional[str]) -> bool: + """ + Return True if a native column type represents a UUID. + + Engines such as PostgreSQL and ClickHouse expose native UUID column types + (e.g. ``UUID``, ``Nullable(UUID)``) that map to ``GenericDataType.STRING`` + yet reject LIKE/ILIKE against the raw column, so these columns need an + explicit cast to string before pattern matching. + """ + return native_type is not None and "uuid" in native_type.lower() + + def convert_uuids(obj: Any) -> Any: """ Convert UUID objects to str so we can use yaml.safe_dump @@ -3863,7 +3875,11 @@ class ExploreMixin: # pylint: disable=too-many-public-methods utils.FilterOperator.ILIKE, utils.FilterOperator.LIKE, }: - if target_generic_type != GenericDataType.STRING: + # Native UUID columns report GenericDataType.STRING but + # reject LIKE/ILIKE without a cast (see issue #41795) + if target_generic_type != GenericDataType.STRING or ( + is_uuid_native_type(col_type) + ): sqla_col = sa.cast(sqla_col, sa.String) if op == utils.FilterOperator.LIKE: @@ -3874,7 +3890,11 @@ class ExploreMixin: # pylint: disable=too-many-public-methods utils.FilterOperator.NOT_LIKE, utils.FilterOperator.NOT_ILIKE, }: - if target_generic_type != GenericDataType.STRING: + # Native UUID columns report GenericDataType.STRING but + # reject LIKE/ILIKE without a cast (see issue #41795) + if target_generic_type != GenericDataType.STRING or ( + is_uuid_native_type(col_type) + ): sqla_col = sa.cast(sqla_col, sa.String) if op == utils.FilterOperator.NOT_LIKE: diff --git a/tests/unit_tests/models/helpers_test.py b/tests/unit_tests/models/helpers_test.py index 05b5e7a54bc..cf359fb7fd5 100644 --- a/tests/unit_tests/models/helpers_test.py +++ b/tests/unit_tests/models/helpers_test.py @@ -3720,3 +3720,71 @@ def test_simple_metric_quotes_column_requiring_quoting(database: Database) -> No assert f"SUM({column_name})" not in rendered, ( f"Column requiring quoting was emitted unquoted: {rendered}" ) + + +@pytest.mark.parametrize( + "native_type", + ["UUID", "uuid", "Nullable(UUID)"], +) +@pytest.mark.parametrize( + "op", + ["LIKE", "ILIKE", "NOT LIKE", "NOT ILIKE"], +) +def test_like_filter_on_uuid_column_casts_to_string( + database: Database, native_type: str, op: str +) -> None: + """ + LIKE-family filters on native UUID columns must cast the column to string. + + UUID columns map to ``GenericDataType.STRING``, so the generic-type guard + alone skips the string cast — but engines such as PostgreSQL and ClickHouse + reject LIKE/ILIKE against a raw UUID column (issue #41795: table chart + server-pagination search fails with e.g. "Illegal type UUID of argument of + function ilike"). The native column type must force the cast. + """ + from superset.connectors.sqla.models import SqlaTable, TableColumn + + table = SqlaTable( + database=database, + schema=None, + table_name="t", + columns=[TableColumn(column_name="event_id", type=native_type)], + ) + + result = table.get_sqla_query( + columns=["event_id"], + metrics=[], + extras={}, + filter=[{"col": "event_id", "op": op, "val": "abc%"}], + granularity=None, + is_timeseries=False, + orderby=[], + ) + sql = str(result.sqla_query) + assert "CAST" in sql.upper(), f"Expected string cast in SQL: {sql}" + + +def test_like_filter_on_string_column_does_not_cast(database: Database) -> None: + """ + LIKE-family filters on plain string columns must not add a redundant cast. + """ + from superset.connectors.sqla.models import SqlaTable, TableColumn + + table = SqlaTable( + database=database, + schema=None, + table_name="t", + columns=[TableColumn(column_name="b", type="TEXT")], + ) + + result = table.get_sqla_query( + columns=["b"], + metrics=[], + extras={}, + filter=[{"col": "b", "op": "ILIKE", "val": "abc%"}], + granularity=None, + is_timeseries=False, + orderby=[], + ) + sql = str(result.sqla_query) + assert "CAST" not in sql.upper(), f"Unexpected cast in SQL: {sql}"