Files
invoke-ai--invokeai/tests/test_sqlite_migrator.py
wehub-resource-sync cddb07a176
build container image / cpu (push) Waiting to run
build container image / cuda (push) Waiting to run
build container image / rocm (push) Waiting to run
frontend tests / frontend-tests (push) Waiting to run
openapi checks / openapi-checks (push) Waiting to run
python tests / py3.12: macos-default (push) Waiting to run
python tests / py3.11: windows-cpu (push) Waiting to run
python tests / py3.12: windows-cpu (push) Waiting to run
python tests / py3.11: linux-cpu (push) Waiting to run
python tests / py3.12: linux-cpu (push) Waiting to run
typegen checks / typegen-checks (push) Waiting to run
uv lock checks / uv-lock-checks (push) Waiting to run
frontend checks / frontend-checks (push) Waiting to run
lfs checks / lfs-check (push) Waiting to run
python checks / python-checks (push) Waiting to run
python tests / py3.11: macos-default (push) Waiting to run
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:22:06 +08:00

764 lines
32 KiB
Python

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()