Migration Guard › demo
See it work
A sample migration and its table sizes go in; Migration Guard ranks the risky statements by how much data each one locks.
Input
CREATE INDEX idx_orders_customer ON orders (customer_id);
ALTER TABLE orders ADD COLUMN gift_note text;
ALTER TABLE feature_flags ALTER COLUMN rollout TYPE numeric(5,2);
ALTER TABLE customers ADD CONSTRAINT customers_email_key UNIQUE (email);
CREATE TABLE refund_reasons (id bigint PRIMARY KEY, label text NOT NULL);
COMMENT ON TABLE refund_reasons IS 'reasons shown in the refund form'; table_name | estimated_rows | total_bytes
---------------+----------------+-------------
orders | 48213664 | 12240512000
customers | 310442 | 164626432
feature_flags | 212 | 49152
(3 rows)Output
1 to fix before merge
6 statements read. Without table sizes, these 4 findings look alike. With your sizes: 1 to fix before merge, 1 apply at any size, 2 low impact at current size.
Tables of 1,024 MB or more count as large.
Fix before merge
orders· ~48.2M rows · 11.4 GBrequire-concurrent-index-creation, line 1: reads the whole table (11.4 GB including indexes) while holding SHARE (writes wait)
Statement, lock, rewrite and docs
Statement SQL CREATE INDEX idx_orders_customer ON orders (customer_id)Lock taken SHARE Blocks writes (INSERT, UPDATE, DELETE); reads still work Why CREATE INDEX without CONCURRENTLY takes a SHARE lock on the table for the whole index build. Inserts, updates and deletes wait until it finishes, so a large table means a long write outage. Lower-risk rewrite CREATE INDEX CONCURRENTLY builds the index without blocking writes (lower risk because it only takes SHARE UPDATE EXCLUSIVE). It cannot run inside a transaction block, so run it outside BEGIN / COMMIT. If it fails it leaves an INVALID index that you must drop and retry. PostgreSQL docs sql-createindex.html
explicit-locking.html#LOCKING-TABLESApplies at any size
orders· ~48.2M rows · 11.4 GBrequire-lock-timeout, line 2: The lock is brief once granted. The risk is waiting for it: while it waits, every later query on the table queues behind it.
Statement, lock, rewrite and docs
Statement SQL ALTER TABLE orders ADD COLUMN gift_note textLock taken ACCESS EXCLUSIVE Blocks reads and writes (SELECT, INSERT, UPDATE, DELETE) Why An ACCESS EXCLUSIVE statement that has to wait for a lock queues in front of every later query on that table, so one slow transaction can stall all reads and writes. No SET lock_timeout appears before this statement. Lower-risk rewrite Put SET lock_timeout = '5s'; at the top of the migration (lower risk because the statement gives up and can be retried instead of queueing indefinitely). PostgreSQL docs runtime-config-client.html#GUC-LOCK-TIMEOUT
explicit-locking.html#LOCKING-TABLESLow impact at current size
customers· ~310.4K rows · 157.0 MBdisallowed-unique-constraint, line 4: reads the whole table (157.0 MB including indexes) while holding ACCESS EXCLUSIVE (reads and writes wait)
Statement, lock, rewrite and docs
Statement SQL ALTER TABLE customers ADD CONSTRAINT customers_email_key UNIQUE (email)Lock taken ACCESS EXCLUSIVE Blocks reads and writes (SELECT, INSERT, UPDATE, DELETE) Why ADD UNIQUE or ADD PRIMARY KEY builds its index while holding an ACCESS EXCLUSIVE lock, so reads and writes wait for the whole build. Lower-risk rewrite CREATE UNIQUE INDEX CONCURRENTLY first (outside a transaction block), then ALTER TABLE ... ADD CONSTRAINT ... UNIQUE USING INDEX (or PRIMARY KEY USING INDEX). Lower risk because the attach step needs no index build. PostgreSQL docs sql-altertable.html
sql-createindex.html
explicit-locking.html#LOCKING-TABLESLow impact at current size
feature_flags· ~212 rows · 48.0 KBchanging-column-type, line 3: usually rewrites all 48.0 KB while holding ACCESS EXCLUSIVE (reads and writes wait)
Statement, lock, rewrite and docs
Statement SQL ALTER TABLE feature_flags ALTER COLUMN rollout TYPE numeric(5,2)Lock taken ACCESS EXCLUSIVE Blocks reads and writes (SELECT, INSERT, UPDATE, DELETE) Why ALTER COLUMN TYPE takes an ACCESS EXCLUSIVE lock and usually rewrites the table and its indexes. Binary-coercible changes (for example widening varchar) skip the rewrite, but the lock is still taken. Lower-risk rewrite Expand and contract: add a new column of the new type, write to both, backfill in batches, switch reads, then drop the old column. Lower risk because no single step rewrites the table under an ACCESS EXCLUSIVE lock. PostgreSQL docs sql-altertable.html
explicit-locking.html#LOCKING-TABLES
What Migration Guard did
Migration Guard: Matched each statement to the lock it takes and what that lock blocks
By hand: look up every statement in the PostgreSQL lock docs
Migration Guard: Looked up the size of every table the migration touches
By hand: query pg_class for each table
Migration Guard: Ranked the findings by how much data each one locks
By hand: judge which ones matter, and hope you did not miss one
Migration Guard: Gives the same answer on every run
By hand: an AI review can answer differently from one run to the next
Want this on every pull request?
The first 100 on the waitlist get $9/repo/mo, locked for as long as you stay subscribed. Then it's $19/repo/mo.
We store your email, a source tag, your consent and the signup time in Cloudflare D1, to email you about Migration Guard. Privacy
Try it on your own migration
Paste a PostgreSQL migration and, if you like, your table sizes. Rules assume PostgreSQL 12 or later. Nothing you paste is stored.
Limits. Checks the statement types listed below; other statements are shown as not analysed. No findings is not a guarantee. Row counts are Postgres estimates.
Statement types checked
- CREATE INDEX (with or without UNIQUE or CONCURRENTLY)
- DROP INDEX
- ALTER TABLE: ADD COLUMN, ADD CONSTRAINT, ALTER COLUMN SET NOT NULL, ALTER COLUMN TYPE, RENAME
- CREATE TABLE, DROP TABLE, CREATE EXTENSION, COMMENT ON, SET, BEGIN, COMMIT (recognised; no lock rule applies)