This is an automated email from the ASF dual-hosted git repository.
kgabryje 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 1ecff6fe5c1 fix(thumbnails): stabilize digest by sorting datasources
and charts (#38079)
1ecff6fe5c1 is described below
commit 1ecff6fe5c13ae98dd55cb40ac3baf259f839bef
Author: Amin Ghadersohi <[email protected]>
AuthorDate: Fri Feb 20 03:51:35 2026 -0500
fix(thumbnails): stabilize digest by sorting datasources and charts (#38079)
Co-authored-by: Claude Opus 4.6 <[email protected]>
---
superset/thumbnails/digest.py | 4 +-
tests/unit_tests/thumbnails/test_digest.py | 59 ++++++++++++++++++++++++++++++
2 files changed, 61 insertions(+), 2 deletions(-)
diff --git a/superset/thumbnails/digest.py b/superset/thumbnails/digest.py
index 2602f2e8a91..9a18dc64fd6 100644
--- a/superset/thumbnails/digest.py
+++ b/superset/thumbnails/digest.py
@@ -79,7 +79,7 @@ def _adjust_string_with_rls(
if table_ids:
security_manager.prefetch_rls_filters(table_ids)
- for datasource in datasources:
+ for datasource in sorted(datasources, key=lambda d: d.id if d else
-1):
if datasource and getattr(datasource, "is_rls_supported",
False):
rls_filters = datasource.get_sqla_row_level_filters()
@@ -110,7 +110,7 @@ def get_dashboard_digest(dashboard: Dashboard) -> str |
None:
return func(dashboard, executor_type, executor)
unique_string = (
- f"{dashboard.id}\n{dashboard.charts}\n{dashboard.position_json}\n"
+
f"{dashboard.id}\n{sorted(dashboard.charts)}\n{dashboard.position_json}\n"
f"{dashboard.css}\n{dashboard.json_metadata}"
)
diff --git a/tests/unit_tests/thumbnails/test_digest.py
b/tests/unit_tests/thumbnails/test_digest.py
index 6223bd8bb76..d6d7f7e2c0d 100644
--- a/tests/unit_tests/thumbnails/test_digest.py
+++ b/tests/unit_tests/thumbnails/test_digest.py
@@ -435,6 +435,65 @@ def test_chart_digest(
assert get_chart_digest(chart=chart) == expected_result
+def test_dashboard_digest_deterministic_datasource_order(
+ app_context: None,
+) -> None:
+ """
+ Test that different datasource orderings produce the same digest.
+
+ dashboard.datasources returns a set, whose iteration order is
+ non-deterministic across Python processes (due to PYTHONHASHSEED).
+ The digest must sort datasources by ID to ensure stability.
+ """
+ from superset import security_manager
+ from superset.models.dashboard import Dashboard
+ from superset.models.slice import Slice
+ from superset.thumbnails.digest import get_dashboard_digest
+
+ kwargs = {**_DEFAULT_DASHBOARD_KWARGS}
+ slices = [Slice(**slice_kwargs) for slice_kwargs in kwargs.pop("slices")]
+ dashboard = Dashboard(**kwargs, slices=slices)
+
+ def make_datasource(ds_id: int) -> MagicMock:
+ ds = MagicMock(spec=BaseDatasource)
+ ds.id = ds_id
+ ds.type = DatasourceType.TABLE
+ ds.is_rls_supported = True
+ ds.get_sqla_row_level_filters =
MagicMock(return_value=[f"filter_ds_{ds_id}"])
+ return ds
+
+ ds_a = make_datasource(5)
+ ds_b = make_datasource(3)
+ ds_c = make_datasource(9)
+
+ user = User(id=1, username="1")
+
+ digests = []
+ for ordering in [[ds_a, ds_b, ds_c], [ds_c, ds_a, ds_b], [ds_b, ds_c,
ds_a]]:
+ with (
+ patch.dict(
+ current_app.config,
+ {
+ "THUMBNAIL_EXECUTORS": [ExecutorType.CURRENT_USER],
+ "THUMBNAIL_DASHBOARD_DIGEST_FUNC": None,
+ },
+ ),
+ patch.object(
+ type(dashboard),
+ "datasources",
+ new_callable=PropertyMock,
+ return_value=ordering,
+ ),
+ patch.object(security_manager, "find_user", return_value=user),
+ patch.object(security_manager, "prefetch_rls_filters",
return_value=None),
+ override_user(user),
+ ):
+ digests.append(get_dashboard_digest(dashboard=dashboard))
+
+ assert digests[0] == digests[1] == digests[2]
+ assert digests[0] is not None
+
+
def test_dashboard_digest_prefetches_rls_filters(
app_context: None,
) -> None: