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

This commit is contained in:
wehub-resource-sync
2026-07-13 13:22:06 +08:00
commit cddb07a176
3370 changed files with 685519 additions and 0 deletions
+763
View File
@@ -0,0 +1,763 @@
import sqlite3
from contextlib import closing
from logging import Logger
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from pydantic import ValidationError
from invokeai.app.services.shared.sqlite.sqlite_database import SqliteDatabase
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import (
MigrateCallback,
Migration,
MigrationError,
MigrationSet,
MigrationVersionError,
)
from invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_impl import (
SqliteMigrator,
)
@pytest.fixture
def logger() -> Logger:
return Logger("test_sqlite_migrator")
@pytest.fixture
def memory_db_conn() -> sqlite3.Connection:
return sqlite3.connect(":memory:")
@pytest.fixture
def memory_db_cursor(memory_db_conn: sqlite3.Connection) -> sqlite3.Cursor:
return memory_db_conn.cursor()
@pytest.fixture
def migrator(logger: Logger) -> SqliteMigrator:
db = SqliteDatabase(db_path=None, logger=logger, verbose=False)
return SqliteMigrator(db=db)
@pytest.fixture
def no_op_migrate_callback() -> MigrateCallback:
def no_op_migrate(cursor: sqlite3.Cursor, **kwargs) -> None:
pass
return no_op_migrate
@pytest.fixture
def migration_no_op(no_op_migrate_callback: MigrateCallback) -> Migration:
return Migration(from_version=0, to_version=1, callback=no_op_migrate_callback)
@pytest.fixture
def migrate_callback_create_table_of_name() -> MigrateCallback:
def migrate(cursor: sqlite3.Cursor, **kwargs) -> None:
table_name = kwargs["table_name"]
cursor.execute(f"CREATE TABLE {table_name} (id INTEGER PRIMARY KEY);")
return migrate
@pytest.fixture
def migrate_callback_create_test_table() -> MigrateCallback:
def migrate(cursor: sqlite3.Cursor, **kwargs) -> None:
cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY);")
return migrate
@pytest.fixture
def migration_create_test_table(migrate_callback_create_test_table: MigrateCallback) -> Migration:
return Migration(from_version=0, to_version=1, callback=migrate_callback_create_test_table)
@pytest.fixture
def failing_migration() -> Migration:
def failing_migration(cursor: sqlite3.Cursor, **kwargs) -> None:
raise Exception("Bad migration")
return Migration(from_version=0, to_version=1, callback=failing_migration)
@pytest.fixture
def failing_migrate_callback() -> MigrateCallback:
def failing_migrate(cursor: sqlite3.Cursor, **kwargs) -> None:
raise Exception("Bad migration")
return failing_migrate
def create_migrate(i: int) -> MigrateCallback:
def migrate(cursor: sqlite3.Cursor, **kwargs) -> None:
cursor.execute(f"CREATE TABLE test{i} (id INTEGER PRIMARY KEY);")
return migrate
def test_migration_to_version_is_one_gt_from_version(no_op_migrate_callback: MigrateCallback) -> None:
with pytest.raises(ValidationError, match="to_version must be one greater than from_version"):
Migration(from_version=0, to_version=2, callback=no_op_migrate_callback)
# not raising is sufficient
Migration(from_version=1, to_version=2, callback=no_op_migrate_callback)
def test_migration_hash(no_op_migrate_callback: MigrateCallback) -> None:
migration = Migration(from_version=0, to_version=1, callback=no_op_migrate_callback)
assert hash(migration) == hash((0, 1))
def test_legacy_migration_gets_stable_id_and_dependency(no_op_migrate_callback: MigrateCallback) -> None:
first_migration = Migration(from_version=0, to_version=1, callback=no_op_migrate_callback)
second_migration = Migration(from_version=1, to_version=2, callback=no_op_migrate_callback)
assert first_migration.id == "migration_1"
assert first_migration.depends_on is None
assert second_migration.id == "migration_2"
assert second_migration.depends_on == "migration_1"
def test_explicit_migration_id_and_dependency_are_preserved(no_op_migrate_callback: MigrateCallback) -> None:
migration = Migration(
id="2026_06_29_add_test_table",
depends_on="migration_1",
callback=no_op_migrate_callback,
)
assert migration.id == "2026_06_29_add_test_table"
assert migration.depends_on == "migration_1"
assert migration.from_version is None
assert migration.to_version is None
def test_migration_set_add_migration(migrator: SqliteMigrator, migration_no_op: Migration) -> None:
migration = migration_no_op
migrator._migration_set.register(migration)
assert migration in migrator._migration_set._migrations
def test_migration_set_may_not_register_dupes(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
migrate_0_to_1_a = Migration(from_version=0, to_version=1, callback=no_op_migrate_callback)
migrate_0_to_1_b = Migration(from_version=0, to_version=1, callback=no_op_migrate_callback)
migrator._migration_set.register(migrate_0_to_1_a)
with pytest.raises(MigrationVersionError, match=r"Migration with from_version or to_version already registered"):
migrator._migration_set.register(migrate_0_to_1_b)
migrate_1_to_2_a = Migration(from_version=1, to_version=2, callback=no_op_migrate_callback)
migrate_1_to_2_b = Migration(from_version=1, to_version=2, callback=no_op_migrate_callback)
migrator._migration_set.register(migrate_1_to_2_a)
with pytest.raises(MigrationVersionError, match=r"Migration with from_version or to_version already registered"):
migrator._migration_set.register(migrate_1_to_2_b)
def test_migration_set_may_not_register_duplicate_ids(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(id="same_id", callback=no_op_migrate_callback))
with pytest.raises(MigrationVersionError, match="Migration with id already registered"):
migration_set.register(Migration(id="same_id", callback=no_op_migrate_callback))
def test_migration_set_gets_migration(migration_no_op: Migration) -> None:
migration_set = MigrationSet()
migration_set.register(migration_no_op)
assert migration_set.get(0) == migration_no_op
assert migration_set.get(1) is None
def test_migration_set_validates_migration_chain(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(from_version=1, to_version=2, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="Migration chain is fragmented"):
# no migration from 0 to 1
migration_set.validate_migration_chain()
migration_set.register(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
migration_set.validate_migration_chain()
migration_set.register(Migration(from_version=2, to_version=3, callback=no_op_migrate_callback))
migration_set.validate_migration_chain()
migration_set.register(Migration(from_version=4, to_version=5, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="Migration chain is fragmented"):
# no migration from 3 to 4
migration_set.validate_migration_chain()
def test_migration_set_validates_dependency_graph(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(id="a", callback=no_op_migrate_callback))
migration_set.register(Migration(id="b", depends_on="a", callback=no_op_migrate_callback))
migration_set.register(Migration(id="c", depends_on="a", callback=no_op_migrate_callback))
migration_set.register(Migration(id="d", depends_on="c", callback=no_op_migrate_callback))
migration_set.validate_dependency_graph()
def test_migration_set_rejects_missing_dependency(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(id="a", depends_on="missing", callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="depends on unknown migration"):
migration_set.validate_dependency_graph()
def test_migration_set_rejects_dependency_cycle(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(id="a", depends_on="b", callback=no_op_migrate_callback))
migration_set.register(Migration(id="b", depends_on="a", callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="cycle"):
migration_set.validate_dependency_graph()
def test_migration_set_rejects_legacy_migration_depending_on_graph_only_migration(
no_op_migrate_callback: MigrateCallback,
) -> None:
migration_set = MigrationSet()
migration_set.register(Migration(id="2026_06_30_graph_only", callback=no_op_migrate_callback))
migration_set.register(
Migration(
id="migration_2",
depends_on="2026_06_30_graph_only",
from_version=1,
to_version=2,
callback=no_op_migrate_callback,
)
)
with pytest.raises(MigrationError, match="cannot depend on graph-only migration"):
migration_set.validate_dependency_graph()
def test_migration_set_plans_branching_migrations(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
migration_a = Migration(id="a", callback=no_op_migrate_callback)
migration_b = Migration(id="b", depends_on="a", callback=no_op_migrate_callback)
migration_c = Migration(id="c", depends_on="a", callback=no_op_migrate_callback)
migration_d = Migration(id="d", depends_on="c", callback=no_op_migrate_callback)
migration_set.register(migration_d)
migration_set.register(migration_c)
migration_set.register(migration_b)
migration_set.register(migration_a)
assert migration_set.get_migration_plan(applied_migration_ids={"a", "b"}) == [migration_c, migration_d]
def test_migration_set_counts_migrations(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
assert migration_set.count == 0
migration_set.register(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
assert migration_set.count == 1
migration_set.register(Migration(from_version=1, to_version=2, callback=no_op_migrate_callback))
assert migration_set.count == 2
def test_migration_set_gets_latest_version(no_op_migrate_callback: MigrateCallback) -> None:
migration_set = MigrationSet()
assert migration_set.latest_version == 0
migration_set.register(Migration(from_version=1, to_version=2, callback=no_op_migrate_callback))
assert migration_set.latest_version == 2
migration_set.register(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
assert migration_set.latest_version == 2
def test_migration_runs(memory_db_cursor: sqlite3.Cursor, migrate_callback_create_test_table: MigrateCallback) -> None:
migration = Migration(
from_version=0,
to_version=1,
callback=migrate_callback_create_test_table,
)
migration.callback(memory_db_cursor)
memory_db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test';")
assert memory_db_cursor.fetchone() is not None
def test_migrator_registers_migration(migrator: SqliteMigrator, migration_no_op: Migration) -> None:
migration = migration_no_op
migrator.register_migration(migration)
assert migration in migrator._migration_set._migrations
def test_migrator_creates_migrations_table(migrator: SqliteMigrator) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='migrations';")
assert cursor.fetchone() is not None
def test_migrator_creates_applied_migrations_table(migrator: SqliteMigrator) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_applied_migrations_table(cursor)
cursor.execute("SELECT * FROM sqlite_master WHERE type='table' AND name='applied_migrations';")
assert cursor.fetchone() is not None
def test_migrator_migration_sets_version(migrator: SqliteMigrator, migration_no_op: Migration) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
migrator.register_migration(migration_no_op)
migrator.run_migrations()
cursor.execute("SELECT MAX(version) FROM migrations;")
assert cursor.fetchone()[0] == 1
def test_migrator_gets_current_version(migrator: SqliteMigrator, migration_no_op: Migration) -> None:
cursor = migrator._db._conn.cursor()
assert migrator._get_current_version(cursor) == 0
migrator._create_migrations_table(cursor)
assert migrator._get_current_version(cursor) == 0
migrator.register_migration(migration_no_op)
migrator.run_migrations()
assert migrator._get_current_version(cursor) == 1
def test_migrator_runs_single_migration(migrator: SqliteMigrator, migration_create_test_table: Migration) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
migrator._run_migration(migration_create_test_table)
assert migrator._get_current_version(cursor) == 1
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test';")
assert cursor.fetchone() is not None
def test_migrator_runs_all_migrations_in_memory(migrator: SqliteMigrator) -> None:
cursor = migrator._db._conn.cursor()
migrations = [Migration(from_version=i, to_version=i + 1, callback=create_migrate(i)) for i in range(0, 3)]
for migration in migrations:
migrator.register_migration(migration)
migrator.run_migrations()
assert migrator._get_current_version(cursor) == 3
def test_migrator_bootstraps_applied_migrations_from_legacy_versions(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
cursor.execute("INSERT INTO migrations (version) VALUES (2);")
cursor.connection.commit()
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
migrator.register_migration(Migration(from_version=1, to_version=2, callback=no_op_migrate_callback))
migrator.register_migration(Migration(from_version=2, to_version=3, callback=no_op_migrate_callback))
migrator.run_migrations()
cursor.execute("SELECT migration_id FROM applied_migrations ORDER BY migration_id;")
assert [row[0] for row in cursor.fetchall()] == ["migration_1", "migration_2", "migration_3"]
assert migrator._get_current_version(cursor) == 3
def test_migrator_backs_up_file_db_before_metadata_only_bootstrap(
logger: Logger, no_op_migrate_callback: MigrateCallback
) -> None:
with TemporaryDirectory() as tempdir:
original_db_path = Path(tempdir) / "invokeai.db"
db = SqliteDatabase(db_path=original_db_path, logger=logger, verbose=False)
cursor = db._conn.cursor()
cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY);")
cursor.execute(
"""--sql
CREATE TABLE migrations (
version INTEGER PRIMARY KEY,
migrated_at DATETIME NOT NULL DEFAULT(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))
);
"""
)
cursor.execute("INSERT INTO migrations (version) VALUES (0);")
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
db._conn.commit()
migrator = SqliteMigrator(db=db)
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
assert migrator.run_migrations() is False
db._conn.close()
assert migrator._backup_path is not None
with closing(sqlite3.connect(migrator._backup_path)) as backup_db_conn:
backup_db_cursor = backup_db_conn.cursor()
backup_db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test';")
assert backup_db_cursor.fetchone() is not None
backup_db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='applied_migrations';")
assert backup_db_cursor.fetchone() is None
def test_migrator_runs_branching_graph_migrations(migrator: SqliteMigrator) -> None:
cursor = migrator._db._conn.cursor()
executed: list[str] = []
def create_migration(migration_id: str, depends_on: str | None) -> Migration:
def migrate(cursor: sqlite3.Cursor) -> None:
executed.append(migration_id)
cursor.execute(f"CREATE TABLE {migration_id} (id INTEGER PRIMARY KEY);")
return Migration(id=migration_id, depends_on=depends_on, callback=migrate)
for migration in [
create_migration("d", "c"),
create_migration("c", "a"),
create_migration("b", "a"),
create_migration("a", None),
]:
migrator.register_migration(migration)
migrator.run_migrations()
assert executed == ["a", "b", "c", "d"]
cursor.execute("SELECT migration_id FROM applied_migrations ORDER BY migration_id;")
assert [row[0] for row in cursor.fetchall()] == ["a", "b", "c", "d"]
def test_migrator_rejects_unknown_applied_migration(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
migrator._create_applied_migrations_table(cursor)
cursor.execute("INSERT INTO applied_migrations (migration_id) VALUES ('future_migration');")
cursor.connection.commit()
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="unknown applied migration"):
migrator.run_migrations()
def test_migrator_rejects_unknown_applied_migration_before_creating_legacy_table(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_applied_migrations_table(cursor)
cursor.execute("INSERT INTO applied_migrations (migration_id) VALUES ('future_migration');")
cursor.connection.commit()
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="unknown applied migration"):
migrator.run_migrations()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations';")
assert cursor.fetchone() is None
def test_migrator_rejects_inconsistent_applied_legacy_version(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
migrator._create_applied_migrations_table(cursor)
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
cursor.execute("INSERT INTO migrations (version) VALUES (2);")
cursor.execute("INSERT INTO applied_migrations (migration_id, legacy_version) VALUES ('migration_1', 2);")
cursor.connection.commit()
callback_ran = False
def migration_callback(cursor: sqlite3.Cursor) -> None:
nonlocal callback_ran
callback_ran = True
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
migrator.register_migration(Migration(from_version=1, to_version=2, callback=migration_callback))
with pytest.raises(MigrationError, match="inconsistent applied migration state"):
migrator.run_migrations()
assert callback_ran is False
def test_migrator_rejects_applied_legacy_migration_missing_legacy_row(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
migrator._create_applied_migrations_table(cursor)
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
cursor.execute("INSERT INTO applied_migrations (migration_id, legacy_version) VALUES ('migration_2', 2);")
cursor.connection.commit()
callback_ran = False
def graph_migration_callback(cursor: sqlite3.Cursor) -> None:
nonlocal callback_ran
callback_ran = True
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
migrator.register_migration(Migration(from_version=1, to_version=2, callback=no_op_migrate_callback))
migrator.register_migration(
Migration(id="2026_06_30_graph_migration", depends_on="migration_2", callback=graph_migration_callback)
)
with pytest.raises(MigrationError, match="inconsistent applied migration state"):
migrator.run_migrations()
assert callback_ran is False
def test_migrator_rejects_unknown_legacy_version(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
cursor.execute("INSERT INTO migrations (version) VALUES (2);")
cursor.connection.commit()
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="unknown legacy migration version"):
migrator.run_migrations()
def test_migrator_rejects_unknown_legacy_version_before_creating_applied_table(
migrator: SqliteMigrator, no_op_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator._create_migrations_table(cursor)
cursor.execute("INSERT INTO migrations (version) VALUES (1);")
cursor.execute("INSERT INTO migrations (version) VALUES (2);")
cursor.connection.commit()
migrator.register_migration(Migration(from_version=0, to_version=1, callback=no_op_migrate_callback))
with pytest.raises(MigrationError, match="unknown legacy migration version"):
migrator.run_migrations()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='applied_migrations';")
assert cursor.fetchone() is None
def test_migrator_runs_all_migrations_file(logger: Logger) -> None:
with TemporaryDirectory() as tempdir:
original_db_path = Path(tempdir) / "invokeai.db"
db = SqliteDatabase(db_path=original_db_path, logger=logger, verbose=False)
migrator = SqliteMigrator(db=db)
migrations = [Migration(from_version=i, to_version=i + 1, callback=create_migrate(i)) for i in range(0, 3)]
for migration in migrations:
migrator.register_migration(migration)
migrator.run_migrations()
with closing(sqlite3.connect(original_db_path)) as original_db_conn:
original_db_cursor = original_db_conn.cursor()
assert SqliteMigrator._get_current_version(original_db_cursor) == 3
# Must manually close else we get an error on Windows
db._conn.close()
def test_migrator_backs_up_db(logger: Logger) -> None:
with TemporaryDirectory() as tempdir:
original_db_path = Path(tempdir) / "invokeai.db"
db = SqliteDatabase(db_path=original_db_path, logger=logger, verbose=False)
# Write some data to the db to test for successful backup
temp_cursor = db._conn.cursor()
temp_cursor.execute("CREATE TABLE test (id INTEGER PRIMARY KEY);")
db._conn.commit()
# Set up the migrator
migrator = SqliteMigrator(db=db)
migrations = [Migration(from_version=i, to_version=i + 1, callback=create_migrate(i)) for i in range(0, 3)]
for migration in migrations:
migrator.register_migration(migration)
migrator.run_migrations()
# Must manually close else we get an error on Windows
db._conn.close()
assert original_db_path.exists()
# We should have a backup file when we migrated a file db
assert migrator._backup_path
# Check that the test table exists as a proxy for successful backup
with closing(sqlite3.connect(migrator._backup_path)) as backup_db_conn:
backup_db_cursor = backup_db_conn.cursor()
backup_db_cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='test';")
assert backup_db_cursor.fetchone() is not None
def test_migrator_makes_no_changes_on_failed_migration(
migrator: SqliteMigrator, migration_no_op: Migration, failing_migrate_callback: MigrateCallback
) -> None:
cursor = migrator._db._conn.cursor()
migrator.register_migration(migration_no_op)
migrator.run_migrations()
assert migrator._get_current_version(cursor) == 1
migrator.register_migration(Migration(from_version=1, to_version=2, callback=failing_migrate_callback))
with pytest.raises(MigrationError, match="Bad migration"):
migrator.run_migrations()
assert migrator._get_current_version(cursor) == 1
cursor.execute("SELECT migration_id FROM applied_migrations ORDER BY migration_id;")
assert [row[0] for row in cursor.fetchall()] == ["migration_1"]
def test_idempotent_migrations(migrator: SqliteMigrator, migration_create_test_table: Migration) -> None:
cursor = migrator._db._conn.cursor()
migrator.register_migration(migration_create_test_table)
migrator.run_migrations()
# not throwing is sufficient
migrator.run_migrations()
assert migrator._get_current_version(cursor) == 1
def test_migration_27_creates_users_table(logger: Logger) -> None:
"""Test that migration 27 creates the users table and related tables."""
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_27 import Migration27Callback
db = SqliteDatabase(db_path=None, logger=logger, verbose=False)
cursor = db._conn.cursor()
# Create minimal tables that migration 27 expects to exist
cursor.execute("CREATE TABLE IF NOT EXISTS boards (board_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS images (image_name TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS workflows (workflow_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS session_queue (item_id INTEGER PRIMARY KEY);")
db._conn.commit()
# Run migration callback directly (not through migrator to avoid chain validation)
migration_callback = Migration27Callback()
migration_callback(cursor)
db._conn.commit()
# Verify users table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users';")
assert cursor.fetchone() is not None
# Verify user_sessions table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_sessions';")
assert cursor.fetchone() is not None
# Verify user_invitations table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='user_invitations';")
assert cursor.fetchone() is not None
# Verify shared_boards table exists
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='shared_boards';")
assert cursor.fetchone() is not None
# Verify system user was created
cursor.execute("SELECT user_id, email FROM users WHERE user_id='system';")
system_user = cursor.fetchone()
assert system_user is not None
assert system_user[0] == "system"
assert system_user[1] == "system@system.invokeai"
# Verify boards table has user_id column
cursor.execute("PRAGMA table_info(boards);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
assert "is_public" in columns
# Verify images table has user_id column
cursor.execute("PRAGMA table_info(images);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
# Verify workflows table has user_id and is_public columns
cursor.execute("PRAGMA table_info(workflows);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
assert "is_public" in columns
# Verify client_state table has the new per-user schema
cursor.execute("PRAGMA table_info(client_state);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
assert "key" in columns
assert "value" in columns
# Verify app_settings table exists and contains a JWT secret
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='app_settings';")
assert cursor.fetchone() is not None
cursor.execute("SELECT value FROM app_settings WHERE key = 'jwt_secret';")
jwt_row = cursor.fetchone()
assert jwt_row is not None
assert len(jwt_row[0]) == 64 # 32 bytes = 64 hex characters
db._conn.close()
def test_migration_27_with_existing_client_state_data(logger: Logger) -> None:
"""Test that migration 27 correctly migrates existing data from the old client_state schema."""
import json
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_21 import Migration21Callback
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_27 import Migration27Callback
db = SqliteDatabase(db_path=None, logger=logger, verbose=False)
cursor = db._conn.cursor()
# Run migration 21 to create old-style client_state with data column
Migration21Callback()(cursor)
# Insert some test data
cursor.execute(
"INSERT INTO client_state (id, data) VALUES (1, ?);",
(json.dumps({"galleryView": "images", "lastBoardId": "board123"}),),
)
db._conn.commit()
# Run migration 27 pre-reqs
cursor.execute("CREATE TABLE IF NOT EXISTS boards (board_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS images (image_name TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS workflows (workflow_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS session_queue (item_id INTEGER PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS style_presets (id TEXT PRIMARY KEY);")
db._conn.commit()
# Run migration 27
Migration27Callback()(cursor)
db._conn.commit()
# Verify new client_state schema
cursor.execute("PRAGMA table_info(client_state);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
assert "key" in columns
assert "value" in columns
assert "updated_at" in columns
assert "data" not in columns
# Verify data was migrated to 'system' user
cursor.execute("SELECT user_id, key, value FROM client_state ORDER BY key;")
rows = [tuple(row) for row in cursor.fetchall()]
assert len(rows) == 2
assert ("system", "galleryView", "images") in rows
assert ("system", "lastBoardId", "board123") in rows
db._conn.close()
def test_migration_27_without_client_state_data_column(logger: Logger) -> None:
"""Test that migration 27 handles old client_state table without the data column."""
from invokeai.app.services.shared.sqlite_migrator.migrations.migration_27 import Migration27Callback
db = SqliteDatabase(db_path=None, logger=logger, verbose=False)
cursor = db._conn.cursor()
# Create old client_state WITHOUT data column (simulating an older migration 21)
cursor.execute(
"""
CREATE TABLE client_state (
id INTEGER PRIMARY KEY CHECK(id = 1),
updated_at DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);
"""
)
db._conn.commit()
# Run migration 27 pre-reqs
cursor.execute("CREATE TABLE IF NOT EXISTS boards (board_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS images (image_name TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS workflows (workflow_id TEXT PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS session_queue (item_id INTEGER PRIMARY KEY);")
cursor.execute("CREATE TABLE IF NOT EXISTS style_presets (id TEXT PRIMARY KEY);")
db._conn.commit()
# Run migration 27 - should not raise even without data column
Migration27Callback()(cursor)
db._conn.commit()
# Verify new client_state schema
cursor.execute("PRAGMA table_info(client_state);")
columns = [row[1] for row in cursor.fetchall()]
assert "user_id" in columns
assert "key" in columns
assert "value" in columns
assert "updated_at" in columns
# No rows should be migrated (nothing to migrate)
cursor.execute("SELECT COUNT(*) FROM client_state;")
assert cursor.fetchone()[0] == 0
db._conn.close()