Free static safety checker for Postgres SQL migrations (catches locking and destructive DDL before it reaches production).
Project description
pg-migration-guard
A free, static safety checker for PostgreSQL SQL migrations. It reads a proposed Flyway or raw SQL migration and flags the DDL patterns that quietly take a heavy lock, rewrite a whole table, or destroy data, before that migration reaches production. Every finding names the hazard, explains why it blocks, and prints a safe rewrite.
It parses SQL with libpg_query, the same C parser PostgreSQL itself uses, so it reads real statements rather than guessing with regexes: a column named like a keyword, a schema-qualified table name, or dollar-quoted code is read correctly, not misread by a pattern match. The parser is bundled as a self-contained BSD-3-Clause binary and bound through the Python standard library (ctypes), so there are no third-party Python dependencies, and the tool never connects to a database or the network.
What it catches
Ten migration hazards (12 rule IDs):
| Rule | Hazard |
|---|---|
| IDX01 / IDX02 | CREATE INDEX without CONCURRENTLY, and a CONCURRENTLY index op inside a transaction (which errors) |
| COL01 | ADD COLUMN with a volatile DEFAULT (rewrites the table) |
| COL03 | ALTER COLUMN ... TYPE (rewrites the table) |
| NN01 | SET NOT NULL (scans the whole table under ACCESS EXCLUSIVE) |
| FK01 | ADD FOREIGN KEY without NOT VALID (validates under a write-blocking lock) |
| CHK01 | ADD CHECK without NOT VALID (scans the whole table) |
| PK01 | ADD PRIMARY KEY / UNIQUE (builds the index under ACCESS EXCLUSIVE) |
| ORD01 / ORD02 | a backfill and its constraint in one migration, and an unbatched UPDATE/DELETE with no WHERE |
| LCK01 | lock-taking DDL with no lock_timeout guard |
| DES01 | destructive ops: TRUNCATE, DROP TABLE, DROP COLUMN, DROP SCHEMA, DROP DATABASE |
Install
pip install pg-migration-guard
Requires Python 3.8+. No other dependencies. A prebuilt parser binary is bundled for Linux (x86_64, aarch64), macOS (arm64, x86_64), and Windows (x86_64).
Use
pg-migration-guard db/migration/V42__add_index.sql
pg-migration-guard --format json db/migration/V42__add_index.sql
pg-migration-guard --fail-on warn --pg-version 15 db/migration/V42__add_index.sql
Options:
--format {human,json}(defaulthuman)--fail-on {blocker,warn,advisory,none}(defaultblocker) sets the severity that makes the command exit nonzero.--pg-version Nassumes a Postgres major version for version-gated rules.--self-testruns the internal checks.--versionprints the version.
Exit codes: 0 clean, 1 a finding at or above --fail-on, 2 a usage or
parse error.
GitHub Action
Add a workflow that fails the pull request and annotates each risky line:
name: migration-guard
on:
pull_request:
paths: ["db/migration/**.sql"]
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- uses: technical-turtle/pg-migration-guard@v1
with:
path: db/migration
fail-on: blocker
# pg-version: "15" # optional
Findings are emitted as GitHub annotations (::error:: for blocker, ::warning::
for warn, ::notice:: for advisory), so they appear inline on the diff.
Before and after
A migration that looks harmless:
CREATE INDEX idx_orders_customer ON orders (customer_id);
ALTER TABLE orders DROP COLUMN legacy_note;
$ pg-migration-guard V42__orders.sql
BLOCKER DES01 Destructive operation (TRUNCATE / DROP TABLE / DROP COLUMN) (high)
stmt #1, line 2: ALTER TABLE orders DROP COLUMN legacy_note
Why: DROP COLUMN is fast and metadata-only, but it is irreversible and breaks any
app version still selecting the column during a rolling deploy.
Safe: Confirm intent. For a live column, stop referencing it first (contract phase), then drop.
WARN IDX01 CREATE INDEX without CONCURRENTLY (high) [latent blocker on a large/hot table]
stmt #0, line 1: CREATE INDEX idx_orders_customer ON orders (customer_id)
Why: Plain CREATE INDEX takes a SHARE lock, blocking all writes to the table until the build finishes.
Safe: CREATE INDEX CONCURRENTLY idx ON tbl (col); -- outside a transaction
Summary: 1 blocker, 1 warn, 0 advisory
The safe rewrite: build the index without holding a write lock, and drop the column in two steps (stop reading it in the app first, then drop it in a later migration):
CREATE INDEX CONCURRENTLY idx_orders_customer ON orders (customer_id); -- outside a transaction
-- ship a release that stops reading legacy_note, then in a later migration:
ALTER TABLE orders DROP COLUMN legacy_note;
Known limitations
- Liquibase changelogs (XML, YAML, JSON) are not parsed. Point this tool at SQL migrations (raw SQL or Flyway SQL); for Liquibase changelog formats, use the paid Postgres Migration Safety Auditor.
- The tool assumes a migration runs inside a transaction (the Flyway and
Liquibase default). A Liquibase formatted-SQL file with
runInTransaction:falsecan therefore get a spurious IDX02 finding on aCONCURRENTLYindex operation, since outside a transaction that operation is actually fine.
Full coverage
pg-migration-guard is the free version of the Postgres Migration Safety Auditor. The paid tool checks 33 rules (not 12), each linked to an official Postgres source, and adds Liquibase XML / YAML / JSON changelog support, Postgres-version and table-size aware checks, a config file with per-rule mute controls, and a Claude Code skill: https://technical-turtle.com/postgres-migration-auditor
License
MIT. Bundles the libpg_query parser (BSD 3-Clause); see THIRD-PARTY-NOTICES.md.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pg_migration_guard-1.0.0.tar.gz.
File metadata
- Download URL: pg_migration_guard-1.0.0.tar.gz
- Upload date:
- Size: 9.4 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
de7448e82ae4986afc6945470550a443a8dc258d645af09011489cffd7a7bc4b
|
|
| MD5 |
ebd0c1b57f2e5bc5f364ba71f9599e8d
|
|
| BLAKE2b-256 |
6e0b2314ae16b99b5cdce9daf920ce3427f0e2b7b296f8bb5609cf038f975bee
|
File details
Details for the file pg_migration_guard-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pg_migration_guard-1.0.0-py3-none-any.whl
- Upload date:
- Size: 9.5 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
55b5654d46cbb32a90b63c3010297c6c5757d9085a6395f69f91a260ee8ab05e
|
|
| MD5 |
d33f18c39c3aef1687cb9cca8df49ea2
|
|
| BLAKE2b-256 |
eeb9aec5df05200929690064492a3937489912cc70ba65254de868b8670b741e
|