Skip to content

fix(mcp): honor extra_form_data time_grain_sqla on Generic Chart Axes charts - #44435

Merged
aminghadersohi merged 4 commits into
apache:masterfrom
aminghadersohi:fix-mcp-time-grain-generic-chart-axes
Sep 21, 2026
Merged

aminghadersohi merged 4 commits into
apache:masterfrom
aminghadersohi:fix-mcp-time-grain-generic-chart-axes

Conversation

@aminghadersohi

@aminghadersohi aminghadersohi commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

SUMMARY

Passing time_grain_sqla through extra_form_data to the MCP get_chart_data /
get_chart_sql tools was silently ignored for any chart built on Generic Chart
Axes (anything with an x_axis — echarts_timeseries_bar, mixed_timeseries,
etc.). The tools returned the chart's saved grain: identical row counts and
timestamps to a call with no override at all, and no error, so an automated
consumer had no way to detect that the request had been dropped.

Root cause. Generic Chart Axes charts carry their temporal x-axis as a
BASE_AXIS adhoc column inside the query's columns, and that column's own
timeGrain is what actually becomes the SQL truncation
(SqlaTable.adhoc_column_to_sqla reads col["timeGrain"]; has_timegrain
requires columnType == "BASE_AXIS"). It also wins in
ExploreMixin.get_time_grain, which prefers columns[0]["timeGrain"] over
extras. The MCP merge path
(merge_form_data_filters_into_query) wrote the override only into
extras["time_grain_sqla"], which those queries never consult — so the saved
grain kept winning.

Fix (MCP and dashboard filter paths). Both paths propagate the override
onto columns tagged columnType == "BASE_AXIS", wherever the axis sits in
columns, matching the gate in SqlaTable.adhoc_column_to_sqla. Neither path
adds or overwrites timeGrain on a non-axis column. Queries without a
BASE_AXIS column continue to use extras.

The shared, public apply_time_grain_to_base_axis helper lives in
superset/common/utils/time_grain_utils.py. This dependency-light query-payload
utility belongs below both chart data handling and MCP: neither service layer
imports the other, and dashboard handling does not acquire an MCP dependency.
Only the column-selection rule is shared; each caller retains its existing
handling of absent/empty overrides and other filter fields.

Reproduced before changing production code at 78adf0c53b: a daily saved
axis at columns[1], preceded by a non-axis val column, overridden to P1M
produced DATETIME(ts, 'start of month') through MCP but
DATETIME(ts, 'start of day') through the dashboard path. The dashboard path
also added an inert timeGrain: P1M to columns[0]; MCP left it untouched.
This PR fixes that divergence rather than deferring it to a follow-up.

Frontend-built Generic Chart Axes payloads put the axis at columns[0]
(Timeseries, MixedTimeseries, Heatmap, BigNumberWithTrendline). Their behavior is
unchanged: the same value is written to the same column, with all other writes
unchanged. A before/after differential check confirmed byte-identical serialized
query contexts and extra form data in 336 comparisons (168 per entry point),
covering daily/weekly/monthly saved grains, absent/null/empty and
daily/weekly/monthly/yearly overrides, ordinary and adhoc trailing dimensions,
and one/two-query contexts.

BEFORE/AFTER SCREENSHOTS OR ANIMATED GIF

Not applicable — no UI change.

TESTING INSTRUCTIONS

New regression tests in
tests/unit_tests/mcp_service/chart/test_time_grain_generic_axes.py execute the
resulting query against a real SQLite dataset (90 consecutive daily rows), so
the override is proven by the returned buckets rather than by an echoed field:

pytest tests/unit_tests/mcp_service/chart/test_time_grain_generic_axes.py
  • A saved P1D chart overridden with P1M returns 3 monthly buckets
    (2024-01-01, 2024-02-01, 2024-03-01) instead of 90 daily ones.
  • The reverse direction (saved P1M, overridden to P1D) returns 90 rows.
  • Non-axis adhoc columns are left untouched, and a request with no grain
    override preserves the saved axis grain.

At the original merge base (43fee87), 7 of the 10 MCP regression tests fail;
the monthly override still returns daily rows.

Manual check: call the MCP get_chart_data tool on an echarts_timeseries_bar
chart saved with a daily grain, passing
extra_form_data={"time_grain_sqla": "P1M"}, and confirm the returned rows are
monthly.

Dashboard regression and validation of the widened scope:

pytest -q tests/unit_tests/charts/test_chart_data_api.py -k grain_targets_base_axis
pytest -q tests/unit_tests/mcp_service/chart/test_time_grain_generic_axes.py tests/unit_tests/charts/test_dashboard_filter_context.py tests/unit_tests/charts/test_chart_data_api.py
  • The dashboard regression replaces the old test that explicitly expected the
    incorrect first-column behavior. It covers axis positions 0 and 1, with and
    without an existing grain on the non-axis column, asserts actual SQLite SQL
    truncation, and compares the entire serialized query context.
  • Against pre-change production code (78adf0c53b): 2 failed, 2 passed.
    Both axis-at-1 cases generated start of day instead of start of month.
  • With the shared helper: 4 passed, including the unchanged axis-at-0 cases.
  • Combined suites: 99 passed (all 10 existing MCP grain tests, 40
    dashboard-filter-context tests, and 49 chart-data API tests).
  • Pre-commit passed on all six files changed by this PR, including mypy,
    formatting, Ruff, and Pylint. User-facing grain override behavior is documented
    in docs/admin_docs/configuration/mcp-server.mdx.

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

… charts

Charts built on Generic Chart Axes (anything with an `x_axis`, e.g.
`echarts_timeseries_bar` and `mixed_timeseries`) keep their temporal axis as a
`BASE_AXIS` adhoc column inside the query's `columns`, and that column's own
`timeGrain` is what `SqlaTable.adhoc_column_to_sqla` turns into the SQL
truncation. It also takes precedence in `ExploreMixin.get_time_grain`, which
reads `columns[0]["timeGrain"]` before falling back to `extras`.

The MCP merge path wrote a `time_grain_sqla` override only into
`extras["time_grain_sqla"]`, which those queries never consult. `get_chart_data`
and `get_chart_sql` therefore returned the chart's saved grain — identical row
counts and timestamps, and no error — so a caller had no way to tell the
override had been dropped.

Propagate the override onto the `BASE_AXIS` column as well, mirroring what
`apply_dashboard_filter_context` already does for the dashboard filter path.
@codecov

codecov Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.90909% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 80.70%. Comparing base (43fee87) to head (340c4f4).
⚠️ Report is 31 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/chart_helpers.py 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #44435      +/-   ##
==========================================
+ Coverage   80.65%   80.70%   +0.04%     
==========================================
  Files        2942     2944       +2     
  Lines      175692   176040     +348     
  Branches    40788    40837      +49     
==========================================
+ Hits       141705   142066     +361     
+ Misses      31325    31311      -14     
- Partials     2662     2663       +1     
Flag Coverage Δ
hive 37.08% <45.45%> (-0.07%) ⬇️
mysql 56.25% <45.45%> (-0.13%) ⬇️
postgres 56.26% <45.45%> (-0.15%) ⬇️
presto 39.05% <45.45%> (+0.01%) ⬆️
python 85.08% <90.90%> (+0.11%) ⬆️
sqlite 55.98% <45.45%> (-0.13%) ⬇️
unit 77.11% <90.90%> (+0.34%) ⬆️

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.

@netlify

netlify Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for superset-docs-preview ready!

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

@bito-code-review

bito-code-review Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Code Review Agent Run #aa841a

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: 62343f3..78adf0c
    • superset/mcp_service/chart/chart_helpers.py
    • tests/unit_tests/mcp_service/chart/test_time_grain_generic_axes.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

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

LGTM. Good catch that the BASE_AXIS column, not extras, is what drives the truncation.

I ran it rather than just reading it. At the merge base (43fee87) 7 of the 10 new tests fail, and execute_chart_data with extra_form_data={"time_grain_sqla": "P1M"} on an echarts_timeseries_bar returns 90 daily rows; on this head the same call returns 3 monthly ones. Two things I wanted to be sure of, both fine:

  • The grain reaches the cache key. QueryObject.to_dict() carries columns, so the mutated BASE_AXIS is hashed by QueryObject.cache_key(), and I confirmed the override and the baseline come out with different keys. A grain cannot change the generated SQL without changing the key.
  • timeGrain is already in GUEST_OVERRIDABLE_VALUE_KEYS (superset/security/manager.py:690), so mutating the saved column payload does not trip the guest tamper guard on the embedded read path.

Not a blocker, and nothing to change here. The docstring says apply_dashboard_filter_context does the same propagation, and it is close but not identical: that one writes columns[0]["timeGrain"] unconditionally (superset/charts/data/dashboard_filter_context.py:379), while yours targets columnType == "BASE_AXIS", which is what adhoc_column_to_sqla actually gates on (superset/connectors/sqla/models.py:2125). They coincide for anything the frontend builds, since Timeseries, MixedTimeseries, Heatmap and BigNumberWithTrendline all put the x-axis at columns[0]. With the axis at columns[1] I get start of month out of your path and start of day out of the dashboard one, which also writes an inert timeGrain onto a non-axis column. Worth pointing that one at _apply_time_grain_to_base_axis in a follow up so there is a single rule?

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

@gabotorresruiz thanks for running it rather than reading it — the columns[1] divergence is a real find.

Agreed on the follow-up: apply_dashboard_filter_context writing columns[0]["timeGrain"] unconditionally is wrong wherever the axis is not at index 0, and it also leaves an inert timeGrain on a non-axis column. Tracked separately so it gets a regression test with the axis at a non-zero index, which is the only shape where the two rules differ.

Nothing changing on this PR.

@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Good catch, and yes — but in this PR rather than a follow-up, so there's one rule and not a promise.

Doing exactly what you suggested: both paths key on columnType == "BASE_AXIS" instead of columns[0], so the axis-at-columns[1] case stops diverging and the dashboard path stops writing an inert timeGrain onto a non-axis column. Adding a regression test for that case, and keeping the axis-at-0 behaviour byte-identical so nothing the frontend builds moves.

Pushing on top — no force-push, your approval stays as is. Ping me when it lands if you want another look at the widened diff.

Use a neutral query-payload helper for MCP and dashboard filters so time grain overrides follow BASE_AXIS rather than column position. Cover reordered axes with SQLite SQL assertions and preserve frontend axis-at-zero payloads.

Co-Authored-By: GPT-6 Astra <noreply@openai.com>
@github-actions github-actions Bot added the doc Namespace | Anything related to documentation label Sep 21, 2026
@aminghadersohi

Copy link
Copy Markdown
Contributor Author

Landed in 340c4f4a. The helper moved to superset/common/utils/time_grain_utils.py — neither caller should own it, and that keeps charts/data from importing the MCP service. Both paths now call it, so the columns[0] write is gone.

Axis-at-columns[1] test failed 2/2 before, passes after; axis-at-0 output compared byte-for-byte across 336 cases and is unchanged.

Your approval is still on the PR and I haven't touched it — the diff is wider than what you signed off on, so take another look if you want one.

@aminghadersohi
aminghadersohi merged commit cf39d22 into apache:master Sep 21, 2026
78 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

villebro pushed a commit that referenced this pull request Sep 22, 2026
… charts (#44435)

Co-authored-by: GPT-6 Astra <noreply@openai.com>
(cherry picked from commit cf39d22)
villebro added a commit that referenced this pull request Sep 22, 2026
#44435's test called module.execute_chart_data, the undecorated core
that master's #44113 (never backported -- it's the feat that added
get_dashboard_data) split out of the @tool-wrapped get_chart_data. 7.0
only has the decorated function, which needs a live FastMCP Client
context to run at all. The fix itself is still covered by this file's
other four tests, which exercise dashboard_filter_context.py and
time_grain_utils.py directly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

doc Namespace | Anything related to documentation size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants