You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ru...@apache.org on 2026/07/16 05:00:52 UTC
(superset) branch master updated: fix(reports): null-guard execution against missing target (#39973)
This is an automated email from the ASF dual-hosted git repository.
rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 635b18103dc fix(reports): null-guard execution against missing target (#39973)
635b18103dc is described below
commit 635b18103dc312e378aa96a3f029aafdfc1b964e
Author: Mike Bridge <mi...@bridgecanada.com>
AuthorDate: Wed Jul 15 23:00:46 2026 -0600
fix(reports): null-guard execution against missing target (#39973)
Co-authored-by: Mike Bridge <mi...@ext.preset.io>
Co-authored-by: Claude Opus 4.7 (1M context) <no...@anthropic.com>
Co-authored-by: Evan Rusackas <ev...@preset.io>
---
superset/commands/report/execute.py | 44 ++++++++++--------
tests/unit_tests/commands/report/execute_test.py | 58 ++++++++++++++++++++++++
2 files changed, 84 insertions(+), 18 deletions(-)
diff --git a/superset/commands/report/execute.py b/superset/commands/report/execute.py
index 8c92d427da9..50cfe33af0b 100644
--- a/superset/commands/report/execute.py
+++ b/superset/commands/report/execute.py
@@ -265,6 +265,9 @@ class BaseReportState:
"""
Get the url for this report schedule: chart or dashboard
"""
+ chart = self._report_schedule.chart
+ dashboard = self._report_schedule.dashboard
+
# Soft delete removed the FK-level guarantee that a report's target
# chart exists: ``chart`` is a visibility-filtered relationship, so a
# chart soft-deleted after this report was created (or attached via a
@@ -276,23 +279,29 @@ class BaseReportState:
# Every content path (_get_screenshots, _get_csv_data,
# _get_embedded_data, _get_notification_content) funnels through this
# method, so this is the single choke point.
- if (
- self._report_schedule.chart_id is not None
- and self._report_schedule.chart is None
- ):
- raise ReportScheduleTargetChartDeletedError()
- # Symmetric guard for dashboard targets. Dashboard soft delete lands
- # in the sibling rollout; until then this cannot fire (a dashboard
- # with dependent reports cannot be deleted), which makes it inert
- # rather than wrong — and it keeps the report-target error vocabulary
- # parallel across entities from day one.
- if (
- self._report_schedule.dashboard_id is not None
- and self._report_schedule.dashboard is None
- ):
- raise ReportScheduleTargetDashboardDeletedError()
+ if chart is None and dashboard is None:
+ if self._report_schedule.chart_id is not None:
+ raise ReportScheduleTargetChartDeletedError()
+ # Symmetric guard for dashboard targets. Dashboard soft delete lands
+ # in the sibling rollout; until then this cannot fire (a dashboard
+ # with dependent reports cannot be deleted), which makes it inert
+ # rather than wrong — and it keeps the report-target error vocabulary
+ # parallel across entities from day one.
+ if self._report_schedule.dashboard_id is not None:
+ raise ReportScheduleTargetDashboardDeletedError()
+ # Defensive fallback for a malformed report with no target IDs.
+ # Missing relationships with a target ID are handled by the
+ # dedicated deleted-target errors above.
+ raise ReportScheduleUnexpectedError(
+ f"Report schedule {self._report_schedule.id} "
+ f"({self._report_schedule.name!r}) has no resolvable target "
+ f"(chart_id={self._report_schedule.chart_id}, "
+ f"dashboard_id={self._report_schedule.dashboard_id}); "
+ "the report has neither a chart nor a dashboard."
+ )
+
force = "true" if self._report_schedule.force_screenshot else "false"
- if self._report_schedule.chart:
+ if chart:
if result_format in {
ChartDataResultFormat.CSV,
ChartDataResultFormat.XLSX,
@@ -318,9 +327,8 @@ class BaseReportState:
) and feature_flag_manager.is_feature_enabled("ALERT_REPORT_TABS"):
return self._get_tab_url(dashboard_state, user_friendly=user_friendly)
- dashboard = self._report_schedule.dashboard
dashboard_id_or_slug = (
- dashboard.uuid if dashboard and dashboard.uuid else dashboard.id
+ dashboard.uuid if dashboard.uuid is not None else dashboard.id
)
return get_url_path(
"Superset.dashboard",
diff --git a/tests/unit_tests/commands/report/execute_test.py b/tests/unit_tests/commands/report/execute_test.py
index ab4e3ae6b33..232d6ab48a0 100644
--- a/tests/unit_tests/commands/report/execute_test.py
+++ b/tests/unit_tests/commands/report/execute_test.py
@@ -2386,6 +2386,8 @@ def test_get_url_raises_when_target_chart_soft_deleted(
report_schedule = mocker.MagicMock()
report_schedule.chart_id = 42
report_schedule.chart = None
+ report_schedule.dashboard_id = None
+ report_schedule.dashboard = None
state = BaseReportState(report_schedule, datetime.utcnow(), uuid4())
with pytest.raises(ReportScheduleTargetChartDeletedError):
@@ -2415,6 +2417,33 @@ def test_get_url_raises_when_target_dashboard_soft_deleted(
state._get_url()
+def test_get_url_uses_valid_chart_with_stale_dashboard_reference(
+ mocker: MockerFixture,
+ app_context: None,
+) -> None:
+ """A stale non-target dashboard must not block a valid chart report."""
+ report_schedule: ReportSchedule = mocker.Mock(spec=ReportSchedule)
+ report_schedule.chart_id = 42
+ report_schedule.chart = mocker.sentinel.chart
+ report_schedule.dashboard_id = 7
+ report_schedule.dashboard = None
+ report_schedule.force_screenshot = False
+ get_url_path = mocker.patch(
+ "superset.commands.report.execute.get_url_path",
+ return_value="/chart",
+ )
+
+ state = BaseReportState(report_schedule, datetime.utcnow(), uuid4())
+
+ assert state._get_url() == "/chart"
+ get_url_path.assert_called_once_with(
+ "ExploreView.root",
+ user_friendly=False,
+ form_data=json.dumps({"slice_id": 42}),
+ force="false",
+ )
+
+
def test_get_dashboard_urls_raises_when_target_dashboard_soft_deleted(
mocker: MockerFixture,
app_context: None,
@@ -2433,3 +2462,32 @@ def test_get_dashboard_urls_raises_when_target_dashboard_soft_deleted(
state = BaseReportState(report_schedule, datetime.utcnow(), uuid4())
with pytest.raises(ReportScheduleTargetDashboardDeletedError):
state.get_dashboard_urls()
+
+
+def test_get_url_raises_unexpected_error_when_target_is_missing(
+ mocker: MockerFixture,
+ app: SupersetApp,
+) -> None:
+ """A malformed schedule without either target raises a useful error."""
+ mock_report_schedule: ReportSchedule = mocker.Mock(spec=ReportSchedule)
+ mock_report_schedule.id = 42
+ mock_report_schedule.name = "orphan_report"
+ mock_report_schedule.chart = None
+ mock_report_schedule.chart_id = None
+ mock_report_schedule.dashboard = None
+ mock_report_schedule.dashboard_id = None
+ mock_report_schedule.force_screenshot = False
+
+ class_instance: BaseReportState = BaseReportState(
+ mock_report_schedule, "January 1, 2021", "execution_id_example"
+ )
+ class_instance._report_schedule = mock_report_schedule
+
+ with pytest.raises(ReportScheduleUnexpectedError) as excinfo:
+ class_instance._get_url()
+
+ message: str = str(excinfo.value)
+ assert "Report schedule 42" in message
+ assert "orphan_report" in message
+ assert "chart_id=None" in message
+ assert "dashboard_id=None" in message