@tamsensedev/runner 0.13.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -5
- package/README.md +61 -57
- package/dist/cli.js +646 -315
- package/dist/cli.js.map +1 -1
- package/dist/recorder.cjs.map +1 -1
- package/library/.claude-plugin/plugin.json +5 -5
- package/library/THIRD_PARTY_NOTICES.md +123 -123
- package/library/agents/code-architect.md +67 -67
- package/library/agents/code-explorer.md +61 -61
- package/library/agents/code-reviewer.md +73 -73
- package/library/agents/code-simplifier.md +62 -62
- package/library/agents/design-critic.md +52 -52
- package/library/agents/second-opinion.md +64 -64
- package/library/agents/security-auditor.md +86 -86
- package/library/agents/silent-failure-hunter.md +70 -70
- package/library/agents/test-coverage-analyzer.md +78 -78
- package/library/agents/test-runner.md +48 -48
- package/library/agents/ui-tester.md +50 -50
- package/library/licenses/Apache-2.0.txt +202 -202
- package/library/skills/accessibility/SKILL.md +144 -144
- package/library/skills/accessibility/references/wcag-checks.md +168 -168
- package/library/skills/addressing-review-feedback/SKILL.md +109 -109
- package/library/skills/app-screens/SKILL.md +96 -96
- package/library/skills/app-screens/references/shell-kpis-charts.md +108 -108
- package/library/skills/app-screens/references/states-density-phone.md +54 -54
- package/library/skills/app-screens/references/tables-forms-settings.md +104 -104
- package/library/skills/avoiding-generic-ai-design/SKILL.md +92 -92
- package/library/skills/avoiding-generic-ai-design/references/tells.md +112 -112
- package/library/skills/clean-commits/SKILL.md +85 -85
- package/library/skills/design-review/SKILL.md +107 -107
- package/library/skills/design-review/references/critique-passes.md +101 -101
- package/library/skills/design-review/references/screenshot-procedure.md +121 -121
- package/library/skills/differential-security-review/LICENSE +427 -427
- package/library/skills/differential-security-review/SKILL.md +105 -105
- package/library/skills/differential-security-review/references/adversarial.md +115 -115
- package/library/skills/differential-security-review/references/methodology.md +145 -145
- package/library/skills/differential-security-review/references/patterns.md +108 -108
- package/library/skills/false-positive-check/LICENSE +427 -427
- package/library/skills/false-positive-check/SKILL.md +95 -95
- package/library/skills/false-positive-check/references/bug-class-verification.md +128 -128
- package/library/skills/false-positive-check/references/deep-verification.md +113 -113
- package/library/skills/false-positive-check/references/evidence-templates.md +93 -93
- package/library/skills/false-positive-check/references/false-positive-patterns.md +122 -122
- package/library/skills/false-positive-check/references/gate-reviews.md +36 -36
- package/library/skills/false-positive-check/references/standard-verification.md +80 -80
- package/library/skills/focused-implementation/SKILL.md +104 -104
- package/library/skills/implementation-plan/SKILL.md +66 -66
- package/library/skills/interface-design/SKILL.md +144 -144
- package/library/skills/interface-design/references/craft-defaults.md +130 -130
- package/library/skills/interface-design/references/new-direction.md +62 -62
- package/library/skills/interface-design/references/writing.md +52 -52
- package/library/skills/interface-polish/SKILL.md +103 -103
- package/library/skills/interface-polish/references/detail-rules.md +154 -154
- package/library/skills/interface-polish/references/worst-case-data.md +158 -158
- package/library/skills/motion-design/SKILL.md +106 -106
- package/library/skills/motion-design/references/recipes.md +243 -243
- package/library/skills/postgres-performance/SKILL.md +97 -97
- package/library/skills/postgres-performance/references/rules.md +265 -265
- package/library/skills/project-orientation/SKILL.md +77 -77
- package/library/skills/reviewing-changes/SKILL.md +183 -183
- package/library/skills/safe-database-changes/SKILL.md +111 -111
- package/library/skills/safe-database-changes/references/backfills.md +75 -75
- package/library/skills/safe-database-changes/references/migration-tools.md +69 -69
- package/library/skills/safe-database-changes/references/postgres-ddl.md +199 -199
- package/library/skills/security-review/SKILL.md +132 -132
- package/library/skills/security-review/references/checks-by-area.md +107 -107
- package/library/skills/security-review/references/do-not-report.md +86 -86
- package/library/skills/spec-writing/SKILL.md +144 -144
- package/library/skills/systematic-debugging/SKILL.md +130 -130
- package/library/skills/systematic-debugging/references/flaky-tests.md +51 -51
- package/library/skills/systematic-debugging/references/root-cause-tracing.md +55 -55
- package/library/skills/task-interview/SKILL.md +84 -84
- package/library/skills/test-driven-development/SKILL.md +116 -116
- package/library/skills/test-driven-development/references/writing-good-tests.md +85 -85
- package/library/skills/ui-testing/SKILL.md +102 -102
- package/library/skills/ui-testing/references/headless-check.md +295 -295
- package/library/skills/ui-testing/references/local-stand.md +51 -51
- package/library/skills/ui-testing/references/playwright-patterns.md +134 -134
- package/library/skills/ui-testing/references/recording.md +61 -61
- package/library/skills/verification-before-completion/SKILL.md +113 -113
- package/library/skills/web-interface-guidelines/SKILL.md +202 -202
- package/package.json +46 -46
|
@@ -1,199 +1,199 @@
|
|
|
1
|
-
# PostgreSQL: lock-aware schema changes
|
|
2
|
-
|
|
3
|
-
Read this when a migration targets PostgreSQL and touches a table that may be large or busy in production. Everything here is for writing the migration; run it only against a throwaway database (see the hard rules in SKILL.md).
|
|
4
|
-
|
|
5
|
-
## Why a "fast" change can still cause an outage
|
|
6
|
-
|
|
7
|
-
Every DDL statement takes a lock. `ACCESS EXCLUSIVE` blocks reads and writes. Even a change that takes milliseconds must first wait for every running transaction on the table to finish, and while it waits, every new query on that table queues behind it:
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
Session 1: SELECT count(*) FROM orders; -- long query, holds a read lock
|
|
11
|
-
Session 2: ALTER TABLE orders ADD COLUMN ...; -- waits for session 1
|
|
12
|
-
Session 3: SELECT * FROM orders WHERE id = 1; -- now blocked behind session 2
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
So: set a short `lock_timeout` on DDL, and prefer the non-blocking forms below.
|
|
16
|
-
|
|
17
|
-
## Lock reference
|
|
18
|
-
|
|
19
|
-
Fast (catalog-only, brief lock):
|
|
20
|
-
|
|
21
|
-
| Operation | Lock | Notes |
|
|
22
|
-
|---|---|---|
|
|
23
|
-
| `ADD COLUMN` nullable, no default | ACCESS EXCLUSIVE, brief | No rewrite |
|
|
24
|
-
| `ADD COLUMN ... DEFAULT <constant>` (PG 11+) | ACCESS EXCLUSIVE, brief | Constant default stored in the catalog |
|
|
25
|
-
| `DROP COLUMN` | ACCESS EXCLUSIVE, brief | Data stays on disk until rewritten; code must stop using it first |
|
|
26
|
-
| `SET DEFAULT` / `DROP DEFAULT` | ACCESS EXCLUSIVE, brief | Existing rows untouched |
|
|
27
|
-
| `RENAME COLUMN` / `RENAME TO` | ACCESS EXCLUSIVE, brief | Fast, but breaks running code: use expand and contract |
|
|
28
|
-
| `CREATE INDEX CONCURRENTLY` | SHARE UPDATE EXCLUSIVE | Reads and writes continue; slower; cannot run in a transaction |
|
|
29
|
-
| `DROP INDEX CONCURRENTLY` | SHARE UPDATE EXCLUSIVE | Cannot run in a transaction |
|
|
30
|
-
| `ADD CONSTRAINT ... NOT VALID` | brief | Checks new rows only (FK, CHECK) |
|
|
31
|
-
| `VALIDATE CONSTRAINT` | SHARE UPDATE EXCLUSIVE | Scans rows but allows reads and writes |
|
|
32
|
-
|
|
33
|
-
Slow or blocking:
|
|
34
|
-
|
|
35
|
-
| Operation | Lock | Why |
|
|
36
|
-
|---|---|---|
|
|
37
|
-
| `ADD COLUMN ... DEFAULT <volatile>` (`now()`, `gen_random_uuid()`) | ACCESS EXCLUSIVE | Rewrites every row |
|
|
38
|
-
| `ALTER COLUMN TYPE` (most changes) | ACCESS EXCLUSIVE | Rewrites the table and its indexes |
|
|
39
|
-
| `SET NOT NULL` without a validated CHECK | ACCESS EXCLUSIVE | Full scan under the lock |
|
|
40
|
-
| `ADD CONSTRAINT` FK / CHECK, validated immediately | SHARE ROW EXCLUSIVE or stronger | Scans all rows, blocks writes |
|
|
41
|
-
| `ADD CONSTRAINT UNIQUE` / `PRIMARY KEY` directly | ACCESS EXCLUSIVE | Builds the index under the lock |
|
|
42
|
-
| `CREATE INDEX` without CONCURRENTLY | SHARE | Blocks writes for the whole build |
|
|
43
|
-
| `VACUUM FULL`, `CLUSTER` | ACCESS EXCLUSIVE | Rewrite the table |
|
|
44
|
-
|
|
45
|
-
Type changes that do not rewrite: `varchar(n)` to a larger `varchar(m)` or to `text`, and `numeric(p,s)` to a larger precision with the same scale. `int` to `bigint` rewrites. If you rely on a change being catalog-only, say so in the report.
|
|
46
|
-
|
|
47
|
-
## Safe patterns
|
|
48
|
-
|
|
49
|
-
Add a column with a computed default: add it nullable, backfill in batches, then set the default and the constraint.
|
|
50
|
-
|
|
51
|
-
```sql
|
|
52
|
-
ALTER TABLE orders ADD COLUMN placed_at timestamptz; -- instant
|
|
53
|
-
-- backfill in batches (references/backfills.md)
|
|
54
|
-
ALTER TABLE orders ALTER COLUMN placed_at SET DEFAULT now(); -- new rows only
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
Add NOT NULL to an existing column (PG 12+): a validated CHECK lets `SET NOT NULL` skip the scan.
|
|
58
|
-
|
|
59
|
-
```sql
|
|
60
|
-
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
|
|
61
|
-
CHECK (status IS NOT NULL) NOT VALID; -- brief
|
|
62
|
-
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null; -- scans, no write block
|
|
63
|
-
ALTER TABLE orders ALTER COLUMN status SET NOT NULL; -- uses the CHECK, no scan
|
|
64
|
-
ALTER TABLE orders DROP CONSTRAINT orders_status_not_null;
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
Add a foreign key:
|
|
68
|
-
|
|
69
|
-
```sql
|
|
70
|
-
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
|
|
71
|
-
FOREIGN KEY (user_id) REFERENCES users (id) NOT VALID;
|
|
72
|
-
ALTER TABLE orders VALIDATE CONSTRAINT orders_user_fk;
|
|
73
|
-
CREATE INDEX CONCURRENTLY orders_user_id_idx ON orders (user_id); -- separate migration
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
Add an index, and check it afterwards: a failed concurrent build leaves an INVALID index behind that slows writes.
|
|
77
|
-
|
|
78
|
-
```sql
|
|
79
|
-
CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_created_at_idx ON orders (created_at);
|
|
80
|
-
|
|
81
|
-
SELECT indexrelid::regclass AS index, indisvalid
|
|
82
|
-
FROM pg_index WHERE indrelid = 'orders'::regclass AND NOT indisvalid;
|
|
83
|
-
-- if listed: DROP INDEX CONCURRENTLY orders_created_at_idx; then build again
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
Add a unique constraint without holding a long lock:
|
|
87
|
-
|
|
88
|
-
```sql
|
|
89
|
-
CREATE UNIQUE INDEX CONCURRENTLY orders_tracking_uniq_idx ON orders (tracking_number);
|
|
90
|
-
ALTER TABLE orders ADD CONSTRAINT orders_tracking_uniq
|
|
91
|
-
UNIQUE USING INDEX orders_tracking_uniq_idx; -- instant
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
Change a column type on a big table: new column, dual writes in code, batched backfill, switch reads, drop the old column later (expand and contract). Do not `ALTER COLUMN TYPE` in place on a large table.
|
|
95
|
-
|
|
96
|
-
Rename a column with no downtime: same pattern. A plain `RENAME COLUMN` is fast but breaks every running instance that still uses the old name.
|
|
97
|
-
|
|
98
|
-
Drop a column: first ship code that no longer reads or writes it (for ORMs, remove it from the model; some ORMs select every mapped column), then drop it in a later migration. `DROP COLUMN` does not erase the data from disk; if the column held sensitive data, mention that a table rewrite is needed to really remove it.
|
|
99
|
-
|
|
100
|
-
Replace a primary key: build a CHECK NOT NULL (NOT VALID, then VALIDATE) and a unique index concurrently on the new key column first, then swap with `ADD CONSTRAINT ... PRIMARY KEY USING INDEX` in a short transaction. Treat this as a high-risk change and spell out the plan in the report.
|
|
101
|
-
|
|
102
|
-
## Timeouts and retries
|
|
103
|
-
|
|
104
|
-
Set a lock timeout on every DDL statement so a blocked migration fails fast instead of freezing the table:
|
|
105
|
-
|
|
106
|
-
```sql
|
|
107
|
-
SET lock_timeout = '5s';
|
|
108
|
-
SET statement_timeout = '30s'; -- not for CONCURRENTLY builds or VALIDATE on big tables
|
|
109
|
-
ALTER TABLE orders ADD COLUMN tracking_number text;
|
|
110
|
-
RESET lock_timeout;
|
|
111
|
-
RESET statement_timeout;
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
High-traffic systems use a shorter lock timeout (50 to 500 ms) with many retries; most apps are fine with a few seconds and a few retries. Many migration tools let you put `SET lock_timeout` at the top of the migration file. If the project's runner retries failed migrations, say so; otherwise describe in the report that the deploy may need a retry when the lock is not acquired.
|
|
115
|
-
|
|
116
|
-
A retry loop inside the database, for runners that cannot retry themselves:
|
|
117
|
-
|
|
118
|
-
```sql
|
|
119
|
-
DO $$
|
|
120
|
-
DECLARE attempt int := 1;
|
|
121
|
-
BEGIN
|
|
122
|
-
LOOP
|
|
123
|
-
BEGIN
|
|
124
|
-
SET LOCAL lock_timeout = '3s';
|
|
125
|
-
ALTER TABLE orders ADD COLUMN tracking_number text;
|
|
126
|
-
EXIT;
|
|
127
|
-
EXCEPTION WHEN lock_not_available THEN
|
|
128
|
-
IF attempt >= 5 THEN RAISE; END IF;
|
|
129
|
-
attempt := attempt + 1;
|
|
130
|
-
PERFORM pg_sleep(2 * attempt);
|
|
131
|
-
END;
|
|
132
|
-
END LOOP;
|
|
133
|
-
END $$;
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
Under heavy write load, retries done by the application (one transaction per attempt) are kinder to replicas than this in-database loop.
|
|
137
|
-
|
|
138
|
-
## Transactions
|
|
139
|
-
|
|
140
|
-
- One transaction around several fast statements gives all-or-nothing, but every lock is held until COMMIT. Do not put a slow statement in the same transaction as an ACCESS EXCLUSIVE one.
|
|
141
|
-
- `CREATE INDEX CONCURRENTLY`, `DROP INDEX CONCURRENTLY` and `REINDEX CONCURRENTLY` cannot run inside a transaction block. Many migration tools wrap each file in a transaction; see [migration-tools.md](migration-tools.md) for how to opt out, and give such statements their own migration.
|
|
142
|
-
- Keep data backfills out of schema transactions.
|
|
143
|
-
|
|
144
|
-
## Checks before and after
|
|
145
|
-
|
|
146
|
-
Run these on the throwaway database, seeded with realistic rows, to see whether the migration would fail on real data. In the report, suggest the "before" checks for the production deploy if the data could break the migration.
|
|
147
|
-
|
|
148
|
-
```sql
|
|
149
|
-
-- NULLs before SET NOT NULL
|
|
150
|
-
SELECT count(*) FROM orders WHERE status IS NULL;
|
|
151
|
-
|
|
152
|
-
-- duplicates before UNIQUE
|
|
153
|
-
SELECT tracking_number, count(*) FROM orders
|
|
154
|
-
WHERE tracking_number IS NOT NULL
|
|
155
|
-
GROUP BY tracking_number HAVING count(*) > 1 LIMIT 20;
|
|
156
|
-
|
|
157
|
-
-- orphans before a foreign key
|
|
158
|
-
SELECT o.id, o.user_id FROM orders o
|
|
159
|
-
LEFT JOIN users u ON u.id = o.user_id
|
|
160
|
-
WHERE o.user_id IS NOT NULL AND u.id IS NULL LIMIT 20;
|
|
161
|
-
|
|
162
|
-
-- size, to judge lock and rewrite risk
|
|
163
|
-
SELECT pg_size_pretty(pg_total_relation_size('orders')) AS total_size,
|
|
164
|
-
reltuples::bigint AS estimated_rows
|
|
165
|
-
FROM pg_class WHERE relname = 'orders';
|
|
166
|
-
|
|
167
|
-
-- after: columns, constraints, indexes as intended
|
|
168
|
-
SELECT column_name, data_type, is_nullable, column_default
|
|
169
|
-
FROM information_schema.columns WHERE table_name = 'orders';
|
|
170
|
-
SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint
|
|
171
|
-
WHERE conrelid = 'orders'::regclass;
|
|
172
|
-
SELECT indexrelid::regclass, indisvalid, pg_get_indexdef(indexrelid)
|
|
173
|
-
FROM pg_index WHERE indrelid = 'orders'::regclass;
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
`EXPLAIN ANALYZE` executes the statement, including UPDATE and DELETE. Use it only on the throwaway database, or inside `BEGIN; ... ROLLBACK;` there.
|
|
177
|
-
|
|
178
|
-
## Rollback
|
|
179
|
-
|
|
180
|
-
| Change | Rollback |
|
|
181
|
-
|---|---|
|
|
182
|
-
| ADD COLUMN, ADD CONSTRAINT, CREATE INDEX | DROP it |
|
|
183
|
-
| RENAME | Rename back |
|
|
184
|
-
| SET DEFAULT | Restore the old default or DROP DEFAULT |
|
|
185
|
-
| DROP COLUMN or TABLE, lossy type change, DELETE, TRUNCATE | Not reversible without a backup: say so in the report |
|
|
186
|
-
|
|
187
|
-
Prefer forward fixes in production: a new migration that undoes the change.
|
|
188
|
-
|
|
189
|
-
## Pitfalls
|
|
190
|
-
|
|
191
|
-
- Testing only on an empty table: a migration that takes 1 ms there can lock a 10M-row table for minutes.
|
|
192
|
-
- Forgetting CONCURRENTLY, or using it inside a transaction.
|
|
193
|
-
- No lock timeout, so one long query freezes the table behind the DDL.
|
|
194
|
-
- One huge UPDATE for a backfill: long locks, table bloat, replica lag.
|
|
195
|
-
- Leaving an INVALID index after a failed concurrent build.
|
|
196
|
-
- Dropping a column before the code that uses it is gone.
|
|
197
|
-
- Assuming a type change is catalog-only without checking.
|
|
198
|
-
|
|
199
|
-
Adapted from timescale/pg-aiguide `skills/postgres-database-migration` (Apache-2.0, commit 187be00d317a51210c132010d947f4546ffb5eef, Copyright 2025 Timescale, Inc., d/b/a Tiger Data) and affaan-m/ECC `skills/database-migrations` (MIT, commit 30b68924ae9729159f44326aa3134b1d9d67c0e2, Copyright (c) 2026 Affaan Mustafa). Changed: condensed and reworded; fork-based testing replaced by throwaway-database checks; vendor links removed; PG 18 NOT NULL syntax and timestamp type-change claims dropped as unverified; examples rewritten.
|
|
1
|
+
# PostgreSQL: lock-aware schema changes
|
|
2
|
+
|
|
3
|
+
Read this when a migration targets PostgreSQL and touches a table that may be large or busy in production. Everything here is for writing the migration; run it only against a throwaway database (see the hard rules in SKILL.md).
|
|
4
|
+
|
|
5
|
+
## Why a "fast" change can still cause an outage
|
|
6
|
+
|
|
7
|
+
Every DDL statement takes a lock. `ACCESS EXCLUSIVE` blocks reads and writes. Even a change that takes milliseconds must first wait for every running transaction on the table to finish, and while it waits, every new query on that table queues behind it:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Session 1: SELECT count(*) FROM orders; -- long query, holds a read lock
|
|
11
|
+
Session 2: ALTER TABLE orders ADD COLUMN ...; -- waits for session 1
|
|
12
|
+
Session 3: SELECT * FROM orders WHERE id = 1; -- now blocked behind session 2
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
So: set a short `lock_timeout` on DDL, and prefer the non-blocking forms below.
|
|
16
|
+
|
|
17
|
+
## Lock reference
|
|
18
|
+
|
|
19
|
+
Fast (catalog-only, brief lock):
|
|
20
|
+
|
|
21
|
+
| Operation | Lock | Notes |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| `ADD COLUMN` nullable, no default | ACCESS EXCLUSIVE, brief | No rewrite |
|
|
24
|
+
| `ADD COLUMN ... DEFAULT <constant>` (PG 11+) | ACCESS EXCLUSIVE, brief | Constant default stored in the catalog |
|
|
25
|
+
| `DROP COLUMN` | ACCESS EXCLUSIVE, brief | Data stays on disk until rewritten; code must stop using it first |
|
|
26
|
+
| `SET DEFAULT` / `DROP DEFAULT` | ACCESS EXCLUSIVE, brief | Existing rows untouched |
|
|
27
|
+
| `RENAME COLUMN` / `RENAME TO` | ACCESS EXCLUSIVE, brief | Fast, but breaks running code: use expand and contract |
|
|
28
|
+
| `CREATE INDEX CONCURRENTLY` | SHARE UPDATE EXCLUSIVE | Reads and writes continue; slower; cannot run in a transaction |
|
|
29
|
+
| `DROP INDEX CONCURRENTLY` | SHARE UPDATE EXCLUSIVE | Cannot run in a transaction |
|
|
30
|
+
| `ADD CONSTRAINT ... NOT VALID` | brief | Checks new rows only (FK, CHECK) |
|
|
31
|
+
| `VALIDATE CONSTRAINT` | SHARE UPDATE EXCLUSIVE | Scans rows but allows reads and writes |
|
|
32
|
+
|
|
33
|
+
Slow or blocking:
|
|
34
|
+
|
|
35
|
+
| Operation | Lock | Why |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `ADD COLUMN ... DEFAULT <volatile>` (`now()`, `gen_random_uuid()`) | ACCESS EXCLUSIVE | Rewrites every row |
|
|
38
|
+
| `ALTER COLUMN TYPE` (most changes) | ACCESS EXCLUSIVE | Rewrites the table and its indexes |
|
|
39
|
+
| `SET NOT NULL` without a validated CHECK | ACCESS EXCLUSIVE | Full scan under the lock |
|
|
40
|
+
| `ADD CONSTRAINT` FK / CHECK, validated immediately | SHARE ROW EXCLUSIVE or stronger | Scans all rows, blocks writes |
|
|
41
|
+
| `ADD CONSTRAINT UNIQUE` / `PRIMARY KEY` directly | ACCESS EXCLUSIVE | Builds the index under the lock |
|
|
42
|
+
| `CREATE INDEX` without CONCURRENTLY | SHARE | Blocks writes for the whole build |
|
|
43
|
+
| `VACUUM FULL`, `CLUSTER` | ACCESS EXCLUSIVE | Rewrite the table |
|
|
44
|
+
|
|
45
|
+
Type changes that do not rewrite: `varchar(n)` to a larger `varchar(m)` or to `text`, and `numeric(p,s)` to a larger precision with the same scale. `int` to `bigint` rewrites. If you rely on a change being catalog-only, say so in the report.
|
|
46
|
+
|
|
47
|
+
## Safe patterns
|
|
48
|
+
|
|
49
|
+
Add a column with a computed default: add it nullable, backfill in batches, then set the default and the constraint.
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
ALTER TABLE orders ADD COLUMN placed_at timestamptz; -- instant
|
|
53
|
+
-- backfill in batches (references/backfills.md)
|
|
54
|
+
ALTER TABLE orders ALTER COLUMN placed_at SET DEFAULT now(); -- new rows only
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add NOT NULL to an existing column (PG 12+): a validated CHECK lets `SET NOT NULL` skip the scan.
|
|
58
|
+
|
|
59
|
+
```sql
|
|
60
|
+
ALTER TABLE orders ADD CONSTRAINT orders_status_not_null
|
|
61
|
+
CHECK (status IS NOT NULL) NOT VALID; -- brief
|
|
62
|
+
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_not_null; -- scans, no write block
|
|
63
|
+
ALTER TABLE orders ALTER COLUMN status SET NOT NULL; -- uses the CHECK, no scan
|
|
64
|
+
ALTER TABLE orders DROP CONSTRAINT orders_status_not_null;
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Add a foreign key:
|
|
68
|
+
|
|
69
|
+
```sql
|
|
70
|
+
ALTER TABLE orders ADD CONSTRAINT orders_user_fk
|
|
71
|
+
FOREIGN KEY (user_id) REFERENCES users (id) NOT VALID;
|
|
72
|
+
ALTER TABLE orders VALIDATE CONSTRAINT orders_user_fk;
|
|
73
|
+
CREATE INDEX CONCURRENTLY orders_user_id_idx ON orders (user_id); -- separate migration
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Add an index, and check it afterwards: a failed concurrent build leaves an INVALID index behind that slows writes.
|
|
77
|
+
|
|
78
|
+
```sql
|
|
79
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_created_at_idx ON orders (created_at);
|
|
80
|
+
|
|
81
|
+
SELECT indexrelid::regclass AS index, indisvalid
|
|
82
|
+
FROM pg_index WHERE indrelid = 'orders'::regclass AND NOT indisvalid;
|
|
83
|
+
-- if listed: DROP INDEX CONCURRENTLY orders_created_at_idx; then build again
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Add a unique constraint without holding a long lock:
|
|
87
|
+
|
|
88
|
+
```sql
|
|
89
|
+
CREATE UNIQUE INDEX CONCURRENTLY orders_tracking_uniq_idx ON orders (tracking_number);
|
|
90
|
+
ALTER TABLE orders ADD CONSTRAINT orders_tracking_uniq
|
|
91
|
+
UNIQUE USING INDEX orders_tracking_uniq_idx; -- instant
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Change a column type on a big table: new column, dual writes in code, batched backfill, switch reads, drop the old column later (expand and contract). Do not `ALTER COLUMN TYPE` in place on a large table.
|
|
95
|
+
|
|
96
|
+
Rename a column with no downtime: same pattern. A plain `RENAME COLUMN` is fast but breaks every running instance that still uses the old name.
|
|
97
|
+
|
|
98
|
+
Drop a column: first ship code that no longer reads or writes it (for ORMs, remove it from the model; some ORMs select every mapped column), then drop it in a later migration. `DROP COLUMN` does not erase the data from disk; if the column held sensitive data, mention that a table rewrite is needed to really remove it.
|
|
99
|
+
|
|
100
|
+
Replace a primary key: build a CHECK NOT NULL (NOT VALID, then VALIDATE) and a unique index concurrently on the new key column first, then swap with `ADD CONSTRAINT ... PRIMARY KEY USING INDEX` in a short transaction. Treat this as a high-risk change and spell out the plan in the report.
|
|
101
|
+
|
|
102
|
+
## Timeouts and retries
|
|
103
|
+
|
|
104
|
+
Set a lock timeout on every DDL statement so a blocked migration fails fast instead of freezing the table:
|
|
105
|
+
|
|
106
|
+
```sql
|
|
107
|
+
SET lock_timeout = '5s';
|
|
108
|
+
SET statement_timeout = '30s'; -- not for CONCURRENTLY builds or VALIDATE on big tables
|
|
109
|
+
ALTER TABLE orders ADD COLUMN tracking_number text;
|
|
110
|
+
RESET lock_timeout;
|
|
111
|
+
RESET statement_timeout;
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
High-traffic systems use a shorter lock timeout (50 to 500 ms) with many retries; most apps are fine with a few seconds and a few retries. Many migration tools let you put `SET lock_timeout` at the top of the migration file. If the project's runner retries failed migrations, say so; otherwise describe in the report that the deploy may need a retry when the lock is not acquired.
|
|
115
|
+
|
|
116
|
+
A retry loop inside the database, for runners that cannot retry themselves:
|
|
117
|
+
|
|
118
|
+
```sql
|
|
119
|
+
DO $$
|
|
120
|
+
DECLARE attempt int := 1;
|
|
121
|
+
BEGIN
|
|
122
|
+
LOOP
|
|
123
|
+
BEGIN
|
|
124
|
+
SET LOCAL lock_timeout = '3s';
|
|
125
|
+
ALTER TABLE orders ADD COLUMN tracking_number text;
|
|
126
|
+
EXIT;
|
|
127
|
+
EXCEPTION WHEN lock_not_available THEN
|
|
128
|
+
IF attempt >= 5 THEN RAISE; END IF;
|
|
129
|
+
attempt := attempt + 1;
|
|
130
|
+
PERFORM pg_sleep(2 * attempt);
|
|
131
|
+
END;
|
|
132
|
+
END LOOP;
|
|
133
|
+
END $$;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Under heavy write load, retries done by the application (one transaction per attempt) are kinder to replicas than this in-database loop.
|
|
137
|
+
|
|
138
|
+
## Transactions
|
|
139
|
+
|
|
140
|
+
- One transaction around several fast statements gives all-or-nothing, but every lock is held until COMMIT. Do not put a slow statement in the same transaction as an ACCESS EXCLUSIVE one.
|
|
141
|
+
- `CREATE INDEX CONCURRENTLY`, `DROP INDEX CONCURRENTLY` and `REINDEX CONCURRENTLY` cannot run inside a transaction block. Many migration tools wrap each file in a transaction; see [migration-tools.md](migration-tools.md) for how to opt out, and give such statements their own migration.
|
|
142
|
+
- Keep data backfills out of schema transactions.
|
|
143
|
+
|
|
144
|
+
## Checks before and after
|
|
145
|
+
|
|
146
|
+
Run these on the throwaway database, seeded with realistic rows, to see whether the migration would fail on real data. In the report, suggest the "before" checks for the production deploy if the data could break the migration.
|
|
147
|
+
|
|
148
|
+
```sql
|
|
149
|
+
-- NULLs before SET NOT NULL
|
|
150
|
+
SELECT count(*) FROM orders WHERE status IS NULL;
|
|
151
|
+
|
|
152
|
+
-- duplicates before UNIQUE
|
|
153
|
+
SELECT tracking_number, count(*) FROM orders
|
|
154
|
+
WHERE tracking_number IS NOT NULL
|
|
155
|
+
GROUP BY tracking_number HAVING count(*) > 1 LIMIT 20;
|
|
156
|
+
|
|
157
|
+
-- orphans before a foreign key
|
|
158
|
+
SELECT o.id, o.user_id FROM orders o
|
|
159
|
+
LEFT JOIN users u ON u.id = o.user_id
|
|
160
|
+
WHERE o.user_id IS NOT NULL AND u.id IS NULL LIMIT 20;
|
|
161
|
+
|
|
162
|
+
-- size, to judge lock and rewrite risk
|
|
163
|
+
SELECT pg_size_pretty(pg_total_relation_size('orders')) AS total_size,
|
|
164
|
+
reltuples::bigint AS estimated_rows
|
|
165
|
+
FROM pg_class WHERE relname = 'orders';
|
|
166
|
+
|
|
167
|
+
-- after: columns, constraints, indexes as intended
|
|
168
|
+
SELECT column_name, data_type, is_nullable, column_default
|
|
169
|
+
FROM information_schema.columns WHERE table_name = 'orders';
|
|
170
|
+
SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint
|
|
171
|
+
WHERE conrelid = 'orders'::regclass;
|
|
172
|
+
SELECT indexrelid::regclass, indisvalid, pg_get_indexdef(indexrelid)
|
|
173
|
+
FROM pg_index WHERE indrelid = 'orders'::regclass;
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`EXPLAIN ANALYZE` executes the statement, including UPDATE and DELETE. Use it only on the throwaway database, or inside `BEGIN; ... ROLLBACK;` there.
|
|
177
|
+
|
|
178
|
+
## Rollback
|
|
179
|
+
|
|
180
|
+
| Change | Rollback |
|
|
181
|
+
|---|---|
|
|
182
|
+
| ADD COLUMN, ADD CONSTRAINT, CREATE INDEX | DROP it |
|
|
183
|
+
| RENAME | Rename back |
|
|
184
|
+
| SET DEFAULT | Restore the old default or DROP DEFAULT |
|
|
185
|
+
| DROP COLUMN or TABLE, lossy type change, DELETE, TRUNCATE | Not reversible without a backup: say so in the report |
|
|
186
|
+
|
|
187
|
+
Prefer forward fixes in production: a new migration that undoes the change.
|
|
188
|
+
|
|
189
|
+
## Pitfalls
|
|
190
|
+
|
|
191
|
+
- Testing only on an empty table: a migration that takes 1 ms there can lock a 10M-row table for minutes.
|
|
192
|
+
- Forgetting CONCURRENTLY, or using it inside a transaction.
|
|
193
|
+
- No lock timeout, so one long query freezes the table behind the DDL.
|
|
194
|
+
- One huge UPDATE for a backfill: long locks, table bloat, replica lag.
|
|
195
|
+
- Leaving an INVALID index after a failed concurrent build.
|
|
196
|
+
- Dropping a column before the code that uses it is gone.
|
|
197
|
+
- Assuming a type change is catalog-only without checking.
|
|
198
|
+
|
|
199
|
+
Adapted from timescale/pg-aiguide `skills/postgres-database-migration` (Apache-2.0, commit 187be00d317a51210c132010d947f4546ffb5eef, Copyright 2025 Timescale, Inc., d/b/a Tiger Data) and affaan-m/ECC `skills/database-migrations` (MIT, commit 30b68924ae9729159f44326aa3134b1d9d67c0e2, Copyright (c) 2026 Affaan Mustafa). Changed: condensed and reworded; fork-based testing replaced by throwaway-database checks; vendor links removed; PG 18 NOT NULL syntax and timestamp type-change claims dropped as unverified; examples rewritten.
|