Skip to content

feat(database): identify configured database connection errors - #42996

Merged
Antonio-RiveroMartnez merged 1 commit into
apache:masterfrom
Antonio-RiveroMartnez:db-errors
Aug 10, 2026
Merged

feat(database): identify configured database connection errors#42996
Antonio-RiveroMartnez merged 1 commit into
apache:masterfrom
Antonio-RiveroMartnez:db-errors

Conversation

@Antonio-RiveroMartnez

@Antonio-RiveroMartnez Antonio-RiveroMartnez commented Aug 10, 2026

Copy link
Copy Markdown
Member

SUMMARY

Identify connection errors from database connections configured in Superset.

Superset uses separate SQLAlchemy engines for:

  • its own metadata database; and
  • databases added through Settings → Database Connections.

A connection error alone does not reliably show which kind of engine produced it. Extensions otherwise have to guess from the exception class or message, which can be identical across databases.

This PR adds a SQLAlchemy handle_error listener to every engine created for a configured database. The listener marks the existing exception. It does not catch, replace, reclassify, or otherwise change the error.

Python extensions can check it through:

from superset.databases.error_provenance import is_database_engine_error
if is_database_engine_error(exception):
    # SQLAlchemy emitted the error from a configured database engine.
    ...

This works for connection failures from all SQLAlchemy-backed database engines without parsing driver-specific messages. It also observes errors raised through SQLAlchemy Core.

Scope and limitation

This API is intended to identify connection failures from configured database engines. SQLAlchemy's handle_error event also observes SQLAlchemy Core operations, but Superset normally executes queries directly through raw DB-API cursors. Those raw cursor errors bypass the event and are not marked by this PR; is_database_engine_error() returns False for them. A test records this boundary explicitly.

This PR intentionally does not catch every exception leaving get_raw_connection(). That context also contains logging, extension, and other application work, so a broad catch could incorrectly label an unrelated database error as coming from the configured engine. Wrapping or replacing every driver cursor would be invasive and could break driver-specific behavior. Refactoring the engine-spec execution API could cover raw query errors safely, but it is a larger cross-engine behavior change and should be proposed and reviewed separately if needed.

The conservative behavior is therefore: connection errors are identified; raw query errors and anything uncertain remain unidentified.

The listener is installed once, immediately after engine creation and before the engine is cached. Existing exception types, messages, tracebacks, OAuth handling, HTTP responses, and query behavior remain unchanged. If SQLAlchemy does not create a wrapper exception, the listener marks the original exception instead.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable; this change has no UI impact.

TESTING INSTRUCTIONS

Run:

pytest -q tests/unit_tests/databases/error_provenance_test.py
pytest -q tests/unit_tests/models/core_test.py
pytest -q tests/integration_tests/model_tests.py \
  -k 'impersonate_user_presto or adjust_engine_params_mysql or impersonate_user_trino or impersonate_user_hive or test_get_sqla_engine'

Validated locally:

  • Database error tests: 10 passed.
  • Full tests/unit_tests/models/core_test.py: 76 passed.
  • Pre-commit on all changed implementation and test files: passed.
  • The selected integration tests could not start because the local metadata database lacked the css_templates table. The errors occurred during test setup, before the selected test bodies ran.

The tests cover:

  • connection errors and SQLAlchemy Core errors;
  • the raw DB-API cursor limitation;
  • fallback to the original exception when no SQLAlchemy wrapper exists;
  • preservation of exception identity, type, message, and original driver error;
  • public, private, and cached engine paths;
  • exactly one listener per created engine;
  • safe behavior when an exception cannot be marked;
  • behavior when a later listener replaces an exception;
  • existing tests that mock engine creation.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
    • Migration is atomic, supports rollback & is backwards-compatible
    • Confirm DB migration upgrade and downgrade tested
    • Runtime estimates and downtime expectations provided
  • Introduces new feature or API
  • Removes existing feature or API

The implementation uses SQLAlchemy 1.4's handle_error event and ExceptionContext. These assumptions should be reviewed as part of a future SQLAlchemy major-version upgrade.

If another handle_error listener later replaces the marked exception, the replacement is intentionally not marked because it is a different exception.

@dosubot dosubot Bot added the data:databases Related to database configurations and connections label Aug 10, 2026
@bito-code-review

bito-code-review Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #702eb3

Actionable Suggestions - 0
Review Details
  • Files reviewed - 5 · Commit Range: d7bbc76..fa754e0
    • superset/databases/error_provenance.py
    • superset/models/core.py
    • tests/integration_tests/model_tests.py
    • tests/unit_tests/databases/error_provenance_test.py
    • tests/unit_tests/models/core_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

@netlify

netlify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit d7bbc76
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7997088ba47e000849ab41
😎 Deploy Preview https://deploy-preview-42996--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.

@Antonio-RiveroMartnez
Antonio-RiveroMartnez marked this pull request as draft August 10, 2026 09:22
@github-actions github-actions Bot added the github_actions Pull requests that update GitHub Actions code label Aug 10, 2026
@netlify

netlify Bot commented Aug 10, 2026

Copy link
Copy Markdown

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit 4b3ac17
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/6a7998e63fc20700089aa35a
😎 Deploy Preview https://deploy-preview-42996--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.

@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.57143% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.41%. Comparing base (916c502) to head (e109eec).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/databases/error_provenance.py 75.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #42996      +/-   ##
==========================================
- Coverage   66.41%   66.41%   -0.01%     
==========================================
  Files        2858     2859       +1     
  Lines      161446   161460      +14     
  Branches    37190    37191       +1     
==========================================
+ Hits       107222   107230       +8     
- Misses      52187    52191       +4     
- Partials     2037     2039       +2     
Flag Coverage Δ
hive 38.21% <78.57%> (+<0.01%) ⬆️
mysql 57.77% <78.57%> (+<0.01%) ⬆️
postgres 57.82% <78.57%> (-0.01%) ⬇️
presto 40.18% <78.57%> (+<0.01%) ⬆️
python 59.21% <78.57%> (-0.01%) ⬇️
sqlite 57.44% <78.57%> (+<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.

@Antonio-RiveroMartnez Antonio-RiveroMartnez changed the title feat(database): preserve engine error provenance feat(database): identify errors from configured database engines Aug 10, 2026
@github-actions github-actions Bot removed the github_actions Pull requests that update GitHub Actions code label Aug 10, 2026
@Antonio-RiveroMartnez
Antonio-RiveroMartnez marked this pull request as ready for review August 10, 2026 10:09
@dosubot dosubot Bot added the change:backend Requires changing the backend label Aug 10, 2026

@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: clean, well-tested mechanism that does exactly what it claims for connection errors — but the handle_error listener never fires for query/statement errors (Superset runs queries on raw DBAPI cursors), so the feature misses its dominant target case, and the tests only exercise the Core path Superset doesn't use.

Verified solid (checked empirically against the repo's pinned SQLAlchemy 1.4.54, not just read): no circular import; from sqlalchemy.engine import ExceptionContext resolves; the callback is closure-free so the engine cache can't retain Database instances; each cached engine gets exactly one listener and cache hits never re-add; no listener/memory leak on the private prequery path (500-iter GC test clean); suppress is load-bearing and correctly never masks the real error; the marker doesn't alter the exception's type/message/orig; connection-establishment errors are marked.

🟡 Medium — handle_error never fires for query errors; feature misses the dominant case

handle_error is a SQLAlchemy Core event — it only fires for Connection.execute(). Every real Superset query path runs on a raw DBAPI cursor: SQL Lab (get_raw_connectioncursor.execute, sql_lab.py) and charts/datasets (get_dfdb_engine_spec.executecursor.execute, core.py:932, db_engine_specs/base.py:2321). Reproduced on 1.4.54: raw cursor.execute() does not trigger handle_error, so a Gamma user's invalid SELECT against a configured Postgres yields an OperationalError with is_database_engine_error(exc) == False — the exact case the feature exists to identify. Only connection-establishment failures get marked.

Compounding this, the PR's own test_statement_error_is_marked_without_changing_exception asserts on engine.connect() + connection.execute(text(...)), a path Superset never uses for queries, giving false confidence that statement errors are covered.

Fix: mark at the raise site where Superset already catches the raw driver error and knows its provenance (e.g. inside BaseEngineSpec.execute / get_dbapi_mapped_exception, or get_raw_connection) so statement errors are covered too — or explicitly scope the feature and docstrings to "connection errors" and add a test through get_raw_connection/cursor.execute that proves what's actually covered.

🟢 Low — sqlalchemy_exception=None errors silently unmarked, undocumented & untested

error_provenance.py mark_database_engine_error: handle_error also fires when context.sqlalchemy_exception is None (SQLAlchemy's should_wrap=False path — non-DBAPI-Error exceptions surfacing during execution, e.g. UnicodeDecodeError/socket errors, and greenlet/BaseException cancellations). Then setattr(None, ...) raises and is swallowed by suppress, so the propagated exception (context.original_exception) goes unmarked and is_database_engine_error returns False for a genuine configured-engine error. The docstrings only mention the "later listener replaces the exception" case, and test_marker_assignment_failure_does_not_mask_error uses a synthetic UnmarkableError, not None.

Fix: fall back to marking context.original_exception when sqlalchemy_exception is None, or at minimum document the gap and add a None-path test.

🟢 Low — engine event.listen tests assert on total spy count, not the handle_error call

core_test.pytest_get_sqla_engine, test_get_sqla_engine_caches_engine_per_url, test_get_sqla_engine_user_impersonation{,_email}, and test_get_sqla_engine_does_not_cache_unsaved_instances — use listen.assert_called_once_with(...) / call_count == 2 on the global event.listen spy. This passes only because the mocked engine is built as return_value before the spy installs (so sqlite's two internal connect listens aren't counted) and because they call private _get_sqla_engine (no prequery listener). Ironically the PR's own new test_cached_engine_has_one_instance_listener_without_database_closure does it right — it filters call_args_list to args[1] == "handle_error". Any second event.listen added to the path later breaks all five with a misleading "Called 2 times".

Fix: use listen.assert_any_call(engine, "handle_error", mark_database_engine_error) or filter to handle_error calls, matching the sibling test.

🟢 Low — redundant __closure__ is None assertion

test_cached_engine_has_one_instance_listener_without_database_closure asserts callback is mark_database_engine_error and then callback.__closure__ is None. Once identity holds, __closure__ is a fixed property of that module-level symbol, so the second line can't catch anything the first doesn't (a closure-wrapping refactor would fail the is check first). Harmless, but drop it or, if the intent is to guard against a future per-instance closure leaking Database refs, assert it against the callback pulled from the engine's actual registered listeners.

🟢 Low — is_database_engine_error has no in-tree consumer

The read helper is referenced only by its own unit test. Landing a neutral extension hook ahead of a consumer is a legitimate pattern, but with no end-to-end exercise the Medium coverage gap above produces no failing test — the feature merges looking complete while delivering nothing at real query call sites.

Fix: land a consumer (or a fast follow), or state in the PR/module which out-of-tree extension consumes it and which SQLAlchemy execution mode it observes.


Review assisted by a multi-agent pass (5 review dimensions, each finding adversarially verified against SQLAlchemy 1.4.54; 9 candidate findings were reproduced-against and dropped).

@Antonio-RiveroMartnez Antonio-RiveroMartnez changed the title feat(database): identify errors from configured database engines feat(database): identify configured database connection errors Aug 10, 2026
@Antonio-RiveroMartnez

Antonio-RiveroMartnez commented Aug 10, 2026

Copy link
Copy Markdown
Member Author

Thanks @rebenitez1802 — addressed the actionable points in e109eec.

handle_error never fires for query errors

You are correct. This PR does not add raw DB-API query attribution. I chose the alternative suggested in the review: scope the API to configured-database connection failures and document/test the boundary. A real get_raw_connection() / cursor.execute() test now proves that raw cursor errors remain unmarked.

I did not add a broad catch to get_raw_connection() because unrelated logging, extension, or metadata-database work can run while that context is open and could be mislabeled. Cursor proxying risks breaking driver-specific behavior. Covering raw queries safely would require a larger refactor of the engine-spec execution contract and should be reviewed separately rather than hidden in this small connection-error change.

sqlalchemy_exception=None

The listener now falls back to context.original_exception, with a dedicated test.

tests assert on total spy count

The tests now select/assert the handle_error registration instead of relying on the total number of event.listen calls.

redundant __closure__ is None

Removed.

One suggestion was intentionally not implemented:

is_database_engine_error has no in-tree consumer

This PR provides a small extension API without changing Superset's error handling. Adding an artificial in-tree consumer only to demonstrate usage would expand behavior and scope. The module and PR instead state exactly what extensions can consume and which failures it identifies. A behavior-changing consumer can be reviewed separately if Superset needs one.

Validation: provenance tests 10 passed, core_test.py 76 passed, and staged-file pre-commit passed.

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

LGTM

@Antonio-RiveroMartnez
Antonio-RiveroMartnez merged commit 9e2cb69 into apache:master Aug 10, 2026
72 checks passed
@bito-code-review

Copy link
Copy Markdown
Contributor

Bito Automatic Review Skipped – PR Already Merged

Bito scheduled an automatic review for this pull request, but the review was skipped because this PR was merged before the review could be run.
No action is needed if you didn't intend to review it. To get a review, you can type /review in a comment and save it

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

Labels

change:backend Requires changing the backend data:databases Related to database configurations and connections size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants