mirror of
https://github.com/apache/superset.git
synced 2026-07-26 00:22:31 +00:00
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
5.6 KiB
Python
139 lines
5.6 KiB
Python
# Licensed to the Apache Software Foundation (ASF) under one
|
|
# or more contributor license agreements. See the NOTICE file
|
|
# distributed with this work for additional information
|
|
# regarding copyright ownership. The ASF licenses this file
|
|
# to you under the Apache License, Version 2.0 (the
|
|
# "License"); you may not use this file except in compliance
|
|
# with the License. You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing,
|
|
# software distributed under the License is distributed on an
|
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
# KIND, either express or implied. See the License for the
|
|
# specific language governing permissions and limitations
|
|
# under the License.
|
|
"""Scope resolution — turn a path entity into the related-entity walk.
|
|
|
|
Composes :mod:`~superset.versioning.activity.queries` (Phase A
|
|
relationship walks) and :mod:`~superset.versioning.activity.windows`
|
|
(pure interval arithmetic) into the
|
|
``list[EntityWindows]`` scope that
|
|
:func:`~superset.versioning.activity.queries.fetch_change_records`
|
|
consumes.
|
|
|
|
The functions here read the DB (via the Phase A helpers in
|
|
:mod:`~superset.versioning.activity.queries`); the pure window-
|
|
arithmetic functions previously colocated here now live in
|
|
:mod:`~superset.versioning.activity.windows` so the package no longer
|
|
needs a lazy import to dodge a ``scope ↔ queries`` cycle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from superset.versioning.activity.kinds import EntityWindows, Window
|
|
from superset.versioning.activity.queries import (
|
|
batch_datasets_used_by_charts,
|
|
charts_attached_to_dashboard,
|
|
datasets_used_by_chart,
|
|
)
|
|
from superset.versioning.activity.windows import (
|
|
intersect_windows,
|
|
merge_entity_windows,
|
|
)
|
|
|
|
|
|
def resolve_scope(
|
|
path_kind: str,
|
|
path_id: int,
|
|
include: str,
|
|
self_start_tx: int | None = None,
|
|
) -> list[EntityWindows]:
|
|
"""Build the ``[(api_kind, entity_id, [windows])]`` list that
|
|
:func:`~superset.versioning.activity.queries.fetch_change_records`
|
|
consumes, branching by *path_kind* and *include* mode.
|
|
|
|
*self_start_tx* lower-bounds the self window at the path entity's first
|
|
tracked transaction (see
|
|
:func:`~superset.versioning.activity.queries.first_tracked_tx`). This
|
|
scopes the self stream to the current entity's own history: matching
|
|
self records on the bare integer id would otherwise inherit a
|
|
hard-deleted predecessor's change records under id reuse
|
|
(SQLite/MySQL reuse ``max(id)+1``). ``None`` means the entity has no
|
|
tracked history yet, so the self stream contributes nothing.
|
|
"""
|
|
want_self: bool = include in ("all", "self")
|
|
want_related: bool = include in ("all", "related")
|
|
|
|
scope: list[EntityWindows] = []
|
|
if want_self and self_start_tx is not None:
|
|
scope.append((path_kind, path_id, [Window(self_start_tx, None)]))
|
|
if want_related and self_start_tx is not None:
|
|
scope.extend(_resolve_related_scope(path_kind, path_id, self_start_tx))
|
|
return scope
|
|
|
|
|
|
def _resolve_related_scope(
|
|
path_kind: str, path_id: int, self_start_tx: int
|
|
) -> list[EntityWindows]:
|
|
"""Walk the dependency edges from the path entity to its related
|
|
entities. Datasets have no transitive layer in V2.
|
|
|
|
The path entity's incarnation boundary clips every related window so a
|
|
reused integer id cannot inherit a hard-deleted predecessor's dependency
|
|
history.
|
|
"""
|
|
if path_kind == "Dashboard":
|
|
related: list[EntityWindows] = _resolve_dashboard_scope(path_id)
|
|
elif path_kind == "Slice":
|
|
related = _resolve_chart_scope(path_id)
|
|
else:
|
|
return []
|
|
|
|
incarnation = Window(self_start_tx, None)
|
|
clipped: list[EntityWindows] = []
|
|
for api_kind, entity_id, windows in related:
|
|
overlaps = [
|
|
overlap
|
|
for window in windows
|
|
if (overlap := intersect_windows(incarnation, window)) is not None
|
|
]
|
|
if overlaps:
|
|
clipped.append((api_kind, entity_id, overlaps))
|
|
return merge_entity_windows(clipped)
|
|
|
|
|
|
def _resolve_dashboard_scope(dashboard_id: int) -> list[EntityWindows]:
|
|
"""Charts on the dashboard during their attachment window, plus
|
|
datasets each chart pointed at during the intersection of (chart-
|
|
attachment, chart-on-dataset)."""
|
|
scope: list[EntityWindows] = []
|
|
chart_windows: dict[int, list[Window]] = {}
|
|
for slice_id, window in charts_attached_to_dashboard(dashboard_id):
|
|
chart_windows.setdefault(slice_id, []).append(window)
|
|
|
|
# One query for the dataset-history of every chart on the dashboard,
|
|
# not one query per chart. The per-slice form was O(n_charts) round-
|
|
# trips which dominated p95 on rich dashboards.
|
|
dataset_windows_by_slice = batch_datasets_used_by_charts(set(chart_windows))
|
|
|
|
for slice_id, attachment_windows in chart_windows.items():
|
|
scope.append(("Slice", slice_id, list(attachment_windows)))
|
|
dataset_windows = dataset_windows_by_slice.get(slice_id, [])
|
|
for attachment in attachment_windows:
|
|
for dataset_id, chart_dataset_window in dataset_windows:
|
|
if (
|
|
intersect := intersect_windows(attachment, chart_dataset_window)
|
|
) is not None:
|
|
scope.append(("SqlaTable", dataset_id, [intersect]))
|
|
return merge_entity_windows(scope)
|
|
|
|
|
|
def _resolve_chart_scope(slice_id: int) -> list[EntityWindows]:
|
|
"""Datasets the chart pointed at over its full history."""
|
|
scope: list[EntityWindows] = []
|
|
for dataset_id, window in datasets_used_by_chart(slice_id):
|
|
scope.append(("SqlaTable", dataset_id, [window]))
|
|
return merge_entity_windows(scope)
|