codeant-ai-for-open-source[bot] commented on code in PR #38859:
URL: https://github.com/apache/superset/pull/38859#discussion_r2991123306
##########
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:
##########
@@ -1218,58 +1244,53 @@ def test_long_title_is_truncated(self):
class TestDashboardSerializationEagerLoading:
- """Tests for eager loading fix in dashboard serialization paths."""
+ """Tests for eager loading fix in dashboard serialization paths.
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_generate_dashboard_refetches_via_dao(self, mock_find_by_id):
- """generate_dashboard re-fetches dashboard via DashboardDAO.find_by_id
+ The re-fetch uses db.session.query(Dashboard) instead of
+ DashboardDAO.find_by_id() to avoid "Can't reconnect until invalid
+ transaction is rolled back" errors in multi-tenant environments.
+ """
+
+ def test_generate_dashboard_refetches_via_session_query(self):
+ """generate_dashboard re-fetches dashboard via db.session.query()
with eager-loaded slice relationships before serialization."""
refetched_dashboard = _mock_dashboard()
refetched_chart = _mock_chart(id=1, slice_name="Refetched Chart")
refetched_dashboard.slices = [refetched_chart]
- mock_find_by_id.return_value = refetched_dashboard
-
- from superset.daos.dashboard import DashboardDAO
-
- result = (
- DashboardDAO.find_by_id(1, query_options=["dummy"]) or
_mock_dashboard()
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_dashboard
)
+ result = mock_query.options().filter().first() or _mock_dashboard()
+
assert result is refetched_dashboard
- mock_find_by_id.assert_called_once_with(1, query_options=["dummy"])
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_add_chart_refetches_dashboard_via_dao(self, mock_find_by_id):
+ def test_add_chart_refetches_dashboard_via_session_query(self):
"""add_chart_to_existing_dashboard re-fetches dashboard via
- DashboardDAO.find_by_id with eager-loaded slice relationships."""
+ db.session.query() with eager-loaded slice relationships."""
original_dashboard = _mock_dashboard()
refetched_dashboard = _mock_dashboard()
refetched_dashboard.slices = [_mock_chart()]
- mock_find_by_id.return_value = refetched_dashboard
-
- from superset.daos.dashboard import DashboardDAO
-
- result = (
- DashboardDAO.find_by_id(original_dashboard.id,
query_options=["dummy"])
- or original_dashboard
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_dashboard
)
+ result = mock_query.options().filter().first() or original_dashboard
+
assert result is refetched_dashboard
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_add_chart_falls_back_on_refetch_failure(self, mock_find_by_id):
+ def test_add_chart_falls_back_on_refetch_failure(self):
"""add_chart_to_existing_dashboard falls back to original dashboard
- if DashboardDAO.find_by_id returns None."""
+ if db.session.query().first() returns None."""
original_dashboard = _mock_dashboard()
- mock_find_by_id.return_value = None
- from superset.daos.dashboard import DashboardDAO
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= None
- result = (
- DashboardDAO.find_by_id(original_dashboard.id,
query_options=["dummy"])
- or original_dashboard
- )
+ result = mock_query.options().filter().first() or original_dashboard
assert result is original_dashboard
Review Comment:
**Suggestion:** The fallback test is also disconnected from production code
and only checks Python `or` semantics on mocks, so it does not verify fallback
behavior in the real tool. Run the tool with refetch returning `None` and
assert the response still uses the updated dashboard. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Fallback-to-updated-dashboard behavior remains untested.
- ⚠️ None-refetch regressions may break dashboard serialization.
```
</details>
```suggestion
@patch("superset.commands.dashboard.update.UpdateDashboardCommand")
@patch("superset.daos.dashboard.DashboardDAO.find_by_id")
@patch("superset.db.session")
@pytest.mark.asyncio()
async def test_add_chart_falls_back_on_refetch_failure(
self, mock_db_session, mock_find_dashboard, mock_update_command,
mcp_server
):
"""add_chart_to_existing_dashboard should return updated dashboard
when refetch is None."""
dashboard = _mock_dashboard(id=8, title="Fallback Dashboard")
dashboard.slices = []
dashboard.position_json = "{}"
mock_find_dashboard.return_value = dashboard
mock_db_session.get.return_value = _mock_chart(id=33,
slice_name="Fallback Chart")
updated_dashboard = _mock_dashboard(id=8, title="Fallback Dashboard")
updated_dashboard.slices = [_mock_chart(id=33, slice_name="Fallback
Chart")]
mock_update_command.return_value.run.return_value = updated_dashboard
refetch = mock_db_session.query.return_value.options.return_value
refetch.filter.return_value.first.return_value = None
async with Client(mcp_server) as client:
result = await client.call_tool(
"add_chart_to_existing_dashboard",
{"request": {"dashboard_id": 8, "chart_id": 33}},
)
assert result.structured_content["error"] is None
assert result.structured_content["dashboard"]["id"] == 8
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Execute `test_add_chart_falls_back_on_refetch_failure` at
`tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:1286-1296`.
2. It only checks Python `or` on a `MagicMock` return (`None or
original_dashboard`) and
never exercises tool code.
3. Actual fallback is implemented in
`add_chart_to_existing_dashboard.py:443` as `(...
.first()) or updated_dashboard`.
4. If that fallback regresses, downstream serialization uses
`updated_dashboard.id` at
`add_chart_to_existing_dashboard.py:454`; this test still passes because it
never reaches
production flow.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py
**Line:** 1286:1296
**Comment:**
*Logic Error: The fallback test is also disconnected from production
code and only checks Python `or` semantics on mocks, so it does not verify
fallback behavior in the real tool. Run the tool with refetch returning `None`
and assert the response still uses the updated dashboard.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=103cd4330456da7faa4b7d1aa28fdfd2141efa52e0d827de5fdd9fc6bc10e96f&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=103cd4330456da7faa4b7d1aa28fdfd2141efa52e0d827de5fdd9fc6bc10e96f&reaction=dislike'>👎</a>
##########
superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py:
##########
@@ -324,6 +324,7 @@ def add_chart_to_existing_dashboard(
"""
try:
from superset.commands.dashboard.update import UpdateDashboardCommand
Review Comment:
**Suggestion:** `SQLAlchemyError` is referenced in the outer `except` tuple
but is only imported later in the `try` block. If any earlier statement raises,
Python evaluates the `except` tuple with an uninitialized local and raises
`UnboundLocalError`, masking the real failure. Import `SQLAlchemyError` before
any potentially failing operations in the `try`. [possible bug]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ add_chart_to_existing_dashboard can fail with internal server error.
- ⚠️ Original CommandException is masked by UnboundLocalError.
```
</details>
```suggestion
from sqlalchemy.exc import SQLAlchemyError
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Start MCP service where tools are registered from
`superset/mcp_service/app.py:408-413`, including
`add_chart_to_existing_dashboard`.
2. Invoke the tool through normal MCP flow (same path used in tests via
`Client.call_tool("add_chart_to_existing_dashboard", ...)` at
`tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:582`).
3. In `add_chart_to_existing_dashboard()`
(`superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py`),
execution
reaches `updated_dashboard = command.run()` at line 426.
4. `UpdateDashboardCommand.run()` is transaction-wrapped at
`superset/commands/dashboard/update.py:58` and can raise
`DashboardUpdateFailedError`
(`superset/commands/dashboard/exceptions.py:57`), which is a
`CommandException` subtype.
5. If that exception is raised before line 435, Python evaluates the outer
`except
(CommandException, SQLAlchemyError, KeyError, ValueError)` at line 510, but
`SQLAlchemyError` is still an uninitialized local (imported later at line
435), causing
`UnboundLocalError` and masking the original command failure.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py
**Line:** 326:326
**Comment:**
*Possible Bug: `SQLAlchemyError` is referenced in the outer `except`
tuple but is only imported later in the `try` block. If any earlier statement
raises, Python evaluates the `except` tuple with an uninitialized local and
raises `UnboundLocalError`, masking the real failure. Import `SQLAlchemyError`
before any potentially failing operations in the `try`.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=1fd9dab1a05d0102f38636f94ac41e1a07f3be566f08cceb609275e22a86d95e&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=1fd9dab1a05d0102f38636f94ac41e1a07f3be566f08cceb609275e22a86d95e&reaction=dislike'>👎</a>
##########
superset/mcp_service/chart/tool/generate_chart.py:
##########
@@ -713,24 +714,25 @@ async def generate_chart( # noqa: C901
if request.save_chart and chart:
from sqlalchemy.orm import joinedload
- from superset.daos.chart import ChartDAO
+ from superset import db
from superset.mcp_service.chart.schemas import
serialize_chart_object
from superset.models.slice import Slice
# Re-fetch with eager-loaded relationships to avoid detached
# instance errors when serialize_chart_object accesses .tags
- # and .owners. Use joinedload (single JOIN query) since we
- # are fetching a single chart.
- chart = (
- ChartDAO.find_by_id(
- chart.id,
- query_options=[
- joinedload(Slice.owners),
- joinedload(Slice.tags),
- ],
- )
- or chart
- )
+ # and .owners. Use a direct db.session.query() instead of
+ # ChartDAO.find_by_id() because the preceding commit may
+ # invalidate the session in multi-tenant environments, causing
+ # "Can't reconnect until invalid transaction is rolled back".
+ try:
+ chart = (
+ db.session.query(Slice)
+ .options(joinedload(Slice.owners), joinedload(Slice.tags))
+ .filter(Slice.id == chart.id)
+ .first()
+ ) or chart
+ except SQLAlchemyError:
+ pass
Review Comment:
**Suggestion:** Swallowing `SQLAlchemyError` with `pass` leaves the
SQLAlchemy session in a failed transaction state, so subsequent ORM access in
the same request can still fail with reconnect/transaction errors. Roll back
the session when the re-fetch fails, then continue with the fallback object.
[possible bug]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ `generate_chart` save flow can fail post-commit.
- ⚠️ Fallback chart serialization may still hit invalid session.
```
</details>
```suggestion
except SQLAlchemyError as ex:
db.session.rollback()
logger.warning(
"Chart re-fetch failed, using existing chart object:
%s", ex
)
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Invoke `generate_chart` with `save_chart=True` (tool is exposed via
`superset/mcp_service/app.py:176-188`; save flow in
`superset/mcp_service/chart/tool/generate_chart.py`).
2. After chart creation (`CreateChartCommand.run` at
`superset/commands/chart/create.py:46-51`), response-building runs the
re-fetch query at
`generate_chart.py:727-733`.
3. If that query raises `SQLAlchemyError`, current code catches it and does
`pass`
(`generate_chart.py:734-735`) without `db.session.rollback()`, leaving the
same session
potentially failed.
4. Code then serializes the fallback chart via `serialize_chart_object`
(`superset/mcp_service/chart/schemas.py:273+`), which accesses lazy
relationships
`chart.tags` and `chart.owners` (`schemas.py:55-67`) and can require DB
access.
5. With a failed session, subsequent ORM access can raise
transaction/reconnect SQLAlchemy
errors; outer handler at `generate_chart.py:791` catches `SQLAlchemyError`
and returns a
failure response even though chart creation already succeeded.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/mcp_service/chart/tool/generate_chart.py
**Line:** 734:735
**Comment:**
*Possible Bug: Swallowing `SQLAlchemyError` with `pass` leaves the
SQLAlchemy session in a failed transaction state, so subsequent ORM access in
the same request can still fail with reconnect/transaction errors. Roll back
the session when the re-fetch fails, then continue with the fallback object.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=0cb55b4293e114118566e318ed7030d816db3d1565342e116d303318c7f240bb&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=0cb55b4293e114118566e318ed7030d816db3d1565342e116d303318c7f240bb&reaction=dislike'>👎</a>
##########
superset/mcp_service/dashboard/tool/generate_dashboard.py:
##########
@@ -339,21 +339,26 @@ def generate_dashboard(
error="Failed to create dashboard due to a database
error.",
)
- # Re-fetch with eager-loaded relationships for serialization
- from superset.daos.dashboard import DashboardDAO
-
- dashboard = (
- DashboardDAO.find_by_id(
- dashboard.id,
- query_options=[
+ # Re-fetch with eager-loaded relationships for serialization.
+ # Use a direct db.session.query() instead of DashboardDAO.find_by_id()
+ # because the preceding commit may invalidate the session in
+ # multi-tenant environments, causing "Can't reconnect until invalid
+ # transaction is rolled back". A fresh query on the current session
+ # avoids that problem.
+ try:
+ dashboard = (
+ db.session.query(Dashboard)
+ .options(
subqueryload(Dashboard.slices).subqueryload(Slice.owners),
subqueryload(Dashboard.slices).subqueryload(Slice.tags),
subqueryload(Dashboard.owners),
subqueryload(Dashboard.tags),
- ],
- )
- or dashboard
- )
+ )
+ .filter(Dashboard.id == dashboard.id)
+ .first()
+ ) or dashboard
+ except SQLAlchemyError:
+ pass
Review Comment:
**Suggestion:** The re-fetch error handler swallows `SQLAlchemyError`
without rolling back the failed transaction. When the query fails due to an
invalid transaction state, the session remains unusable and later attribute
access can raise `PendingRollbackError`, causing this flow to fail even though
the dashboard was already committed. Roll back the session in this `except`
block before continuing with the fallback object. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ❌ generate_dashboard may fail after successful dashboard commit.
- ⚠️ Users may retry, creating duplicate dashboards unintentionally.
```
</details>
```suggestion
except SQLAlchemyError as ex:
db.session.rollback()
logger.warning(
"Dashboard re-fetch failed; returning committed object: %s",
ex,
exc_info=True,
)
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Call MCP tool `generate_dashboard` (tool entrypoint at
`superset/mcp_service/dashboard/tool/generate_dashboard.py:192`; also
invoked in tests via
`client.call_tool("generate_dashboard", ...)` at
`tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:195`).
2. In normal flow, dashboard creation commits successfully at
`generate_dashboard.py:328`,
then code immediately performs a re-fetch query at
`generate_dashboard.py:349-359`.
3. If that re-fetch raises `SQLAlchemyError` (the PR itself documents this
invalid-transaction scenario in comments at
`generate_dashboard.py:343-347`), current code
catches it at `generate_dashboard.py:360-361` and does nothing (`pass`),
leaving failed
transaction state unhandled.
4. Function then serializes ORM fields (`dashboard.created_by_name` at
`generate_dashboard.py:377`; `dashboard.tags` at
`generate_dashboard.py:389`). These can
trigger lazy-load queries (relationship-backed properties in
`superset/models/helpers.py:592-600` and `superset/models/dashboard.py:153`).
5. With session still pending rollback, subsequent ORM access can raise
`PendingRollbackError` (confirmed subclass of `SQLAlchemyError`), which is
caught by outer
handler at `generate_dashboard.py:411`, returning failure even though
dashboard was
already committed.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** superset/mcp_service/dashboard/tool/generate_dashboard.py
**Line:** 360:361
**Comment:**
*Logic Error: The re-fetch error handler swallows `SQLAlchemyError`
without rolling back the failed transaction. When the query fails due to an
invalid transaction state, the session remains unusable and later attribute
access can raise `PendingRollbackError`, causing this flow to fail even though
the dashboard was already committed. Roll back the session in this `except`
block before continuing with the fallback object.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=0b07bb977f8fc6aa772d0c7532e701904cafaeaf7e6b26c263ce40ebaa5098fb&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=0b07bb977f8fc6aa772d0c7532e701904cafaeaf7e6b26c263ce40ebaa5098fb&reaction=dislike'>👎</a>
##########
tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py:
##########
@@ -410,38 +410,31 @@ def
test_serialize_chart_object_fails_on_detached_instance(self):
with pytest.raises(DetachedInstanceError):
serialize_chart_object(chart)
- @patch("superset.daos.chart.ChartDAO.find_by_id")
- def test_generate_chart_refetches_via_dao(self, mock_find_by_id):
- """The serialization path re-fetches the chart via ChartDAO.find_by_id
- with joinedload query_options for owners and tags."""
+ def test_generate_chart_refetches_via_session_query(self):
+ """The serialization path re-fetches the chart via db.session.query()
+ with joinedload options for owners and tags, avoiding DAO calls that
+ fail in multi-tenant with invalidated sessions."""
refetched_chart = _make_mock_chart()
refetched_chart.tags = [Mock(id=1, name="tag1", type="custom")]
refetched_chart.tags[0].description = ""
- mock_find_by_id.return_value = refetched_chart
-
- from superset.daos.chart import ChartDAO
-
- chart = (
- ChartDAO.find_by_id(42, query_options=["dummy_option"])
- or _make_mock_chart()
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_chart
)
+ chart = mock_query.options().filter().first() or _make_mock_chart()
+
assert chart is refetched_chart
Review Comment:
**Suggestion:** This test does not execute the `generate_chart` code path it
claims to verify; it only evaluates a locally mocked chain, so it will still
pass even if production code stops using eager-loaded `db.session.query(...)`.
Replace this with assertions that at least validate `options(...)` is called
with eager-load arguments so the test can actually fail when eager loading is
removed. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ Eager-loading regressions can pass unit tests unnoticed.
- ⚠️ `generate_chart` serialization path not actually exercised.
```
</details>
```suggestion
mock_query.options.return_value.filter.return_value.first.return_value =
refetched_chart
owners_joinedload = Mock(name="owners_joinedload")
tags_joinedload = Mock(name="tags_joinedload")
chart = (
mock_query.options(owners_joinedload,
tags_joinedload).filter("slice_id_filter").first()
or _make_mock_chart()
)
mock_query.options.assert_called_once_with(owners_joinedload,
tags_joinedload)
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Inspect `test_generate_chart_refetches_via_session_query` in
`tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py:413-429`; it
only executes
`mock_query.options().filter().first()` and never calls `generate_chart()`.
2. Verify imports in the same file: `from
superset.mcp_service.chart.tool.generate_chart
import (_compile_chart, CompileResult)`
(`.../test_generate_chart.py:36-39`), so the
tested function is not even imported.
3. Inspect real eager-loading logic in
`superset/mcp_service/chart/tool/generate_chart.py:722-733`, where
production calls
`db.session.query(Slice).options(joinedload(Slice.owners),
joinedload(Slice.tags))...`.
4. Because the test never reaches that code path, a production regression in
eager loading
would not fail this test; the current assertion is effectively validating
only local mock
wiring, not behavior.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py
**Line:** 422:428
**Comment:**
*Logic Error: This test does not execute the `generate_chart` code path
it claims to verify; it only evaluates a locally mocked chain, so it will still
pass even if production code stops using eager-loaded `db.session.query(...)`.
Replace this with assertions that at least validate `options(...)` is called
with eager-load arguments so the test can actually fail when eager loading is
removed.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=fceaeeb222899ef4c38ef11dc7dfb32aa181dafe677524c1203f4ae9544084e9&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=fceaeeb222899ef4c38ef11dc7dfb32aa181dafe677524c1203f4ae9544084e9&reaction=dislike'>👎</a>
##########
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:
##########
@@ -1218,58 +1244,53 @@ def test_long_title_is_truncated(self):
class TestDashboardSerializationEagerLoading:
- """Tests for eager loading fix in dashboard serialization paths."""
+ """Tests for eager loading fix in dashboard serialization paths.
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_generate_dashboard_refetches_via_dao(self, mock_find_by_id):
- """generate_dashboard re-fetches dashboard via DashboardDAO.find_by_id
+ The re-fetch uses db.session.query(Dashboard) instead of
+ DashboardDAO.find_by_id() to avoid "Can't reconnect until invalid
+ transaction is rolled back" errors in multi-tenant environments.
+ """
+
+ def test_generate_dashboard_refetches_via_session_query(self):
+ """generate_dashboard re-fetches dashboard via db.session.query()
with eager-loaded slice relationships before serialization."""
refetched_dashboard = _mock_dashboard()
refetched_chart = _mock_chart(id=1, slice_name="Refetched Chart")
refetched_dashboard.slices = [refetched_chart]
- mock_find_by_id.return_value = refetched_dashboard
-
- from superset.daos.dashboard import DashboardDAO
-
- result = (
- DashboardDAO.find_by_id(1, query_options=["dummy"]) or
_mock_dashboard()
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_dashboard
)
+ result = mock_query.options().filter().first() or _mock_dashboard()
+
assert result is refetched_dashboard
Review Comment:
**Suggestion:** This test never calls `generate_dashboard`; it only
exercises a `MagicMock` chain, so it will pass even if the real re-fetch logic
is broken. Replace it with an actual tool invocation and assert that the
`db.session.query(...).options(...)` path is used. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ CI misses generate_dashboard eager-load path regressions.
- ⚠️ Multi-tenant session-fix behavior unverified by this test.
```
</details>
```suggestion
@patch("superset.models.dashboard.Dashboard")
@patch("superset.db.session")
@pytest.mark.asyncio()
async def test_generate_dashboard_refetches_via_session_query(
self, mock_db_session, mock_dashboard_cls, mcp_server
):
"""generate_dashboard should execute session-query re-fetch before
serialization."""
charts = [_mock_chart(id=1, slice_name="Refetched Chart")]
dashboard = _mock_dashboard(id=101, title="Refetch Dashboard")
_setup_generate_dashboard_mocks(
mock_db_session, Mock(), mock_dashboard_cls, charts, dashboard
)
request = {"chart_ids": [1], "dashboard_title": "Refetch Dashboard"}
async with Client(mcp_server) as client:
result = await client.call_tool("generate_dashboard",
{"request": request})
assert result.structured_content["error"] is None
mock_db_session.query.return_value.options.assert_called_once()
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run `pytest` for `test_generate_dashboard_refetches_via_session_query` in
`tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:1254-1268`.
2. Observe the test only executes `mock_query.options().filter().first()`
and never
invokes MCP `Client.call_tool("generate_dashboard", ...)`.
3. Compare with production path `generate_dashboard()` at
`superset/mcp_service/dashboard/tool/generate_dashboard.py:192`, where
re-fetch actually
happens at `:343-351` using
`db.session.query(Dashboard).options(...).filter(...).first()`.
4. Because the test is disconnected from `generate_dashboard()`, it will
pass even if the
real re-fetch block regresses; this is a real false-positive test.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py
**Line:** 1254:1268
**Comment:**
*Logic Error: This test never calls `generate_dashboard`; it only
exercises a `MagicMock` chain, so it will pass even if the real re-fetch logic
is broken. Replace it with an actual tool invocation and assert that the
`db.session.query(...).options(...)` path is used.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=2b03bd6f2906257911abb7548d4edbad6e2a56c5c74b328d6f037fb5911d690f&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=2b03bd6f2906257911abb7548d4edbad6e2a56c5c74b328d6f037fb5911d690f&reaction=dislike'>👎</a>
##########
tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py:
##########
@@ -410,38 +410,31 @@ def
test_serialize_chart_object_fails_on_detached_instance(self):
with pytest.raises(DetachedInstanceError):
serialize_chart_object(chart)
- @patch("superset.daos.chart.ChartDAO.find_by_id")
- def test_generate_chart_refetches_via_dao(self, mock_find_by_id):
- """The serialization path re-fetches the chart via ChartDAO.find_by_id
- with joinedload query_options for owners and tags."""
+ def test_generate_chart_refetches_via_session_query(self):
+ """The serialization path re-fetches the chart via db.session.query()
+ with joinedload options for owners and tags, avoiding DAO calls that
+ fail in multi-tenant with invalidated sessions."""
refetched_chart = _make_mock_chart()
refetched_chart.tags = [Mock(id=1, name="tag1", type="custom")]
refetched_chart.tags[0].description = ""
- mock_find_by_id.return_value = refetched_chart
-
- from superset.daos.chart import ChartDAO
-
- chart = (
- ChartDAO.find_by_id(42, query_options=["dummy_option"])
- or _make_mock_chart()
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_chart
)
+ chart = mock_query.options().filter().first() or _make_mock_chart()
+
assert chart is refetched_chart
- mock_find_by_id.assert_called_once_with(42,
query_options=["dummy_option"])
- @patch("superset.daos.chart.ChartDAO.find_by_id")
- def test_generate_chart_falls_back_to_original_on_refetch_failure(
- self, mock_find_by_id
- ):
- """Falls back to original chart if ChartDAO.find_by_id returns None."""
+ def test_generate_chart_falls_back_to_original_on_refetch_failure(self):
+ """Falls back to original chart if db.session.query().first()
+ returns None."""
original_chart = _make_mock_chart()
- mock_find_by_id.return_value = None
- from superset.daos.chart import ChartDAO
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= None
- chart = (
- ChartDAO.find_by_id(original_chart.id, query_options=[]) or
original_chart
- )
+ chart = mock_query.options().filter().first() or original_chart
Review Comment:
**Suggestion:** The fallback test only covers a `None` return and still does
not exercise a real refetch failure path; if the refetch raises
`SQLAlchemyError` (which production handles), this test gives no protection.
Make the mock raise `SQLAlchemyError` and assert fallback to the original
chart. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ SQLAlchemy exception fallback path lacks test coverage.
- ⚠️ Multi-tenant session regressions may evade CI detection.
```
</details>
```suggestion
from sqlalchemy.exc import SQLAlchemyError
mock_query = MagicMock()
mock_query.options.return_value.filter.return_value.first.side_effect = (
SQLAlchemyError("session failure")
)
try:
chart = mock_query.options("owners_joinedload",
"tags_joinedload").filter("slice_id_filter").first()
except SQLAlchemyError:
chart = original_chart
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Inspect fallback logic in production `generate_chart` at
`superset/mcp_service/chart/tool/generate_chart.py:727-756`;
`db.session.query(...).first()` is wrapped in `try/except SQLAlchemyError`,
then falls
back to existing `chart`.
2. Inspect `test_generate_chart_falls_back_to_original_on_refetch_failure` in
`tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py:430-440`; it
only tests
`first()` returning `None`.
3. No test in this file simulates `first()` raising `SQLAlchemyError`
(search result shows
no such exception usage in this test module).
4. Therefore, the exception fallback branch in production remains
unverified; regressions
in that branch can ship while tests still pass.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:** tests/unit_tests/mcp_service/chart/tool/test_generate_chart.py
**Line:** 435:438
**Comment:**
*Logic Error: The fallback test only covers a `None` return and still
does not exercise a real refetch failure path; if the refetch raises
`SQLAlchemyError` (which production handles), this test gives no protection.
Make the mock raise `SQLAlchemyError` and assert fallback to the original chart.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=7a77b9a30269d5379b6f24a83fc717d3f8baa6b539f2f83de2ac1cb6e20870f7&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=7a77b9a30269d5379b6f24a83fc717d3f8baa6b539f2f83de2ac1cb6e20870f7&reaction=dislike'>👎</a>
##########
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:
##########
@@ -1218,58 +1244,53 @@ def test_long_title_is_truncated(self):
class TestDashboardSerializationEagerLoading:
- """Tests for eager loading fix in dashboard serialization paths."""
+ """Tests for eager loading fix in dashboard serialization paths.
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_generate_dashboard_refetches_via_dao(self, mock_find_by_id):
- """generate_dashboard re-fetches dashboard via DashboardDAO.find_by_id
+ The re-fetch uses db.session.query(Dashboard) instead of
+ DashboardDAO.find_by_id() to avoid "Can't reconnect until invalid
+ transaction is rolled back" errors in multi-tenant environments.
+ """
+
+ def test_generate_dashboard_refetches_via_session_query(self):
+ """generate_dashboard re-fetches dashboard via db.session.query()
with eager-loaded slice relationships before serialization."""
refetched_dashboard = _mock_dashboard()
refetched_chart = _mock_chart(id=1, slice_name="Refetched Chart")
refetched_dashboard.slices = [refetched_chart]
- mock_find_by_id.return_value = refetched_dashboard
-
- from superset.daos.dashboard import DashboardDAO
-
- result = (
- DashboardDAO.find_by_id(1, query_options=["dummy"]) or
_mock_dashboard()
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_dashboard
)
+ result = mock_query.options().filter().first() or _mock_dashboard()
+
assert result is refetched_dashboard
- mock_find_by_id.assert_called_once_with(1, query_options=["dummy"])
- @patch("superset.daos.dashboard.DashboardDAO.find_by_id")
- def test_add_chart_refetches_dashboard_via_dao(self, mock_find_by_id):
+ def test_add_chart_refetches_dashboard_via_session_query(self):
"""add_chart_to_existing_dashboard re-fetches dashboard via
- DashboardDAO.find_by_id with eager-loaded slice relationships."""
+ db.session.query() with eager-loaded slice relationships."""
original_dashboard = _mock_dashboard()
refetched_dashboard = _mock_dashboard()
refetched_dashboard.slices = [_mock_chart()]
- mock_find_by_id.return_value = refetched_dashboard
-
- from superset.daos.dashboard import DashboardDAO
-
- result = (
- DashboardDAO.find_by_id(original_dashboard.id,
query_options=["dummy"])
- or original_dashboard
+ mock_query = MagicMock()
+ mock_query.options.return_value.filter.return_value.first.return_value
= (
+ refetched_dashboard
)
+ result = mock_query.options().filter().first() or original_dashboard
+
assert result is refetched_dashboard
Review Comment:
**Suggestion:** This test does not execute
`add_chart_to_existing_dashboard`; it only asserts `MagicMock` behavior and
cannot catch regressions in the actual re-fetch implementation. Convert it into
a real tool call and verify the eager-load query path runs. [logic error]
<details>
<summary><b>Severity Level:</b> Major ⚠️</summary>
```mdx
- ⚠️ add_chart eager-loading query path lacks direct test.
- ⚠️ Refetch regressions can slip through unit CI.
```
</details>
```suggestion
@patch("superset.commands.dashboard.update.UpdateDashboardCommand")
@patch("superset.daos.dashboard.DashboardDAO.find_by_id")
@patch("superset.db.session")
@pytest.mark.asyncio()
async def test_add_chart_refetches_dashboard_via_session_query(
self, mock_db_session, mock_find_dashboard, mock_update_command,
mcp_server
):
"""add_chart_to_existing_dashboard should execute session-query
re-fetch."""
original_dashboard = _mock_dashboard(id=7, title="Existing")
original_dashboard.slices = [_mock_chart(id=10)]
original_dashboard.position_json = "{}"
mock_find_dashboard.return_value = original_dashboard
mock_db_session.get.return_value = _mock_chart(id=20,
slice_name="Added")
updated_dashboard = _mock_dashboard(id=7, title="Existing")
updated_dashboard.slices = [_mock_chart(id=10), _mock_chart(id=20)]
mock_update_command.return_value.run.return_value = updated_dashboard
refetch = mock_db_session.query.return_value.options.return_value
refetch.filter.return_value.first.return_value = updated_dashboard
async with Client(mcp_server) as client:
result = await client.call_tool(
"add_chart_to_existing_dashboard",
{"request": {"dashboard_id": 7, "chart_id": 20}},
)
assert result.structured_content["error"] is None
mock_db_session.query.return_value.options.assert_called_once()
```
<details>
<summary><b>Steps of Reproduction ✅ </b></summary>
```mdx
1. Run `test_add_chart_refetches_dashboard_via_session_query` at
`tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py:1270-1284`.
2. Confirm it only evaluates a local `MagicMock` chain and does not call
`add_chart_to_existing_dashboard()` through MCP client.
3. Real logic lives in
`superset/mcp_service/dashboard/tool/add_chart_to_existing_dashboard.py:318`,
with
re-fetch at `:431-444`
(`db.session.query(Dashboard).options(...).filter(...).first()`).
4. Since the test never enters that function, regressions in eager-load
query path are
undetectable by this test.
```
</details>
<details>
<summary><b>Prompt for AI Agent 🤖 </b></summary>
```mdx
This is a comment left during a code review.
**Path:**
tests/unit_tests/mcp_service/dashboard/tool/test_dashboard_generation.py
**Line:** 1270:1284
**Comment:**
*Logic Error: This test does not execute
`add_chart_to_existing_dashboard`; it only asserts `MagicMock` behavior and
cannot catch regressions in the actual re-fetch implementation. Convert it into
a real tool call and verify the eager-load query path runs.
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.
```
</details>
<a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=bce6bbb8f75d2584ece184be1b66fc74029a73889f3a8e1699d25b74afbb7bcd&reaction=like'>👍</a>
| <a
href='https://app.codeant.ai/feedback?pr_url=https%3A%2F%2Fgithub.com%2Fapache%2Fsuperset%2Fpull%2F38859&comment_hash=bce6bbb8f75d2584ece184be1b66fc74029a73889f3a8e1699d25b74afbb7bcd&reaction=dislike'>👎</a>
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]