Safety Rules

March 3, 2026 · View on GitHub

pg-safe-migrate ships with 10 built-in safety rules that catch common migration anti-patterns. All rules are enabled by default.

Rules Reference

PGSM001 — Drop Table

Severity: Error

Dropping a table permanently deletes all data. Use the expand/contract pattern instead.

-- ✗ Blocked
DROP TABLE users;

-- ✓ Safe alternative: deprecate in code first, then drop in a later migration

PGSM002 — Drop Column

Severity: Error

Dropping a column permanently removes data and may break applications still querying it.

-- ✗ Blocked
ALTER TABLE users DROP COLUMN legacy_field;

-- ✓ Safe: stop reading the column in code first, then drop it later

PGSM003 — Add NOT NULL Column Without Default

Severity: Error

Adding a NOT NULL column without a DEFAULT requires a full table scan and will fail if rows exist.

-- ✗ Blocked
ALTER TABLE users ADD COLUMN name TEXT NOT NULL;

-- ✓ Safe
ALTER TABLE users ADD COLUMN name TEXT NOT NULL DEFAULT '';

PGSM004 — Rename Table

Severity: Error

Renaming a table breaks all queries referencing the old name.

-- ✗ Blocked
ALTER TABLE users RENAME TO people;

-- ✓ Safe: create a view with the old name, or use expand/contract

PGSM005 — Rename Column

Severity: Error

Renaming a column breaks queries referencing the old name.

-- ✗ Blocked
ALTER TABLE users RENAME COLUMN name TO full_name;

-- ✓ Safe: add new column, backfill, update code, drop old column

PGSM006 — Change Column Type

Severity: Warning

Changing a column type may require a full table rewrite and lock.

-- ⚠ Warning
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;

-- ✓ Safe: add new column with desired type, backfill, swap

PGSM007 — CREATE INDEX Without CONCURRENTLY

Severity: Error

CREATE INDEX without CONCURRENTLY locks the table for writes.

-- ✗ Blocked
CREATE INDEX idx_email ON users(email);

-- ✓ Safe
CREATE INDEX CONCURRENTLY idx_email ON users(email);

PGSM008 — DROP INDEX Without CONCURRENTLY

Severity: Error

DROP INDEX without CONCURRENTLY locks the table.

-- ✗ Blocked
DROP INDEX idx_email;

-- ✓ Safe
DROP INDEX CONCURRENTLY idx_email;

PGSM009 — Foreign Key Without NOT VALID

Severity: Warning

Adding a foreign key constraint validates all rows, locking both tables.

-- ⚠ Warning
ALTER TABLE orders ADD CONSTRAINT fk_user
  FOREIGN KEY (user_id) REFERENCES users(id);

-- ✓ Safe: two-step approach
ALTER TABLE orders ADD CONSTRAINT fk_user
  FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;

ALTER TABLE orders VALIDATE CONSTRAINT fk_user;

PGSM010 — CHECK Constraint Without NOT VALID

Severity: Warning

Adding a CHECK constraint validates all rows, locking the table.

-- ⚠ Warning
ALTER TABLE users ADD CONSTRAINT chk_age CHECK (age > 0);

-- ✓ Safe: two-step approach
ALTER TABLE users ADD CONSTRAINT chk_age CHECK (age > 0) NOT VALID;
ALTER TABLE users VALIDATE CONSTRAINT chk_age;

Severity Levels

LevelMeaningExit Code
errorMust be fixed or explicitly overridden1
warningShould be reviewed; does not block by default0
infoInformational; does not block0

Machine-Readable Output

pg-safe-migrate lint --format json
[
  {
    "ruleId": "PGSM007",
    "severity": "error",
    "message": "CREATE INDEX without CONCURRENTLY will lock the table for writes.",
    "file": "migrations/001_add_index.sql",
    "statementIndex": 0,
    "snippet": "CREATE INDEX idx_name ON users(name);"
  }
]

See Also