chore: import upstream snapshot with attribution
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
This commit is contained in:
+70
@@ -0,0 +1,70 @@
|
||||
import sqlite3
|
||||
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_2026_07_01_add_workflow_call_queue_metadata import (
|
||||
AddWorkflowCallQueueMetadataCallback,
|
||||
build_migration,
|
||||
)
|
||||
|
||||
|
||||
def _get_columns(cursor: sqlite3.Cursor, table_name: str) -> set[str]:
|
||||
cursor.execute(f"PRAGMA table_info({table_name});")
|
||||
return {row[1] for row in cursor.fetchall()}
|
||||
|
||||
|
||||
def _get_indexes(cursor: sqlite3.Cursor) -> set[str]:
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type = 'index';")
|
||||
return {row[0] for row in cursor.fetchall()}
|
||||
|
||||
|
||||
def test_adds_workflow_call_columns_and_indexes_to_session_queue() -> None:
|
||||
db = sqlite3.connect(":memory:")
|
||||
cursor = db.cursor()
|
||||
cursor.execute("CREATE TABLE session_queue (item_id INTEGER PRIMARY KEY);")
|
||||
|
||||
AddWorkflowCallQueueMetadataCallback()(cursor)
|
||||
|
||||
assert _get_columns(cursor, "session_queue") >= {
|
||||
"workflow_call_id",
|
||||
"parent_item_id",
|
||||
"parent_session_id",
|
||||
"root_item_id",
|
||||
"workflow_call_depth",
|
||||
}
|
||||
assert _get_indexes(cursor) >= {
|
||||
"idx_session_queue_workflow_call_id",
|
||||
"idx_session_queue_parent_item_id",
|
||||
"idx_session_queue_parent_session_id",
|
||||
"idx_session_queue_root_item_id",
|
||||
"idx_session_queue_workflow_call_depth",
|
||||
}
|
||||
|
||||
db.close()
|
||||
|
||||
|
||||
def test_migration_is_idempotent_and_tolerates_missing_session_queue() -> None:
|
||||
db = sqlite3.connect(":memory:")
|
||||
cursor = db.cursor()
|
||||
|
||||
AddWorkflowCallQueueMetadataCallback()(cursor)
|
||||
cursor.execute("CREATE TABLE session_queue (item_id INTEGER PRIMARY KEY, workflow_call_id TEXT);")
|
||||
AddWorkflowCallQueueMetadataCallback()(cursor)
|
||||
AddWorkflowCallQueueMetadataCallback()(cursor)
|
||||
|
||||
assert _get_columns(cursor, "session_queue") >= {
|
||||
"workflow_call_id",
|
||||
"parent_item_id",
|
||||
"parent_session_id",
|
||||
"root_item_id",
|
||||
"workflow_call_depth",
|
||||
}
|
||||
|
||||
db.close()
|
||||
|
||||
|
||||
def test_build_migration_declares_stable_id_and_dependency() -> None:
|
||||
migration = build_migration()
|
||||
|
||||
assert migration.id == "2026_07_01_add_workflow_call_queue_metadata"
|
||||
assert migration.depends_on == "migration_33"
|
||||
assert migration.from_version is None
|
||||
assert migration.to_version is None
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Tests for migration 31: Add image_subfolder column to images table."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_31 import (
|
||||
Migration31Callback,
|
||||
build_migration_31,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db() -> sqlite3.Connection:
|
||||
"""In-memory SQLite database with a minimal images table mimicking pre-migration schema."""
|
||||
conn = sqlite3.connect(":memory:")
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE images (
|
||||
image_name TEXT NOT NULL PRIMARY KEY,
|
||||
image_origin TEXT NOT NULL,
|
||||
image_category TEXT NOT NULL,
|
||||
width INTEGER NOT NULL DEFAULT 0,
|
||||
height INTEGER NOT NULL DEFAULT 0,
|
||||
session_id TEXT,
|
||||
node_id TEXT,
|
||||
metadata TEXT,
|
||||
is_intermediate BOOLEAN DEFAULT FALSE,
|
||||
created_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%f', 'NOW')),
|
||||
updated_at DATETIME NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%f', 'NOW')),
|
||||
deleted_at DATETIME,
|
||||
starred BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
has_workflow BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
"""
|
||||
)
|
||||
return conn
|
||||
|
||||
|
||||
class TestMigration31:
|
||||
def test_adds_image_subfolder_column(self, db: sqlite3.Connection):
|
||||
"""Migration adds image_subfolder column to existing images table."""
|
||||
callback = Migration31Callback()
|
||||
cursor = db.cursor()
|
||||
callback(cursor)
|
||||
|
||||
cursor.execute("PRAGMA table_info(images);")
|
||||
columns = {row[1] for row in cursor.fetchall()}
|
||||
assert "image_subfolder" in columns
|
||||
|
||||
def test_existing_rows_get_empty_string_default(self, db: sqlite3.Connection):
|
||||
"""Pre-existing image rows should get image_subfolder = '' after migration."""
|
||||
db.execute(
|
||||
"INSERT INTO images (image_name, image_origin, image_category, width, height, has_workflow) "
|
||||
"VALUES ('old_image.png', 'internal', 'general', 512, 512, 0)"
|
||||
)
|
||||
db.commit()
|
||||
|
||||
callback = Migration31Callback()
|
||||
callback(db.cursor())
|
||||
db.commit()
|
||||
|
||||
row = db.execute("SELECT image_subfolder FROM images WHERE image_name = 'old_image.png'").fetchone()
|
||||
assert row is not None
|
||||
assert row[0] == ""
|
||||
|
||||
def test_idempotent_migration(self, db: sqlite3.Connection):
|
||||
"""Running migration twice should not fail (column already exists)."""
|
||||
callback = Migration31Callback()
|
||||
cursor = db.cursor()
|
||||
callback(cursor)
|
||||
# Running again should be safe
|
||||
callback(cursor)
|
||||
|
||||
cursor.execute("PRAGMA table_info(images);")
|
||||
columns = [row[1] for row in cursor.fetchall()]
|
||||
assert columns.count("image_subfolder") == 1
|
||||
|
||||
def test_no_images_table_is_noop(self):
|
||||
"""If images table doesn't exist, migration is a no-op."""
|
||||
conn = sqlite3.connect(":memory:")
|
||||
callback = Migration31Callback()
|
||||
cursor = conn.cursor()
|
||||
# Should not raise
|
||||
callback(cursor)
|
||||
|
||||
def test_build_migration_31_version_numbers(self):
|
||||
"""build_migration_31 returns correct version range."""
|
||||
migration = build_migration_31()
|
||||
assert migration.from_version == 30
|
||||
assert migration.to_version == 31
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Tests for migration 32: Repair model_relationships foreign keys."""
|
||||
|
||||
import sqlite3
|
||||
|
||||
import pytest
|
||||
|
||||
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_32 import (
|
||||
Migration32Callback,
|
||||
build_migration_32,
|
||||
)
|
||||
|
||||
|
||||
def _create_models_table(conn: sqlite3.Connection) -> None:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE models (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
config TEXT NOT NULL
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
def _create_broken_relationships_table(conn: sqlite3.Connection) -> None:
|
||||
"""Recreates the broken state left by migration 22: FKs reference the dropped models_old table."""
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE model_relationships (
|
||||
model_key_1 TEXT NOT NULL,
|
||||
model_key_2 TEXT NOT NULL,
|
||||
created_at TEXT DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')),
|
||||
PRIMARY KEY (model_key_1, model_key_2),
|
||||
FOREIGN KEY (model_key_1) REFERENCES "models_old"(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (model_key_2) REFERENCES "models_old"(id) ON DELETE CASCADE
|
||||
);
|
||||
"""
|
||||
)
|
||||
conn.execute("CREATE INDEX keyx_model_relationships_model_key_2 ON model_relationships(model_key_2);")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def db() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
_create_models_table(conn)
|
||||
_create_broken_relationships_table(conn)
|
||||
conn.execute("INSERT INTO models (id, config) VALUES ('a', '{}'), ('b', '{}'), ('c', '{}')")
|
||||
return conn
|
||||
|
||||
|
||||
class TestMigration32:
|
||||
def test_repoints_foreign_keys_to_models(self, db: sqlite3.Connection):
|
||||
"""After migration, the foreign keys reference models, not models_old."""
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
sql = db.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='model_relationships'").fetchone()[
|
||||
0
|
||||
]
|
||||
assert "models_old" not in sql
|
||||
assert "REFERENCES models(id)" in sql
|
||||
|
||||
def test_preserves_valid_links(self, db: sqlite3.Connection):
|
||||
"""Links between existing models are preserved."""
|
||||
db.execute("INSERT INTO model_relationships (model_key_1, model_key_2) VALUES ('a', 'b')")
|
||||
db.commit()
|
||||
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
rows = db.execute("SELECT model_key_1, model_key_2 FROM model_relationships ORDER BY model_key_1").fetchall()
|
||||
assert rows == [("a", "b")]
|
||||
|
||||
def test_drops_orphaned_links(self, db: sqlite3.Connection):
|
||||
"""Links referencing missing models are dropped so the restored FKs are satisfiable."""
|
||||
db.execute("INSERT INTO model_relationships (model_key_1, model_key_2) VALUES ('a', 'b')")
|
||||
db.execute("INSERT INTO model_relationships (model_key_1, model_key_2) VALUES ('a', 'gone')")
|
||||
db.commit()
|
||||
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
rows = db.execute("SELECT model_key_1, model_key_2 FROM model_relationships").fetchall()
|
||||
assert rows == [("a", "b")]
|
||||
|
||||
def test_cascade_works_after_repair(self, db: sqlite3.Connection):
|
||||
"""ON DELETE CASCADE against models works once the FKs are repaired."""
|
||||
db.execute("INSERT INTO model_relationships (model_key_1, model_key_2) VALUES ('a', 'b')")
|
||||
db.commit()
|
||||
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
db.execute("PRAGMA foreign_keys = ON;")
|
||||
db.execute("DELETE FROM models WHERE id = 'a'")
|
||||
db.commit()
|
||||
|
||||
rows = db.execute("SELECT * FROM model_relationships").fetchall()
|
||||
assert rows == []
|
||||
|
||||
def test_index_recreated(self, db: sqlite3.Connection):
|
||||
"""The lookup index on model_key_2 is recreated on the rebuilt table."""
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
idx = db.execute(
|
||||
"SELECT name FROM sqlite_master WHERE type='index' AND name='keyx_model_relationships_model_key_2'"
|
||||
).fetchone()
|
||||
assert idx is not None
|
||||
|
||||
def test_idempotent_when_already_correct(self, db: sqlite3.Connection):
|
||||
"""Running on an already-correct table is a no-op (no rebuild)."""
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
# Second run should detect the correct FKs and do nothing.
|
||||
Migration32Callback()(db.cursor())
|
||||
db.commit()
|
||||
|
||||
sql = db.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='model_relationships'").fetchone()[
|
||||
0
|
||||
]
|
||||
assert "REFERENCES models(id)" in sql
|
||||
|
||||
def test_no_relationships_table_is_noop(self):
|
||||
"""If the table doesn't exist, migration is a no-op."""
|
||||
conn = sqlite3.connect(":memory:")
|
||||
Migration32Callback()(conn.cursor()) # should not raise
|
||||
|
||||
def test_build_migration_32_version_numbers(self):
|
||||
migration = build_migration_32()
|
||||
assert migration.from_version == 31
|
||||
assert migration.to_version == 32
|
||||
Reference in New Issue
Block a user