Rules
Every hazard pgsafe checks. Click a rule for the full explanation and the safe rewrite.
Locking & rewrites
add-column-generated-storederrorAdding aGENERATED ALWAYS AS (…) STOREDcolumn rewrites the table under anACCESS EXCLUSIVElock.add-column-identityerrorAdding aGENERATED ... AS IDENTITYcolumn creates a sequence and rewrites every row under anACCESS EXCLUSIVElock.add-column-not-null-no-defaulterrorADD COLUMN ... NOT NULLwith noDEFAULTfails immediately on any non-empty table.add-column-serialerrorAdding aserial/bigserialcolumn creates a sequence and rewrites every row under anACCESS EXCLUSIVElock.add-column-volatile-defaulterrorAdding a column with a volatileDEFAULTrewrites every existing row under anACCESS EXCLUSIVElock.add-index-non-concurrenterrorBuilding an index withoutCONCURRENTLYblocks all writes for the whole build.add-triggerwarningCREATE TRIGGERtakes aSHARE ROW EXCLUSIVElock and changes behavior for every subsequent write.alter-column-typeerrorALTER COLUMN ... TYPEusually rewrites the table under a lock and invalidates cached query plans.attach-partitionwarningATTACH PARTITIONlocks and scans the table being attached to validate the partition bound.concurrently-in-transactionerrorACONCURRENTLYindex operation inside a transaction fails at runtime — Postgres rejects it.detach-partition-non-concurrenterrorDETACH PARTITIONtakesACCESS EXCLUSIVEon the parent and partition, blocking the whole partitioned table.drop-index-non-concurrenterrorDROP INDEXwithoutCONCURRENTLYtakes anACCESS EXCLUSIVElock, blocking reads and writes.enum-value-used-in-transactionwarningALTER TYPE … ADD VALUEthen using that value in the same transaction fails at runtime.refresh-matview-non-concurrenterrorREFRESH MATERIALIZED VIEWwithoutCONCURRENTLYtakes anACCESS EXCLUSIVElock and blocks all reads.reindex-non-concurrenterrorREINDEXwithoutCONCURRENTLYtakes anACCESS EXCLUSIVElock on each index it rebuilds.require-timeoutwarningA blocking-lock statement runs with nolock_timeout/statement_timeoutset.set-access-methoderrorSET ACCESS METHODrewrites the entire table and rebuilds its indexes under anACCESS EXCLUSIVElock.set-logged-unloggederrorALTER TABLE ... SET LOGGED/UNLOGGEDrewrites the entire table and its indexes under anACCESS EXCLUSIVElock.set-not-nullerrorALTER COLUMN ... SET NOT NULLscans the entire table under anACCESS EXCLUSIVElock.vacuum-full-clustererrorVACUUM FULLandCLUSTERrewrite the whole table under anACCESS EXCLUSIVElock.
Constraints
add-check-without-not-validerrorAdding aCHECKconstraint withoutNOT VALIDscans the whole table under a lock.add-exclusion-constrainterrorAdding anEXCLUDEconstraint builds an index under anACCESS EXCLUSIVElock, scanning the whole table.add-fk-without-not-validerrorAdding a foreign key withoutNOT VALIDscans and locks both tables.add-primary-key-without-indexerrorAdding aPRIMARY KEYinline builds its unique index under anACCESS EXCLUSIVElock.add-unique-constrainterrorAdding aUNIQUEconstraint inline builds its index while holdingACCESS EXCLUSIVEfor the whole build.drop-constraintwarningDROP CONSTRAINTremoves an integrity guarantee and can break logical-replication replica identity.fk-without-covering-indexwarningA foreign key with no covering index makes every parent change scan and lock the child.
Destructive
drop-columnwarningDropping a column breaks any app code still referencing it the moment it runs.drop-tablewarningDROP TABLEpermanently removes the table and all its data; in-flight queries fail immediately.renamewarningRenaming a table, column, type, or enum value breaks existing queries, views, and functions.truncatewarningTRUNCATEtakes anACCESS EXCLUSIVElock and irreversibly removes all rows.
Schema design
identifier-too-longwarningAn identifier longer than 63 bytes is silently truncated by PostgreSQL, so two names can collide.prefer-bigint-primary-keywarningA small-integer primary key overflows its id space; usebigint/bigserial/identity.prefer-jsonbwarningAjsoncolumn has no equality/ordering operators (DISTINCT,GROUP BY,ORDER BYfail); usejsonb.
Policy
forbid-nullable-fkwarning(opt-in) A nullable foreign-key column in aCREATE TABLE.forbidden-column-typewarning(opt-in) A column whose type is in the configured forbidden set.naming-conventionwarning(opt-in) An introduced name that doesn't match the configured regex for its kind.require-columnswarning(opt-in) ACREATE TABLEmissing a configured required column (e.g.created_at).require-commentwarning(opt-in) A new table or column left without aCOMMENT.require-if-existswarning(opt-in) ACREATE/DROPwithoutIF NOT EXISTS/IF EXISTS.require-not-nullwarning(opt-in) ACREATE TABLEwith a column left nullable.require-primary-keywarning(opt-in) ACREATE TABLEthe migration leaves without a primary key.unchecked-do-blockwarning(opt-in) ADOblock containing SQL pgsafe can't statically analyze.