Skip to content

Commit 4f208d6

Browse files
notSumit25claude
andcommitted
docs: add measured FK/CHECK constraint rules to migration review spec
ADD FOREIGN KEY takes ShareRowExclusiveLock on the referenced table as well as the altered one — a statement can block writes on a table it does not name. Output shape now reports locks per table rather than as a single field. NOT VALID surfaced as the safer path for both constraint types: same lock, skips the validating scan, with VALIDATE CONSTRAINT running under ShareUpdateExclusiveLock afterwards. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent d4ab7e4 commit 4f208d6

1 file changed

Lines changed: 22 additions & 3 deletions

File tree

‎docs/superpowers/specs/2026-09-04-preflight-migration-review-design.md‎

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,28 @@ Every row verified against a live instance via `pg_locks` and
9090
| `RENAME COLUMN` | AccessExclusive | no | SAFE (breaks app code, not the DB) |
9191
| `CREATE INDEX` | AccessExclusive + Share | n/a | DANGER: blocks writes for the build |
9292
| `CREATE INDEX CONCURRENTLY` | ShareUpdateExclusive | n/a | SAFE, can leave an invalid index |
93+
| `ADD CHECK` (validating) | AccessExclusive | no | CAUTION (full scan to validate) |
94+
| `ADD CHECK ... NOT VALID` | AccessExclusive | no | SAFE (no scan; validate separately) |
95+
| `ADD FOREIGN KEY` (validating) | ShareRowExclusive **on both tables** | no | DANGER: blocks writes on the referenced table too |
96+
| `ADD FOREIGN KEY ... NOT VALID` | ShareRowExclusive on both tables | no | CAUTION (still locks both, but skips the scan) |
97+
| `VALIDATE CONSTRAINT` | ShareUpdateExclusive | no | SAFE (concurrent writes allowed) |
9398

9499
**Lock type and duration are independent axes.** `ADD COLUMN` with no default takes
95100
AccessExclusive — the strongest lock — yet is harmless because it is held for
96101
milliseconds. The same lock across a 48M-row rewrite is the outage. The tool must
97102
report both; reporting the lock name alone is alarming and useless.
98103

104+
**A statement can lock tables it does not name.** `ADD FOREIGN KEY` takes
105+
`ShareRowExclusiveLock` on the *referenced* table as well as the altered one —
106+
measured, and the reason `child`-side migrations cause unexplained write stalls on
107+
a parent table nobody touched. The output therefore reports locks **per table**,
108+
not as a single field, and names every table affected.
109+
110+
`NOT VALID` is the alternative worth surfacing for both constraint types: it takes
111+
the same lock but skips the validating scan, so the expensive half moves to a
112+
separate `VALIDATE CONSTRAINT` that runs under `ShareUpdateExclusiveLock` and
113+
allows concurrent writes. Two cheap steps instead of one long block.
114+
99115
### Output shape
100116

101117
```json
@@ -105,8 +121,9 @@ report both; reporting the lock name alone is alarming and useless.
105121
"safeToRun": false,
106122
"operation": "ADD_COLUMN",
107123
"table": "orders",
108-
"lockMode": "AccessExclusiveLock",
109-
"blocks": ["read", "write"],
124+
"locks": [
125+
{ "table": "orders", "mode": "AccessExclusiveLock", "blocks": ["read", "write"] }
126+
],
110127
"rewritesTable": true,
111128
"tableRows": 48000000,
112129
"tableSizeBytes": 9200000000,
@@ -119,7 +136,9 @@ report both; reporting the lock name alone is alarming and useless.
119136
```
120137

121138
`estimatedDuration` is a coarse bucket, never a false-precision number — real time
122-
depends on disk, cache and load.
139+
depends on disk, cache and load, none of which table size predicts. A numeric
140+
range would imply an accuracy the inputs cannot support; the bucket is the
141+
honest form of the answer.
123142

124143
### Placement
125144

0 commit comments

Comments
 (0)