Skip to content

fix(sqllab): wrap cost-estimate JSON parse to prevent raw JSONDecodeError leak - #43888

Open
eschutho wants to merge 1 commit into
masterfrom
fix-presto-estimate-cost-jsondecodeerror
Open

fix(sqllab): wrap cost-estimate JSON parse to prevent raw JSONDecodeError leak#43888
eschutho wants to merge 1 commit into
masterfrom
fix-presto-estimate-cost-jsondecodeerror

Conversation

@eschutho

@eschutho eschutho commented Sep 4, 2026

Copy link
Copy Markdown
Member

SUMMARY

Running Estimate cost in SQL Lab against a Presto/Trino table that lacks computed statistics currently crashes with an opaque 500 GENERIC_BACKEND_ERROR instead of a clean, typed error.

Root cause. QueryEstimationCommand.run() (superset/commands/sql_lab/estimate.py) calls db_engine_spec.estimate_query_cost(...). For Presto/Trino, PrestoBaseEngineSpec.estimate_statement_cost runs EXPLAIN (TYPE IO, FORMAT JSON) <statement> and does json.loads(cursor.fetchone()[0]) using the simplejson-backed superset.utils.json (allow_nan=False). Trino/Presto represent unknown/uncollected cost statistics as a literal NaN token in that JSON (e.g. {"estimate": {"outputRowCount": NaN, ...}}), which happens for any table without an ANALYZE. allow_nan=False then makes json.loads raise a raw simplejson.errors.JSONDecodeError.

That exception is not caught anywhere in the call chain:

  • BaseEngineSpec.estimate_query_cost calls estimate_statement_cost with no guard;
  • QueryEstimationCommand.run() only wraps the call in try/except SupersetTimeoutException;
  • SqlLabRestApi.estimate_query_cost has no try/except.

So it falls through to Flask's global @app.errorhandler(Exception) and leaks to the user as an unhandled 500.

FIX

Add a sibling except json.JSONDecodeError block to the existing try/except around the estimate_query_cost(...) call, converting it to a typed SupersetErrorException. This is a backend/driver response-parsing failure (missing table statistics on the remote engine), not a user-input error, so it is bucketed as 500 GENERIC_BACKEND_ERROR (code 1011) rather than 400 GENERIC_COMMAND_ERROR.

This mirrors the pre-existing sibling conversion in the very same function — the raw jinja2.TemplateError handling added by #42757 (fix(sqllab): wrap process_template() in QueryEstimationCommand to prevent raw UndefinedError leak) — and continues the SQL Lab exception-cleanup series (#43883 / #43795 / #43772).

The change is strictly additive: successful estimates are unchanged; only the previously-unhandled malformed/NaN cost-estimate JSON now returns a clean typed 500.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend error-handling change.

Before: opaque unhandled 500 GENERIC_BACKEND_ERROR from an escaped simplejson.JSONDecodeError.
After: typed SupersetErrorException500 GENERIC_BACKEND_ERROR with message "Unable to parse the cost estimate returned by the database."

TESTING INSTRUCTIONS

Automated (new regression test in tests/unit_tests/commands/sql_lab/test_estimate.py):

pytest tests/unit_tests/commands/sql_lab/test_estimate.py

test_run_wraps_raw_jsondecodeerror_from_cost_estimation mocks estimate_query_cost to raise the concrete simplejson JSONDecodeError (via superset.utils.json.JSONDecodeError, which is simplejson.errors.JSONDecodeError — distinct from the stdlib class) and asserts run() raises SupersetErrorException with .status == 500 and .error.error_type == GENERIC_BACKEND_ERROR. Verified failing on pre-fix code and passing after.

Manual: in SQL Lab, point at a Presto/Trino database, select a table with no computed statistics, enter any SELECT, and click Estimate cost. Previously an opaque 500; now a clean typed backend error.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration (follow approval process in SIP-59)
    • 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

🤖 Generated with Claude Code

…rror leak

QueryEstimationCommand.run() calls estimate_query_cost(), which for
Presto/Trino parses the EXPLAIN (TYPE IO, FORMAT JSON) output via
simplejson-backed superset.utils.json (allow_nan=False). When a table
lacks computed statistics, the engine emits a literal NaN token, so
json.loads() raises a raw simplejson JSONDecodeError that propagates
uncaught through the command and the /api/v1/sqllab/estimate/ endpoint,
surfacing to the user as an opaque 500 GENERIC_BACKEND_ERROR crash.

Catch json.JSONDecodeError as a sibling of the existing
SupersetTimeoutException handler and convert it to a typed
SupersetErrorException (500, GENERIC_BACKEND_ERROR) -- a backend/driver
response-parsing failure, not user input error -- mirroring the sibling
TemplateError conversion in the same function (#42757).

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

bito-code-review Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #af741d

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: cd109e0..cd109e0
    • superset/commands/sql_lab/estimate.py
    • tests/unit_tests/commands/sql_lab/test_estimate.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 an incremental AI Review.

  • /review full - 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 Sep 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 79.45%. Comparing base (d9b201d) to head (cd109e0).
⚠️ Report is 3 commits behind head on master.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #43888   +/-   ##
=======================================
  Coverage   79.45%   79.45%           
=======================================
  Files        2895     2895           
  Lines      168167   168170    +3     
  Branches    38995    38995           
=======================================
+ Hits       133624   133627    +3     
  Misses      32044    32044           
  Partials     2499     2499           
Flag Coverage Δ
hive 37.77% <25.00%> (-0.01%) ⬇️
mysql 57.48% <25.00%> (-0.01%) ⬇️
postgres 57.51% <25.00%> (-0.01%) ⬇️
presto 39.66% <25.00%> (-0.01%) ⬇️
python 83.87% <100.00%> (+<0.01%) ⬆️
sqlite 57.21% <25.00%> (-0.01%) ⬇️
unit 74.41% <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.

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.

1 participant