API Reference¶
Auto-generated API documentation from source code using mkdocstrings.
Testing¶
MigrationTestBase¶
Bases: MigrationSchemaMixin, MigrationConsistencyMixin, MigrationNamingMixin
Base class for database migration tests.
Inherit from this class and provide the following fixtures to get all seven migration tests for free:
Required fixtures
migration_db_url: str— async DSN to the test database.orm_metadata: MetaData— SQLAlchemy metadata of your ORM models.
Auto-provided fixtures (override if needed):
- alembic_config — reads alembic.ini from the current directory.
- migration_engine — async engine with NullPool.
- isolated_migration_schema — unique schema per test, auto-dropped.
Optional class attributes
migration_diff_ignore_tables: list[str]— table names to exclude from the schema diff, check constraint, enum and naming checks (e.g. auto-generated partition tables).migration_diff_compare_server_default: bool— also compare server defaults in the schema diff. Off by default; seeMigrationConsistencyMixin.allowed_index_prefixes,allowed_index_suffixes,allowed_fk_prefixes,allowed_fk_suffixes,allowed_check_prefixes,allowed_check_suffixes,allowed_uq_prefixes,allowed_uq_suffixes,allowed_pk_prefixes,allowed_pk_suffixes— override naming convention rules. When not set, rules are auto-derived fromorm_metadata.naming_convention(if present), falling back to built-in defaults.
Example — zero config when MetaData carries a naming_convention::
DB_NAMING_CONVENTION = {
"ix": "%(column_0_label)s_idx",
"uq": "%(table_name)s_%(column_0_name)s_key",
"ck": "%(table_name)s_%(constraint_name)s_check",
"fk": "%(table_name)s_%(column_0_name)s_fkey",
"pk": "%(table_name)s_pkey",
}
Base = declarative_base(metadata=MetaData(naming_convention=DB_NAMING_CONVENTION))
class TestMyMigrations(MigrationTestBase):
@pytest.fixture
def orm_metadata(self) -> MetaData:
return Base.metadata # naming rules derived automatically
Example — explicit override::
class TestMyMigrations(MigrationTestBase):
migration_diff_ignore_tables = ["events_partitioned_default"]
allowed_fk_prefixes = ["fk_"]
@pytest.fixture
def orm_metadata(self) -> MetaData:
return Base.metadata
isolated_migration_schema(migration_db_url)
async
¶
Create a unique PostgreSQL schema for this test run and drop it afterwards.
test_check_constraints_match(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify the CHECK constraints after a full upgrade match the SQLAlchemy ORM metadata, by name.
compare_metadata() never compares CHECK constraints, so a constraint a
migration forgot is invisible to test_migrations_up_to_date.
test_downgrade_all_the_way(alembic_config, migration_engine, isolated_migration_schema)
async
¶
Verify all migrations can be downgraded to base one by one.
test_enum_values_match(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify every native Enum column's values after a full upgrade match the type in the database, in order.
compare_metadata() never compares enum members, so a value a migration
forgot to add is invisible to test_migrations_up_to_date.
test_migrations_up_to_date(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify the database schema after a full upgrade matches the SQLAlchemy ORM metadata.
test_naming_conventions(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify indexes and constraints follow naming conventions after a full upgrade.
Naming rules are auto-derived from orm_metadata.naming_convention when present,
falling back to built-in defaults. Explicit class attributes always take priority.
test_single_head_revision(alembic_config)
async
¶
Verify there is exactly one head revision (no unmerged branches).
test_stairway_upgrade_downgrade(alembic_config, migration_engine, isolated_migration_schema)
async
¶
Verify every migration can be applied and rolled back individually (stairway test).
For each revision in chronological order: 1. Upgrade to this revision. 2. Assert current revision matches. 3. Downgrade to the previous revision (or base). 4. Assert the downgrade succeeded. 5. Upgrade back before moving to the next step.
MigrationSchemaMixin¶
Provides the isolated_migration_schema fixture.
isolated_migration_schema(migration_db_url)
async
¶
Create a unique PostgreSQL schema for this test run and drop it afterwards.
MigrationConsistencyMixin¶
Core migration correctness tests.
Optional class attributes
migration_diff_compare_server_default: bool— passcompare_server_defaultto Alembic intest_migrations_up_to_date. Off by default, because a default present on one side only — a Python-sidedefault=in the model, aserver_defaultin the migration — is reported as drift.
test_check_constraints_match(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify the CHECK constraints after a full upgrade match the SQLAlchemy ORM metadata, by name.
compare_metadata() never compares CHECK constraints, so a constraint a
migration forgot is invisible to test_migrations_up_to_date.
test_downgrade_all_the_way(alembic_config, migration_engine, isolated_migration_schema)
async
¶
Verify all migrations can be downgraded to base one by one.
test_enum_values_match(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify every native Enum column's values after a full upgrade match the type in the database, in order.
compare_metadata() never compares enum members, so a value a migration
forgot to add is invisible to test_migrations_up_to_date.
test_migrations_up_to_date(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify the database schema after a full upgrade matches the SQLAlchemy ORM metadata.
test_single_head_revision(alembic_config)
async
¶
Verify there is exactly one head revision (no unmerged branches).
test_stairway_upgrade_downgrade(alembic_config, migration_engine, isolated_migration_schema)
async
¶
Verify every migration can be applied and rolled back individually (stairway test).
For each revision in chronological order: 1. Upgrade to this revision. 2. Assert current revision matches. 3. Downgrade to the previous revision (or base). 4. Assert the downgrade succeeded. 5. Upgrade back before moving to the next step.
MigrationNamingMixin¶
Naming convention tests for database objects.
Naming rules are resolved in three layers (last wins):
- Built-in defaults (e.g.
allowed_fk_suffixes = ["_fkey"]). - Rules derived from
orm_metadata.naming_convention(if present). - Attributes explicitly set on the subclass.
Layer 3 wins only when the attribute is explicitly overridden on the class,
not inherited from MigrationNamingMixin itself.
test_naming_conventions(alembic_config, migration_engine, isolated_migration_schema, orm_metadata)
async
¶
Verify indexes and constraints follow naming conventions after a full upgrade.
Naming rules are auto-derived from orm_metadata.naming_convention when present,
falling back to built-in defaults. Explicit class attributes always take priority.
Utilities¶
Migration utilities¶
Migration runner and schema isolation utilities.
run_alembic_upgrade(engine, alembic_config, target_schema='public', revision='head')
async
¶
Run alembic upgrade programmatically without spawning a subprocess.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
AsyncEngine
|
Async SQLAlchemy engine. |
required |
alembic_config
|
Config
|
Alembic config object (typically from |
required |
target_schema
|
str
|
PostgreSQL schema to run migrations in. Defaults to |
'public'
|
revision
|
str
|
Target revision identifier. Defaults to |
'head'
|
Source code in alembic_gauntlet/utils/migrations.py
run_alembic_downgrade(engine, alembic_config, target_schema='public', revision='base')
async
¶
Run alembic downgrade programmatically without spawning a subprocess.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
AsyncEngine
|
Async SQLAlchemy engine. |
required |
alembic_config
|
Config
|
Alembic config object. |
required |
target_schema
|
str
|
PostgreSQL schema to run migrations in. Defaults to |
'public'
|
revision
|
str
|
Target revision identifier. Defaults to |
'base'
|
Source code in alembic_gauntlet/utils/migrations.py
get_current_revision(engine, target_schema='public')
async
¶
Return the current Alembic revision for the given schema, or None if at base.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
AsyncEngine
|
Async SQLAlchemy engine. |
required |
target_schema
|
str
|
PostgreSQL schema to inspect. Defaults to |
'public'
|
Source code in alembic_gauntlet/utils/migrations.py
get_all_revisions(alembic_config)
¶
Return all migration revision IDs in chronological order (base → head).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
alembic_config
|
Config
|
Alembic config object. |
required |
Source code in alembic_gauntlet/utils/migrations.py
create_isolated_migration_schema(migration_db_url)
async
¶
Create a unique PostgreSQL schema for one test run, yield it, then drop it.
Uses a dedicated engine so that schema creation and deletion do not interfere with the connection pool used by the migration engine.
Schema name format: test_mig_{8-char hex}.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
migration_db_url
|
str
|
Async DSN for the test database. |
required |
Yields:
| Type | Description |
|---|---|
AsyncGenerator[str, None]
|
The name of the freshly created schema. |
Source code in alembic_gauntlet/utils/migrations.py
Schema validation¶
Schema name validation utilities.
validate_schema_name(name, connection=None)
¶
Validate a PostgreSQL schema name to prevent SQL injection and ensure sanity.
Checks format, length, and optionally reserved words (requires a live connection).
Source code in alembic_gauntlet/utils/validation.py
get_pg_reserved_words(connection)
¶
Fetch PostgreSQL reserved keywords from system catalog.
Source code in alembic_gauntlet/utils/validation.py
Diff utilities¶
Schema diff helpers for migration consistency tests.
DEFAULT_IGNORE_TABLES = frozenset({'alembic_version'})
module-attribute
¶
is_ignored_diff_item(diff_item, ignore_tables)
¶
Return True if this diff item should be excluded from schema diff checks.
Filters out tables and their indexes that exist in the database but are absent from ORM metadata — for example, partition tables auto-created by PostgreSQL.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
diff_item
|
tuple
|
A single item from Alembic's |
required |
ignore_tables
|
frozenset[str]
|
Set of table names to skip. |
required |
Source code in alembic_gauntlet/utils/diff.py
compare_check_constraints(sync_conn, metadata, schema, ignore_tables=frozenset())
¶
Compare the CHECK constraints metadata declares with the ones in schema, by name.
Alembic's compare_metadata() never looks at CHECK constraints. Names are resolved
the way the DDL would render them — the metadata's naming convention applied, a
deferred name such as Boolean(create_constraint=True) filled in, anything over 63
characters truncated — through the same Alembic helper autogenerate uses for index
and unique constraint names.
A named constraint the models declare and the database lacks is always reported. One the database has and the models do not is reported unless the table also declares an unnamed check constraint, which carries whatever name PostgreSQL gave it. Unnamed constraints are never compared.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sync_conn
|
Connection
|
Synchronous SQLAlchemy connection. |
required |
metadata
|
MetaData
|
SQLAlchemy |
required |
schema
|
str
|
PostgreSQL schema the migrations ran in. |
required |
ignore_tables
|
frozenset[str]
|
Table names to skip. |
frozenset()
|
Returns:
| Type | Description |
|---|---|
list[str]
|
One line per difference; empty when the constraints match. |
Source code in alembic_gauntlet/utils/diff.py
compare_enums(sync_conn, metadata, schema, ignore_tables=frozenset())
¶
Compare the values of every native Enum column in metadata with its type in the database.
Alembic's compare_metadata() never looks at enum members. Values are compared as
ordered lists, so a value added in the wrong position counts as a difference. The type
is looked up in Enum.schema when set, otherwise in schema. Non-native and
unnamed enums have no type in the database and are skipped; a type in the database
that no column uses is not reported.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sync_conn
|
Connection
|
Synchronous SQLAlchemy connection. |
required |
metadata
|
MetaData
|
SQLAlchemy |
required |
schema
|
str
|
PostgreSQL schema the migrations ran in. |
required |
ignore_tables
|
frozenset[str]
|
Table names to skip. |
frozenset()
|
Returns:
| Type | Description |
|---|---|
list[str]
|
One line per difference; empty when the enums match. |
Source code in alembic_gauntlet/utils/diff.py
Naming utilities¶
Database naming convention validation.
TableNamingResults
¶
Bases: TypedDict
Collected naming data for a single table.
Source code in alembic_gauntlet/utils/naming.py
ForeignKeyInfo
¶
Bases: TypedDict
Foreign key info as returned by SQLAlchemy Inspector.get_foreign_keys().
Source code in alembic_gauntlet/utils/naming.py
fetch_table_naming_results(sync_conn, schema)
¶
Fetch index, foreign key, and constraint names for every table in the given schema.
The alembic_version table is always excluded.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
sync_conn
|
Connection
|
Synchronous SQLAlchemy connection. |
required |
schema
|
str
|
PostgreSQL schema to inspect. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, TableNamingResults]
|
Mapping of |
Source code in alembic_gauntlet/utils/naming.py
validate_naming_results(results, allowed_index_prefixes, allowed_index_suffixes, allowed_fk_prefixes, allowed_fk_suffixes, allowed_check_prefixes, allowed_check_suffixes, allowed_uq_prefixes, allowed_uq_suffixes, allowed_pk_prefixes, allowed_pk_suffixes)
¶
Assert that every index, foreign key, and constraint follows naming conventions.
Each name must match at least one allowed prefix or one allowed suffix.
An optional trailing digit is accepted on index suffixes to accommodate PostgreSQL
partition auto-naming (e.g. users_pkey1).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
results
|
dict[str, TableNamingResults]
|
Output of :func: |
required |
allowed_index_prefixes
|
list[str]
|
Prefixes an index name may start with (e.g. |
required |
allowed_index_suffixes
|
list[str]
|
Suffixes an index name may end with (e.g. |
required |
allowed_fk_prefixes
|
list[str]
|
Prefixes a foreign key name may start with (e.g. |
required |
allowed_fk_suffixes
|
list[str]
|
Suffixes a foreign key name must end with (e.g. |
required |
allowed_check_prefixes
|
list[str]
|
Prefixes a check constraint name may start with (e.g. |
required |
allowed_check_suffixes
|
list[str]
|
Suffixes a check constraint name may end with. |
required |
allowed_uq_prefixes
|
list[str]
|
Prefixes a unique constraint name may start with (e.g. |
required |
allowed_uq_suffixes
|
list[str]
|
Suffixes a unique constraint name may end with. |
required |
allowed_pk_prefixes
|
list[str]
|
Prefixes a primary key constraint name may start with (e.g. |
required |
allowed_pk_suffixes
|
list[str]
|
Suffixes a primary key constraint name may end with (e.g. |
required |
Source code in alembic_gauntlet/utils/naming.py
Naming convention rules¶
Utilities for deriving naming rules from SQLAlchemy MetaData.naming_convention.
NamingConventionRules
dataclass
¶
Naming rules derived from a SQLAlchemy naming_convention dict.
Source code in alembic_gauntlet/utils/convention.py
rules_from_metadata(metadata)
¶
Derive :class:NamingConventionRules from metadata.naming_convention.
Only literal prefixes and suffixes are extracted — fully dynamic templates (no literal prefix or suffix) are silently ignored.
The SQLAlchemy built-in default convention ({"ix": "ix_%(column_0_label)s"})
is ignored; only user-supplied conventions are considered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata
|
MetaData
|
SQLAlchemy |
required |
Returns:
| Type | Description |
|---|---|
NamingConventionRules
|
class: |
NamingConventionRules
|
Fields for which no literal prefix/suffix could be extracted are empty. |
Source code in alembic_gauntlet/utils/convention.py
Fixtures¶
Core fixtures¶
Standard pytest fixtures for Alembic migration testing.
alembic_config()
¶
Create an Alembic :class:~alembic.config.Config from alembic.ini.
Override this fixture in your test class to point to a different alembic.ini::
@pytest.fixture
def alembic_config(self) -> Config:
from pathlib import Path
from alembic.config import Config
ini = Path(__file__).parent.parent / "alembic.ini"
return Config(str(ini))
Source code in alembic_gauntlet/fixtures.py
migration_engine(migration_db_url)
async
¶
Async engine with :class:~sqlalchemy.pool.NullPool for migration tests.
NullPool prevents connection reuse between tests, which is important for schema isolation when running tests in parallel.
Requires the migration_db_url fixture to be provided.
Source code in alembic_gauntlet/fixtures.py
Exceptions¶
Exception classes for alembic-gauntlet.
EmptySchemaNameError
¶
Bases: SchemaValidationError
Schema name is empty.
InvalidSchemaNameError
¶
Bases: SchemaValidationError
Schema name has invalid format.
ReservedWordSchemaNameError
¶
Bases: SchemaValidationError
Schema name is a PostgreSQL reserved word.
SchemaNameTooLongError
¶
Bases: SchemaValidationError
Schema name exceeds the PostgreSQL maximum identifier length of 63 characters.
Contrib¶
Testcontainers¶
Optional migration_db_url fixture powered by testcontainers.
Import this fixture in your conftest.py to get a fully managed PostgreSQL
container without any external setup::
# tests/conftest.py
from alembic_gauntlet.contrib.testcontainers import migration_db_url # noqa: F401
The fixture has session scope, so the container starts once per test session.
Override migration_db_url in your own conftest to use a different database.
Requires the optional extra::
pip install "alembic-gauntlet[testcontainers]"
migration_db_url()
¶
Start a PostgreSQL 17 container and yield its async DSN.
The container is stopped automatically at the end of the test session.
Raises:
| Type | Description |
|---|---|
ImportError
|
If |