aminghadersohi commented on code in PR #43838:
URL: https://github.com/apache/superset/pull/43838#discussion_r3931366905
##########
superset/versioning/schemas.py:
##########
@@ -267,6 +281,17 @@ class ActivityImpactSchema(Schema):
)
},
)
+ chart_names = fields.List(
Review Comment:
**Naming: `chart_names` holds objects, not names.**
The field is a list of `{id, name}`, and the description has to spell that
out ("The affected sibling charts (id + name)"). `charts` is already taken by
the count, but something like `affected_charts` or `chart_refs` would describe
the payload without the description having to correct the field name. Public
API field names are hard to change once shipped, so it's worth a moment now.
Purely a naming call — no behavioural concern.
Minor, same block: the literal "Capped at 50 entries" duplicates
`IMPACT_CHART_NAMES_CAP`; if the constant moves, this description goes stale
silently.
##########
superset/versioning/activity/impact.py:
##########
@@ -159,28 +179,56 @@ def batch_chart_counts(
row["slice_end"] is None or row["slice_end"] > target_tx
)
if in_m2m and in_slice:
- matches.setdefault((ds_id, target_tx),
set()).add(row["slice_id"])
+ matches.setdefault((ds_id, target_tx), {})[row["slice_id"]] = (
Review Comment:
**The m2m half of this predicate never closes, so charts detached from the
dashboard are still matched — and this PR promotes that from an inflated number
into a named, user-visible list.**
`row["m2m_end"]` is `dashboard_slices_version.end_transaction_id`. That's an
*association* shadow, and SQLAlchemy-Continuum never populates
`end_transaction_id` on association tables:
`UnitOfWork.update_version_validity` — the thing that closes a window — is
reached only from `process_operation`, i.e. for mapped parent objects.
Association rows go through `create_association_versions`, which does nothing
but `INSERT` with `transaction_id` set. A detach is recorded as a **separate
row with `operation_type = 2`**, not as a close of the original row, and the
`operation_type != 2` filter on line 144 removes exactly that row — leaving the
still-open `operation_type = 0` row to match forever.
I verified this rather than taking it on faith. Standalone probe (Continuum,
`strategy: validity`, in-memory SQLite): attach charts A+B at tx1 → detach B at
tx2 → rename A at tx3 → re-attach B at tx4.
```
== dash_slices_version ==
{'dashboard_id': 1, 'slice_id': 1, 'transaction_id': 1,
'end_transaction_id': None, 'operation_type': 0}
{'dashboard_id': 1, 'slice_id': 2, 'transaction_id': 1,
'end_transaction_id': None, 'operation_type': 0}
{'dashboard_id': 1, 'slice_id': 2, 'transaction_id': 2,
'end_transaction_id': None, 'operation_type': 2}
{'dashboard_id': 1, 'slice_id': 2, 'transaction_id': 4,
'end_transaction_id': None, 'operation_type': 0}
== slices_version ==
{'id': 1, 'slice_name': 'Chart A', 'transaction_id': 1,
'end_transaction_id': 3, 'operation_type': 0}
{'id': 2, 'slice_name': 'Chart B', 'transaction_id': 1,
'end_transaction_id': None, 'operation_type': 0}
{'id': 1, 'slice_name': 'Chart A renamed', 'transaction_id': 3,
'end_transaction_id': None, 'operation_type': 1}
```
Every m2m `end_transaction_id` is `NULL`, including for the chart that was
detached; the parent shadow's is correctly populated (`3`). So `row["m2m_end"]
is None or ...` on line 176 is vacuously true and the m2m window is unbounded
on the right.
**Failure scenario.** Chart "Q3 revenue" is on dashboard D pointing at
dataset X. It is removed from D at tx2. Someone edits a metric on X at tx5. The
rollup row for that edit counts the chart as affected, and with this PR
hovering now *displays* "Q3 revenue" as a chart the change affected — two
transactions after it stopped being on the dashboard.
The `in_slice` half is fine: `slices_version` is a parent shadow and its
windows really are closed, as the probe shows. Only the m2m half is affected.
**Scope / suggested handling.** This is pre-existing on master, not
introduced by you, and it's shared with at least
`activity/queries.py:charts_attached_to_dashboard`,
`changes/shadow_queries.py`, and `restore.py:_restore_dashboard_membership`,
which all apply the same validity predicate to this same M2M table. So I
wouldn't ask you to fix it inside this PR. But since #43837 is already
correcting exactly this in `charts_attached_to_dashboard`, it may be worth
making that a shared helper and pointing `impact.py` at it too, rather than
leaving this path on the unbounded predicate while the sibling path is fixed.
The predicate Continuum itself uses for associations is "latest non-DELETE row
per `(dashboard_id, slice_id)` with `transaction_id <= target`" — a
`max(transaction_id)` group-by rather than an end-window (see
`RelationshipBuilder.association_subquery`).
##########
superset/versioning/activity/impact.py:
##########
@@ -50,6 +52,19 @@
)
+class ChartRef(TypedDict):
+ """One affected chart in an ``impact`` payload: id plus
name-at-transaction."""
+
+ id: int
+ name: str
+
+
+# The wire ``chart_names`` list is capped so one dataset feeding very many
+# charts cannot balloon every related record on the page (page sizes reach
+# 200 records); ``charts`` always carries the full count.
+IMPACT_CHART_NAMES_CAP = 50
Review Comment:
**The cap is per record, so it doesn't bound the response the comment
reasons about.**
The comment argues from page size ("page sizes reach 200 records"), but the
cap is applied in `impact_for_record`, once per record, and
`charts[:IMPACT_CHART_NAMES_CAP]` produces a fresh list per record even when
several records share one `(dataset, tx)` pair. Worst case is 200 × 50 = 10,000
chart refs in one response — roughly 0.5-1 MB of JSON — not 50.
In practice the cap only binds when a single dataset feeds more than 50
charts *on one dashboard*, so the realistic bound is 200 × (charts on that
dashboard for that dataset), and 50 is a sensible per-tooltip number. Not
asking you to change the value. But the comment reads as if it bounds the page
when it bounds a record — either reword it, or add a page-level budget if you
want a hard ceiling.
##########
superset/versioning/activity/render.py:
##########
@@ -168,7 +167,7 @@ def apply_record_decoration(
record["impact"] = None
else:
record["summary"] = _build_summary(api_kind, record)
- record["impact"] = impact_for_record(record, path_kind,
impact_counts)
+ record["impact"] = impact_for_record(record, path_kind,
impact_refs)
Review Comment:
**`impact` isn't cleared by the tombstone redaction below, and it just
became a lot more descriptive.**
The block on lines 171-191 deliberately enumerates what a deleted related
entity must not disclose — `entity_name`, `summary`, `changed_by`,
`from_value`, `to_value`, `path` — but `record["impact"]`, set on this line, is
left as-is. Before this PR it was a bare count; now it carries chart ids and
names.
My own read is that this is **probably not** a boundary violation: `impact`
is non-null only when the related entity is a `SqlaTable`, and the charts it
names are members of the *path* dashboard, which the requester is already gated
to via `raise_for_access`. So it names the requester's own dashboard's charts,
not anything identifying the deleted dataset. The one edge is that the named
charts are those attached at the historical transaction, which may no longer be
on the dashboard.
Filing this as a question rather than a finding, per this repo's guidance: I
can't point at a role/capability row in `SECURITY.md` that it violates. But the
redaction block is an allowlist-by-omission, and the thing it omits is richer
than when the block was written — worth an explicit decision, and perhaps a
comment recording it, either way. (Independently raised by bito.)
--
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]