Skip to content

fix(sqla): don't mislabel DB errors as ColumnNotFoundException in adhoc_column_to_sqla - #42889

Merged
eschutho merged 1 commit into
masterfrom
fix-adhoc-column-error-mislabel
Aug 7, 2026
Merged

fix(sqla): don't mislabel DB errors as ColumnNotFoundException in adhoc_column_to_sqla#42889
eschutho merged 1 commit into
masterfrom
fix-adhoc-column-error-mislabel

Conversation

@eschutho

@eschutho eschutho commented Aug 7, 2026

Copy link
Copy Markdown
Member

SUMMARY

Fixes Sentry SUPERSET-PYTHON-WE6 (3677 events / 28 users, still firing) — Shortcut SC-116904.

Root cause: SqlaTable.adhoc_column_to_sqla() (in superset/connectors/sqla/models.py) wrapped the entire adhoc-column type-probe in a try/except SupersetGenericDBErrorException that relabeled every such error as ColumnNotFoundException:

if not col_desc:
    raise SupersetGenericDBErrorException("Column not found")
...
except SupersetGenericDBErrorException as ex:
    raise ColumnNotFoundException(message=str(ex)) from ex

But get_columns_description() (in superset/connectors/sqla/utils.py) wraps any exception thrown while executing the probe query — real DB/connectivity failures like SSL EOF, timeouts, and dropped connections — in a SupersetGenericDBErrorException. So the except above caught two very different things and mislabeled both as "column not found":

  1. the deliberate zero-columns sentinel raised two lines above, and
  2. genuine transient DB errors that escaped get_columns_description().

Why this matters beyond Sentry noise: ColumnNotFoundException is caught in superset/models/helpers.py (where_clause's adhoc-filter handling), where the column is treated as non-existent and the filter is silently dropped from the query. A transient SSL/connection blip during the type-probe therefore produced wrong query results (a missing filter) rather than a surfaced DB error.

Fix: removed the blanket try/except SupersetGenericDBErrorException wrapper around the probe and changed the genuine zero-columns case to raise ColumnNotFoundException("Column not found") directly. Now only a true empty probe result maps to ColumnNotFoundException; a real DB failure from get_columns_description() propagates unchanged with its original message. The now-unused SupersetGenericDBErrorException import was removed from models.py (verified it is not referenced anywhere else in the file). The get_from_clause/compile_sqla_query calls above are unchanged.

TRADEOFFS

DB/connectivity errors during the adhoc-column type-probe now surface as SupersetGenericDBErrorException (a 400 GENERIC_DB_ENGINE_ERROR) instead of being silently converted into a dropped filter. This is an intentional failure-mode change: the user will see a database error where before they'd have silently gotten results with the adhoc filter missing. This matches how DB errors are handled everywhere else in this code path — surfacing a transient DB error is strictly better than silently returning wrong data — but it is a visible behavior change and is flagged here explicitly.

adhoc_column_to_sqla has other callers (models/helpers.py: groupby, orderby, select construction) that don't catch ColumnNotFoundException today. For those, a probe DB error changes from a 404 ColumnNotFoundException ("Column not found") to a 400 SupersetGenericDBErrorException with the real DB message — still surfaced as an error either way, not a new silent failure, but the specific exception type/status code a caller might see does change.

TESTING INSTRUCTIONS

Added a regression test and confirmed the genuine not-found path is unchanged.

$ pytest tests/unit_tests/models/helpers_test.py -k adhoc_column -x -q
.................                                                        [100%]
17 passed, 132 deselected, 1 warning in 2.37s

$ pytest tests/unit_tests/models/helpers_test.py \
    -k "propagates_db_error or without_comment_safe_retry_hook" -v
tests/unit_tests/models/helpers_test.py::test_adhoc_column_type_probe_propagates_db_error PASSED
tests/unit_tests/models/helpers_test.py::test_adhoc_column_type_probe_raises_without_comment_safe_retry_hook PASSED
  • New test test_adhoc_column_type_probe_propagates_db_error mocks get_columns_description with side_effect=SupersetGenericDBErrorException("SSL error: unexpected eof while reading") and asserts adhoc_column_to_sqla(..., force_type_check=True) raises SupersetGenericDBErrorException with that message and not ColumnNotFoundException.
  • Existing test_adhoc_column_type_probe_raises_without_comment_safe_retry_hook confirms the genuine empty-result case still raises ColumnNotFoundException — no regression.

Lint (repo-pinned ruff 0.9.7):

$ ruff check superset/connectors/sqla/models.py tests/unit_tests/models/helpers_test.py
All checks passed!
$ ruff format --check superset/connectors/sqla/models.py tests/unit_tests/models/helpers_test.py
2 files already formatted

ADDITIONAL INFORMATION

  • Has associated issue: Sentry SUPERSET-PYTHON-WE6 / Shortcut SC-116904
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

…oc_column_to_sqla (SC-116904)

The type-probe in SqlaTable.adhoc_column_to_sqla wrapped the whole probe in
a try/except SupersetGenericDBErrorException that relabeled every such error
as ColumnNotFoundException. Because get_columns_description wraps any
execution failure (SSL EOF, timeouts, dropped connections) as a
SupersetGenericDBErrorException, real transient DB errors were mislabeled as
"column not found". where_clause's adhoc-filter handling then silently dropped
the filter instead of surfacing the DB error.

Only the genuine empty-result case now raises ColumnNotFoundException; a real
DB/connectivity failure from get_columns_description propagates unchanged.

Fixes SUPERSET-PYTHON-WE6

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@bito-code-review

bito-code-review Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #1f0664

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 349a7f6..349a7f6
    • superset/connectors/sqla/models.py
    • tests/unit_tests/models/helpers_test.py
  • Files skipped - 0
  • Tools
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ Successful
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful

Bito Usage Guide

Commands

Type the following command in the pull request comment and save the comment.

  • /review - Manually triggers a full AI review.

  • /pause - Pauses automatic reviews on this pull request.

  • /resume - Resumes automatic reviews.

  • /resolve - Marks all Bito-posted review comments as resolved.

  • /abort - Cancels all in-progress reviews.

Refer to the documentation for additional commands.

Configuration

This repository uses Superset You can customize the agent settings here or contact your Bito workspace admin at evan@preset.io.

Documentation & Help

AI Code Review powered by Bito Logo

@codecov

codecov Bot commented Aug 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.00000% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.37%. Comparing base (fe06ebe) to head (349a7f6).

Files with missing lines Patch % Lines
superset/connectors/sqla/models.py 60.00% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #42889   +/-   ##
=======================================
  Coverage   66.37%   66.37%           
=======================================
  Files        2857     2857           
  Lines      161048   161045    -3     
  Branches    37046    37046           
=======================================
- Hits       106892   106891    -1     
+ Misses      52141    52139    -2     
  Partials     2015     2015           
Flag Coverage Δ
hive 38.26% <0.00%> (+<0.01%) ⬆️
mysql 57.75% <0.00%> (+<0.01%) ⬆️
postgres 57.80% <60.00%> (+<0.01%) ⬆️
presto 40.22% <0.00%> (+<0.01%) ⬆️
python 59.20% <60.00%> (+<0.01%) ⬆️
sqlite 57.42% <0.00%> (+<0.01%) ⬆️
unit 100.00% <ø> (ø)

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.

@eschutho
eschutho requested a review from rebenitez1802 August 7, 2026 15:21

@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.

Approve — correct, well-scoped fix that removes a real mislabeling bug, with the security model intact (no authz/RLS impact; adhoc filters aren't an entitlement gate). No blockers. The root-cause analysis is precise and the refactor is behavior-preserving for every non-target exception — nicely surgical. Two items are worth addressing before merge (disclosure + a caller-level test); the rest are optional polish.

🟡 Medium — Behavior change is broader than TRADEOFFS admits
get_columns_description funnels every driver exception into SupersetGenericDBErrorException (superset/connectors/sqla/utils.py:212, blanket except Exception). So the newly-propagating path covers not just transient SSL/timeout blips (the stated target) but also the common, non-transient case: an adhoc filter whose expression references a dropped/renamed/typo'd column — that raises inside cursor.execute, becomes SupersetGenericDBErrorException, and never reaches the if not col_desc branch. On the filter path (superset/models/helpers.py:4024, the only catcher, always force_type_check=True) that flips from render-with-rejected-filter-notice to a hard 400 GENERIC_DB_ENGINE_ERROR — so a saved dashboard with a stale adhoc filter that renders today will hard-fail after deploy. This is defensible as the safer fail-closed direction (silently dropping a WHERE predicate serves under-filtered data as HTTP 200), so it's a judgment call, not a defect — but the TRADEOFFS framing ("transient DB/connectivity errors") understates the affected population. Fix: name the stale/renamed-column case in TRADEOFFS + add an UPDATING.md note; ideally distinguish "known-missing column" from "transient connectivity error" at the source so genuinely-missing columns keep degrading gracefully.

🟢 Low — No caller-level test for the behavior that actually changes
The new test_adhoc_column_type_probe_propagates_db_error asserts only the exception type at the adhoc_column_to_sqla boundary; nothing exercises the user-visible consequence through where_clause (propagate vs. silent-drop). A future widening of that except ColumnNotFoundException to also catch SupersetGenericDBErrorException would silently reintroduce this exact bug and the new test would still pass. There's a ready copy-target: test_numeric_adhoc_filter_value_is_unquoted_in_where_clause already drives get_sqla_query with a patched get_columns_description. Fix: add a get_sqla_query-level test asserting a probe SupersetGenericDBErrorException propagates (filter not appended to rejected_filter_columns), plus a small explicit test that patches get_columns_description to return [] and asserts ColumnNotFoundException. (The empty-result → 404 branch is already covered by test_adhoc_column_type_probe_raises_without_comment_safe_retry_hook, so that half is only a make-it-explicit nicety.)

🟢 Low — In-code comment is inaccurate about when ColumnNotFoundException fires
The new comment (superset/connectors/sqla/models.py:1878-1881) says "only a genuine empty result (the column truly isn't there) is a ColumnNotFoundException." That's misleading: the if not col_desc branch is reachable only when the probe succeeds but returns an empty cursor.description — a driver quirk (e.g. clickhouse-connect zero-row probes, which the utils.py retry logic exists for). A genuinely-missing column raises during execute and becomes SupersetGenericDBErrorException instead — the opposite of what the comment implies, and the exact case the whole PR turns on. Fix: reword to note it fires only for the empty-cursor.description driver-quirk case, not a genuinely-missing column.

🟢 Low — Transient probe blip is now fatal on the filter path, with no retry
get_columns_description opens its own raw connection purely for type inference (utils.py:187), and its only retry (get_column_description_retry_sql) covers empty cursor.description, not connection errors. Post-change, a momentary blip during this best-effort probe kills a chart whose main data query might have succeeded. This is a deliberate tradeoff (surfacing beats silent wrong data), not a bug — but since transient resilience is the stated motivation, consider a bounded retry / best-effort fallback for the probe, or at least call this out explicitly.

🟢 Low (nit) — Redundant inline import in the test
test_adhoc_column_type_probe_propagates_db_error re-imports from unittest.mock import patch inside the function although patch is already imported at module scope (helpers_test.py:27). Unlike the file's deferred superset-module imports, unittest.mock needs no app-context deferral. Fix: drop the inline import.

@eschutho
eschutho merged commit eb8592d into master Aug 7, 2026
77 checks passed
@eschutho
eschutho deleted the fix-adhoc-column-error-mislabel branch August 7, 2026 22:03
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