diff --git a/internal/diff/constraint.go b/internal/diff/constraint.go index 2897e409..df1d06a1 100644 --- a/internal/diff/constraint.go +++ b/internal/diff/constraint.go @@ -2,6 +2,7 @@ package diff import ( "fmt" + "slices" "strings" "github.com/pgplex/pgschema/ir" @@ -59,9 +60,7 @@ func generateConstraintSQL(constraint *ir.Constraint, targetSchema string, quali if constraint.UpdateRule != "" && constraint.UpdateRule != "NO ACTION" { stmt += fmt.Sprintf(" ON UPDATE %s", constraint.UpdateRule) } - if constraint.DeleteRule != "" && constraint.DeleteRule != "NO ACTION" { - stmt += fmt.Sprintf(" ON DELETE %s", constraint.DeleteRule) - } + stmt += onDeleteClause(constraint) // Add deferrable clause stmt += deferrableClause(constraint) // Add NOT VALID if needed @@ -182,6 +181,11 @@ func constraintsEqual(old, new *ir.Constraint) bool { if old.DeleteRule != new.DeleteRule { return false } + // The SET NULL/SET DEFAULT column list is a set: PostgreSQL stores it in the + // order written, so compare order-independently to avoid a needless recreate. + if !slices.Equal(slices.Sorted(slices.Values(old.DeleteSetColumns)), slices.Sorted(slices.Values(new.DeleteSetColumns))) { + return false + } if old.UpdateRule != new.UpdateRule { return false } @@ -237,3 +241,21 @@ func constraintsEqual(old, new *ir.Constraint) bool { return true } + +// onDeleteClause renders the ON DELETE action of a foreign key, including the +// optional column list of SET NULL / SET DEFAULT (PG15+, issue #589). Returns +// "" for the default NO ACTION. +func onDeleteClause(constraint *ir.Constraint) string { + if constraint.DeleteRule == "" || constraint.DeleteRule == "NO ACTION" { + return "" + } + clause := fmt.Sprintf(" ON DELETE %s", constraint.DeleteRule) + if len(constraint.DeleteSetColumns) > 0 { + cols := make([]string, len(constraint.DeleteSetColumns)) + for i, col := range constraint.DeleteSetColumns { + cols[i] = ir.QuoteIdentifier(col) + } + clause += fmt.Sprintf(" (%s)", strings.Join(cols, ", ")) + } + return clause +} diff --git a/internal/diff/table.go b/internal/diff/table.go index b1a8686f..95562051 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -2036,9 +2036,7 @@ func generateForeignKeyClauseMode(constraint *ir.Constraint, targetSchema string if constraint.UpdateRule != "" && constraint.UpdateRule != "NO ACTION" { clause += fmt.Sprintf(" ON UPDATE %s", constraint.UpdateRule) } - if constraint.DeleteRule != "" && constraint.DeleteRule != "NO ACTION" { - clause += fmt.Sprintf(" ON DELETE %s", constraint.DeleteRule) - } + clause += onDeleteClause(constraint) // Add deferrable clause if constraint.Deferrable { diff --git a/internal/plan/rewrite.go b/internal/plan/rewrite.go index 3126b1ae..8bb90dda 100644 --- a/internal/plan/rewrite.go +++ b/internal/plan/rewrite.go @@ -308,6 +308,14 @@ func generateForeignKeyRewrite(constraint *ir.Constraint) []RewriteStep { } if constraint.DeleteRule != "" && constraint.DeleteRule != "NO ACTION" { fkClause += fmt.Sprintf(" ON DELETE %s", constraint.DeleteRule) + // SET NULL / SET DEFAULT column list (PG15+, issue #589) + if len(constraint.DeleteSetColumns) > 0 { + var setCols []string + for _, col := range constraint.DeleteSetColumns { + setCols = append(setCols, ir.QuoteIdentifier(col)) + } + fkClause += fmt.Sprintf(" (%s)", joinStrings(setCols, ", ")) + } } // Add DEFERRABLE clauses if specified diff --git a/ir/inspector.go b/ir/inspector.go index 8d487790..d441b644 100644 --- a/ir/inspector.go +++ b/ir/inspector.go @@ -3,6 +3,7 @@ package ir import ( "context" "database/sql" + "encoding/json" "fmt" "sort" "strconv" @@ -557,6 +558,15 @@ func (i *Inspector) buildConstraints(ctx context.Context, schema *IR, targetSche if deleteRule := i.safeInterfaceToString(constraint.DeleteRule); deleteRule != "" && deleteRule != "" { c.DeleteRule = deleteRule } + // ON DELETE SET NULL/SET DEFAULT (column list) arrives as a JSON array of + // column names (empty string when absent or on PG14). Issue #589. + if setCols := constraint.DeleteSetColumns.String; setCols != "" { + var cols []string + if err := json.Unmarshal([]byte(setCols), &cols); err != nil { + return fmt.Errorf("failed to parse delete set columns for constraint %s.%s.%s: %w", schemaName, tableName, constraintName, err) + } + c.DeleteSetColumns = cols + } if updateRule := i.safeInterfaceToString(constraint.UpdateRule); updateRule != "" && updateRule != "" { c.UpdateRule = updateRule } diff --git a/ir/ir.go b/ir/ir.go index 7140dba3..495d24a1 100644 --- a/ir/ir.go +++ b/ir/ir.go @@ -244,6 +244,7 @@ type Constraint struct { CheckClause string `json:"check_clause,omitempty"` ExclusionDefinition string `json:"exclusion_definition,omitempty"` // Full EXCLUDE definition from pg_get_constraintdef() DeleteRule string `json:"delete_rule,omitempty"` + DeleteSetColumns []string `json:"delete_set_columns,omitempty"` // PG15+: column list of ON DELETE SET NULL/SET DEFAULT (confdelsetcols) UpdateRule string `json:"update_rule,omitempty"` Deferrable bool `json:"deferrable,omitempty"` InitiallyDeferred bool `json:"initially_deferred,omitempty"` diff --git a/ir/queries/queries.sql b/ir/queries/queries.sql index 78e2639b..a6127822 100644 --- a/ir/queries/queries.sql +++ b/ir/queries/queries.sql @@ -337,7 +337,11 @@ SELECT c.condeferred AS initially_deferred, c.convalidated AS is_valid, COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period, - c.connoinherit AS no_inherit + c.connoinherit AS no_inherit, + -- ON DELETE SET NULL/SET DEFAULT (column list), PG15+ (pg_constraint.confdelsetcols). + -- Rendered as a JSON array of column names so it survives the per-column row fan-out + -- and stays a single scalar on PG14 (where the attribute does not exist). Issue #589. + COALESCE(ds.delete_set_columns, '') AS delete_set_columns FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid JOIN pg_namespace n ON cl.relnamespace = n.oid @@ -345,6 +349,15 @@ LEFT JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey) LEFT JOIN pg_class fcl ON c.confrelid = fcl.oid LEFT JOIN pg_namespace fn ON fcl.relnamespace = fn.oid LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid AND fa.attnum = c.confkey[array_position(c.conkey, a.attnum)] +LEFT JOIN LATERAL ( + SELECT jsonb_agg(da.attname ORDER BY e.ord)::text AS delete_set_columns + FROM jsonb_array_elements_text( + CASE WHEN jsonb_typeof(to_jsonb(c) -> 'confdelsetcols') = 'array' + THEN to_jsonb(c) -> 'confdelsetcols' + ELSE '[]'::jsonb END + ) WITH ORDINALITY AS e(attnum, ord) + JOIN pg_attribute da ON da.attrelid = c.conrelid AND da.attnum = e.attnum::int +) ds ON true LEFT JOIN LATERAL ( SELECT -- Render with search_path set to the table's own schema so same-schema @@ -1054,7 +1067,11 @@ SELECT c.connoinherit AS no_inherit, -- pg_index.indnullsnotdistinct is PG15+. Use to_jsonb so the column reference -- doesn't fail to plan on PG14 (where the attribute does not exist on pg_index). - COALESCE((to_jsonb(i) ->> 'indnullsnotdistinct')::boolean, false) AS nulls_not_distinct + COALESCE((to_jsonb(i) ->> 'indnullsnotdistinct')::boolean, false) AS nulls_not_distinct, + -- ON DELETE SET NULL/SET DEFAULT (column list), PG15+ (pg_constraint.confdelsetcols). + -- Rendered as a JSON array of column names so it survives the per-column row fan-out + -- and stays a single scalar on PG14 (where the attribute does not exist). Issue #589. + COALESCE(ds.delete_set_columns, '') AS delete_set_columns FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid JOIN pg_namespace n ON cl.relnamespace = n.oid @@ -1063,6 +1080,15 @@ LEFT JOIN pg_class fcl ON c.confrelid = fcl.oid LEFT JOIN pg_namespace fn ON fcl.relnamespace = fn.oid LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid AND fa.attnum = c.confkey[array_position(c.conkey, a.attnum)] LEFT JOIN pg_index i ON i.indexrelid = c.conindid +LEFT JOIN LATERAL ( + SELECT jsonb_agg(da.attname ORDER BY e.ord)::text AS delete_set_columns + FROM jsonb_array_elements_text( + CASE WHEN jsonb_typeof(to_jsonb(c) -> 'confdelsetcols') = 'array' + THEN to_jsonb(c) -> 'confdelsetcols' + ELSE '[]'::jsonb END + ) WITH ORDINALITY AS e(attnum, ord) + JOIN pg_attribute da ON da.attrelid = c.conrelid AND da.attnum = e.attnum::int +) ds ON true LEFT JOIN LATERAL ( SELECT -- Render with search_path set to the table's own schema so same-schema diff --git a/ir/queries/queries.sql.go b/ir/queries/queries.sql.go index e37427f8..4b945720 100644 --- a/ir/queries/queries.sql.go +++ b/ir/queries/queries.sql.go @@ -942,7 +942,11 @@ SELECT c.condeferred AS initially_deferred, c.convalidated AS is_valid, COALESCE((to_jsonb(c) ->> 'conperiod')::boolean, false) AS is_period, - c.connoinherit AS no_inherit + c.connoinherit AS no_inherit, + -- ON DELETE SET NULL/SET DEFAULT (column list), PG15+ (pg_constraint.confdelsetcols). + -- Rendered as a JSON array of column names so it survives the per-column row fan-out + -- and stays a single scalar on PG14 (where the attribute does not exist). Issue #589. + COALESCE(ds.delete_set_columns, '') AS delete_set_columns FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid JOIN pg_namespace n ON cl.relnamespace = n.oid @@ -950,6 +954,15 @@ LEFT JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = ANY(c.conkey) LEFT JOIN pg_class fcl ON c.confrelid = fcl.oid LEFT JOIN pg_namespace fn ON fcl.relnamespace = fn.oid LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid AND fa.attnum = c.confkey[array_position(c.conkey, a.attnum)] +LEFT JOIN LATERAL ( + SELECT jsonb_agg(da.attname ORDER BY e.ord)::text AS delete_set_columns + FROM jsonb_array_elements_text( + CASE WHEN jsonb_typeof(to_jsonb(c) -> 'confdelsetcols') = 'array' + THEN to_jsonb(c) -> 'confdelsetcols' + ELSE '[]'::jsonb END + ) WITH ORDINALITY AS e(attnum, ord) + JOIN pg_attribute da ON da.attrelid = c.conrelid AND da.attnum = e.attnum::int +) ds ON true LEFT JOIN LATERAL ( SELECT -- Render with search_path set to the table's own schema so same-schema @@ -991,6 +1004,7 @@ type GetConstraintsRow struct { IsValid bool `db:"is_valid" json:"is_valid"` IsPeriod sql.NullBool `db:"is_period" json:"is_period"` NoInherit bool `db:"no_inherit" json:"no_inherit"` + DeleteSetColumns sql.NullString `db:"delete_set_columns" json:"delete_set_columns"` } // GetConstraints retrieves all table constraints @@ -1023,6 +1037,7 @@ func (q *Queries) GetConstraints(ctx context.Context) ([]GetConstraintsRow, erro &i.IsValid, &i.IsPeriod, &i.NoInherit, + &i.DeleteSetColumns, ); err != nil { return nil, err } @@ -1081,7 +1096,11 @@ SELECT c.connoinherit AS no_inherit, -- pg_index.indnullsnotdistinct is PG15+. Use to_jsonb so the column reference -- doesn't fail to plan on PG14 (where the attribute does not exist on pg_index). - COALESCE((to_jsonb(i) ->> 'indnullsnotdistinct')::boolean, false) AS nulls_not_distinct + COALESCE((to_jsonb(i) ->> 'indnullsnotdistinct')::boolean, false) AS nulls_not_distinct, + -- ON DELETE SET NULL/SET DEFAULT (column list), PG15+ (pg_constraint.confdelsetcols). + -- Rendered as a JSON array of column names so it survives the per-column row fan-out + -- and stays a single scalar on PG14 (where the attribute does not exist). Issue #589. + COALESCE(ds.delete_set_columns, '') AS delete_set_columns FROM pg_constraint c JOIN pg_class cl ON c.conrelid = cl.oid JOIN pg_namespace n ON cl.relnamespace = n.oid @@ -1090,6 +1109,15 @@ LEFT JOIN pg_class fcl ON c.confrelid = fcl.oid LEFT JOIN pg_namespace fn ON fcl.relnamespace = fn.oid LEFT JOIN pg_attribute fa ON fa.attrelid = c.confrelid AND fa.attnum = c.confkey[array_position(c.conkey, a.attnum)] LEFT JOIN pg_index i ON i.indexrelid = c.conindid +LEFT JOIN LATERAL ( + SELECT jsonb_agg(da.attname ORDER BY e.ord)::text AS delete_set_columns + FROM jsonb_array_elements_text( + CASE WHEN jsonb_typeof(to_jsonb(c) -> 'confdelsetcols') = 'array' + THEN to_jsonb(c) -> 'confdelsetcols' + ELSE '[]'::jsonb END + ) WITH ORDINALITY AS e(attnum, ord) + JOIN pg_attribute da ON da.attrelid = c.conrelid AND da.attnum = e.attnum::int +) ds ON true LEFT JOIN LATERAL ( SELECT -- Render with search_path set to the table's own schema so same-schema @@ -1130,6 +1158,7 @@ type GetConstraintsForSchemaRow struct { IsPeriod sql.NullBool `db:"is_period" json:"is_period"` NoInherit bool `db:"no_inherit" json:"no_inherit"` NullsNotDistinct sql.NullBool `db:"nulls_not_distinct" json:"nulls_not_distinct"` + DeleteSetColumns sql.NullString `db:"delete_set_columns" json:"delete_set_columns"` } // GetConstraintsForSchema retrieves all table constraints for a specific schema @@ -1163,6 +1192,7 @@ func (q *Queries) GetConstraintsForSchema(ctx context.Context, dollar_1 sql.Null &i.IsPeriod, &i.NoInherit, &i.NullsNotDistinct, + &i.DeleteSetColumns, ); err != nil { return nil, err } diff --git a/testdata/diff/create_table/add_fk/diff.sql b/testdata/diff/create_table/add_fk/diff.sql index 01db911b..2f2a2026 100644 --- a/testdata/diff/create_table/add_fk/diff.sql +++ b/testdata/diff/create_table/add_fk/diff.sql @@ -1,3 +1,6 @@ +ALTER TABLE audit_log +ADD CONSTRAINT audit_log_org_id_actor_member_id_fkey FOREIGN KEY (org_id, actor_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (actor_member_id); + ALTER TABLE books ADD CONSTRAINT books_author_id_fkey FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE; @@ -7,6 +10,11 @@ ADD CONSTRAINT employees_department_id_fkey FOREIGN KEY (department_id) REFERENC ALTER TABLE nodes ADD CONSTRAINT nodes_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES nodes (id); +ALTER TABLE notes DROP CONSTRAINT notes_org_id_author_member_id_fkey; + +ALTER TABLE notes +ADD CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (author_member_id); + ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES customers (id); @@ -25,6 +33,9 @@ ADD CONSTRAINT products_category_code_fkey FOREIGN KEY (category_code) REFERENCE ALTER TABLE projects ADD CONSTRAINT projects_tenant_id_org_id_fkey FOREIGN KEY (tenant_id, org_id) REFERENCES organizations (tenant_id, org_id); +ALTER TABLE tasks +ADD CONSTRAINT tasks_org_id_owner_member_id_fkey FOREIGN KEY (org_id, owner_member_id) REFERENCES members (org_id, id) ON DELETE SET DEFAULT (owner_member_id); + ALTER TABLE teams ADD CONSTRAINT teams_manager_id_fkey FOREIGN KEY (manager_id) REFERENCES managers (id) ON DELETE SET NULL; diff --git a/testdata/diff/create_table/add_fk/new.sql b/testdata/diff/create_table/add_fk/new.sql index 51ba6013..71e5ab5b 100644 --- a/testdata/diff/create_table/add_fk/new.sql +++ b/testdata/diff/create_table/add_fk/new.sql @@ -116,6 +116,40 @@ CREATE TABLE public.orders ( CONSTRAINT orders_manager_id_fkey FOREIGN KEY (manager_id) REFERENCES public.managers(id) ON DELETE SET NULL ); +-- Composite FK with ON DELETE SET NULL / SET DEFAULT column list (PG15+, issue #589) +CREATE TABLE public.members ( + id integer NOT NULL, + org_id integer NOT NULL, + CONSTRAINT members_pkey PRIMARY KEY (id), + CONSTRAINT members_org_id_id_key UNIQUE (org_id, id) +); + +CREATE TABLE public.audit_log ( + id integer NOT NULL, + org_id integer NOT NULL, + actor_member_id integer, + CONSTRAINT audit_log_pkey PRIMARY KEY (id), + CONSTRAINT audit_log_org_id_actor_member_id_fkey FOREIGN KEY (org_id, actor_member_id) REFERENCES public.members(org_id, id) ON DELETE SET NULL (actor_member_id) +); + +CREATE TABLE public.tasks ( + id integer NOT NULL, + org_id integer NOT NULL, + owner_member_id integer DEFAULT 0, + CONSTRAINT tasks_pkey PRIMARY KEY (id), + CONSTRAINT tasks_org_id_owner_member_id_fkey FOREIGN KEY (org_id, owner_member_id) REFERENCES public.members(org_id, id) ON DELETE SET DEFAULT (owner_member_id) +); + +-- Existing composite FK that lacks the column list: the desired state adds it, +-- so the constraint must be recreated (the column list must be compared). +CREATE TABLE public.notes ( + id integer NOT NULL, + org_id integer NOT NULL, + author_member_id integer, + CONSTRAINT notes_pkey PRIMARY KEY (id), + CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES public.members(org_id, id) ON DELETE SET NULL (author_member_id) +); + -- Temporal FK case (PG18+) CREATE TABLE public.price_history ( product_id integer NOT NULL, diff --git a/testdata/diff/create_table/add_fk/old.sql b/testdata/diff/create_table/add_fk/old.sql index 4b2c707d..6132e13d 100644 --- a/testdata/diff/create_table/add_fk/old.sql +++ b/testdata/diff/create_table/add_fk/old.sql @@ -106,6 +106,38 @@ CREATE TABLE public.orders ( CONSTRAINT orders_pkey PRIMARY KEY (id) ); +-- Composite FK with ON DELETE SET NULL / SET DEFAULT column list (PG15+, issue #589) +CREATE TABLE public.members ( + id integer NOT NULL, + org_id integer NOT NULL, + CONSTRAINT members_pkey PRIMARY KEY (id), + CONSTRAINT members_org_id_id_key UNIQUE (org_id, id) +); + +CREATE TABLE public.audit_log ( + id integer NOT NULL, + org_id integer NOT NULL, + actor_member_id integer, + CONSTRAINT audit_log_pkey PRIMARY KEY (id) +); + +CREATE TABLE public.tasks ( + id integer NOT NULL, + org_id integer NOT NULL, + owner_member_id integer DEFAULT 0, + CONSTRAINT tasks_pkey PRIMARY KEY (id) +); + +-- Existing composite FK that lacks the column list: the desired state adds it, +-- so the constraint must be recreated (the column list must be compared). +CREATE TABLE public.notes ( + id integer NOT NULL, + org_id integer NOT NULL, + author_member_id integer, + CONSTRAINT notes_pkey PRIMARY KEY (id), + CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES public.members(org_id, id) ON DELETE SET NULL +); + -- Temporal FK case (PG18+) CREATE TABLE public.price_history ( product_id integer NOT NULL, diff --git a/testdata/diff/create_table/add_fk/plan.json b/testdata/diff/create_table/add_fk/plan.json index f8cad05d..6fac29fd 100644 --- a/testdata/diff/create_table/add_fk/plan.json +++ b/testdata/diff/create_table/add_fk/plan.json @@ -3,9 +3,29 @@ "pgschema_version": "1.13.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "9b85e377059726a1c480a565f28fa2caf1e39ba1512de5c1adc8efbabc669dc1" + "hash": "d362a1d307cacbda3523c26227836ff883ea2839ece7b16eec4eb53c5d6959d6" }, "groups": [ + { + "steps": [ + { + "sql": "ALTER TABLE audit_log\nADD CONSTRAINT audit_log_org_id_actor_member_id_fkey FOREIGN KEY (org_id, actor_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (actor_member_id) NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.audit_log.audit_log_org_id_actor_member_id_fkey" + } + ] + }, + { + "steps": [ + { + "sql": "ALTER TABLE audit_log VALIDATE CONSTRAINT audit_log_org_id_actor_member_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.audit_log.audit_log_org_id_actor_member_id_fkey" + } + ] + }, { "steps": [ { @@ -66,6 +86,32 @@ } ] }, + { + "steps": [ + { + "sql": "ALTER TABLE notes DROP CONSTRAINT notes_org_id_author_member_id_fkey;", + "type": "table.constraint", + "operation": "drop", + "path": "public.notes.notes_org_id_author_member_id_fkey" + }, + { + "sql": "ALTER TABLE notes\nADD CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (author_member_id) NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.notes.notes_org_id_author_member_id_fkey" + } + ] + }, + { + "steps": [ + { + "sql": "ALTER TABLE notes VALIDATE CONSTRAINT notes_org_id_author_member_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.notes.notes_org_id_author_member_id_fkey" + } + ] + }, { "steps": [ { @@ -186,6 +232,26 @@ } ] }, + { + "steps": [ + { + "sql": "ALTER TABLE tasks\nADD CONSTRAINT tasks_org_id_owner_member_id_fkey FOREIGN KEY (org_id, owner_member_id) REFERENCES members (org_id, id) ON DELETE SET DEFAULT (owner_member_id) NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.tasks.tasks_org_id_owner_member_id_fkey" + } + ] + }, + { + "steps": [ + { + "sql": "ALTER TABLE tasks VALIDATE CONSTRAINT tasks_org_id_owner_member_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.tasks.tasks_org_id_owner_member_id_fkey" + } + ] + }, { "steps": [ { diff --git a/testdata/diff/create_table/add_fk/plan.sql b/testdata/diff/create_table/add_fk/plan.sql index 93d7cc82..74b1dddc 100644 --- a/testdata/diff/create_table/add_fk/plan.sql +++ b/testdata/diff/create_table/add_fk/plan.sql @@ -1,3 +1,8 @@ +ALTER TABLE audit_log +ADD CONSTRAINT audit_log_org_id_actor_member_id_fkey FOREIGN KEY (org_id, actor_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (actor_member_id) NOT VALID; + +ALTER TABLE audit_log VALIDATE CONSTRAINT audit_log_org_id_actor_member_id_fkey; + ALTER TABLE books ADD CONSTRAINT books_author_id_fkey FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE NOT VALID; @@ -13,6 +18,13 @@ ADD CONSTRAINT nodes_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES nodes (id ALTER TABLE nodes VALIDATE CONSTRAINT nodes_parent_id_fkey; +ALTER TABLE notes DROP CONSTRAINT notes_org_id_author_member_id_fkey; + +ALTER TABLE notes +ADD CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (author_member_id) NOT VALID; + +ALTER TABLE notes VALIDATE CONSTRAINT notes_org_id_author_member_id_fkey; + ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID; @@ -43,6 +55,11 @@ ADD CONSTRAINT projects_tenant_id_org_id_fkey FOREIGN KEY (tenant_id, org_id) RE ALTER TABLE projects VALIDATE CONSTRAINT projects_tenant_id_org_id_fkey; +ALTER TABLE tasks +ADD CONSTRAINT tasks_org_id_owner_member_id_fkey FOREIGN KEY (org_id, owner_member_id) REFERENCES members (org_id, id) ON DELETE SET DEFAULT (owner_member_id) NOT VALID; + +ALTER TABLE tasks VALIDATE CONSTRAINT tasks_org_id_owner_member_id_fkey; + ALTER TABLE teams ADD CONSTRAINT teams_manager_id_fkey FOREIGN KEY (manager_id) REFERENCES managers (id) ON DELETE SET NULL NOT VALID; diff --git a/testdata/diff/create_table/add_fk/plan.txt b/testdata/diff/create_table/add_fk/plan.txt index 63ea1ee3..692bc4d3 100644 --- a/testdata/diff/create_table/add_fk/plan.txt +++ b/testdata/diff/create_table/add_fk/plan.txt @@ -1,15 +1,20 @@ -Plan: 9 to modify. +Plan: 12 to modify. Summary by type: - tables: 9 to modify + tables: 12 to modify Tables: + ~ audit_log + + audit_log_org_id_actor_member_id_fkey (constraint) ~ books + books_author_id_fkey (constraint) ~ employees + employees_department_id_fkey (constraint) ~ nodes + nodes_parent_id_fkey (constraint) + ~ notes + - notes_org_id_author_member_id_fkey (constraint) + + notes_org_id_author_member_id_fkey (constraint) ~ orders + orders_customer_id_fkey (constraint) + orders_manager_id_fkey (constraint) @@ -20,6 +25,8 @@ Tables: + products_category_code_fkey (constraint) ~ projects + projects_tenant_id_org_id_fkey (constraint) + ~ tasks + + tasks_org_id_owner_member_id_fkey (constraint) ~ teams + teams_manager_id_fkey (constraint) ~ user_profiles @@ -29,78 +36,101 @@ DDL to be executed: -------------------------------------------------- -- Transaction Group #1 +ALTER TABLE audit_log +ADD CONSTRAINT audit_log_org_id_actor_member_id_fkey FOREIGN KEY (org_id, actor_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (actor_member_id) NOT VALID; + +-- Transaction Group #2 +ALTER TABLE audit_log VALIDATE CONSTRAINT audit_log_org_id_actor_member_id_fkey; + +-- Transaction Group #3 ALTER TABLE books ADD CONSTRAINT books_author_id_fkey FOREIGN KEY (author_id) REFERENCES authors (id) ON DELETE CASCADE NOT VALID; --- Transaction Group #2 +-- Transaction Group #4 ALTER TABLE books VALIDATE CONSTRAINT books_author_id_fkey; --- Transaction Group #3 +-- Transaction Group #5 ALTER TABLE employees ADD CONSTRAINT employees_department_id_fkey FOREIGN KEY (department_id) REFERENCES departments (id) NOT VALID; --- Transaction Group #4 +-- Transaction Group #6 ALTER TABLE employees VALIDATE CONSTRAINT employees_department_id_fkey; --- Transaction Group #5 +-- Transaction Group #7 ALTER TABLE nodes ADD CONSTRAINT nodes_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES nodes (id) NOT VALID; --- Transaction Group #6 +-- Transaction Group #8 ALTER TABLE nodes VALIDATE CONSTRAINT nodes_parent_id_fkey; --- Transaction Group #7 +-- Transaction Group #9 +ALTER TABLE notes DROP CONSTRAINT notes_org_id_author_member_id_fkey; + +ALTER TABLE notes +ADD CONSTRAINT notes_org_id_author_member_id_fkey FOREIGN KEY (org_id, author_member_id) REFERENCES members (org_id, id) ON DELETE SET NULL (author_member_id) NOT VALID; + +-- Transaction Group #10 +ALTER TABLE notes VALIDATE CONSTRAINT notes_org_id_author_member_id_fkey; + +-- Transaction Group #11 ALTER TABLE orders ADD CONSTRAINT orders_customer_id_fkey FOREIGN KEY (customer_id) REFERENCES customers (id) NOT VALID; --- Transaction Group #8 +-- Transaction Group #12 ALTER TABLE orders VALIDATE CONSTRAINT orders_customer_id_fkey; --- Transaction Group #9 +-- Transaction Group #13 ALTER TABLE orders ADD CONSTRAINT orders_manager_id_fkey FOREIGN KEY (manager_id) REFERENCES managers (id) ON DELETE SET NULL NOT VALID; --- Transaction Group #10 +-- Transaction Group #14 ALTER TABLE orders VALIDATE CONSTRAINT orders_manager_id_fkey; --- Transaction Group #11 +-- Transaction Group #15 ALTER TABLE orders ADD CONSTRAINT orders_product_id_fkey FOREIGN KEY (product_id) REFERENCES products (id) ON DELETE CASCADE NOT VALID; --- Transaction Group #12 +-- Transaction Group #16 ALTER TABLE orders VALIDATE CONSTRAINT orders_product_id_fkey; --- Transaction Group #13 +-- Transaction Group #17 ALTER TABLE price_adjustments ADD CONSTRAINT price_adjustments_product_fkey FOREIGN KEY (product_id, PERIOD adjustment_period) REFERENCES price_history (product_id, PERIOD valid_period) NOT VALID; --- Transaction Group #14 +-- Transaction Group #18 ALTER TABLE price_adjustments VALIDATE CONSTRAINT price_adjustments_product_fkey; --- Transaction Group #15 +-- Transaction Group #19 ALTER TABLE products ADD CONSTRAINT products_category_code_fkey FOREIGN KEY (category_code) REFERENCES categories (code) ON UPDATE CASCADE NOT VALID; --- Transaction Group #16 +-- Transaction Group #20 ALTER TABLE products VALIDATE CONSTRAINT products_category_code_fkey; --- Transaction Group #17 +-- Transaction Group #21 ALTER TABLE projects ADD CONSTRAINT projects_tenant_id_org_id_fkey FOREIGN KEY (tenant_id, org_id) REFERENCES organizations (tenant_id, org_id) NOT VALID; --- Transaction Group #18 +-- Transaction Group #22 ALTER TABLE projects VALIDATE CONSTRAINT projects_tenant_id_org_id_fkey; --- Transaction Group #19 +-- Transaction Group #23 +ALTER TABLE tasks +ADD CONSTRAINT tasks_org_id_owner_member_id_fkey FOREIGN KEY (org_id, owner_member_id) REFERENCES members (org_id, id) ON DELETE SET DEFAULT (owner_member_id) NOT VALID; + +-- Transaction Group #24 +ALTER TABLE tasks VALIDATE CONSTRAINT tasks_org_id_owner_member_id_fkey; + +-- Transaction Group #25 ALTER TABLE teams ADD CONSTRAINT teams_manager_id_fkey FOREIGN KEY (manager_id) REFERENCES managers (id) ON DELETE SET NULL NOT VALID; --- Transaction Group #20 +-- Transaction Group #26 ALTER TABLE teams VALIDATE CONSTRAINT teams_manager_id_fkey; --- Transaction Group #21 +-- Transaction Group #27 ALTER TABLE user_profiles ADD CONSTRAINT user_profiles_user_id_fkey FOREIGN KEY (user_id) REFERENCES users (id) DEFERRABLE INITIALLY DEFERRED NOT VALID; --- Transaction Group #22 +-- Transaction Group #28 ALTER TABLE user_profiles VALIDATE CONSTRAINT user_profiles_user_id_fkey;