From be90f9f85764b8123dcf30b7b48b7f4011400d02 Mon Sep 17 00:00:00 2001 From: Curt Hasselschwert Date: Wed, 28 Jan 2026 13:35:51 -0500 Subject: [PATCH] fix: composite FK referenced column order when FK order differs from table column order (#266) When dumping composite foreign key constraints where the FK column order differs from the table's column definition order, pgschema was outputting referenced columns in the wrong order, causing type mismatch errors when applying the generated DDL. The bug was in buildConstraints: ForeignOrdinalPosition (fa.attnum - the column's position in the foreign table was used for the referenced column's Position instead of using the local column's constraint position. Since local and referenced columns are paired in the FK definition, they should share the same position value. Fixes the issue by using the local column's constraint position for the paired referenced column. EOF ) --- ir/inspector.go | 14 +++++----- .../composite_fk_column_order/diff.sql | 2 ++ .../composite_fk_column_order/new.sql | 22 ++++++++++++++++ .../composite_fk_column_order/old.sql | 19 ++++++++++++++ .../composite_fk_column_order/plan.json | 26 +++++++++++++++++++ .../composite_fk_column_order/plan.sql | 4 +++ .../composite_fk_column_order/plan.txt | 16 ++++++++++++ 7 files changed, 96 insertions(+), 7 deletions(-) create mode 100644 testdata/diff/create_table/composite_fk_column_order/diff.sql create mode 100644 testdata/diff/create_table/composite_fk_column_order/new.sql create mode 100644 testdata/diff/create_table/composite_fk_column_order/old.sql create mode 100644 testdata/diff/create_table/composite_fk_column_order/plan.json create mode 100644 testdata/diff/create_table/composite_fk_column_order/plan.sql create mode 100644 testdata/diff/create_table/composite_fk_column_order/plan.txt diff --git a/ir/inspector.go b/ir/inspector.go index 4d39426a..1cdfbc0b 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -550,15 +550,15 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche // Add referenced column only if it doesn't exist if !refColumnExists { - // Get the foreign ordinal position for proper ordering - refPosition := position // Default fallback to source position - if constraint.ForeignOrdinalPosition.Valid { - refPosition = int(constraint.ForeignOrdinalPosition.Int32) - } - + // Use the local column's constraint position for the referenced column. + // The local and referenced columns are paired together in the FK definition, + // so they must have the same position to maintain correct ordering. + // Note: ForeignOrdinalPosition from the query is fa.attnum (column position + // in the foreign table's definition), which is wrong - we need the constraint + // array position, which is the same as the local column's position. refConstraintCol := &ConstraintColumn{ Name: refColumnName, - Position: refPosition, // Use foreign ordinal position for referenced column + Position: position, // Use local column's constraint position } c.ReferencedColumns = append(c.ReferencedColumns, refConstraintCol) } diff --git a/testdata/diff/create_table/composite_fk_column_order/diff.sql b/testdata/diff/create_table/composite_fk_column_order/diff.sql new file mode 100644 index 00000000..98f68775 --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/diff.sql @@ -0,0 +1,2 @@ +ALTER TABLE order_items +ADD CONSTRAINT fk_order_items_order FOREIGN KEY (customer_id, order_id) REFERENCES orders (customer_id, order_id); diff --git a/testdata/diff/create_table/composite_fk_column_order/new.sql b/testdata/diff/create_table/composite_fk_column_order/new.sql new file mode 100644 index 00000000..bbee912c --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/new.sql @@ -0,0 +1,22 @@ +-- Test case: Composite FK where FK column order differs from table column order +-- This tests that constraint column ordering is preserved independently of table column order + +-- Referenced table with composite primary key +CREATE TABLE public.orders ( + order_id integer NOT NULL, + customer_id integer NOT NULL, + name varchar(255) NOT NULL, + CONSTRAINT orders_pkey PRIMARY KEY (customer_id, order_id) +); + +-- Referencing table where columns are defined in OPPOSITE order from how FK references them +CREATE TABLE public.order_items ( + id serial PRIMARY KEY, + -- Table defines columns in this order: order_id first (lower attnum), then customer_id (higher attnum) + order_id integer NOT NULL, + customer_id integer NOT NULL, + quantity integer NOT NULL, + -- FK references columns in DIFFERENT order than table definition + -- customer_id is listed first in FK, but has higher attnum in table + CONSTRAINT fk_order_items_order FOREIGN KEY (customer_id, order_id) REFERENCES public.orders(customer_id, order_id) +); diff --git a/testdata/diff/create_table/composite_fk_column_order/old.sql b/testdata/diff/create_table/composite_fk_column_order/old.sql new file mode 100644 index 00000000..5afc72b6 --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/old.sql @@ -0,0 +1,19 @@ +-- Test case: Composite FK where FK column order differs from table column order +-- This tests that constraint column ordering is preserved independently of table column order + +-- Referenced table with composite primary key +CREATE TABLE public.orders ( + order_id integer NOT NULL, + customer_id integer NOT NULL, + name varchar(255) NOT NULL, + CONSTRAINT orders_pkey PRIMARY KEY (customer_id, order_id) +); + +-- Referencing table where columns are defined in OPPOSITE order from how FK references them +CREATE TABLE public.order_items ( + id serial PRIMARY KEY, + -- Table defines columns in this order: order_id first (lower attnum), then customer_id (higher attnum) + order_id integer NOT NULL, + customer_id integer NOT NULL, + quantity integer NOT NULL +); diff --git a/testdata/diff/create_table/composite_fk_column_order/plan.json b/testdata/diff/create_table/composite_fk_column_order/plan.json new file mode 100644 index 00000000..4b4004bc --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/plan.json @@ -0,0 +1,26 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.6.1", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "1948e0ca8413d527d01353e9a8b4f4581198c99a5b994959bda5e05beb462153" + }, + "groups": [ + { + "steps": [ + { + "sql": "ALTER TABLE order_items\nADD CONSTRAINT fk_order_items_order FOREIGN KEY (customer_id, order_id) REFERENCES orders (customer_id, order_id) NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.order_items.fk_order_items_order" + }, + { + "sql": "ALTER TABLE order_items VALIDATE CONSTRAINT fk_order_items_order;", + "type": "table.constraint", + "operation": "create", + "path": "public.order_items.fk_order_items_order" + } + ] + } + ] +} diff --git a/testdata/diff/create_table/composite_fk_column_order/plan.sql b/testdata/diff/create_table/composite_fk_column_order/plan.sql new file mode 100644 index 00000000..13a6be43 --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/plan.sql @@ -0,0 +1,4 @@ +ALTER TABLE order_items +ADD CONSTRAINT fk_order_items_order FOREIGN KEY (customer_id, order_id) REFERENCES orders (customer_id, order_id) NOT VALID; + +ALTER TABLE order_items VALIDATE CONSTRAINT fk_order_items_order; diff --git a/testdata/diff/create_table/composite_fk_column_order/plan.txt b/testdata/diff/create_table/composite_fk_column_order/plan.txt new file mode 100644 index 00000000..54da9e0b --- /dev/null +++ b/testdata/diff/create_table/composite_fk_column_order/plan.txt @@ -0,0 +1,16 @@ +Plan: 1 to modify. + +Summary by type: + tables: 1 to modify + +Tables: + ~ order_items + + fk_order_items_order (constraint) + +DDL to be executed: +-------------------------------------------------- + +ALTER TABLE order_items +ADD CONSTRAINT fk_order_items_order FOREIGN KEY (customer_id, order_id) REFERENCES orders (customer_id, order_id) NOT VALID; + +ALTER TABLE order_items VALIDATE CONSTRAINT fk_order_items_order;