MP062critical

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

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

migrationpilot analyze migration.sql
✗ [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:

.migrationpilotrc.yml
rules:
  MP062: false

# or keep it, and downgrade it
rules:
  MP062:
    severity: warning

Try 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