You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
plan emits GENERATED ALWAYS AS (...) STORED correctly when it creates a column, but the generation clause is not part of the column comparison. Changing the expression of an existing generated column, or removing the generation entirely, produces an empty plan. New databases get the new expression and every existing database silently keeps the old one.
Tested on pgschema 1.13.0 against Postgres 18.
Reproduction
Desired state:
CREATETABLEIF NOT EXISTS t (
a INTNOT NULL,
b INT GENERATED ALWAYS AS (a *2) STORED
);
Plan against each of these targets:
Target schema
Expected
Actual
b INT GENERATED ALWAYS AS (a * 2) STORED
no changes
no changes ✅
b INT GENERATED ALWAYS AS (a * 3) STORED
alter the expression
no changes detected ❌
b INT (not generated)
make it generated
no changes detected ❌
column absent
add it
ALTER TABLE t ADD COLUMN b integer GENERATED ALWAYS AS ((a * 2)) STORED; ✅
The last row shows the clause round-trips into emitted DDL, so this is only about comparison.
Where it comes from
The IR already carries the information and the inspector populates it:
ir/queries/queries.sql — selects attgenerated and pg_get_expr(...) for generated columns
ir/inspector.go — sets all three
internal/diff/table.go — emits the clause in CREATE TABLE
columnsEqual in internal/diff/column.go compares name, type, nullability, default, max length,
identity and comment, and returns true without consulting any of the three generated fields. So
the columns compare equal and no ALTER is generated.
What a fix runs into
Postgres support for altering a generated column is uneven, which may be why this was left out:
expression change — ALTER TABLE t ALTER COLUMN c SET EXPRESSION AS (expr), Postgres 17+.
Rewrites the table and recomputes stored values (verified on 18).
generated to plain — ALTER TABLE t ALTER COLUMN c DROP EXPRESSION, Postgres 13+.
plain to generated — no ALTER exists; it needs DROP COLUMN + ADD COLUMN, which is
destructive.
Since pgschema supports 14–18, the expression case cannot be expressed on 14–16.
Suggested behaviour
Even without emitting DDL on every version, the difference should stop being invisible:
Include IsGenerated, GeneratedExpr and GeneratedKind in columnsEqual.
Emit SET EXPRESSION AS on 17+ and DROP EXPRESSION for generated-to-plain.
planemitsGENERATED ALWAYS AS (...) STOREDcorrectly when it creates a column, but the generation clause is not part of the column comparison. Changing the expression of an existing generated column, or removing the generation entirely, produces an empty plan. New databases get the new expression and every existing database silently keeps the old one.Tested on pgschema 1.13.0 against Postgres 18.
Reproduction
Desired state:
Plan against each of these targets:
b INT GENERATED ALWAYS AS (a * 2) STOREDb INT GENERATED ALWAYS AS (a * 3) STOREDb INT(not generated)ALTER TABLE t ADD COLUMN b integer GENERATED ALWAYS AS ((a * 2)) STORED;✅The last row shows the clause round-trips into emitted DDL, so this is only about comparison.
Where it comes from
The IR already carries the information and the inspector populates it:
ir/ir.go—Column.IsGenerated,Column.GeneratedExpr,Column.GeneratedKindir/queries/queries.sql— selectsattgeneratedandpg_get_expr(...)for generated columnsir/inspector.go— sets all threeinternal/diff/table.go— emits the clause inCREATE TABLEcolumnsEqualininternal/diff/column.gocompares name, type, nullability, default, max length,identity and comment, and returns
truewithout consulting any of the three generated fields. Sothe columns compare equal and no
ALTERis generated.What a fix runs into
Postgres support for altering a generated column is uneven, which may be why this was left out:
ALTER TABLE t ALTER COLUMN c SET EXPRESSION AS (expr), Postgres 17+.Rewrites the table and recomputes stored values (verified on 18).
ALTER TABLE t ALTER COLUMN c DROP EXPRESSION, Postgres 13+.ALTERexists; it needsDROP COLUMN+ADD COLUMN, which isdestructive.
Since pgschema supports 14–18, the expression case cannot be expressed on 14–16.
Suggested behaviour
Even without emitting DDL on every version, the difference should stop being invisible:
IsGenerated,GeneratedExprandGeneratedKindincolumnsEqual.SET EXPRESSION ASon 17+ andDROP EXPRESSIONfor generated-to-plain.ALTERexists (14–16, or plain-to-generated), fail the plan with a clear messagerather than reporting no changes — the same reasoning as ON DELETE SET NULL (column) column list is dropped, and the difference is invisible to plan #589 and Index
NULLS FIRST/NULLS LASTordering is dropped bydumpand ignored byplan#561, where the problem wasthat the difference was invisible rather than merely unhandled.
Silently reporting "no changes" is the harmful part: a schema edit passes review, passes the deploy, and leaves production computing the old value.