Skip to content

fix(mcp): surface rejected filter columns in get_chart_data - #43598

Merged
aminghadersohi merged 1 commit into
apache:masterfrom
aminghadersohi:aminghadersohi/fix-mcp-rejected-filter-surfacing
Aug 27, 2026
Merged

fix(mcp): surface rejected filter columns in get_chart_data#43598
aminghadersohi merged 1 commit into
apache:masterfrom
aminghadersohi:aminghadersohi/fix-mcp-rejected-filter-surfacing

Conversation

@aminghadersohi

@aminghadersohi aminghadersohi commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Why

get_chart_data accepts filters through extra_form_data. When a filter names a column
that does not exist on the dataset, the datasource drops that predicate and runs the query
unfiltered. A guard was added to catch this and return a ValidationError, but it reads
rejected_filter_columns off the ChartDataCommand result, and that key never reaches the
tool: _materialize_full_payload deletes it and emits rejected_filters entries
({"reason": ..., "column": ...}) in its place before the payload is returned.

The guard therefore intersected the requested columns against an empty set and never fired,
so the original failure mode was still live: a filter on an unknown column returns every
row with a success response. On an agent-facing API this is worse than an error, because the
caller has no signal that the filter was dropped and will present unfiltered data as filtered.

The existing regression test did not catch this because it mocked ChartDataCommand.run()
with the pre-materialization shape, so it exercised a payload production never produces.

What

Read the rejected columns from rejected_filters, which is the shape every consumer of a
chart-data payload sees, and keep rejected_filter_columns as a fallback for payloads
captured before that conversion. Only entries carrying a string column are considered, which
guards against malformed entries rather than against time extras: get_time_filter_status
rejections do carry a string column such as __time_range. What keeps those from being
attributed to the caller is the intersection with the columns the request actually named, and
test_rejected_requested_filter_columns_ignores_rejected_time_filters pins that behavior.

Blast radius

Limited to the get_chart_data MCP tool. No change to query construction, execution, or the
chart-data REST API: this only reads a field that was already present on the payload. Filters
supplied by the request are the only ones validated, so a stale filter saved on an older chart
config still cannot fail the call.

How to test

tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py

  • test_rejected_requested_filter_columns_reads_materialized_payload — fails on master.
  • test_rejected_requested_filter_columns_ignores_rejected_time_filters — pins that a
    rejected time extra is not reported as a rejected request filter.
  • TestSavedChartExtraFormDataFilters::test_unknown_adhoc_filter_column_returns_validation_error
    now drives the tool with the materialized payload shape; it fails on master, where it
    previously passed against the mocked shape.

Verified by reverting the source change with the tests in place: the two cases above fail,
the remaining 105 pass.

Risk & rollback

Low. The behavior change is that a request naming an unknown filter column now returns a
ValidationError instead of unfiltered rows — the intended behavior, and the reason the
guard exists. A caller relying on the silent-unfiltered response would see an error instead;
that response was incorrect. Straight revert if needed.

Review guidance

Start with _rejected_columns_in_query in get_chart_data.py, then the updated _Command.run()
mock in the test file — the mock shape is the crux of why this went unnoticed.

get_chart_data validated request filters by reading rejected_filter_columns
off the ChartDataCommand result. That key never survives to the tool: the
chart-data payload is materialized before it is returned, which deletes
rejected_filter_columns and emits rejected_filters entries in its place.

The check therefore always intersected against an empty set, and a filter
naming a column that does not exist on the dataset was silently dropped,
returning unfiltered rows as a successful response.

Read rejected_filters, keeping the raw key as a fallback, and fix the
regression test that mocked the pre-materialization shape and so passed
against the broken check.
@codecov

codecov Bot commented Aug 27, 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 79.13%. Comparing base (905a35d) to head (e99e930).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/tool/get_chart_data.py 66.66% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##           master   #43598   +/-   ##
=======================================
  Coverage   79.13%   79.13%           
=======================================
  Files        2879     2879           
  Lines      165745   165770   +25     
  Branches    38315    38321    +6     
=======================================
+ Hits       131159   131180   +21     
- Misses      32103    32106    +3     
- Partials     2483     2484    +1     
Flag Coverage Δ
hive 37.94% <16.66%> (-0.01%) ⬇️
mysql 57.67% <16.66%> (-0.01%) ⬇️
postgres 57.71% <16.66%> (-0.01%) ⬇️
presto 39.86% <16.66%> (-0.01%) ⬇️
python 83.71% <66.66%> (+<0.01%) ⬆️
sqlite 57.40% <16.66%> (-0.01%) ⬇️
unit 73.87% <66.66%> (+<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.

@aminghadersohi
aminghadersohi marked this pull request as ready for review August 27, 2026 17:02
@dosubot dosubot Bot added the api Related to the REST API label Aug 27, 2026

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

Thanks @aminghadersohi, good catch on closing the gap in your own guard from #43478, and the writeup made this easy to verify.

I verified the mechanism and ran everything locally on this branch, in a fresh venv on current master deps:

  • _materialize_full_payload in superset/common/query_actions.py does delete rejected_filter_columns and emit rejected_filters entries in its place, so the old guard was intersecting against an empty set. I also verified it live rather than only through the mocked tests: with a real physical dataset and the real ChartDataCommand.run(), a filter on a nonexistent column produces a payload carrying rejected_filters: [{"reason": "not_in_datasource", "column": "does_not_exist"}], no rejected_filter_columns key, and every row of the table. On this branch the helper returns ["does_not_exist"] and the tool answers with the ValidationError, while the same payload under master's logic yields [] and the unfiltered rows come back as a success.
  • Regression validity: reverting just the source hunk with the new tests in place, exactly test_rejected_requested_filter_columns_reads_materialized_payload and TestSavedChartExtraFormDataFilters::test_unknown_adhoc_filter_column_returns_validation_error fail and the remaining 105 pass, matching the PR description.
  • Full tests/unit_tests/mcp_service suite on this branch: 3655 passed.
  • Error shape: the guard returns a ChartError model, so the rejection reaches the client as structured content with error_type: "ValidationError" instead of a raised ToolError, consistent with the other guidance errors in this tool. The message only echoes back column names the caller itself supplied via the requested & rejected intersection, so nothing new is disclosed.

Just a small nit on the description, not the code: time-extra rejections are not excluded by the string check, since get_time_filter_status rejections do carry string columns like __time_range. It is the intersection with the caller's requested columns that keeps them out, and the new test pins the right behavior either way.

LGTM.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Thanks @gabotorresruiz — you're right, and thanks for verifying it against a real dataset rather than just the mocks.

Corrected the description: the string check only guards against malformed entries. get_time_filter_status rejections do carry a string column like __time_range, and what actually keeps them out is the intersection with the columns the request named. Description-only change; no code touched, so the approved commit e99e930 stands.

@bito-code-review

bito-code-review Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Code Review Agent Run #c2c368

Actionable Suggestions - 0
Additional Suggestions - 1
  • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py - 1
    • Misleading test for reason filtering · Line 118-137
      The test name and docstring imply the code filters `rejected_filters` by reason, but `_rejected_columns_in_query` (get_chart_data.py:97-101) reads ALL entries regardless of reason. The test passes only because `__time_range` is never in the requested set (`country` is requested, not `__time_range`). To actually verify reason-based filtering, add a case where `__time_range` IS requested and assert the result is empty — this would currently fail, revealing the gap.
Review Details
  • Files reviewed - 2 · Commit Range: e99e930..e99e930
    • superset/mcp_service/chart/tool/get_chart_data.py
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.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

@aminghadersohi
aminghadersohi merged commit ea3206b into apache:master Aug 27, 2026
129 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api Related to the REST API size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants