Migrations¶
SQLSpec ships with a built-in migration system backed by the SQL file loader. Use it when you want a lightweight, code-first workflow without pulling in Alembic or a full ORM stack.
Core Concepts¶
Migrations are SQL or Python files stored in a migrations directory.
Each database configuration can include its own migration settings.
Extension migrations (ADK, events, Litestar sessions) are opt-in and versioned.
Common Commands¶
sqlspec init
sqlspec create-migration -m "add users"
sqlspec upgrade
Configuration¶
Set migration_config on your database configuration to customize script
locations, version table names, and extension migration behavior.
The migration CLI resolves config from --config, SQLSPEC_CONFIG, or
[tool.sqlspec] in pyproject.toml.
from sqlspec.adapters.duckdb import DuckDBConfig
config = DuckDBConfig(
connection_config={"database": "/tmp/analytics.db"},
migration_config={
"script_location": "migrations/duckdb",
"version_table": "_schema_versions",
},
)
# Apply all pending migrations
config.migrate_up()
# Apply up to a specific revision
config.migrate_up(revision="003")
# Dry run to see what would happen
config.migrate_up(dry_run=True)
For async configs, migrate_up() returns an awaitable:
from sqlspec.adapters.asyncpg import AsyncpgConfig
config = AsyncpgConfig(
connection_config={"dsn": "postgresql://localhost/app"},
migration_config={"script_location": "migrations/postgres"},
)
await config.migrate_up()
Extension migrations are auto-included when the corresponding entry exists in
extension_config. Use migration_config["exclude_extensions"] to skip a
specific extension, migration_config["include_extensions"] to opt in
explicitly by extension name, or migration_config["enabled"] = False to
disable migrations entirely for a database config.
Configuring a Default Schema¶
Use migration_config["default_schema"] when migration SQL should run
against a pre-existing schema without qualifying every table in each migration
file. SQLSpec validates the schema before creating the tracker table or applying
DDL, then configures the migration session before each migration is executed.
Use migration_config["version_table_schema"] when the migration tracker
table should live somewhere different from the objects managed by migrations.
If version_table_schema is not set, the tracker schema resolves to
default_schema. If neither field is set, the tracker table is unqualified and
uses the adapter's normal default namespace.
from sqlspec.adapters.asyncpg import AsyncpgConfig
config = AsyncpgConfig(
connection_config={"dsn": "postgresql://localhost/app"},
migration_config={
"script_location": "migrations/postgres",
"version_table_name": "schema_versions",
"default_schema": "app_schema",
"version_table_schema": "admin_schema",
},
)
The operator must create the target schema before running migrations. The
migration role also needs the database-specific privileges to create objects
there. For PostgreSQL, that usually means USAGE and
CREATE on the target schema, plus permission to create or update the
tracker table.
Adapter support is opt-in via the supports_migration_schemas class flag on
each config. Configuring default_schema against an adapter that does not
opt in raises MigrationError before any DDL is issued.
Adapter |
Default schema |
Mechanism |
|---|---|---|
|
Supported |
|
|
Supported |
Same as |
|
Supported |
Same as |
|
Supported |
Inherits |
|
Supported |
Inherits |
|
Supported |
Same as |
|
Supported |
|
|
Supported |
|
|
Not supported |
SQLite has no schema namespace; use |
|
Not supported |
MySQL conflates schema and database. Select the target database in
the connection URL or via |
|
Not supported |
ADBC does not expose a portable per-session schema setter for these dialects. Configure the default schema at the user or login level in the underlying database. |
|
Not supported |
SQL Server resolves the default schema from the login. Set it with
|
|
Not supported |
BigQuery requires fully qualified |
|
Not supported |
Cloud Spanner ties objects to a single schema per database; there is no session-scoped switch. |
|
Not supported |
ODBC connection-string semantics vary per driver. Configure the default schema through the underlying DSN. |
Example with unqualified DDL:
from sqlspec.adapters.duckdb import DuckDBConfig
from sqlspec.migrations.commands import SyncMigrationCommands
migration_dir = tmp_path / "migrations"
db_path = tmp_path / "app.duckdb"
config = DuckDBConfig(
connection_config={"database": str(db_path)},
migration_config={
"script_location": str(migration_dir),
"version_table_name": "schema_versions",
"default_schema": "app_schema",
"version_table_schema": "admin_schema",
},
)
try:
with config.provide_session() as session:
session.execute("CREATE SCHEMA app_schema")
session.execute("CREATE SCHEMA admin_schema")
commands = SyncMigrationCommands(config)
commands.init(str(migration_dir), package=True)
(migration_dir / "0001_create_users.py").write_text(
'''"""Create users."""
def up():
"""Create an unqualified table in app_schema."""
return ["CREATE TABLE users (id INTEGER PRIMARY KEY, name VARCHAR NOT NULL)"]
def down():
"""Drop the unqualified table from app_schema."""
return ["DROP TABLE IF EXISTS users"]
'''
)
commands.upgrade()
with config.provide_session() as session:
users_table = session.select_value(
"""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?
""",
("app_schema", "users"),
)
tracker_table = session.select_value(
"""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = ? AND table_name = ?
""",
("admin_schema", "schema_versions"),
)
assert users_table == "users"
assert tracker_table == "schema_versions"
finally:
if config.connection_instance:
config.close_pool()
Logging and Echo Controls¶
Configure output behavior with migration_config or CLI flags:
use_loggerto emit structured logs instead of console output.echoto control console output when not using the logger.summary_onlyto emit a single summary log entry when logger output is enabled.
The CLI equivalents are --use-logger, --no-echo, and --summary.