Repository navigation
fix(bigquery): avoid ESCAPE clause in column-value typeahead search (SC-121408) - #44429
Conversation
…SC-121408) BigQuery datasets threw `DatabaseError: Syntax error: Expected end of input but got keyword ESCAPE` when a user typed into a filter's column-value typeahead search box. `build_like_predicate` compiled a `LIKE ... ESCAPE '!'` clause via `.like(pattern, escape=...)`. SQLAlchemy's base compiler always appends a literal `ESCAPE '<char>'` for LIKE, and `sqlalchemy-bigquery`'s `BigQueryCompiler` does not override `visit_like_op_binary` / `visit_not_like_op_binary`, so the `ESCAPE '!'` clause leaked verbatim into GoogleSQL, which has no ESCAPE keyword and rejects it. Switch to `.contains(search, autoescape=True)`, which IS covered by BigQueryCompiler's `visit_contains_op_binary` override (it pops the escape modifier and re-encodes wildcards using BigQuery's backslash convention). Other engines still emit a dialect-native `LIKE ... ESCAPE` predicate, so behavior is unchanged: same case-insensitive substring match everywhere. The now-unused `LIKE_ESCAPE_CHAR` constant and `escape_like_pattern()` helper are removed. Fixes SUPERSET-PYTHON-176K Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code Review Agent Run #53d44bActionable Suggestions - 0Review Details
Bito Usage GuideCommands Type the following command in the pull request comment and save the comment.
Refer to the documentation for additional commands. Configuration This repository uses Documentation & Help |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #44429 +/- ##
==========================================
- Coverage 80.57% 80.56% -0.01%
==========================================
Files 2940 2940
Lines 175316 175312 -4
Branches 40696 40696
==========================================
- Hits 141253 141247 -6
- Misses 31398 31400 +2
Partials 2665 2665
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@rebenitez1802 — routing this to you. It's the same Pipeline-authored (eschutho, Sentry burndown SUPERSET-PYTHON-176K / SC-121408), self-reviewed, CI green-bar, small diff (+34/-45, two files, new regression test). Two things worth confirming in your read: (1) that Your tracker load is the highest on the roster right now — happy to re-route if you're underwater. |
rebenitez1802
left a comment
There was a problem hiding this comment.
Approve: correct, well-targeted fix. The reported bug is real — sqlalchemy-bigquery's BigQueryCompiler overrides visit_contains_op_binary but not visit_like_op_binary, so the old .like(pattern, escape="!") leaked a literal ESCAPE '!' clause into GoogleSQL, which rejects it. Switching to .contains(..., autoescape=True) is the right primitive: BigQuery renders wildcard-escaping in its own syntax (no ESCAPE keyword) while other engines keep their dialect-native LIKE ... ESCAPE. The removed escape_like_pattern/LIKE_ESCAPE_CHAR are fully unreferenced, the new sqlalchemy_bigquery test import is already available in the unit-test CI env (development.txt), the updated test is a genuine regression guard (fails on the old code, passes on the new), and the security model is intact (value still reaches SQL via literal_binds single-quote escaping; wildcards remain neutralized). A few non-blocking notes below.
🟢 Low — Private Sentry issue ID committed into public source
The new comment at tests/unit_tests/models/helpers_test.py:130 embeds a private preset-inc.sentry.io issue short-ID (it's absent on master, so this diff introduces it into permanent public git history). The comment reads fine without it:
# Regression test: BigQuery's GoogleSQL has no ESCAPE keyword, so the
# compiled predicate must not emit one.
🟢 Low — PR title/body carry private tracker references
The trailing (SC-…) in the title and the Shortcut/Sentry URLs in the description are private Preset references on a public PR; a squash-merge would also carry the story ID into the public commit message. Consider dropping the story ID from the title and the internal URLs from the body before merge.
🟢 Low — BigQuery: literal backslash in the search term matches the wrong rows
autoescape only escapes /, %, and _, and sqlalchemy-bigquery's _maybe_reescape leaves a raw backslash untouched — yet GoogleSQL uses \ as the LIKE escape character. So a search containing \ (e.g. c\d) compiles to a pattern where \d is read as an escaped d, matching cd rather than c\d; a lone or trailing \ similarly mis-escapes the wildcard. This is not a regression — the pre-PR BigQuery path was fully broken by the syntax error, so this is a strict improvement — and there's no security impact (results stay within a column the user can already query). Fine to merge as-is; optionally pre-escape backslashes before .contains, or note the limitation.
SUMMARY
Fixes a
DatabaseError: Syntax error: Expected end of input but got keyword ESCAPEthrown on BigQuery datasets whenever a user types into a filter's column-value typeahead search box.Root cause
The typeahead path is
DatasourceRestApi.get_column_values→SqlaTable.values_for_column→build_like_predicate(added by #43518, "feat(filters): search filter values server-side in Explore").build_like_predicatebuilt its predicate with:SQLAlchemy's base compiler (
visit_like_op_binary/visit_not_like_op_binary) always appends a literalESCAPE '<char>'clause when an escape char is passed to.like().sqlalchemy-bigquery'sBigQueryCompilerdoes not override those two methods — it only overridesvisit_contains_op_binary/visit_startswith_op_binary/visit_endswith_op_binary(via a_maybe_reescapehelper that pops theescapemodifier and re-encodes wildcards using BigQuery's backslash-escape convention, since GoogleSQL'sLIKEhas noESCAPEkeyword). So the literalESCAPE '!'leaked straight into the SQL sent to BigQuery, which rejects it — exactly the reported syntax error.Reproduced by compiling the predicate against
BigQueryDialect():Fix
Switch
build_like_predicateto SQLAlchemy's.contains(..., autoescape=True)operator instead of a hand-rolled.like(pattern, escape=...)string..contains()compiles throughvisit_contains_op_binary, which BigQueryCompiler does override, so BigQuery renders wildcard-escaping in its own supported syntax (backslash-escaping, noESCAPEclause) while other engines keep their dialect-nativeLIKE ... ESCAPEpredicate.Compiled output after the fix:
The now-unused
LIKE_ESCAPE_CHARconstant andescape_like_pattern()helper are removed (build_like_predicatewas their only caller — confirmed by grep).TESTING INSTRUCTIONS
Manual: on a BigQuery dataset in Explore, open a filter on a string column and type into the value typeahead — values now load instead of erroring.
Automated (
tests/unit_tests/models/helpers_test.py):test_escape_like_pattern(function no longer exists).test_build_like_predicate_is_case_insensitive_and_escaped— asserts the postgres compilation still lower-cases + escapes and still emits anESCAPEclause (other engines unaffected), and compiles againstsqlalchemy_bigquery.BigQueryDialect()asserting"ESCAPE" not inthe SQL (the regression test for this bug).test_values_for_column_search—.contains()compiles to... LIKE '%' || 'ali' || '%'on sqlite, so it now asserts bothLIKEand'ali'appear.Results:
pytest tests/unit_tests/models/helpers_test.py -v→ 178 passedpytest tests/unit_tests/models/→ 526 passedruff check+ruff format --checkon both files → cleanpre-commit run --files ...(mypy, ruff, pylint) → passedTradeoffs
escape_like_pattern's custom!-based wildcard escaping is replaced by SQLAlchemy's dialect-default escape char (e.g./on postgres/mysql). This is purely an internal SQL-generation detail and is not observable to users.Follow-ups
None expected. There are currently no other BigQuery
LIKE-with-escape call sites (confirmed by grep —build_like_predicatewas the sole user of the removed helpers). If any newLIKE ... ESCAPEcall sites are added in the future, they should be checked against BigQuery for the sameESCAPE-clause incompatibility.ADDITIONAL INFORMATION
🤖 Generated with Claude Code