Skip to content

Schema change: renaming a column keeps the old name working until the last step - #11

Merged
swsarancodes merged 2 commits into
mainfrom
feat/schema-change-column-rename
Oct 25, 2026
Merged

swsarancodes merged 2 commits into
mainfrom
feat/schema-change-column-rename

Conversation

@swsarancodes

Copy link
Copy Markdown
Owner

What changed

A column rename used to break every query using the old name the moment it ran. For a column of an existing table it now adds a second column and keeps the two in step, so both names work until applications have switched:

  1. Expand: ADD COLUMN <new> <same type>, then a BEFORE INSERT OR UPDATE trigger that keeps the two in step in both directions (an insert that gives both keeps the new name's value; an update that changes both keeps the old name's; an insert that gives neither gets the old column's default in both), then the data copied in batches, a check that nothing differs, copies of the plain btree indexes built CONCURRENTLY, and NOT NULL on the new column (through the validated CHECK) if the old one is.
  2. Contract (optional, not run by default, destructive): drop the trigger and its function, give the new column the old default (not earlier: two columns with a volatile default would each draw their own value), drop the old column. The indexes on the old column go with it; the copies stay.

Refused, with the way out (compatColumns, default on, turns the whole behaviour off and renames directly, where constraints and views follow the column): a column in a key, a unique constraint, a foreign key or a check; one a view, policy or trigger depends on; a generated or identity column; a partitioned table; and a rename combined with other changes in the same operation. Partial, expression and non-btree indexes are not copied (a warning says so). A table the plan itself creates is renamed directly. The notes say what is not copied (collation, storage settings, statistics targets, comments, column-level grants).

How it was checked

  • 5 new unit tests; the repo suite passes (1,765 tests) and both type-checks are clean.
  • A scenario in scripts/schema-change-pg.ts runs the rename on a table of a few hundred thousand rows (lock levels of every step against pg_locks and the predicted end state against the catalog, like every scenario), and a new columnrename section exercises the trigger on a real server: existing rows copied, the index copied and valid, the new column NOT NULL, inserts and updates through either name reaching the other, the old default applied to both, an insert that gives both, and then the last step removing the old column, the trigger and its function and leaving the new column with the old default. npm run test:schema-pg:versions (16 sections) passes on PostgreSQL 12, 13, 14, 15, 16, 17 and 18.

Reviewer notes

  • This is the last item left on the schema change list; the README's operation list, section count and test commands are updated.

… last step

For an existing table, an alter_column that renames a column adds a column with the new name and the same type, keeps the two in step in both directions with a BEFORE INSERT OR UPDATE trigger (an insert that gives both keeps the new name's value, an update that changes both keeps the old name's), copies the data in batches, checks the copy, builds copies of the plain btree indexes concurrently and makes the new column NOT NULL if the old one is. Dropping the old column and the trigger, and giving the new column the old default, is a last step that is not run by default. A column in a key, a foreign key or a check, one a view uses, a generated or identity column, a partitioned table, and a rename combined with other changes are refused, with the way out: compatColumns (default on) turns it off and the column is renamed directly.
A scenario runs the rename on a table of a few hundred thousand rows (lock levels and the predicted end state are checked like every scenario), and a section exercises the trigger: existing rows copied, the index copied and valid, the new column NOT NULL, inserts and updates through either name reaching the other, the old default applied to both, an insert that gives both, then the last step removing the old column, the trigger and its function and leaving the new column with the old default.
@swsarancodes
swsarancodes merged commit 629ab15 into main Oct 25, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant