Skip to main content

alembic-guard

Catch Alembic migrations that lock tables or break running code, before they reach production.

alembic-guard catching a dangerous migration, then passing the zero-downtime version

See it on a real pull request: alembic-guard-demo.

This migration passes code review, passes tests on an empty dev database, and takes down production:

def upgrade():
    op.add_column("users", sa.Column("email", sa.String(255), nullable=False))
    op.create_index("ix_users_email", "users", ["email"], unique=True)
    op.alter_column("users", "name", new_column_name="full_name")
    op.create_foreign_key("fk_orders_user", "orders", "users", ["user_id"], ["id"])
$ alembic-guard
migrations/versions/0002_add_email.py:13:5  AG001 error  Adding NOT NULL column "users.email" without a server_default fails on a table with rows.
    fix: Add a server_default, or add the column as nullable, backfill it, then set NOT NULL in a later migration.
migrations/versions/0002_add_email.py:14:5  AG002 warning  Creating index "ix_users_email" on "users" blocks writes until it is built.
    fix: Pass postgresql_concurrently=True and run it inside `with op.get_context().autocommit_block():`.
migrations/versions/0002_add_email.py:15:5  AG004 error  Renaming "users.name" to "full_name" breaks code still using the old name.
    fix: Expand and contract: add the new column or table, write to both, backfill, move reads over, then drop the old one in a later release.
migrations/versions/0002_add_email.py:16:5  AG008 warning  Foreign key from "orders" to "users" is validated while blocking writes on both tables.
    fix: Pass postgresql_not_valid=True, then run `ALTER TABLE ... VALIDATE CONSTRAINT ...` in a separate migration. Validation only takes a light lock.
Found 2 errors and 2 warnings in 3 migrations.

Tools like squawk do this for raw SQL. alembic-guard reads your Alembic Python migrations directly, so it understands op.add_column, batch_alter_table, autocommit_block and the rest, with no database and no SQL generation step.

Install

pip install alembic-guard      # or: uv tool install alembic-guard

Usage

alembic-guard                                 # finds every Alembic versions/ dir
alembic-guard migrations/versions             # or point it at files / dirs
alembic-guard --diff-base origin/main         # only migrations this branch added or changed
alembic-guard --strict                        # fail on warnings too
alembic-guard --explain AG003                 # why a rule exists and how to fix it
alembic-guard --format github | json          # CI annotations or machine-readable output

Exit code 0 = clean, 1 = findings that fail the run (errors, or anything with --strict), 2 = usage error.

GitHub Action

Findings show up as annotations on the pull request diff.

# .github/workflows/migrations.yml
on: pull_request
jobs:
  alembic-guard:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0 # needed to diff against the base branch
      - uses: ShalomHunukumbura/alembic-guard@v0
        with:
          strict: "false"        # set "true" to fail on warnings
          # paths: "app/migrations/versions"
          # ignore: "AG005"

By default only migrations changed in the PR are checked, so adopting it on a repo with years of history doesn't flood you with old findings.

Rules

ID Severity Catches
AG001 error NOT NULL column added without a server_default: fails on any table with rows
AG002 warning Index created without CONCURRENTLY: blocks writes while it builds
AG003 error CONCURRENTLY inside Alembic's transaction: Postgres rejects it
AG004 error Table or column rename: old app version breaks mid-deploy
AG005 warning Table or column drop: running code that still reads it breaks
AG006 warning Column type change: usually rewrites the table under an exclusive lock
AG007 warning SET NOT NULL on an existing column: full scan under an exclusive lock
AG008 warning Foreign key or check constraint without NOT VALID: validated while blocking writes
AG009 warning Column added with a volatile default (gen_random_uuid(), nextval): table rewrite
AG010 warning Unique or primary key constraint on an existing table: builds an index while blocking writes

Operations on a table created earlier in the same migration are not flagged, since nobody can be using it yet. Raw SQL in op.execute("...") is checked for the common cases too.

Silencing a finding

Sometimes you know better (the table has 12 rows, or it's a maintenance window):

op.drop_column("users", "legacy_flag")  # alembic-guard: ignore[AG005]

# alembic-guard: ignore
op.rename_table("tmp_import", "imports")

Or project-wide in pyproject.toml:

[tool.alembic-guard]
paths = ["app/migrations/versions"]
ignore = ["AG005"]
strict = false
include-downgrade = false

How it works

Migrations are parsed with Python's ast module and never imported or executed. The checker walks upgrade() in order, keeping track of three things: which tables were created in this migration, which variables are batch_alter_table contexts (and for which table), and whether it's inside an autocommit_block(). Each op.* call is matched against Alembic's real signatures, so positional and keyword arguments are both understood.

The rules target PostgreSQL, where these locking behaviours are well documented.

Development

uv sync
uv run pytest
uv run alembic-guard examples/migrations/versions

License

MIT

Metadata

Release files for alembic-guard 0.1.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for alembic-guard 0.1.1
File Size Uploaded
alembic_guard-0.1.1.tar.gz 220.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for alembic-guard 0.1.1
File Interpreter ABI Platform
alembic_guard-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 237.3 kB

Release files / alembic_guard-0.1.1.tar.gz

Download URL alembic_guard-0.1.1.tar.gz
Size 220.6 kB
Tags Source
SHA-256 checksum
How to use checksums
c3027a89e2f7ea88af97cc858cba010cacc11b2b62b3e9ede9c020c85fe8748b
BLAKE2b-256 checksum
How to use checksums
b53eb6415f83b2bab5650eb9531ee8db1c88430f2d4f158db7e3aa85c7df8bec
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / alembic_guard-0.1.1-py3-none-any.whl

Download URL alembic_guard-0.1.1-py3-none-any.whl
Size 16.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7968c5e2891c3e72b64f8b87f8eae7dd799f19ddb094ec0098b14096e2845035
BLAKE2b-256 checksum
How to use checksums
85432896adc1d935011c5251c2ae4a482f2f47e3f3fec5cb41a37d0f1c1d0629
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.21 {"installer":{"name":"uv","version":"0.12.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page