--- name: postgres-migrations-forward-only description: Forward-only, reversible-by-new-migration policy for Postgres 17. No `--rollback`, no ALTER TABLE without concurrent-safe patterns on hot tables. when_to_use: You are the db_engineer role or any coder authoring a `migrations/NNNN_*.sql` file. tags: [backend, postgres, migrations, versioned] --- # Postgres migrations (17-safe) Anchored to **Postgres 17** (latest stable as of 2026-07). Postgres 18 is in beta at time of writing — don't rely on 18-only syntax without a version guard. ## Cardinal rule: forward-only - Every migration file is applied once and never rolled back. If you need to undo, ship a NEW migration that undoes. - `sqlx-cli` reads `migrations/NNNN_*.sql` in ascending order; each is wrapped in a txn where possible. - The migration is part of the commit that introduces the code depending on it. Split ONLY when a two-step deploy is required (see below). ## Two-step deploys (for column drops + renames) - **Adding a column**: single migration, always safe. - **Removing a column**: 1. **Migration N** stops writing the column (code change first). 2. **Migration N+1** (weeks later) drops the column, after every replica has caught up. - **Renaming a column**: don't. Add new, dual-write, backfill, stop-writing-old, drop-old. ## Concurrent-safe patterns on hot tables Anything > 1M rows or in the request path needs concurrent-safe DDL: ```sql -- Index adds CREATE INDEX CONCURRENTLY foo_bar_idx ON foo(bar); -- (CONCURRENTLY can't run in a txn — omit sqlx's txn wrap by prefixing -- the migration file with `-- fx-no-tx`; sqlx-cli honors it.) -- Adding NOT NULL to an existing column ALTER TABLE foo ADD CONSTRAINT foo_bar_nn CHECK (bar IS NOT NULL) NOT VALID; ALTER TABLE foo VALIDATE CONSTRAINT foo_bar_nn; -- Later: drop the CHECK, add NOT NULL to the column — cheap now. -- Backfill in batches UPDATE foo SET bar = new_value WHERE id IN (SELECT id FROM foo WHERE bar IS NULL LIMIT 10000); -- Loop until 0 rows. ``` ## Foreign keys - Every FK gets an index UNLESS the table is small (< 100k rows expected long-term). - `ON DELETE CASCADE` for owned child rows; `ON DELETE SET NULL` for weak references; explicit `NO ACTION` for anything else. - Deferring: `DEFERRABLE INITIALLY IMMEDIATE` on tables where cross-table batch writes need to defer FK checks. ## Naming - Migration filename: `NNNN_snake_case_description.sql` — 4-digit prefix, no gaps. - Table names: singular is fine, plural is fine — pick one per crate and never mix. - Index name: `