Files
DumiFlex-ComfyUI-Wildcard-P…/engine/db/lazy_migrate.py
T
DumiFlex 07fe3ab064 feat(engine): lazy_migrate_row — the ONE migration runtime
Implements spec §4 Flow 5 in full:
- No-op when row is at CURRENT with no future original
- NULL-drop optimization when row.sv == original.sv == CURRENT
- Recovery exact catch-up: promote original into payload (the §4 bug fix —
  write-back gate is 'base_v < CURRENT OR base IS original', not just
  'base_v < CURRENT'; otherwise exact catch-up silently no-ops)
- Still-drifted re-project: tolerantly strip original down to CURRENT
- Deferred strict-validate failure → tolerant_drift_status='broken'
- Standard forward chain otherwise

original_payload_json is inert storage: two writers only (initial
tolerant install + this NULL-drop). No other path rewrites it.
schema_migrated_at is separate from updated_at (structural migration
is not a user edit).
2026-06-05 13:46:49 +03:00

166 lines
6.3 KiB
Python

"""Lazy-read routine — the ONE migration runtime.
See community-web spec
D:\\Desktop\\Wildcard-Pipeline-Community\\docs\\superpowers\\specs\\
2026-06-05-schema-versioning-migration-design.md §4 Flow 5. Every row
read (loadModule / loadBundle) goes through this function; the engine
startup bulk runner also calls it for each stale row.
Six branches:
1. No-op (row at CURRENT, original NULL or original.sv ≤ CURRENT).
2. NULL-drop optimization (row.sv == CURRENT, original.sv == CURRENT).
3. Recovery exact catch-up (original.sv ≤ CURRENT, strict-validate
succeeds): promote original into payload, NULL original column.
4. Still-drifted re-project (original.sv > CURRENT): tolerantly strip.
5. Deferred strict-validate failure: mark row 'broken'.
6. Standard forward chain (row.sv < CURRENT, no recovery applicable).
CRITICAL bug-fix from spec §4: the write-back gate is
'base_v < current OR base IS original', NOT just 'base_v < current'.
Otherwise the recovery exact-catch-up case (base_v == current AND base
IS original) silently no-ops and the row stays drifted.
The three dependency-injected dicts:
migrators[(kind, v)] → (payload, ctx) -> next_payload
validators[(kind, v, subtype)] → payload -> bool
tolerant_strip[(kind, target_v, sub)] → payload -> stripped_payload
Subtype lookup: for modules, derived from payload["type"]; for bundles, None.
original_payload_json is INERT — only the initial tolerant install + this
NULL-drop touch it. schema_migrated_at is SEPARATE from updated_at
(structural migration is not a user edit).
"""
from __future__ import annotations
import json
import sqlite3
from collections.abc import Callable
from typing import Any
from engine._utils import now_iso
Migrators = dict[tuple[str, int], Callable[[dict[str, Any], dict], dict]]
Validators = dict[tuple[str, int, str | None], Callable[[dict[str, Any]], bool]]
TolerantStrip = dict[tuple[str, int, str | None], Callable[[dict[str, Any]], dict]]
def lazy_migrate_row(
conn: sqlite3.Connection,
*,
kind: str,
row_id: str,
current_version: int,
migrators: Migrators,
validators: Validators,
tolerant_strip: TolerantStrip | None = None,
) -> None:
"""Apply lazy migration to a single row.
Idempotent: if row is already at CURRENT and original is NULL or
matches CURRENT, returns without writing (or fires NULL-drop only).
"""
table = "modules" if kind == "module" else "bundles"
row = conn.execute(f"SELECT * FROM {table} WHERE id=?", (row_id,)).fetchone()
if row is None:
return
sv = row["schema_version"]
original_raw = row["original_payload_json"]
original = json.loads(original_raw) if original_raw else None
# Narrow original_v to int so the rest of the function's `original is not None`
# checks correctly imply an int. A malformed original (no schema_version, or
# non-int) is demoted to the no-original branch — the row's payload column is
# the only thing we trust.
original_v: int | None = None
if original is not None:
raw_v = original.get("schema_version")
if isinstance(raw_v, int):
original_v = raw_v
else:
original = None
payload = json.loads(row["payload"])
subtype = payload.get("type") if kind == "module" else None
# Branch 1: no-op or NULL-drop
if sv == current_version and (
original is None or (original_v is not None and original_v <= current_version)
):
if original is not None and original_v == sv:
conn.execute(
f"UPDATE {table} SET original_payload_json=NULL, "
f"tolerant_drift_status='none' WHERE id=?",
(row_id,),
)
conn.commit()
return
# Decide base
base_is_original = False
base_v: int
if original is not None and original_v is not None and original_v > sv:
if original_v <= current_version:
# Recovery candidate — deferred strict-validate FIRST
strict = validators.get((kind, original_v, subtype))
if strict is not None and not strict(original):
conn.execute(
f"UPDATE {table} SET tolerant_drift_status='broken', "
f"schema_migrated_at=? WHERE id=?",
(now_iso(), row_id),
)
conn.commit()
return
base = original
base_v = original_v
base_is_original = True
else:
# Still drifted; tolerantly strip original down to current
strip = (tolerant_strip or {}).get((kind, current_version, subtype))
if strip is None:
return
projected = strip(original)
conn.execute(
f"UPDATE {table} SET payload=?, schema_version=?, "
f"tolerant_drift_status='pending', schema_migrated_at=? WHERE id=?",
(
json.dumps(projected),
current_version,
now_iso(),
row_id,
),
)
conn.commit()
return
else:
base = payload
base_v = sv
# Write-back gate (THE BUG FIX): write when base_v < current OR base IS original.
# The recovery exact-catch-up case has zero chain steps (base_v == current_version)
# but still needs the write to promote original into payload + NULL the original
# column. Gating on 'base_v < current_version' alone silently no-ops the recovery.
needs_write = (base_v < current_version) or base_is_original
if not needs_write:
return
new_payload = base
for v in range(base_v, current_version):
migrator = migrators.get((kind, v))
if migrator is None:
return
new_payload = migrator(new_payload, {})
promote_from_original = base_is_original
conn.execute(
f"UPDATE {table} SET payload=?, schema_version=?, schema_migrated_at=?, "
f"original_payload_json=?, tolerant_drift_status=? WHERE id=?",
(
json.dumps(new_payload),
current_version,
now_iso(),
None if promote_from_original else row["original_payload_json"],
"none" if promote_from_original else row["tolerant_drift_status"],
row_id,
),
)
conn.commit()