Skip to content

fix(mcp): eager-load dataset.metrics to prevent Excel export DetachedInstanceError#39483

Merged
msyavuz merged 4 commits into
masterfrom
msyavuz/fix/mcp-excel-export
May 21, 2026
Merged

fix(mcp): eager-load dataset.metrics to prevent Excel export DetachedInstanceError#39483
msyavuz merged 4 commits into
masterfrom
msyavuz/fix/mcp-excel-export

Conversation

@msyavuz
Copy link
Copy Markdown
Member

@msyavuz msyavuz commented Apr 20, 2026

SUMMARY

get_chart_data with format="excel" could fail with:

Parent instance <SqlaTable ...> is not bound to a Session; lazy load operation of attribute 'metrics' cannot proceed

The dataset's metrics relationship is lazy-loaded. If the request-scoped session detaches between ChartDAO.find_by_id and later access of chart.table.metrics, SQLAlchemy has no session to issue the SELECT and raises DetachedInstanceError. Excel export surfaces this more than CSV/JSON because openpyxl encoding widens the wall-clock window between lookup and any subsequent metrics access.

Fix: pass a subqueryload(Slice.table).subqueryload(SqlaTable.metrics) query option on the ChartDAO.find_by_id calls in get_chart_data, so metrics is materialized at fetch time and later detachment is harmless. Mirrors the eager-loading pattern from #39206 (database relationship) and #38129 (owners/tags on get_chart_info).

TESTING INSTRUCTIONS

Automated:

pytest tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py -v

Three new regression tests in TestChartLookupEagerLoading assert that the subqueryload chain (tablemetrics) is passed as query_options on both the numeric-ID and UUID lookup paths, for every format.

Manual:

  1. Start MCP in streamable-http mode: FASTMCP_TRANSPORT=streamable-http python -m superset.mcp_service
  2. Call get_chart_data with { identifier: <id>, format: "excel", limit: 10 } against a chart whose dataset has saved metrics.
  3. Expect a ChartData response with format="excel" and a decodable excel_data base64 blob — no lazy-load error.
  4. Verify format="csv" and format="json" still work unchanged.

ADDITIONAL INFORMATION

  • Has associated issue:
  • Required feature flags:
  • Changes UI
  • Includes DB Migration
  • Introduces new feature or API
  • Removes existing feature or API

🤖 Generated with Claude Code

…InstanceError

get_chart_data with format=excel could fail with
"Parent instance SqlaTable is not bound to a Session; lazy load operation
of attribute metrics cannot proceed" when the dataset's metrics relationship
was accessed after the request-scoped session detached. Pass a
subqueryload(Slice.table -> SqlaTable.metrics) query option on ChartDAO
lookup so metrics are materialized up front and later detachment is harmless.

Mirrors the eager-loading pattern from #39206.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Apr 20, 2026

Code Review Agent Run #2bbc04

Actionable Suggestions - 0
Review Details
  • Files reviewed - 2 · Commit Range: b9faefb..b9faefb
    • 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
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ 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

@dosubot dosubot Bot added change:backend Requires changing the backend viz:charts:export Related to exporting charts labels Apr 20, 2026
Comment on lines +863 to +867
@pytest.fixture
def mcp_server():
from superset.mcp_service.app import mcp

return mcp
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.

Suggestion: Add an explicit return type annotation to the new fixture function. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
@pytest.fixture
def mcp_server():
from superset.mcp_service.app import mcp
return mcp
def mcp_server() -> Any:
Why it matters? 🤔

The new fixture is defined without a return annotation, which matches the custom typing rule being enforced here. The improved signature adds -> Any, and Any is already imported at the top of the file, so the change is syntactically valid and executable.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
**Line:** 863:867
**Comment:**
	*Custom Rule: Add an explicit return type annotation to the new fixture function.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +870 to +894
@pytest.fixture
def mock_auth():
"""Mock MCP auth so Client.call_tool() doesn't need a real admin user."""
from contextlib import contextmanager
from unittest.mock import Mock, patch

@contextmanager
def _noop_log_context(*_args: Any, **_kwargs: Any) -> Any:
yield lambda **_kw: None

# Also neutralize event_logger.log_context: the default DBEventLogger
# would otherwise insert a log row referencing our mock user_id and
# fail a FK constraint against the real users table.
with (
patch("superset.mcp_service.auth.get_user_from_request") as mock_get_user,
patch(
"superset.mcp_service.chart.tool.get_chart_data.event_logger.log_context",
side_effect=_noop_log_context,
),
):
user = Mock()
user.id = 1
user.username = "admin"
mock_get_user.return_value = user
yield mock_get_user
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.

Suggestion: Add an explicit return type annotation to this new fixture function. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
@pytest.fixture
def mock_auth():
"""Mock MCP auth so Client.call_tool() doesn't need a real admin user."""
from contextlib import contextmanager
from unittest.mock import Mock, patch
@contextmanager
def _noop_log_context(*_args: Any, **_kwargs: Any) -> Any:
yield lambda **_kw: None
# Also neutralize event_logger.log_context: the default DBEventLogger
# would otherwise insert a log row referencing our mock user_id and
# fail a FK constraint against the real users table.
with (
patch("superset.mcp_service.auth.get_user_from_request") as mock_get_user,
patch(
"superset.mcp_service.chart.tool.get_chart_data.event_logger.log_context",
side_effect=_noop_log_context,
),
):
user = Mock()
user.id = 1
user.username = "admin"
mock_get_user.return_value = user
yield mock_get_user
def mock_auth() -> Any:
Why it matters? 🤔

The new fixture lacks a return annotation, so the suggestion addresses the stated typing rule. Any is already imported, and annotating the fixture as -> Any is syntactically correct and safe for a pytest fixture.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
**Line:** 870:894
**Comment:**
	*Custom Rule: Add an explicit return type annotation to this new fixture function.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +917 to +946
async def test_numeric_id_lookup_passes_metrics_eager_load(
self, mcp_server, mock_auth
):
"""Integer identifier lookup must eager-load Slice.table.metrics."""
from unittest.mock import patch

from fastmcp import Client

with patch(
"superset.daos.chart.ChartDAO.find_by_id", return_value=None
) as mock_find:
async with Client(mcp_server) as client:
await client.call_tool(
"get_chart_data",
{"request": {"identifier": 42, "format": "excel"}},
)

mock_find.assert_called_once()
call = mock_find.call_args
assert call.args == (42,)
query_options = call.kwargs.get("query_options")
assert query_options is not None, (
"Chart lookup must pass query_options for eager-loading."
)
assert len(query_options) == 1
load_path = _extract_metrics_load_path(query_options[0])
assert load_path == ["table", "metrics"], (
f"Expected subqueryload chain 'table' -> 'metrics', got {load_path}"
)

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.

Suggestion: Add explicit type hints for fixture parameters and return type in this new async test method. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
async def test_numeric_id_lookup_passes_metrics_eager_load(
self, mcp_server, mock_auth
):
"""Integer identifier lookup must eager-load Slice.table.metrics."""
from unittest.mock import patch
from fastmcp import Client
with patch(
"superset.daos.chart.ChartDAO.find_by_id", return_value=None
) as mock_find:
async with Client(mcp_server) as client:
await client.call_tool(
"get_chart_data",
{"request": {"identifier": 42, "format": "excel"}},
)
mock_find.assert_called_once()
call = mock_find.call_args
assert call.args == (42,)
query_options = call.kwargs.get("query_options")
assert query_options is not None, (
"Chart lookup must pass query_options for eager-loading."
)
assert len(query_options) == 1
load_path = _extract_metrics_load_path(query_options[0])
assert load_path == ["table", "metrics"], (
f"Expected subqueryload chain 'table' -> 'metrics', got {load_path}"
)
self, mcp_server: Any, mock_auth: Any
) -> None:
Why it matters? 🤔

The method currently leaves the injected fixture parameters untyped and omits a return annotation, which is consistent with the typing rule being targeted. The proposed annotations use Any, which is already available in the module, and -> None is correct for an async test method that does not return a value.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
**Line:** 917:946
**Comment:**
	*Custom Rule: Add explicit type hints for fixture parameters and return type in this new async test method.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Comment on lines +947 to +977
@pytest.mark.asyncio
async def test_uuid_lookup_passes_metrics_eager_load(self, mcp_server, mock_auth):
"""UUID identifier lookup must also eager-load Slice.table.metrics."""
from unittest.mock import patch

from fastmcp import Client

uuid = "a1b2c3d4-5678-90ab-cdef-1234567890ab"

with patch(
"superset.daos.chart.ChartDAO.find_by_id", return_value=None
) as mock_find:
async with Client(mcp_server) as client:
await client.call_tool(
"get_chart_data",
{"request": {"identifier": uuid, "format": "excel"}},
)

mock_find.assert_called_once()
call = mock_find.call_args
assert call.args == (uuid,)
assert call.kwargs.get("id_column") == "uuid"
query_options = call.kwargs.get("query_options")
assert query_options is not None, (
"UUID chart lookup must pass query_options for eager-loading."
)
load_path = _extract_metrics_load_path(query_options[0])
assert load_path == ["table", "metrics"], (
f"Expected subqueryload chain 'table' -> 'metrics', got {load_path}"
)

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.

Suggestion: Add explicit type hints for fixture parameters and return type in this new async test method. [custom_rule]

Severity Level: Minor ⚠️

Suggested change
@pytest.mark.asyncio
async def test_uuid_lookup_passes_metrics_eager_load(self, mcp_server, mock_auth):
"""UUID identifier lookup must also eager-load Slice.table.metrics."""
from unittest.mock import patch
from fastmcp import Client
uuid = "a1b2c3d4-5678-90ab-cdef-1234567890ab"
with patch(
"superset.daos.chart.ChartDAO.find_by_id", return_value=None
) as mock_find:
async with Client(mcp_server) as client:
await client.call_tool(
"get_chart_data",
{"request": {"identifier": uuid, "format": "excel"}},
)
mock_find.assert_called_once()
call = mock_find.call_args
assert call.args == (uuid,)
assert call.kwargs.get("id_column") == "uuid"
query_options = call.kwargs.get("query_options")
assert query_options is not None, (
"UUID chart lookup must pass query_options for eager-loading."
)
load_path = _extract_metrics_load_path(query_options[0])
assert load_path == ["table", "metrics"], (
f"Expected subqueryload chain 'table' -> 'metrics', got {load_path}"
)
async def test_uuid_lookup_passes_metrics_eager_load(self, mcp_server: Any, mock_auth: Any) -> None:
Why it matters? 🤔

This async test method also lacks type annotations for its fixture parameters and return type, so the suggested fix is aligned with the rule. The replacement is valid Python, and Any is already imported in the file, so no additional dependencies are introduced.

Fix in Cursor | Fix in VSCode Claude

(Use Cmd/Ctrl + Click for best experience)

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
**Line:** 947:977
**Comment:**
	*Custom Rule: Add explicit type hints for fixture parameters and return type in this new async test method.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 20, 2026

Codecov Report

❌ Patch coverage is 22.22222% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 64.16%. Comparing base (d1d0711) to head (89bd1d1).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
superset/mcp_service/chart/tool/get_chart_data.py 20.00% 4 Missing ⚠️
superset/mcp_service/chart/chart_helpers.py 25.00% 3 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master   #39483      +/-   ##
==========================================
- Coverage   64.16%   64.16%   -0.01%     
==========================================
  Files        2592     2592              
  Lines      138629   138634       +5     
  Branches    32143    32143              
==========================================
+ Hits        88951    88952       +1     
- Misses      48146    48150       +4     
  Partials     1532     1532              
Flag Coverage Δ
hive 39.41% <22.22%> (-0.01%) ⬇️
mysql 59.01% <22.22%> (-0.01%) ⬇️
postgres 59.09% <22.22%> (-0.01%) ⬇️
presto 41.09% <22.22%> (-0.01%) ⬇️
python 60.51% <22.22%> (-0.01%) ⬇️
sqlite 58.73% <22.22%> (-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 Sentry.
📢 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
Copy link
Copy Markdown

netlify Bot commented Apr 20, 2026

Deploy Preview for superset-docs-preview ready!

Name Link
🔨 Latest commit b9faefb
🔍 Latest deploy log https://app.netlify.com/projects/superset-docs-preview/deploys/69e5f4fde8b06c0008602ae6
😎 Deploy Preview https://deploy-preview-39483--superset-docs-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

The `tool/__init__.py` re-exports the `get_chart_data` function from its
submodule of the same name, which shadows the module binding in the
`tool` package namespace. mock.patch walks the dotted string by
getattr()-ing the package namespace, so
`superset.mcp_service.chart.tool.get_chart_data.event_logger` resolves
`get_chart_data` to the function, then raises AttributeError on
`event_logger`, then falls back to `__import__` and fails with
ModuleNotFoundError. Obtain the actual module via importlib and
patch.object the attribute directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@msyavuz msyavuz force-pushed the msyavuz/fix/mcp-excel-export branch from 9690753 to 3e2e3da Compare April 20, 2026 10:34
@bito-code-review
Copy link
Copy Markdown
Contributor

bito-code-review Bot commented Apr 20, 2026

Code Review Agent Run #da3022

Actionable Suggestions - 0
Review Details
  • Files reviewed - 1 · Commit Range: b9faefb..3e2e3da
    • tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
  • Files skipped - 0
  • Tools
    • Whispers (Secret Scanner) - ✔︎ Successful
    • Detect-secrets (Secret Scanner) - ✔︎ Successful
    • MyPy (Static Code Analysis) - ✔︎ Successful
    • Astral Ruff (Static Code Analysis) - ✔︎ 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

Copy link
Copy Markdown
Contributor

@alexandrusoare alexandrusoare left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM - the comments from Codeant-AI don't seem relevant

@alexandrusoare alexandrusoare added the hold:testing! On hold for testing label Apr 20, 2026
@msyavuz msyavuz removed the hold:testing! On hold for testing label May 21, 2026
# Conflicts:
#	superset/mcp_service/chart/tool/get_chart_data.py
@msyavuz msyavuz force-pushed the msyavuz/fix/mcp-excel-export branch from c7cc058 to 36ebb7c Compare May 21, 2026 13:01
# Conflicts:
#	tests/unit_tests/mcp_service/chart/tool/test_get_chart_data.py
@msyavuz msyavuz merged commit 2f95d28 into master May 21, 2026
65 checks passed
@msyavuz msyavuz deleted the msyavuz/fix/mcp-excel-export branch May 21, 2026 13:34
sha174n pushed a commit to sha174n/superset that referenced this pull request May 22, 2026
…InstanceError (apache#39483)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@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 preset-io size/L viz:charts:export Related to exporting charts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants