Skip to content

async-workflows/alembic-drift-check

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

alembic-drift-check

Catch the migration you forgot to write. alembic-drift-check compares your SQLAlchemy ORM models against a live database and fails the build when they disagree — the classic "we changed a model but never generated (or never applied) the Alembic migration" incident that only shows up in production.

It is not a reimplementation of Alembic's diff engine. It drives the exact same machinery alembic revision --autogenerate uses (compare_metadata), so what it flags is precisely what a fresh autogenerated migration would contain.

Why you want this in CI

A migration that never got written — or a migration that got merged but never alembic upgraded against staging — leaves your database schema lagging behind your models. Everything looks fine locally because SQLAlchemy emits DDL from the models, but the deployed database is a different shape. alembic-drift-check turns that silent divergence into a red build.

Quickstart

pip install "git+https://github.com/async-workflows/alembic-drift-check.git"

alembic-drift-check \
  --metadata myapp.models:Base.metadata \
  --database-url postgresql://user:pass@localhost/app

If the models and the database are in sync you get exit code 0. If drift exists you get a report and exit code 1:

Schema drift detected: 2 differences.

  + add_table: posts
  + add_column: users.full_name

-- checked by alembic-drift-check

Installation

The tool is distributed from GitHub (it is not published on PyPI):

pip install "git+https://github.com/async-workflows/alembic-drift-check.git"

To pin a specific revision, append @<tag-or-sha>:

pip install "git+https://github.com/async-workflows/alembic-drift-check.git@v0.1.0"

Requires Python 3.10+, SQLAlchemy 2.0+, and Alembic 1.13+.

Usage

alembic-drift-check --metadata MODULE:ATTR [--database-url URL] [options]

CLI flags

Flag Description
--metadata MODULE:ATTR Required. Import spec for the target MetaData or a declarative Base. Both myapp.models:Base.metadata and myapp.models:target_metadata work — a Base is unwrapped to its .metadata.
--database-url URL SQLAlchemy database URL to compare against. If omitted, falls back to $DATABASE_URL, then to sqlalchemy.url in alembic.ini.
--alembic-ini PATH Path to alembic.ini used to resolve sqlalchemy.url (default: alembic.ini).
--exclude GLOB Table-name glob to ignore. Repeatable, e.g. --exclude 'tmp_*' --exclude 'celery_*'.
--ignore-removed Do not treat objects that exist in the database but not in the models as drift (useful during staged column/table removals).
--generate-stub Render an Alembic revision-script stub with the op.* calls for the detected operations.
--stub-out FILE Write the generated stub to FILE instead of stdout (implies --generate-stub).
--json Emit machine-readable JSON instead of the human report.
--version Print the version and exit.

Generating a migration stub

--generate-stub renders a ready-to-review upgrade() body from the drift so you can drop it into a new revision:

alembic-drift-check \
  --metadata myapp.models:Base.metadata \
  --database-url sqlite:///app.db \
  --generate-stub
def upgrade() -> None:
    op.create_table('posts',
        sa.Column('id', sa.Integer(), nullable=False),
        sa.Column('title', sa.String(length=200), nullable=False),
        sa.PrimaryKeyConstraint('id')
    )
    op.add_column('users', sa.Column('full_name', sa.String(length=120), nullable=True))

Treat the stub as a starting point — always review autogenerated operations (see Further reading).

JSON output

--json prints a stable structure suitable for downstream tooling:

{
  "has_drift": true,
  "entries": [
    { "kind": "add_table", "table": "posts", "detail": null, "is_removal": false },
    { "kind": "add_column", "table": "users", "detail": "full_name", "is_removal": false }
  ],
  "ignored_removals": 0,
  "stub": null,
  "database_url": "sqlite:///app.db",
  "attribution": "checked by alembic-drift-check"
}

GitHub Action

A composite action ships in this repository. Add a job that first brings up (or migrates) a database, then runs the check against it:

name: schema-drift
on: [pull_request]

jobs:
  drift:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: postgres
        ports: ["5432:5432"]
        options: >-
          --health-cmd pg_isready --health-interval 10s
          --health-timeout 5s --health-retries 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Install app and apply migrations
        run: |
          pip install -e .
          alembic upgrade head
        env:
          DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres

      - name: Check for schema drift
        uses: async-workflows/alembic-drift-check@main
        with:
          metadata: myapp.models:Base.metadata
          database-url: postgresql://postgres:postgres@localhost:5432/postgres
          exclude: "tmp_* celery_*"

The step migrates the database to head first, then asserts that the migrated schema still matches the models — so a model change with no accompanying migration fails the PR. Action inputs mirror the CLI flags (metadata, database-url, exclude, ignore-removed, generate-stub, ref).

How drift is computed

  1. Your target MetaData is imported from --metadata (a Base is unwrapped to Base.metadata).
  2. A SQLAlchemy engine connects to --database-url; the driver decides whether a sync or async engine is used.
  3. An alembic.migration.MigrationContext is configured on that connection and compare_metadata diffs the reflected database schema against the models (with compare_type and compare_server_default enabled).
  4. Each diff is classified — add_table, remove_table, add_column, remove_column, modify_type, add_index, add_constraint, and so on — and summarized. --generate-stub additionally renders the operations to Python via render_python_code.

Because it leans on Alembic's own comparison, the results track Alembic's known capabilities and blind spots exactly (see Limitations).

Exit codes

Code Meaning
0 No drift — models and database are in sync.
1 Drift detected.
2 Usage or configuration error (bad --metadata spec, unresolvable URL, etc.).

With --ignore-removed, differences that only remove objects from the database no longer count toward the exit code.

Async support

Async database URLs are handled transparently. If the URL uses an async driver (for example postgresql+asyncpg://… or sqlite+aiosqlite://…), the tool opens an async engine and runs the comparison through Connection.run_sync, because Alembic's reflection and comparison code is synchronous. Sync URLs use a plain synchronous engine. You do not need to configure anything — detection is based on the URL's dialect.

Limitations

  • Drift detection is only as complete as Alembic autogenerate. Some changes are not reliably detected across all backends — for example certain CHECK-constraint edits, some server-default changes, and comment changes. See Alembic's documentation on what autogenerate detects.
  • Type comparison depends on the dialect; a --generate-stub on SQLite may render slightly different types than the same models against PostgreSQL.
  • The tool compares against whatever schema the database currently has. Point it at a database that has already had alembic upgrade head applied, so that a clean result means "migrations plus models agree."
  • --exclude filters by table name; it does not currently filter by schema.

Further reading

Background on the Alembic workflow this check plugs into: Alembic async migrations and schema evolution, autogenerating and reviewing migration scripts, configuring Alembic with async SQLAlchemy engines, setting up env.py for asyncpg, and excluding tables and schemas from autogenerate.

License

MIT © 2026 async-workflows

About

Fail your CI when SQLAlchemy models drift from the live database schema — real Alembic autogenerate diffing, an optional revision-stub generator, sync and async engines.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages