ban-add-generated-stored-column
Adding a stored generated column causes a full table rewrite under ACCESS EXCLUSIVE lock.
- operation
- Columns
- lock taken
- ACCESS EXCLUSIVE
- remediation
- Manual rewrite
- category
- Types & schema style
- Blocks reads
- Blocks writes
- Rewrites the table
What triggers it
The AT_AddColumn subtype of ALTER TABLE where the new column's constraint list contains a CONSTR_GENERATED constraint — i.e. ADD COLUMN ... GENERATED ALWAYS AS (...) STORED.
What does not
Any AlterTableCmd subtype other than AT_AddColumn (ALTER COLUMN, DROP COLUMN, ADD CONSTRAINT, and so on) is skipped before the generated-column check ever runs, and a plain ADD COLUMN whose constraint list has no CONSTR_GENERATED entry never matches.
Where it applies
Applies to every PostgreSQL version MigrationPilot targets. It works on the SQL text alone — no database connection needed.
The lock, and what it blocks
ACCESS EXCLUSIVE for the whole rewrite: every existing row is read and rewritten to compute and store the generated expression, and the lock blocks all reads and writes on the table for that duration.
Why it matters
ALTER TABLE ADD COLUMN with GENERATED ALWAYS AS ... STORED rewrites every row to compute and store the expression. On tables with millions of rows, this holds an ACCESS EXCLUSIVE lock for the entire rewrite — blocking all reads and writes.
Unsafe, and safe
Flagged
ALTER TABLE users ADD COLUMN full_name TEXT GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;
Safe alternative
-- Use a regular column + trigger instead ALTER TABLE users ADD COLUMN full_name TEXT; CREATE FUNCTION update_full_name() RETURNS trigger AS $$ BEGIN NEW.full_name := NEW.first_name || ' ' || NEW.last_name; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_full_name BEFORE INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION update_full_name();
What it assumes
The rule fires identically regardless of table size, so a small or empty table is flagged the same as one with millions of rows, even though the rewrite cost — and the real risk — scales with row count.
What the CLI prints
✗ [MP062] CRITICAL (line 1) Adding stored generated column "full_name" to "users" causes a full table rewrite. Use a regular column with a trigger, or a virtual generated column (PG 17+). Why: ALTER TABLE ADD COLUMN with GENERATED ALWAYS AS ... STORED rewrites every row to compute and store the expression. On tables with millions of rows, this holds an ACCESS EXCLUSIVE lock for the entire rewrite, blocking all reads and writes. Docs: https://migrationpilot.dev/rules/mp062
Generated by running the CLI's own formatter over the flagged example above, so it is the text the tool actually produces. A real run also reports the other rules that fire on the same statement; those blocks are left out here.
Turning it off
For one statement, put a comment on the line before it:
-- migrationpilot-disable MP062 ALTER TABLE users
For the whole project, in .migrationpilotrc.yml — by name or by id:
rules:
MP062: false
# or keep it, and downgrade it
rules:
MP062:
severity: warningTry it
Open this rule's flagged example in the playground. It runs in your browser — edit it and watch the finding appear and disappear.
Run MP062 in the playground