Files
DumiFlex-ComfyUI-Wildcard-P…/tests/engine/db/test_migrations.py
T
DumiFlex 39529a669b feat(engine): migrate library v1->v2 (multi-tag) + bump schema version
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.
2026-06-07 01:40:53 +03:00

382 lines
14 KiB
Python

"""Tests for the versioned migration runner."""
import sqlite3
from engine.db.connection import get_connection
from engine.db.migrations import _discover, current_version, migrate
def test_initial_version_is_zero(tmp_path):
conn = get_connection(tmp_path / "v.db")
assert current_version(conn) == 0
conn.close()
def test_migrate_creates_modules_and_categories_and_migrations(tmp_path):
conn = get_connection(tmp_path / "m.db")
migrate(conn)
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table';"
).fetchall()
}
assert "modules" in tables
assert "module_categories" in tables
assert "migrations" in tables
conn.close()
def test_migrate_records_version(tmp_path):
conn = get_connection(tmp_path / "v2.db")
migrate(conn)
# Keep this assertion in sync with the highest-numbered migration in
# ``engine/db/migrations_sql``. (016_wildcard_multi_subcategory_v2.py
# is current head.)
assert current_version(conn) == 16
conn.close()
def test_migrate_creates_bundles_table_with_expected_columns(tmp_path):
"""Migration 005 adds the bundles library table."""
conn = get_connection(tmp_path / "b.db")
migrate(conn)
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table';"
).fetchall()
}
assert "bundles" in tables
cols = {row[1] for row in conn.execute("PRAGMA table_info(bundles);")}
assert cols == {
"id", "name", "description", "color", "category_id",
"tags", "is_favorite", "children", "payload_hash",
"version", "created_at", "updated_at",
# Migration 013 — community install origin.
"community_post_slug", "community_version_number",
# Migration 014 — payload schema_version + verbatim-local mirror.
"schema_version", "original_payload_json",
"tolerant_drift_status", "schema_migrated_at",
# Migration 015 — NSFW flag.
"content_rating",
}
conn.close()
def test_migrate_creates_import_undo_table_with_expected_columns(tmp_path):
"""Migration 007 adds the import_undo metadata table used by
`engine.importer` to persist enough info to reverse a commit."""
conn = get_connection(tmp_path / "u.db")
migrate(conn)
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table';"
).fetchall()
}
assert "import_undo" in tables
cols = {row[1] for row in conn.execute("PRAGMA table_info(import_undo);")}
assert cols == {
"id", "created_at", "imported_records",
"replaced_snapshots", "rename_map",
}
conn.close()
def test_migrate_creates_cascade_undo_table_with_expected_columns(tmp_path):
"""Migration 009 adds the cascade_undo metadata table used by
cascade-edit transactions to persist BEFORE/AFTER snapshots for undo."""
conn = get_connection(tmp_path / "c_undo.db")
migrate(conn)
tables = {
row[0]
for row in conn.execute(
"SELECT name FROM sqlite_master WHERE type='table';"
).fetchall()
}
assert "cascade_undo" in tables
cols = {row[1] for row in conn.execute("PRAGMA table_info(cascade_undo);")}
assert cols == {
"id", "created_at", "target_kind", "target_id",
"action", "snapshot_before", "snapshot_after",
}
conn.close()
def test_migrate_is_idempotent(tmp_path):
conn = get_connection(tmp_path / "i.db")
migrate(conn)
before = current_version(conn)
migrate(conn)
assert current_version(conn) == before
conn.close()
def test_modules_table_has_expected_columns(tmp_path):
"""End-state schema, post all migrations.
Migration 004 dropped the standalone `uuid` column — `id` IS the
canonical 8-hex uuid now."""
conn = get_connection(tmp_path / "c.db")
migrate(conn)
cols = {row[1] for row in conn.execute("PRAGMA table_info(modules);")}
assert cols == {
"id", "type", "name", "description", "category_id",
"tags", "is_favorite", "payload", "snapshot_fingerprint",
"version", "created_at", "updated_at",
# Migration 013 — community install origin.
"community_post_slug", "community_version_number",
# Migration 014 — payload schema_version + verbatim-local mirror.
"schema_version", "original_payload_json",
"tolerant_drift_status", "schema_migrated_at",
# Migration 015 — NSFW flag.
"content_rating",
}
conn.close()
def _migrate_up_to(conn: sqlite3.Connection, max_version: int) -> None:
"""Apply migrations 001..max_version (inclusive) to a fresh DB.
Used by intermediate-state tests below — migration 003 is meant
to be exercised in isolation since 004 supersedes it. We can't
rely on the public `migrate()` because it always migrates to head."""
for version, mig_file in _discover():
if version > max_version:
break
if mig_file.suffix == ".py":
from engine.db.migrations import _load_py_migration
up = _load_py_migration(mig_file)
with conn:
up(conn)
conn.execute(
"INSERT INTO migrations(version, applied_at) VALUES(?, datetime('now'));",
(version,),
)
else:
sql = mig_file.read_text()
with conn:
conn.executescript(sql)
conn.execute(
"INSERT OR IGNORE INTO migrations(version, applied_at) "
"VALUES(?, datetime('now'));",
(version,),
)
def test_004_unifies_id_with_uuid_and_drops_uuid_column(tmp_path):
"""Migration 004 rewrites every `modules.id` to its 8-hex `uuid`
suffix and removes the standalone uuid column. Verified end-to-end
against a real sqlite3 connection so we catch syntax errors the
runner ignores."""
db = tmp_path / "lib.db"
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
# Stop at 003 — that's the legacy state with the dual id+uuid model.
_migrate_up_to(conn, 3)
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('wc_color_aabbccdd', 'aabbccdd', 'wildcard', 'color', '', NULL, "
"'[]', 0, '{}', 1, datetime('now'), datetime('now'));"
)
conn.commit()
# Now apply 004 (and subsequent migrations, if any).
migrate(conn)
cols = {r["name"] for r in conn.execute("PRAGMA table_info(modules);")}
assert "uuid" not in cols, "004 should drop the standalone uuid column"
row = conn.execute("SELECT id FROM modules;").fetchone()
assert row["id"] == "aabbccdd", "id should be rewritten to its 8-hex form"
def test_004_rewrites_pipeline_step_module_id_cross_refs(tmp_path):
"""Migration 004 must rewrite cross-refs inside payload JSON, not
just the id column. A pipeline whose step references the legacy
slug `wc_color_aabbccdd` must end up referencing `aabbccdd`."""
db = tmp_path / "lib.db"
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
_migrate_up_to(conn, 3)
# Two rows: a wildcard that will be referenced, and a pipeline
# that points at it via the legacy slug form.
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('wc_color_aabbccdd', 'aabbccdd', 'wildcard', 'color', '', NULL, "
"'[]', 0, '{}', 1, datetime('now'), datetime('now'));"
)
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('pl_main_11223344', '11223344', 'pipeline', 'main', '', NULL, "
"'[]', 0, ?, 1, datetime('now'), datetime('now'));",
('{"steps":[{"module_id":"wc_color_aabbccdd","enabled":true}]}',),
)
conn.commit()
migrate(conn)
import json
row = conn.execute(
"SELECT payload FROM modules WHERE id = '11223344';"
).fetchone()
payload = json.loads(row["payload"])
assert payload["steps"][0]["module_id"] == "aabbccdd"
def test_004_rewrites_constraint_source_target_cross_refs(tmp_path):
"""Constraint payloads carry source/target wildcard ids — they
have to be rewritten alongside pipeline step ids."""
db = tmp_path / "lib.db"
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
_migrate_up_to(conn, 3)
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('wc_outfit_a1a1a1a1', 'a1a1a1a1', 'wildcard', 'outfit', '', NULL, "
"'[]', 0, '{}', 1, datetime('now'), datetime('now'));"
)
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('wc_color_b2b2b2b2', 'b2b2b2b2', 'wildcard', 'color', '', NULL, "
"'[]', 0, '{}', 1, datetime('now'), datetime('now'));"
)
conn.execute(
"INSERT INTO modules(id, uuid, type, name, description, category_id, "
"tags, is_favorite, payload, version, created_at, updated_at) "
"VALUES ('ct_x_c3c3c3c3', 'c3c3c3c3', 'constraint', 'x', '', NULL, "
"'[]', 0, ?, 1, datetime('now'), datetime('now'));",
(
'{"source_wildcard_id":"wc_outfit_a1a1a1a1",'
'"target_wildcard_id":"wc_color_b2b2b2b2"}',
),
)
conn.commit()
migrate(conn)
import json
row = conn.execute(
"SELECT payload FROM modules WHERE id = 'c3c3c3c3';"
).fetchone()
payload = json.loads(row["payload"])
assert payload["source_wildcard_id"] == "a1a1a1a1"
assert payload["target_wildcard_id"] == "b2b2b2b2"
def test_004_is_idempotent(tmp_path):
"""Running 004 twice (via re-running migrate on an already-migrated
DB) must not error or re-mangle ids.
Version assertion uses the head migration number — keep in sync
when new migrations land.
"""
conn = get_connection(tmp_path / "i.db")
migrate(conn)
migrate(conn) # second call should be a no-op
assert current_version(conn) == 16
conn.close()
def test_008_backfills_null_fingerprints(tmp_path):
"""Migration 008 must hash every NULL-fingerprint row up to the
same value ModuleRepository.create() would produce for the same
content. Without this, exported-then-reimported legacy rows show
the amber EXISTING badge (exists-unknown) instead of silent-skip."""
db = tmp_path / "fp.db"
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
# Stop at 007 — that's the schema before 008 runs.
_migrate_up_to(conn, 7)
conn.execute(
"INSERT INTO modules(id, type, name, description, category_id, "
"tags, is_favorite, payload, snapshot_fingerprint, "
"version, created_at, updated_at) "
"VALUES ('aabbccdd', 'wildcard', 'color', 'palette', NULL, "
"'[\"warm\",\"primary\"]', 0, ?, NULL, "
"1, datetime('now'), datetime('now'));",
('{"values":["red","blue"]}',),
)
# Row with an already-set fingerprint should stay untouched.
conn.execute(
"INSERT INTO modules(id, type, name, description, category_id, "
"tags, is_favorite, payload, snapshot_fingerprint, "
"version, created_at, updated_at) "
"VALUES ('11223344', 'wildcard', 'x', '', NULL, '[]', 0, '{}', "
"'cafebabe', 1, datetime('now'), datetime('now'));"
)
conn.commit()
migrate(conn)
from engine._fingerprint import module_fingerprint
from engine.modules.snapshot import payload_hash
row = conn.execute(
"SELECT snapshot_fingerprint FROM modules WHERE id = 'aabbccdd';"
).fetchone()
expected = module_fingerprint({
"type": "wildcard",
"name": "color",
"description": "palette",
"tags": ["warm", "primary"],
"payload_hash": payload_hash({"values": ["red", "blue"]}),
})
assert row["snapshot_fingerprint"] == expected
untouched = conn.execute(
"SELECT snapshot_fingerprint FROM modules WHERE id = '11223344';"
).fetchone()
assert untouched["snapshot_fingerprint"] == "cafebabe"
conn.close()
def test_008_matches_repository_create_fingerprint(tmp_path):
"""A row inserted pre-008 with NULL fingerprint should end up
matching the fingerprint a freshly-created row with identical
content gets via ``ModuleRepository.create``. This is the property
that turns ``exists-unknown`` into ``silent-skip`` on re-import."""
from engine.db.connection import get_connection
from engine.db.repositories import ModuleRepository
db = tmp_path / "match.db"
conn = sqlite3.connect(str(db))
conn.row_factory = sqlite3.Row
_migrate_up_to(conn, 7)
conn.execute(
"INSERT INTO modules(id, type, name, description, category_id, "
"tags, is_favorite, payload, snapshot_fingerprint, "
"version, created_at, updated_at) "
"VALUES ('deadbeef', 'wildcard', 'mood', 'm', NULL, "
"'[\"a\",\"b\"]', 0, ?, NULL, "
"1, datetime('now'), datetime('now'));",
('{"values":["hi"]}',),
)
conn.commit()
migrate(conn)
backfilled = conn.execute(
"SELECT snapshot_fingerprint FROM modules WHERE id='deadbeef';"
).fetchone()["snapshot_fingerprint"]
conn.close()
fresh_db = tmp_path / "fresh.db"
fresh = get_connection(fresh_db)
migrate(fresh)
repo = ModuleRepository(fresh)
row = repo.create(
type="wildcard", name="mood", description="m",
category_id=None, tags=["a", "b"],
payload={"values": ["hi"]},
)
assert row["snapshot_fingerprint"] == backfilled
fresh.close()