Files
superset2/superset/versioning/activity/windows.py
Mike Bridge caf017bd0b feat(versioning): cross-entity version activity view (#41076)
Co-authored-by: Mike Bridge <michael.bridge@ext.preset.io>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-18 11:06:54 -07:00

109 lines
4.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
"""Pure window arithmetic on half-open ``[start_tx, end_tx)`` intervals.
Extracted from the DB-touching scope resolution so that:
* :mod:`scope` (DB-touching) can import this module at module-top.
* :mod:`queries.fetch_change_records` can import
:func:`row_within_any_window` at module-top instead of through a
lazy import that previously dodged a ``scope ↔ queries`` cycle.
Everything here is pure Python — no DB, no Flask. ``end_tx = None``
means "open-ended (current)" and behaves like positive infinity.
"""
from __future__ import annotations
from typing import Any
from superset.versioning.activity.kinds import EntityWindows, Window
def intersect_windows(outer: Window, inner: Window) -> Window | None:
"""Intersect two half-open ``[start_tx, end_tx)`` windows.
Returns the clipped overlap, or ``None`` when they are disjoint.
``end_tx = None`` means "open ended (current)" and acts like
positive infinity. Thin wrapper over :meth:`Window.intersect` —
kept as a free function so callers and tests don't have to migrate
to method form in lockstep with the dataclass promotion.
"""
return outer.intersect(inner)
def row_within_any_window(row: dict[str, Any], windows: list[Window]) -> bool:
"""``True`` iff ``row['transaction_id']`` falls inside at least one
of *windows*. Half-open interval semantics match
:func:`intersect_windows`."""
if not windows:
return False
tx_id: int = row["transaction_id"]
return any(w.contains(tx_id) for w in windows)
def merge_entity_windows(scope: list[EntityWindows]) -> list[EntityWindows]:
"""Collapse repeated ``(api_kind, entity_id)`` entries by unioning
their window lists, and collapse overlapping/touching windows
within each entity into one.
The OR-clause in
:func:`~superset.versioning.activity.queries.fetch_change_records`
generates one branch per (kind, id, window) tuple. Without the
within-entity union, a chart that's been attached-and-detached
many times (or that repeated fixture loads have populated the M2M
shadow for) yields a separate clause per redundant window — at
~10 entities × ~50 windows the SQL hits SQLite's
``SQLITE_MAX_EXPR_DEPTH`` (1000). Merging here keeps the clause
count proportional to the number of *distinct* validity intervals,
not the number of shadow rows.
"""
merged: dict[tuple[str, int], list[Window]] = {}
for api_kind, entity_id, windows in scope:
merged.setdefault((api_kind, entity_id), []).extend(windows)
return [
(api_kind, entity_id, union_windows(windows))
for (api_kind, entity_id), windows in merged.items()
]
def union_windows(windows: list[Window]) -> list[Window]:
"""Sort + merge overlapping/touching half-open intervals.
Pure function — no DB. Touching ``[a, b)`` and ``[b, c)`` merge into
``[a, c)``. ``end_tx = None`` (open-ended) absorbs everything to its
right. Returns a minimal disjoint cover of the input set.
"""
if not windows:
return []
sorted_windows: list[Window] = sorted(windows, key=lambda w: w.start_tx)
out: list[Window] = [sorted_windows[0]]
for current in sorted_windows[1:]:
prev = out[-1]
if not prev.merges_with(current):
out.append(current)
continue
if prev.end_tx is None:
# Prior window is open-ended; it absorbs everything past.
continue
# Overlapping or touching — extend the prior window.
new_end: int | None = (
None if current.end_tx is None else max(prev.end_tx, current.end_tx)
)
out[-1] = Window(prev.start_tx, new_end)
return out