Skip to content

fix(query-object): reject malformed ad-hoc metrics - #43353

Open
sadpandajoe wants to merge 3 commits into
masterfrom
fix-sankey-calculated-column-metric-error
Open

fix(query-object): reject malformed ad-hoc metrics#43353
sadpandajoe wants to merge 3 commits into
masterfrom
fix-sankey-calculated-column-metric-error

Conversation

@sadpandajoe

Copy link
Copy Markdown
Member

SUMMARY

A metric shaped like a Custom SQL ad-hoc metric (carries sqlExpression, aggregate, or column) but missing expressionType was silently coerced by QueryObject._set_metrics into a bare string, on the assumption it was a legacy {"label": "name"} reference to a saved metric. For a Custom SQL metric the label is the SQL expression text itself, so metric resolution then looked that text up among the dataset's saved metrics, found nothing, and raised a misleading Metric '<sql text>' does not exist error instead of a clear validation error.

This PR narrows the coercion: a dict carrying any ad-hoc-only key (sqlExpression, aggregate, column) but missing expressionType is now rejected with a QueryObjectValidationError naming the metric and the missing field. The chart-data API endpoints already catch QueryObjectValidationError around query-context deserialization, so the error surfaces as a clean 400 response instead of the previous misleading failure. The legacy {"label": "name"} saved-metric reference (no ad-hoc keys at all) still resolves exactly as before.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

N/A — backend validation change, not a UI change.

TESTING INSTRUCTIONS

  1. pytest tests/unit_tests/charts/data/malformed_adhoc_metric_test.py -v
  2. pytest tests/unit_tests/common/test_query_object_factory.py -v

The new test file covers: a malformed ad-hoc metric now raises a clear validation error instead of silently resolving to a bogus saved-metric lookup; the pre-existing legacy label-only reference still resolves correctly; a well-formed Custom SQL metric is unaffected.

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

sadpandajoe and others added 3 commits August 20, 2026 00:39
A metric object shaped like a Custom SQL ad-hoc metric but missing
`expressionType` is silently rewritten into a bare string by
`QueryObject._set_metrics`, because `is_adhoc_metric` classifies a metric
solely by the presence of that one key. The string used is the metric's
`label`, which for a Custom SQL metric is the SQL text itself, so metric
resolution then reports `Metric '<sql text>' does not exist`.

`ChartDataAdhocMetricSchema` declares `expressionType` as required, but
`ChartDataQueryObjectSchema.metrics` is a list of `fields.Raw`, so that
contract is never enforced at the API boundary.

Adds a regression guard that reproduces the coercion through the real
request-deserialization path and the resulting error through the real query
builder, plus two tests fencing a fix from over-correcting: the legacy
`{"label": ...}` saved-metric reference must keep collapsing to its name, and
a well-formed Custom SQL metric must stay an ad-hoc definition.

The first test is red until the coercion is narrowed; the fix is not applied
here. RCA.md records the analysis, including the unresolved question of what
omits `expressionType` from the request in the first place.

Co-Authored-By: Claude <noreply@anthropic.com>
QueryObject._set_metrics supported a legacy {"label": "name"} shape as a
reference to a metric saved on the dataset, and fell back to that
coercion for any dict that wasn't recognized as an ad-hoc metric (i.e.
missing `expressionType`). This meant a Custom SQL ad-hoc metric that
lost its `expressionType` key on the way in was silently reduced to its
auto-derived label, which is the SQL expression text itself. Downstream
metric resolution then looked that string up among the dataset's saved
metrics, missed, and raised a misleading "Metric '<sql text>' does not
exist" error.

The fix narrows the fallback: a dict is only treated as a legacy
saved-metric-name reference when it carries none of the keys that are
exclusive to an ad-hoc metric definition (sqlExpression, aggregate,
column). A dict that carries one of those keys but is missing
expressionType is now left as-is instead of being collapsed to a
string, so it no longer masquerades as a request for a saved metric
that was never intended.
…y preserving them

The previous version of this fix left a malformed ad-hoc metric (one that
carries ad-hoc-only keys like sqlExpression/aggregate/column but is missing
expressionType) as an untouched dict, expecting later validation to catch it.
In practice nothing downstream recognizes that shape either, so it fell
through to a bare ValueError instead of a QueryObjectValidationError -
an uncontrolled error rather than a clean, user-facing one.

QueryObject._set_metrics now raises QueryObjectValidationError directly for
this shape. The chart-data API endpoints already catch
QueryObjectValidationError around query-context deserialization and return a
400, so the error surfaces as a controlled response instead of propagating
uncaught.

The guard test is updated to assert the raise (with a message naming the
missing field) instead of asserting on a returned value, since the malformed
metric is now rejected at deserialization time rather than passed through.
@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 66.66667% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.63%. Comparing base (c2d653b) to head (e5434b2).
⚠️ Report is 14 commits behind head on master.

Files with missing lines Patch % Lines
superset/common/query_object.py 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #43353      +/-   ##
==========================================
- Coverage   66.73%   66.63%   -0.10%     
==========================================
  Files        2876     2876              
  Lines      164201   164538     +337     
  Branches    37887    37938      +51     
==========================================
+ Hits       109580   109645      +65     
- Misses      52465    52729     +264     
- Partials     2156     2164       +8     
Flag Coverage Δ
hive 38.02% <0.00%> (-0.09%) ⬇️
mysql 57.60% <66.66%> (-0.16%) ⬇️
postgres 57.64% <66.66%> (-0.16%) ⬇️
presto 39.94% <50.00%> (-0.10%) ⬇️
python 59.02% <66.66%> (-0.17%) ⬇️
sqlite 57.28% <66.66%> (-0.16%) ⬇️
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.

@sadpandajoe
sadpandajoe marked this pull request as ready for review August 20, 2026 18:44
@dosubot dosubot Bot added the explore:metrics Related to metrics of Explore label Aug 20, 2026
@bito-code-review

bito-code-review Bot commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #38188d

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 6ef6996..e5434b2
    • superset/common/query_object.py
    • tests/unit_tests/charts/data/malformed_adhoc_metric_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

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

Labels

explore:metrics Related to metrics of Explore review:draft size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant