Locks and rollout
Some statements hold a lock for a time proportional to the size of the table. Whether that matters depends on how many rows the table holds, which is not in the schema — so this package will not decide for you. It tells you instead:
if blocking := m.Blocking(); len(blocking) > 0 { // refuse, or route to whoever sequences an expand/contract rollout}migrate.Unblock rewrites the ones whose remedy is mechanical:
- A scanning
ADD CONSTRAINT(aCHECKorFOREIGN KEY) becomesADD … NOT VALIDplus aVALIDATE CONSTRAINTin a later file, moving the scan under a lock writers pass through. - A
SET NOT NULLbecomes the same pair with the requirement set between them, since Postgres accepts a validated check as proof and skips its own scan. - A
UNIQUEorPRIMARY KEY— which has noNOT VALIDform, because there is no way to build an index without reading every row — becomesCREATE UNIQUE INDEX CONCURRENTLYplus anADD CONSTRAINT … USING INDEXthat adopts it.
A type change is left alone. Rewriting a table has no in-place form: the alternative is a second column, a batched backfill and a cutover, and only you know what a batch costs or when the cutover can happen.
Unblock is opt-in rather than the default, because the sequence is longer,
splits the migration across files, and buys nothing on a table small enough that
the scan is instant — which most tables are.
migrate.Split separates changes that cannot share a file. Transaction control
in both goose and golang-migrate is per file, not per statement, so a
CREATE INDEX CONCURRENTLY would otherwise disable transactions for every other
change generated alongside it, silently removing their rollback guarantee.
- Diffing and rendering — producing the changes this rewrites
- Adopting a database