engine/migrations/v1_to_v2.py: option sub_category->sub_categories[]; sub-category name slugify (whitespace/grammar chars->_, reserved suffixed, de-duped) cascaded to options + tag_groups; instance category_filter list->boolean-expr string + exclude_null flag; nested @{uuid:a,b,null} refs->@{uuid:a or b!null}. Idempotent. Two entry points share one core: envelope migrate_v1_to_v2 (import/export, registered in _CHAIN) + per-row migrate_module/bundle_v1_to_v2 (lazy/bulk DB path).
Register _CHAIN[1] + bump engine CURRENT_SCHEMA_VERSION 1->2. New SQL migration 016 marches stale DB rows 1->2 at boot via run_bulk_lazy + the per-row migrators (014's documented bump pattern). TS mirror (migrations.ts) + client CURRENT bump follow next (no cross-language version assertion couples them). Update version/entity-count assertions in payload-migration, migration-runner, and export-build tests.
66 lines
2.5 KiB
Python
66 lines
2.5 KiB
Python
"""Versioned payload migration chain. Mirror of TypeScript
|
|
`src/manager/import-export/migrations.ts`.
|
|
|
|
Used by `engine/importer.py` on server-side parse paths (re-validate
|
|
on commit). Client-side migration is the source of truth for what
|
|
the user saw in the picker; server re-runs as defense in depth.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from engine.migrations.v0_to_v1 import migrate_v0_to_v1
|
|
from engine.migrations.v1_to_v2 import migrate_v1_to_v2
|
|
|
|
CURRENT_SCHEMA_VERSION = 2
|
|
|
|
_CHAIN = {
|
|
0: migrate_v0_to_v1,
|
|
1: migrate_v1_to_v2,
|
|
}
|
|
|
|
|
|
def migrate_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
"""Walk the migration chain from `payload.schema_version` up to current.
|
|
|
|
Returns one of:
|
|
- {"ok": True, "migrated": <dict>, "migrated_entity_count": int}
|
|
where migrated_entity_count is the cumulative entity count across
|
|
all migration steps (total bundles+wildcards+fixed_values+combines+
|
|
derivations+constraints+categories that passed through any step).
|
|
Matches TS `migratedEntityCount`.
|
|
- {"ok": False, "reason": str}
|
|
"""
|
|
version = payload.get("schema_version")
|
|
if not isinstance(version, int):
|
|
return {"ok": False, "reason": "payload missing schema_version field"}
|
|
if version > CURRENT_SCHEMA_VERSION:
|
|
return {
|
|
"ok": False,
|
|
"reason": f"future schema version {version} (current: {CURRENT_SCHEMA_VERSION})",
|
|
}
|
|
current = {
|
|
"schema_version": version,
|
|
"bundles": payload.get("bundles", []),
|
|
"wildcards": payload.get("wildcards", []),
|
|
"fixed_values": payload.get("fixed_values", []),
|
|
"combines": payload.get("combines", []),
|
|
"derivations": payload.get("derivations", []),
|
|
"constraints": payload.get("constraints", []),
|
|
"categories": payload.get("categories", []),
|
|
}
|
|
migrated_entity_count = 0
|
|
while current["schema_version"] < CURRENT_SCHEMA_VERSION:
|
|
fn = _CHAIN.get(current["schema_version"])
|
|
if fn is None:
|
|
ver = current["schema_version"]
|
|
return {"ok": False, "reason": f"no migration registered for v{ver}"}
|
|
before = sum(
|
|
len(current[k])
|
|
for k in ("bundles", "wildcards", "fixed_values", "combines",
|
|
"derivations", "constraints", "categories")
|
|
)
|
|
current = fn(current)
|
|
migrated_entity_count += before
|
|
return {"ok": True, "migrated": current, "migrated_entity_count": migrated_entity_count}
|