diff --git a/internal/diff/table.go b/internal/diff/table.go index 39e6c2be..c5c2a927 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -590,10 +590,9 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector stmt += fmt.Sprintf(" DEFAULT %s", *column.DefaultValue) } - // Don't add NOT NULL for identity columns or SERIAL columns as they are implicitly NOT NULL + // Don't add NOT NULL for identity columns, SERIAL columns, or generated columns as they are implicitly NOT NULL // Also skip NOT NULL if we're adding PRIMARY KEY inline (PRIMARY KEY implies NOT NULL) - // For generated columns, include NOT NULL if explicitly specified (but before GENERATED clause) - if !column.IsNullable && column.Identity == nil && !isSerialColumn(column) && pkConstraint == nil { + if !column.IsNullable && column.Identity == nil && !isSerialColumn(column) && pkConstraint == nil && !column.IsGenerated { stmt += " NOT NULL" } @@ -607,14 +606,19 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector } } + // Add PRIMARY KEY inline before generated column syntax (PostgreSQL requires this order) + if pkConstraint != nil && column.IsGenerated { + stmt += " PRIMARY KEY" + } + // Add generated column syntax if column.IsGenerated && column.GeneratedExpr != nil { // TODO: Add support for GENERATED ALWAYS AS (...) VIRTUAL when PostgreSQL 18 is supported stmt += fmt.Sprintf(" GENERATED ALWAYS AS (%s) STORED", *column.GeneratedExpr) } - // Add PRIMARY KEY inline if present - if pkConstraint != nil { + // Add PRIMARY KEY inline if present (for non-generated columns) + if pkConstraint != nil && !column.IsGenerated { stmt += " PRIMARY KEY" } diff --git a/ir/parser.go b/ir/parser.go index fd705a8c..de286fae 100644 --- a/ir/parser.go +++ b/ir/parser.go @@ -763,8 +763,6 @@ func (p *Parser) parseColumnDef(colDef *pg_query.ColumnDef, position int, schema if generatedExpr != "" { column.GeneratedExpr = &generatedExpr column.IsGenerated = true - // Generated columns are implicitly NOT NULL - column.IsNullable = false } } } diff --git a/testdata/diff/create_table/add_column_generated/diff.sql b/testdata/diff/create_table/add_column_generated/diff.sql index 1bf85d91..022ac15e 100644 --- a/testdata/diff/create_table/add_column_generated/diff.sql +++ b/testdata/diff/create_table/add_column_generated/diff.sql @@ -1 +1,3 @@ -ALTER TABLE merge_request ADD COLUMN iid integer NOT NULL GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; \ No newline at end of file +ALTER TABLE merge_request +ADD COLUMN iid integer PRIMARY KEY GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; +ALTER TABLE merge_request ADD COLUMN title text GENERATED ALWAYS AS (data ->> 'title') STORED; \ No newline at end of file diff --git a/testdata/diff/create_table/add_column_generated/new.sql b/testdata/diff/create_table/add_column_generated/new.sql index 1e9e885f..cd2ffd56 100644 --- a/testdata/diff/create_table/add_column_generated/new.sql +++ b/testdata/diff/create_table/add_column_generated/new.sql @@ -1,4 +1,5 @@ CREATE TABLE public.merge_request ( data jsonb NOT NULL, - iid integer NOT NULL GENERATED ALWAYS AS ((data ->> 'iid')::integer) STORED + iid integer PRIMARY KEY GENERATED ALWAYS AS ((data ->> 'iid')::integer) STORED, + title text GENERATED ALWAYS AS (data ->> 'title') STORED ); \ No newline at end of file diff --git a/testdata/diff/create_table/add_column_generated/plan.json b/testdata/diff/create_table/add_column_generated/plan.json index 17586e0c..649a5c07 100644 --- a/testdata/diff/create_table/add_column_generated/plan.json +++ b/testdata/diff/create_table/add_column_generated/plan.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "pgschema_version": "1.1.1", + "pgschema_version": "1.2.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { "hash": "67e51d7bd6b2b020d7a95dbe7f8e5f9bd9ec2e3f4068b9ed09bb63b79e656354" @@ -9,10 +9,16 @@ { "steps": [ { - "sql": "ALTER TABLE merge_request ADD COLUMN iid integer NOT NULL GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED;", + "sql": "ALTER TABLE merge_request\nADD COLUMN iid integer PRIMARY KEY GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED;", "type": "table.column", "operation": "create", "path": "public.merge_request.iid" + }, + { + "sql": "ALTER TABLE merge_request ADD COLUMN title text GENERATED ALWAYS AS (data ->> 'title') STORED;", + "type": "table.column", + "operation": "create", + "path": "public.merge_request.title" } ] } diff --git a/testdata/diff/create_table/add_column_generated/plan.sql b/testdata/diff/create_table/add_column_generated/plan.sql index 53e7aa12..902621be 100644 --- a/testdata/diff/create_table/add_column_generated/plan.sql +++ b/testdata/diff/create_table/add_column_generated/plan.sql @@ -1 +1,4 @@ -ALTER TABLE merge_request ADD COLUMN iid integer NOT NULL GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; +ALTER TABLE merge_request +ADD COLUMN iid integer PRIMARY KEY GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; + +ALTER TABLE merge_request ADD COLUMN title text GENERATED ALWAYS AS (data ->> 'title') STORED; diff --git a/testdata/diff/create_table/add_column_generated/plan.txt b/testdata/diff/create_table/add_column_generated/plan.txt index a0705884..0003ad5a 100644 --- a/testdata/diff/create_table/add_column_generated/plan.txt +++ b/testdata/diff/create_table/add_column_generated/plan.txt @@ -6,8 +6,12 @@ Summary by type: Tables: ~ merge_request + iid (column) + + title (column) DDL to be executed: -------------------------------------------------- -ALTER TABLE merge_request ADD COLUMN iid integer NOT NULL GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; +ALTER TABLE merge_request +ADD COLUMN iid integer PRIMARY KEY GENERATED ALWAYS AS (CAST(data ->> 'iid' AS int)) STORED; + +ALTER TABLE merge_request ADD COLUMN title text GENERATED ALWAYS AS (data ->> 'title') STORED;