Skip to content

fix(filters): cast non-text columns to string for server-side filter value search - #45892

Open
eschutho wants to merge 2 commits into
masterfrom
fix-filter-values-search-non-text-column
Open

eschutho wants to merge 2 commits into
masterfrom
fix-filter-values-search-non-text-column

Conversation

@eschutho

Copy link
Copy Markdown
Member

SUMMARY

Searching (?q=) in a filter value dropdown on a non-text column (e.g. a NUMERIC column on PostgreSQL) returned a 500, because the generated predicate was lower(<numeric col>) LIKE ... and lower() is not defined for non-string types on strict engines.

Root cause: build_like_predicate in superset/models/helpers.py always did sa.func.lower(expr).contains(...). It is called from ExploreMixin.values_for_column, so any non-string column failed on engines such as PostgreSQL (function lower(numeric) does not exist).

Fix: if expr.type is not a sa.String subtype (numeric, integer, boolean, date, or untyped/NullType), wrap it in sa.cast(expr, sa.String) before lower(). Text columns are untouched, so their generated SQL is unchanged. BigQuery's no-ESCAPE behaviour is preserved.

Tradeoffs

  • Numeric search now matches against the CAST(... AS VARCHAR) string representation. How engines render floats/booleans as strings differs, so matches on those types are engine-dependent.
  • No change for text columns.
  • ClickHouse rejecting ESCAPE on this same predicate is a known, separate issue and is not addressed here.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A

TESTING INSTRUCTIONS

  • pytest tests/unit_tests/models/helpers_test.py -k like_predicate
  • Manually: on a Postgres dataset with a numeric column, open a filter on that column and type a search term in the value dropdown; it should return matching values instead of an error.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

…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 <noreply@anthropic.com>
@codeant-ai-for-open-source

codeant-ai-for-open-source Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

CodeAnt  CodeAnt PR Risk

🟢  Low Risk

  • The PR appears safe to merge; no actionable issues were identified in the review.

Assessed commit: ac524871085a

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.73%. Comparing base (9e4292e) to head (ac52487).
⚠️ Report is 10 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #45892   +/-   ##
=======================================
  Coverage   84.73%   84.73%           
=======================================
  Files        3069     3069           
  Lines      205771   205793   +22     
  Branches    48396    48400    +4     
=======================================
+ Hits       174354   174376   +22     
  Misses      28123    28123           
  Partials     3294     3294           
Flag Coverage Δ
hive 34.50% <0.00%> (-0.01%) ⬇️
mysql 52.60% <0.00%> (-0.01%) ⬇️
postgres 52.60% <0.00%> (-0.01%) ⬇️
presto 36.23% <0.00%> (-0.01%) ⬇️
python 87.76% <100.00%> (+<0.01%) ⬆️
sqlite 52.37% <0.00%> (-0.01%) ⬇️
unit 82.52% <100.00%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rebenitez1802 rebenitez1802 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: the approach is right and fixes the 500 on Postgres and Trino, but the cast type has no length, which breaks Oracle (and Db2) on columns that work today. Details and applyable suggestions inline.

Comment thread superset/models/helpers.py Outdated
Comment on lines +257 to +258
if not isinstance(expr.type, sa.String):
expr = sa.cast(expr, sa.String)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 High — Cast without a length is invalid SQL on Oracle and Db2

sa.cast(expr, sa.String) has no length. Oracle renders it as CAST(n AS VARCHAR2), which fails with ORA-00906: missing left parenthesis. On a real Oracle Free 23, base lower(n) LIKE '%4%' returns the row, and this branch errors for NUMBER, DATE and untyped calculated columns, all of which Oracle converts implicitly on master. Db2 (ibm_db_sa) renders VARCHAR(None). Typed text columns are not affected.

Giving the cast type a length where the dialect needs one fixes it: VARCHAR2(4000 CHAR) on Oracle and NVARCHAR(max) on MSSQL (see the next comment). Every other dialect is unchanged (VARCHAR/CHAR/STRING):

Suggested change
if not isinstance(expr.type, sa.String):
expr = sa.cast(expr, sa.String)
if not isinstance(expr.type, 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)

Comment thread superset/models/helpers.py Outdated
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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — MSSQL: untyped calculated columns lose non-codepage characters

Calculated columns usually have no type, so they now get CAST(... AS VARCHAR(max)), which is non-Unicode. On SQL Server 2022 with the default SQL_Latin1_General_CP1_CI_AS collation, CAST(N'Ñandú 東京' AS VARCHAR(max)) returns Ñandú ??. A search for 東京 on a calculated UPPER(nvc) column returns 1 row on master and 0 on this branch. The sa.Unicode() mssql variant in the suggestion above covers it.

Comment on lines +174 to +178
# 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium — Tests miss the untyped and dialect-specific cases

This test only compiles for Postgres and BigQuery. Changing the existing test to sa.column("c", sa.String()) hides the change in behavior for untyped columns rather than asserting it, and a "skip untyped columns" mutation still passes every test. With the fix above applied, this version passes and fails again if the fix is reverted:

Suggested change
# 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
# 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()
)
assert "ESCAPE" not in bigquery_sql

…le, 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 <noreply@anthropic.com>
@netlify

netlify Bot commented Oct 30, 2026

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit ac52487
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6ae505e785680e229d599784
😎 Deploy Preview https://deploy-preview-45892--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants