Database Migrations
InvokeAI migrates its database with its own migrator. Migrations live in invokeai/app/services/shared/sqlite_migrator/migrations/ and are discovered automatically when the database is initialized.
Use a migration when a change modifies persisted database schema or persisted database data in a way that existing installs must receive on startup.
New migrations are portable: they run on every database backend, SQLite, MySQL and MariaDB. The migrations up to PORTABLE_CUTOVER (in migration_loader.py) run on SQLite only; they stay as written, and no new one of that kind is added. See Database Layer for the schema metadata a migration changes along with the database.
Naming
Section titled “Naming”New migration modules should use a date-stamped descriptive name:
invokeai/app/services/shared/sqlite_migrator/migrations/migration_2026_10_05_add_bananas.pyDate-stamped migration modules must expose a build_migration() builder:
def build_migration() -> PortableMigration: ...The date prefix should be the date the migration is authored or merged, in YYYY_MM_DD format. Add a short snake_case description after the date.
Legacy numeric migration modules (migration_33.py, with build_migration_33()) are still discovered, in numeric order. Date-stamped modules are imported in lexical order, but dependency metadata drives execution order. Non-matching migration module names, missing builders, unknown builder dependencies, and builders that do not return a migration fail at startup.
IDs and Dependencies
Section titled “IDs and Dependencies”Every migration has a stable id. For new date-stamped migrations, use the module name without the migration_ prefix:
id="2026_10_05_add_bananas"The loader enforces this match. If migration_2026_10_05_add_bananas.py returns a different ID, startup fails instead of persisting a typo that could later cause the migration to run again.
New production migrations always set depends_on, to the migration that introduced the schema or data they need, or else to the newest migration. Only a true root migration uses depends_on=None: a migration with no dependency is considered independently runnable, so omitting depends_on can let it run before the schema it expects exists.
Dependencies drive execution order. If two migrations both depend on the same migration, either may run first unless one explicitly depends on the other. Do not rely on filename or lexical ordering to express a real dependency. Single-parent dependencies are supported.
Template
Section titled “Template”from sqlalchemy import Column, inspect
from invokeai.app.services.shared.database.schema.metadata import default, inserted_atfrom invokeai.app.services.shared.database.types import BigInt, Keyfrom invokeai.app.services.shared.sqlite_migrator.sqlite_migrator_common import ( PortableMigration, PortableMigrationContext,)
def _add_bananas(context: PortableMigrationContext) -> None: if not inspect(context.conn).has_table("bananas"): context.create_table( "bananas", Column("banana_id", Key(), primary_key=True), Column("ripeness", BigInt(), nullable=False, server_default=default(0)), inserted_at(), )
def build_migration() -> PortableMigration: return PortableMigration( id="2026_10_05_add_bananas", depends_on="2026_10_04_add_db_locks", callback=_add_bananas, )The callback changes the schema with Alembic’s operations (context.op.add_column, context.op.create_index, context.op.batch_alter_table, …) and reads and writes rows through context.conn. It does not commit or roll back. The migrator records the migration as applied in the same transaction, once the callback has succeeded.
- Create tables with
context.create_table(), notop.create_table. It creates a table exactly as the schema metadata does: InnoDB, the binary collation and DYNAMIC rows on a server whatever its defaults, and each backend’s rules for defaults, generated columns and indexes. A foreign key names a table incontext.metadata: one created earlier in the migration, or an existing one loaded withTable(name, context.metadata, autoload_with=context.conn). - Make it idempotent. On SQLite the migration runs in a transaction that a failure rolls back, DDL included. On MySQL and MariaDB every DDL statement commits as it runs, so a migration that failed halfway runs again from the start at the next startup and finds part of its work done. Check what is there before changing it (
inspect(context.conn)). - Mind foreign keys on SQLite, and create no triggers. There a portable migration runs with foreign keys off, so that rebuilding a table with
op.batch_alter_table(SQLite’s only way to alter most columns) does not delete, by cascade, the rows of the tables that reference it. They are checked before the migration commits: a migration that leaves rows referencing nothing fails, so one that deletes rows others reference deletes those itself. (Rows that referenced nothing before it ran do not count.) No database has triggers (2026_10_07_drop_sqlite_triggersdropped the last): the application sets what a trigger would, on every backend, andtest_schema_parity.pyfails when a migration creates one. - Spell out what Alembic does not know.
op.add_columnapplies none of the metadata’s rules for a backend: a column that the metadata gives a SQLite-only default (inserted_at(),updated_at()) or a generatedNOT NULLcolumn needs its DDL written per backend (context.conn.dialect.name). - Change the schema metadata in the same commit. New server databases are created from the metadata, so it must describe what the migration makes.
test_schema_parity.pyholds it to the migrated SQLite schema, andtest_portable_migrations.pytakes a server database from the cutover through every portable migration and compares it with one created from the metadata.
Builder Dependencies
Section titled “Builder Dependencies”Migration builders may request known application dependencies by parameter name. The loader inspects the builder signature and passes only the dependencies requested.
Supported dependency names:
app_configorconfigloggerimage_files
Example:
def build_migration(app_config: InvokeAIAppConfig, logger: Logger) -> PortableMigration: return PortableMigration( id="2026_10_05_normalize_model_paths", depends_on="2026_10_05_add_bananas", callback=NormalizeModelPathsCallback(app_config=app_config, logger=logger), )Do not use *args, **kwargs, or positional-only parameters in migration builders.
A new MySQL or MariaDB database is not migrated: it is created at the newest schema, with the rows the migrations seed copied from a SQLite database that the migration chain builds in memory, in a temporary root. A migration’s effects outside the database (files it moves or deletes) therefore happen on SQLite installs and on existing server databases, never when a server database is created.
Registration
Section titled “Registration”Do not manually import or register migrations in services/shared/database/startup.py.
Database initialization builds a MigrationBuildContext, discovers migration modules, calls their builders, and registers the resulting migrations with the Migrator.
Manual registration is still available for tests:
migrator.register_migration(PortableMigration(...))Applied State and Compatibility
Section titled “Applied State and Compatibility”The migrator records stable migration IDs in the applied_migrations table. Legacy numeric versions are still written to the existing migrations table for the legacy migrations that define to_version.
Existing SQLite databases are bootstrapped from legacy numeric rows:
- legacy version
1maps tomigration_1 - legacy version
2maps tomigration_2 - and so on
If a database contains an applied migration ID or legacy numeric version unknown to the current code, startup fails before running migrations. This prevents older code from running against a newer schema. Downgrading to an InvokeAI version that does not know about migrations already applied by a newer version is not supported.
For legacy migrations, the two metadata tables must agree. For example, applied_migrations may only record migration_2 with legacy_version = 2 if the legacy migrations table also contains version 2. If these records are inconsistent, startup fails before user migration callbacks run.
When opening a file-backed legacy SQLite database for the first time after this migration system change, the migrator may need to create and populate applied_migrations even if no user migration callbacks need to run. This metadata bootstrap is backed up before the new metadata table is written. The migrator backs up a SQLite database file before it migrates it; it makes no backups of a server database.
Add focused tests for each migration under:
tests/app/services/shared/sqlite_migrator/migrations/At minimum, cover:
- the schema or data change performed by the migration
- running it again after it failed halfway, and already-migrated behavior where relevant
- missing optional source tables or columns when the migration is expected to tolerate them
- the builder’s
idanddepends_on
Tests that use the empty_database fixture (no tables) or the database fixture (the newest schema) run on SQLite and, with INVOKEAI_TEST_DB_URL set, on a MySQL or MariaDB server (see Database Layer). Also run the migrator tests:
pytest tests/app/services/shared/sqlite_migrator tests/test_sqlite_migrator.pyThese tests use in-memory databases, pytest temporary directories or a schema of their own on the test server. They should not touch a real InvokeAI database.