From ac59268b5336743b837ccc4bc54db1667904c9fb Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Fri, 30 Oct 2026 15:05:23 +0000 Subject: [PATCH 1/2] fix(filters): cast non-text columns to string for server-side filter value search lower() is undefined for numeric/boolean/date columns on strict engines such as PostgreSQL, so searching a filter value dropdown on a non-text column raised an error. Cast non-string expressions to a string before lower(); string columns are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- superset/models/helpers.py | 6 +++++ tests/unit_tests/models/helpers_test.py | 35 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index fddb0947996f..cb3d5f166825 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -249,7 +249,13 @@ def build_like_predicate( ``lower(expr) LIKE lower('%term%')`` is used rather than ``ILIKE`` because the latter is not portable across engines. + + ``lower()`` is only defined for string types on strict engines (e.g. + PostgreSQL), so non-string expressions (numeric, boolean, date, or + untyped) are cast to a string first. String columns are left untouched. """ + if not isinstance(expr.type, sa.String): + expr = sa.cast(expr, sa.String) return sa.func.lower(expr).contains(search.lower(), autoescape=True) diff --git a/tests/unit_tests/models/helpers_test.py b/tests/unit_tests/models/helpers_test.py index 4ef8a2ff4f13..1508b14d3402 100644 --- a/tests/unit_tests/models/helpers_test.py +++ b/tests/unit_tests/models/helpers_test.py @@ -115,7 +115,7 @@ def test_build_like_predicate_is_case_insensitive_and_escaped() -> None: from superset.models.helpers import build_like_predicate - predicate = build_like_predicate(sa.column("c"), "50%") + predicate = build_like_predicate(sa.column("c", sa.String()), "50%") # The search term is lower-cased for a case-insensitive match, and the # user's ``%`` wildcard is neutralized (autoescape) rather than matching @@ -145,6 +145,39 @@ def test_build_like_predicate_is_case_insensitive_and_escaped() -> None: assert "ESCAPE" not in bigquery_sql +def test_build_like_predicate_casts_non_text_columns() -> None: + import sqlalchemy as sa + from sqlalchemy_bigquery import BigQueryDialect + + from superset.models.helpers import build_like_predicate + + postgres = sa.dialects.registry.load("postgresql")() + + def compile_sql(predicate: sa.ColumnElement, dialect: sa.engine.Dialect) -> str: + return str( + predicate.compile(dialect=dialect, compile_kwargs={"literal_binds": True}) + ) + + for column_type in (sa.Numeric(), sa.Integer()): + sql = compile_sql( + build_like_predicate(sa.column("n", column_type), "0"), postgres + ) + assert "lower(CAST(n AS VARCHAR))" in sql + + # String columns are left untouched: no CAST is introduced. + text_sql = compile_sql( + build_like_predicate(sa.column("s", sa.String()), "a"), postgres + ) + assert "lower(s)" in text_sql + assert "CAST" not in text_sql + + # BigQuery still has no ESCAPE clause for a non-text column. + bigquery_sql = compile_sql( + build_like_predicate(sa.column("n", sa.Numeric()), "0"), BigQueryDialect() + ) + assert "ESCAPE" not in bigquery_sql + + def test_values_for_column_search(database: Database) -> None: """``search`` narrows the distinct-value query in the database.""" import pandas as pd From ac524871085a0067d456056192e7b8edda3a852a Mon Sep 17 00:00:00 2001 From: Elizabeth Thompson Date: Fri, 30 Oct 2026 21:02:25 +0000 Subject: [PATCH 2/2] fix(filters): give non-text search cast a length/unicode type on Oracle, Db2, MSSQL Address review: bare CAST(... AS VARCHAR) is invalid on Oracle/Db2 and non-Unicode on MSSQL. Use dialect variants and extend unit tests. Co-Authored-By: Claude Sonnet 5.5 --- superset/models/helpers.py | 11 ++++++++++- tests/unit_tests/models/helpers_test.py | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/superset/models/helpers.py b/superset/models/helpers.py index cb3d5f166825..158574579462 100644 --- a/superset/models/helpers.py +++ b/superset/models/helpers.py @@ -255,7 +255,16 @@ def build_like_predicate( untyped) are cast to a string first. String columns are left untouched. """ if not isinstance(expr.type, sa.String): - expr = sa.cast(expr, sa.String) + # A bare VARCHAR is invalid where a length is required (Oracle renders + # VARCHAR2, Db2 VARCHAR(None)); MSSQL needs NVARCHAR to keep characters + # outside the database code page. + cast_type = ( + sa.String() + .with_variant(sa.String(4000), "oracle") + .with_variant(sa.String(32672), "ibm_db_sa") + .with_variant(sa.Unicode(), "mssql") + ) + expr = sa.cast(expr, cast_type) return sa.func.lower(expr).contains(search.lower(), autoescape=True) diff --git a/tests/unit_tests/models/helpers_test.py b/tests/unit_tests/models/helpers_test.py index 1508b14d3402..36af9172616e 100644 --- a/tests/unit_tests/models/helpers_test.py +++ b/tests/unit_tests/models/helpers_test.py @@ -171,6 +171,24 @@ def compile_sql(predicate: sa.ColumnElement, dialect: sa.engine.Dialect) -> str: assert "lower(s)" in text_sql assert "CAST" not in text_sql + # Untyped expressions (e.g. calculated columns) are cast as well. + untyped_sql = compile_sql( + build_like_predicate(sa.literal_column("upper(s)"), "a"), postgres + ) + assert "lower(CAST(upper(s) AS VARCHAR))" in untyped_sql + + # Dialects that need a length or a unicode type get one. + oracle_sql = compile_sql( + build_like_predicate(sa.column("n", sa.Numeric()), "0"), + sa.dialects.registry.load("oracle")(), + ) + assert "CAST(n AS VARCHAR2(4000 CHAR))" in oracle_sql + mssql_sql = compile_sql( + build_like_predicate(sa.column("n", sa.Numeric()), "0"), + sa.dialects.registry.load("mssql")(), + ) + assert "CAST(n AS NVARCHAR(max))" in mssql_sql + # BigQuery still has no ESCAPE clause for a non-text column. bigquery_sql = compile_sql( build_like_predicate(sa.column("n", sa.Numeric()), "0"), BigQueryDialect()