mikebridge commented on PR #41076:
URL: https://github.com/apache/superset/pull/41076#issuecomment-4927469980

   ## Versioning base-infra — verified review notes (head `bf31b236`)
   
   _Generated by a multi-lens review (committer / python / sqlalchemy / DDD 
lenses) and then independently verified against the source — every finding 
below was re-checked by reading the code at this head, and a few of the raw 
findings were dropped or down-scoped in that pass (noted where relevant). Line 
numbers are against `bf31b236`. SIP/process items are tracked separately and 
excluded here._
   
   **TL;DR** — nothing here blocks the dark launch. It ships inert behind 
`ENABLE_VERSIONING_CAPTURE` (default off), the migration is 
additive/reversible, and the security posture (per-object `raise_for_access` on 
every read endpoint, fail-closed visibility filter, fail-open write path) is 
solid. The items below are (A) capture-path correctness bugs to fix **before** 
flipping the flag on, (B) small cleanups, and (C) forward-looking notes.
   
   ### A. Fix before enabling `ENABLE_VERSIONING_CAPTURE`
   
   These are latent while capture is off, so they don't block merge — but they 
bite the moment capture is enabled.
   
   **`superset/versioning/diff.py:425-430` — duplicate natural keys silently 
overwrite, dropping change records**
   
   The by-key maps are built last-write-wins: two adhoc filters on the same 
`subject` column (or two metrics sharing a `label`) collapse to one, so 
editing/removing one emits a mangled single record and the other's change is 
lost. The `_filter_key` docstring's promise that same-column filters stay 
disambiguable can't hold once the collision drops one.
   
   Would it be worth disambiguating on collision (secondary key, or fall back 
to positional index only for colliding items)?
   
   **`superset/versioning/diff.py:420-427` (+ listener catch at 
`changes/listener.py:239`) — an unhashable `subject` raises `TypeError`, losing 
that entity's scalar diff**
   
   `_filter_key` returns `f.get("subject")` (typed `Key`, but it's `Any` from 
user JSON). A list/dict subject flows unguarded into `from_by_key[<list>]` → 
`TypeError: unhashable`. It's caught per-entity, so the blast radius is just 
that one entity's scalar change records for the save (not the whole save) — but 
the record loss is silent. A `str(...)` coercion (or a fallback in 
`_effective_key` when the key isn't `str`/`int`/`None`) closes it.
   
   **`superset/commands/importers/v1/utils.py:391-419` — 
`_prime_versioning_unit_of_work`'s broad `except` gives false assurance**
   
   The docstring promises it "never breaks an import," but the broad `except 
Exception` only genuinely covers the ImportError (continuum-absent) case. If 
`unit_of_work()` fails for any other reason while capture is enabled, the very 
next `dashboard_slices.insert()` fires Continuum's `before_execute` with no UoW 
and raises `KeyError` — the import dies anyway, now with a misleading 
"proceeding without it" log. Narrowing to `except ImportError:` lets the real 
failure surface at its source. (Narrow trigger — `unit_of_work` rarely fails — 
but the false assurance is the issue.)
   
   **`superset/versioning/activity/queries.py:444-489` — per-kind budget can 
drop later chunks' records and mislabel the window as complete**
   
   When one kind's entities span multiple id-chunks and fill the ~5000-row 
budget, the loop `break`s and later chunks contribute zero rows — including 
records newer than the truncation floor — yet `truncated=True` presents it as a 
clean "older records omitted" cut. (Correction to the raw finding: the drop 
order is arbitrary but **deterministic** — not `PYTHONHASHSEED`-dependent — 
since these are `set[int]`. High bar: >500 entities of one kind **and** >5000 
matching rows.) Applying the ceiling after concatenating + re-sorting all 
chunks (and `sorted(entity_ids)` before chunking) makes any residual drop 
deterministic and correctly labeled.
   
   ### B. Small cleanups (unconditional)
   
   **`superset/versioning/diff.py:799-805` — `_meta_excluding_position` doesn't 
exclude anything**
   
   Name + docstring promise positional bits are stripped from `meta`; the body 
just returns `dict(meta)` unchanged. It's correct-by-accident today (position 
lives on `node["parents"]`, handled separately), but the next field added to 
`meta` silently breaks the move-vs-edit contract the name advertises. Rename to 
`_meta_copy` (+ honest docstring) or actually implement the strip.
   
   **`Slice` → `chart` dispatch is spread across ~8 parallel structures keyed 
on the class-name string**
   
   `ENTITY_KIND_BY_CLASS_NAME` (`changes/table.py:86`), `TABLE_KIND_TO_API` / 
`API_KIND_TO_TABLE` / `API_KIND_LABEL` / `USER_FACING_KIND` / `NOT_FOUND_EXC` / 
`NAME_COLUMN` (`activity/kinds.py:73-115`), `_RAISE_FOR_ACCESS_KWARG` 
(`api_helpers.py:136`), plus string-dispatch ladders in `changes/state.py`. 
Adding a fourth versioned entity means editing all of them in lock-step with 
nothing catching a miss. A single registry (one entry per entity, 
constructor-checked for completeness) turns "did I update every table?" into a 
type error. The DDD lens flagged this as the one durable must-fix as the 
surface grows.
   
   **`superset/daos/dataset.py:534-537` & `:602-605` — `_upsert_columns` / 
`update_metrics` lack the `protected_keys` guard**
   
   The sibling `_override_columns` (`:512-516`) protects `("id", "table_id")` 
before `setattr`; these two loops set every payload key unguarded. Benign 
today, but it's the exact footgun the sibling was hardened against — worth the 
same guard for symmetry.
   
   ### C. Forward-looking (not this PR, but worth a note)
   
   - **Shadow-table FKs to `version_transaction` have no `ondelete`, while 
`version_changes.transaction_id` has `ondelete="CASCADE"`** (migration 
`56cd24c07170`). When the retention task (stacked PR) deletes 
`version_transaction` rows, it must delete referencing shadow rows first, in 
order, or every prune cycle hits an FK violation on PG/MySQL. Either add 
`ondelete="CASCADE"` to the six shadow FKs, or make the deletion-ordering 
contract explicit where retention lands.
   - **Migration idempotency is inconsistent** — `add_versioning_tables` uses 
raw `create_table` (a partial failure on MySQL's non-transactional DDL isn't 
re-runnable), while the index follow-up migration is inspector-guarded. Worth 
matching the guarded pattern (or documenting the manual-cleanup caveat).
   - **Per-save cost once capture is on** — 
`SkipUnmodifiedPlugin._matches_previous_version` (`factory.py:294`) + 
`_read_pre_state` add two full-row reads (incl. large `MediumText` columns) per 
versioned save. `perf_validation_tests.py` is the right place to surface 
numbers before the default flips.
   
   ### Checked and cleared
   
   - `SqlaTable.__versioned__` excluding `editors` but not `viewers` — **not a 
problem**: `SqlaTable` has no `viewers` relationship, so there's nothing to 
exclude (unlike Dashboard/Slice).
   - Import-time `make_versioned()` in `superset/extensions/__init__.py` — no 
model classes are imported at that point, so it's the correct Continuum 
ordering, not an import-safety regression.
   
   Praise where due: the fail-open write path + dedicated capture-error 
metrics, the thorough kill-switch (detaches Continuum's own listeners *and* 
verifies), the deterministic UUIDv5 version identity, the pure/functional diff 
engine with `ChangeRecord`/`Window` as clean value objects, and an unusually 
honest `UPDATING.md` are all genuinely well done.
   


-- 
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]

Reply via email to