Skip to content

GENERATED ALWAYS AS expression changes are ignored by plan #591

Description

@Ketrebu

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:

CREATE TABLE IF NOT EXISTS t (
    a INT NOT 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/ir.goColumn.IsGenerated, Column.GeneratedExpr, Column.GeneratedKind
  • 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 changeALTER TABLE t ALTER COLUMN c SET EXPRESSION AS (expr), Postgres 17+.
    Rewrites the table and recomputes stored values (verified on 18).
  • generated to plainALTER 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:

  1. Include IsGenerated, GeneratedExpr and GeneratedKind in columnsEqual.
  2. Emit SET EXPRESSION AS on 17+ and DROP EXPRESSION for generated-to-plain.
  3. Where no ALTER exists (14–16, or plain-to-generated), fail the plan with a clear message
    rather 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 LAST ordering is dropped by dump and ignored by plan #561, where the problem was
    that 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions