diff --git a/internal/diff/diff.go b/internal/diff/diff.go index fe00209f..b5c1f3a8 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -309,6 +309,15 @@ type ddlDiff struct { // exist when the view body is parsed (issue #414). deferredAddedViews []*ir.View functionsAwaitingDeferredViews []*ir.Function + // Foreign keys that depend on a unique/PK constraint being dropped or + // recreated by this migration: existing ones are dropped before the table + // modifications (fkPreDrops) and desired-state ones are (re)created + // afterwards (fkPostAdds). FKs on newly added tables that would bind to the + // old constraint are kept out of CREATE TABLE (suppressedInlineFKs) and + // created via fkPostAdds instead (issue #439). + fkPreDrops []*ir.Constraint + fkPostAdds []*deferredConstraint + suppressedInlineFKs map[string]bool } // schemaDiff represents changes to a schema @@ -1459,6 +1468,10 @@ func GenerateMigration(oldIR, newIR *ir.IR, targetSchema string) []Diff { // Sort individual table objects (indexes, triggers, policies, constraints) within each table sortTableObjects(diff.modifiedTables) + // Detect foreign keys bound to unique/PK constraints being replaced; they + // must be dropped before and recreated after the replacement (issue #439) + diff.fkPreDrops, diff.fkPostAdds, diff.suppressedInlineFKs = planFKRecreationForReplacedConstraints(diff.modifiedTables, diff.addedTables, oldTables, newTables) + // Create a diffCollector and generate SQL collector := newDiffCollector() diff.collectMigrationSQL(targetSchema, collector) @@ -1772,7 +1785,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto } // Create tables WITHOUT function/domain dependencies first (functions may reference these) - deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables, shouldDeferPolicy) + deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables, shouldDeferPolicy, d.suppressedInlineFKs) // Build view lookup - needed for detecting functions that depend on views newViewLookup := buildViewLookup(d.addedViews) @@ -1803,7 +1816,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto generateCreateProceduresSQL(d.addedProcedures, targetSchema, collector) // Create tables WITH function/domain dependencies (now that functions and deferred domains exist) - deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables, shouldDeferPolicy) + deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables, shouldDeferPolicy, d.suppressedInlineFKs) // Add deferred foreign key constraints from BOTH batches AFTER all tables are created // This ensures FK references to tables in the second batch (function-dependent tables) work correctly @@ -1912,8 +1925,15 @@ func (d *ddlDiff) generateModifySQL(targetSchema string, collector *diffCollecto // Modify sequences generateModifySequencesSQL(d.modifiedSequences, targetSchema, collector) + // Drop foreign keys bound to unique/PK constraints being replaced, so the + // constraint drops below succeed; they are recreated right after (issue #439) + generateDropRecreatedFKsSQL(d.fkPreDrops, targetSchema, collector) + // Modify tables - generateModifyTablesSQL(d.modifiedTables, d.droppedTables, targetSchema, collector) + generateModifyTablesSQL(d.modifiedTables, d.droppedTables, d.fkPreDrops, targetSchema, collector) + + // (Re)create the dependent foreign keys now that the replacement constraints exist + generateDeferredConstraintsSQL(d.fkPostAdds, targetSchema, collector) // Create views deferred from generateCreateSQL — their bodies reference // columns just added by ALTER TABLE above (issue #414). Likewise, emit diff --git a/internal/diff/table.go b/internal/diff/table.go index bb5578ed..2b94b5e4 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -400,6 +400,7 @@ func generateCreateTablesSQL( collector *diffCollector, existingTables map[string]bool, shouldDeferPolicy func(*ir.RLSPolicy) bool, + suppressedInlineFKs map[string]bool, ) ([]*ir.RLSPolicy, []*deferredConstraint) { var deferredPolicies []*ir.RLSPolicy var deferredConstraints []*deferredConstraint @@ -408,7 +409,7 @@ func generateCreateTablesSQL( // Process tables in the provided order (already topologically sorted) for _, table := range tables { // Create the table, deferring FK constraints that reference not-yet-created tables - sql, tableDeferred := generateTableSQL(table, targetSchema, createdTables, existingTables) + sql, tableDeferred := generateTableSQL(table, targetSchema, createdTables, existingTables, suppressedInlineFKs) deferredConstraints = append(deferredConstraints, tableDeferred...) // Create context for this statement @@ -541,13 +542,20 @@ func generateDeferredConstraintsSQL(deferred []*deferredConstraint, targetSchema } // generateModifyTablesSQL generates ALTER TABLE statements -func generateModifyTablesSQL(diffs []*tableDiff, droppedTables []*ir.Table, targetSchema string, collector *diffCollector) { +func generateModifyTablesSQL(diffs []*tableDiff, droppedTables []*ir.Table, fkPreDrops []*ir.Constraint, targetSchema string, collector *diffCollector) { // Build a set of tables being dropped (CASCADE will remove their dependent FK constraints) droppedTableSet := make(map[string]bool, len(droppedTables)) for _, t := range droppedTables { droppedTableSet[t.Schema+"."+t.Name] = true } + // Build a set of FKs already dropped in the pre-drop step because they were + // bound to a unique/PK constraint being replaced (#439) + preDroppedFKSet := make(map[string]bool, len(fkPreDrops)) + for _, fk := range fkPreDrops { + preDroppedFKSet[constraintPathKey(fk)] = true + } + // Diffs are already sorted by the Diff operation for _, diff := range diffs { // Build a set of columns being dropped (DROP COLUMN will remove dependent constraints) @@ -557,7 +565,163 @@ func generateModifyTablesSQL(diffs []*tableDiff, droppedTables []*ir.Table, targ } // Pass collector to generateAlterTableStatements to collect with proper context - diff.generateAlterTableStatements(targetSchema, collector, droppedTableSet, droppedColumnSet) + diff.generateAlterTableStatements(targetSchema, collector, droppedTableSet, droppedColumnSet, preDroppedFKSet) + } +} + +// planFKRecreationForReplacedConstraints handles foreign keys that depend on a +// unique/primary-key constraint this migration drops or recreates. Postgres +// ties a foreign key to the specific unique constraint it was created against, +// so the constraint cannot be dropped while the FK exists (SQLSTATE 2BP01), +// and an FK created while the old constraint still exists binds to it. (#439) +// +// It returns: +// - preDrops: old-state FKs bound to a replaced constraint, to drop before +// the table modifications. The drop/modify entries they cover are skipped +// at emission time (see generateAlterTableStatements) so they are not +// dropped twice. +// - postAdds: desired-state FKs to (re)create after the table modifications, +// once the replacement constraint exists. +// - suppressedInlineFKs: FKs on newly added tables (keyed by +// schema.table.constraint) that must be left out of CREATE TABLE — emitted +// inline they would bind to the old constraint before the replacement runs. +// They are recreated via postAdds instead. +// +// Note: Postgres matches an FK's referenced columns to a unique constraint as +// a column set, not an ordered list — FOREIGN KEY (x, y) REFERENCES t (b, a) +// is valid against UNIQUE (a, b) — so matching here is order-insensitive. +func planFKRecreationForReplacedConstraints(modifiedTables []*tableDiff, addedTables []*ir.Table, oldTables, newTables map[string]*ir.Table) (preDrops []*ir.Constraint, postAdds []*deferredConstraint, suppressedInlineFKs map[string]bool) { + // Unique/PK constraints removed by this migration, keyed by their table + replaced := make(map[string][]*ir.Constraint) + for _, td := range modifiedTables { + key := td.Table.Schema + "." + td.Table.Name + for _, c := range td.DroppedConstraints { + if c.Type == ir.ConstraintTypeUnique || c.Type == ir.ConstraintTypePrimaryKey { + replaced[key] = append(replaced[key], c) + } + } + // Modified unique/PK constraints are recreated via DROP + ADD + for _, cd := range td.ModifiedConstraints { + if cd.Old.Type == ir.ConstraintTypeUnique || cd.Old.Type == ir.ConstraintTypePrimaryKey { + replaced[key] = append(replaced[key], cd.Old) + } + } + } + if len(replaced) == 0 { + return nil, nil, nil + } + + // Existing FKs bound to a replaced constraint + for _, tableKey := range sortedKeys(oldTables) { + oldTable := oldTables[tableKey] + newTable := newTables[tableKey] + if newTable == nil { + continue // table is dropped before the modify phase; its FKs go with it + } + for _, name := range sortedKeys(oldTable.Constraints) { + fk := oldTable.Constraints[name] + if fk.Type != ir.ConstraintTypeForeignKey { + continue + } + newFK := newTable.Constraints[name] + oldBound := fkReferencesAnyConstraint(fk, replaced[fkReferencedTableKey(fk)]) + // A changed FK whose new definition targets a replaced constraint + // must also wait for the replacement, even if its old definition + // was bound elsewhere. + newBound := newFK != nil && !constraintsEqual(fk, newFK) && + fkReferencesAnyConstraint(newFK, replaced[fkReferencedTableKey(newFK)]) + if !oldBound && !newBound { + continue + } + + preDrops = append(preDrops, fk) + if newFK != nil { + postAdds = append(postAdds, &deferredConstraint{table: newTable, constraint: newFK}) + } + } + } + + // FKs on newly added tables that target a replaced constraint: keep them + // out of CREATE TABLE (create phase runs before the modify-phase swap) and + // add them with the other recreated FKs instead. + suppressedInlineFKs = make(map[string]bool) + for _, table := range addedTables { + for _, name := range sortedKeys(table.Constraints) { + fk := table.Constraints[name] + if fk.Type != ir.ConstraintTypeForeignKey { + continue + } + if !fkReferencesAnyConstraint(fk, replaced[fkReferencedTableKey(fk)]) { + continue + } + suppressedInlineFKs[constraintPathKey(fk)] = true + postAdds = append(postAdds, &deferredConstraint{table: table, constraint: fk}) + } + } + + sort.Slice(preDrops, func(i, j int) bool { + return constraintPathKey(preDrops[i]) < constraintPathKey(preDrops[j]) + }) + sort.Slice(postAdds, func(i, j int) bool { + return constraintPathKey(postAdds[i].constraint) < constraintPathKey(postAdds[j].constraint) + }) + return preDrops, postAdds, suppressedInlineFKs +} + +// constraintPathKey returns the schema-qualified identity of a constraint. +func constraintPathKey(c *ir.Constraint) string { + return c.Schema + "." + c.Table + "." + c.Name +} + +// fkReferencedTableKey returns the schema-qualified key of the table an FK references. +func fkReferencedTableKey(fk *ir.Constraint) string { + refSchema := fk.ReferencedSchema + if refSchema == "" { + refSchema = fk.Schema + } + return refSchema + "." + fk.ReferencedTable +} + +// fkReferencesAnyConstraint reports whether the FK's referenced columns match +// the column set of any of the given unique/PK constraints. +func fkReferencesAnyConstraint(fk *ir.Constraint, candidates []*ir.Constraint) bool { + refCols := make(map[string]bool, len(fk.ReferencedColumns)) + for _, col := range fk.ReferencedColumns { + refCols[col.Name] = true + } + for _, c := range candidates { + if len(c.Columns) != len(refCols) { + continue + } + matched := true + for _, col := range c.Columns { + if !refCols[col.Name] { + matched = false + break + } + } + if matched { + return true + } + } + return false +} + +// generateDropRecreatedFKsSQL drops foreign keys bound to unique/PK constraints +// being replaced. Must run before the table modifications. (#439) +func generateDropRecreatedFKsSQL(fks []*ir.Constraint, targetSchema string, collector *diffCollector) { + for _, fk := range fks { + tableName := getTableNameWithSchema(fk.Schema, fk.Table, targetSchema) + sql := fmt.Sprintf("ALTER TABLE %s DROP CONSTRAINT %s;", tableName, ir.QuoteIdentifier(fk.Name)) + + context := &diffContext{ + Type: DiffTypeTableConstraint, + Operation: DiffOperationDrop, + Path: fmt.Sprintf("%s.%s.%s", fk.Schema, fk.Table, fk.Name), + Source: fk, + CanRunInTransaction: true, + } + collector.collect(context, sql) } } @@ -583,7 +747,7 @@ func generateDropTablesSQL(tables []*ir.Table, targetSchema string, collector *d } // generateTableSQL generates CREATE TABLE statement and returns any deferred FK constraints -func generateTableSQL(table *ir.Table, targetSchema string, createdTables map[string]bool, existingTables map[string]bool) (string, []*deferredConstraint) { +func generateTableSQL(table *ir.Table, targetSchema string, createdTables map[string]bool, existingTables map[string]bool, suppressedInlineFKs map[string]bool) (string, []*deferredConstraint) { // Only include table name without schema if it's in the target schema tableName := ir.QualifyEntityNameWithQuotes(table.Schema, table.Name, targetSchema) @@ -618,6 +782,11 @@ func generateTableSQL(table *ir.Table, targetSchema string, createdTables map[st var deferred []*deferredConstraint currentKey := fmt.Sprintf("%s.%s", table.Schema, table.Name) for _, constraint := range inlineConstraints { + // FKs bound to a unique/PK constraint being replaced by this migration + // are created after the replacement instead (issue #439) + if suppressedInlineFKs[constraintPathKey(constraint)] { + continue + } if shouldDeferConstraint(table, constraint, currentKey, createdTables, existingTables) { deferred = append(deferred, &deferredConstraint{ table: table, @@ -696,7 +865,10 @@ func constraintDroppedWithColumns(constraint *ir.Constraint, droppedColumnSet ma // FK constraints referencing these tables are skipped since CASCADE already removes them. // droppedColumnSet contains column names being dropped from this table; constraints that // depend on those columns are skipped because DROP COLUMN already removes them. (#384) -func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector *diffCollector, droppedTableSet map[string]bool, droppedColumnSet map[string]bool) { +// preDroppedFKSet contains "schema.table.constraint" keys for FKs already dropped in the +// pre-drop step because they were bound to a replaced unique/PK constraint; their drop +// and modify entries are skipped since the pre-drop/post-add steps handle them. (#439) +func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector *diffCollector, droppedTableSet map[string]bool, droppedColumnSet map[string]bool, preDroppedFKSet map[string]bool) { // Persistence change (UNLOGGED to LOGGED or vice versa) should emit first // because PostgreSQL rewrites the heap so doing it before column/constraint // changes reduces data movement on subsequent steps @@ -725,6 +897,11 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector continue } + // Skip FKs already dropped in the pre-drop step. (#439) + if preDroppedFKSet[constraintPathKey(constraint)] { + continue + } + // Skip FK constraints whose referenced table is being dropped with CASCADE, // since the CASCADE will already remove the constraint. (#382) if constraint.Type == ir.ConstraintTypeForeignKey && constraint.ReferencedTable != "" { @@ -1013,6 +1190,12 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector // Handle modified constraints - drop and recreate them as separate operations for _, ConstraintDiff := range td.ModifiedConstraints { + // Skip FKs already dropped in the pre-drop step; the post-add step + // recreates them from the desired-state definition. (#439) + if preDroppedFKSet[constraintPathKey(ConstraintDiff.Old)] { + continue + } + tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema) constraint := ConstraintDiff.New diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/diff.sql b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/diff.sql new file mode 100644 index 00000000..4c1a2d2e --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/diff.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS case_files ( + id integer, + case_id integer, + CONSTRAINT case_files_pkey PRIMARY KEY (id) +); + +ALTER TABLE case_comments DROP CONSTRAINT case_comments_case_id_fkey; + +ALTER TABLE case_notes DROP CONSTRAINT case_notes_case_id_fkey; + +ALTER TABLE case_tags DROP CONSTRAINT case_tags_case_id_fkey; + +ALTER TABLE cases DROP CONSTRAINT cases_id_key; + +ALTER TABLE cases +ADD CONSTRAINT cases_pkey PRIMARY KEY (id); + +ALTER TABLE case_comments +ADD CONSTRAINT case_comments_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE; + +ALTER TABLE case_files +ADD CONSTRAINT case_files_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id); + +ALTER TABLE case_notes +ADD CONSTRAINT case_notes_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id); + +ALTER TABLE case_tags +ADD CONSTRAINT case_tags_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE; diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/new.sql b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/new.sql new file mode 100644 index 00000000..0b85a46d --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/new.sql @@ -0,0 +1,28 @@ +CREATE TABLE cases ( + id integer PRIMARY KEY, + name text +); + +-- unchanged FK +CREATE TABLE case_notes ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_notes_case_id_fkey REFERENCES cases (id) +); + +-- unchanged FK with ON DELETE CASCADE (clause must survive recreation) +CREATE TABLE case_tags ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_tags_case_id_fkey REFERENCES cases (id) ON DELETE CASCADE +); + +-- FK that itself changes in the same migration (gains ON DELETE CASCADE) +CREATE TABLE case_comments ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_comments_case_id_fkey REFERENCES cases (id) ON DELETE CASCADE +); + +-- newly added table whose FK references the replaced constraint +CREATE TABLE case_files ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_files_case_id_fkey REFERENCES cases (id) +); diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/old.sql b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/old.sql new file mode 100644 index 00000000..1b408bfd --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/old.sql @@ -0,0 +1,23 @@ +CREATE TABLE cases ( + id integer NOT NULL, + name text, + CONSTRAINT cases_id_key UNIQUE (id) +); + +-- unchanged FK +CREATE TABLE case_notes ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_notes_case_id_fkey REFERENCES cases (id) +); + +-- unchanged FK with ON DELETE CASCADE (clause must survive recreation) +CREATE TABLE case_tags ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_tags_case_id_fkey REFERENCES cases (id) ON DELETE CASCADE +); + +-- FK that itself changes in the same migration (gains ON DELETE CASCADE) +CREATE TABLE case_comments ( + id integer PRIMARY KEY, + case_id integer CONSTRAINT case_comments_case_id_fkey REFERENCES cases (id) +); diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.json b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.json new file mode 100644 index 00000000..9c3a5406 --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.json @@ -0,0 +1,92 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.11.0", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "d3de037447ad5303358cf43db984d109e25c0672dd39bab2b072a0ab6b4a075a" + }, + "groups": [ + { + "steps": [ + { + "sql": "CREATE TABLE IF NOT EXISTS case_files (\n id integer,\n case_id integer,\n CONSTRAINT case_files_pkey PRIMARY KEY (id)\n);", + "type": "table", + "operation": "create", + "path": "public.case_files" + }, + { + "sql": "ALTER TABLE case_comments DROP CONSTRAINT case_comments_case_id_fkey;", + "type": "table.constraint", + "operation": "drop", + "path": "public.case_comments.case_comments_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_notes DROP CONSTRAINT case_notes_case_id_fkey;", + "type": "table.constraint", + "operation": "drop", + "path": "public.case_notes.case_notes_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_tags DROP CONSTRAINT case_tags_case_id_fkey;", + "type": "table.constraint", + "operation": "drop", + "path": "public.case_tags.case_tags_case_id_fkey" + }, + { + "sql": "ALTER TABLE cases DROP CONSTRAINT cases_id_key;", + "type": "table.constraint", + "operation": "drop", + "path": "public.cases.cases_id_key" + }, + { + "sql": "ALTER TABLE cases\nADD CONSTRAINT cases_pkey PRIMARY KEY (id);", + "type": "table.constraint", + "operation": "create", + "path": "public.cases.cases_pkey" + }, + { + "sql": "ALTER TABLE case_comments\nADD CONSTRAINT case_comments_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_comments.case_comments_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_comments VALIDATE CONSTRAINT case_comments_case_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_comments.case_comments_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_files\nADD CONSTRAINT case_files_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id);", + "type": "table.constraint", + "operation": "create", + "path": "public.case_files.case_files_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_notes\nADD CONSTRAINT case_notes_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_notes.case_notes_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_notes VALIDATE CONSTRAINT case_notes_case_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_notes.case_notes_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_tags\nADD CONSTRAINT case_tags_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_tags.case_tags_case_id_fkey" + }, + { + "sql": "ALTER TABLE case_tags VALIDATE CONSTRAINT case_tags_case_id_fkey;", + "type": "table.constraint", + "operation": "create", + "path": "public.case_tags.case_tags_case_id_fkey" + } + ] + } + ] +} diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.sql b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.sql new file mode 100644 index 00000000..6b5a1fb0 --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.sql @@ -0,0 +1,34 @@ +CREATE TABLE IF NOT EXISTS case_files ( + id integer, + case_id integer, + CONSTRAINT case_files_pkey PRIMARY KEY (id) +); + +ALTER TABLE case_comments DROP CONSTRAINT case_comments_case_id_fkey; + +ALTER TABLE case_notes DROP CONSTRAINT case_notes_case_id_fkey; + +ALTER TABLE case_tags DROP CONSTRAINT case_tags_case_id_fkey; + +ALTER TABLE cases DROP CONSTRAINT cases_id_key; + +ALTER TABLE cases +ADD CONSTRAINT cases_pkey PRIMARY KEY (id); + +ALTER TABLE case_comments +ADD CONSTRAINT case_comments_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID; + +ALTER TABLE case_comments VALIDATE CONSTRAINT case_comments_case_id_fkey; + +ALTER TABLE case_files +ADD CONSTRAINT case_files_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id); + +ALTER TABLE case_notes +ADD CONSTRAINT case_notes_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) NOT VALID; + +ALTER TABLE case_notes VALIDATE CONSTRAINT case_notes_case_id_fkey; + +ALTER TABLE case_tags +ADD CONSTRAINT case_tags_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID; + +ALTER TABLE case_tags VALIDATE CONSTRAINT case_tags_case_id_fkey; diff --git a/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.txt b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.txt new file mode 100644 index 00000000..c7fff552 --- /dev/null +++ b/testdata/diff/dependency/issue_439_unique_to_pk_fk_dependent/plan.txt @@ -0,0 +1,58 @@ +Plan: 1 to add, 4 to modify. + +Summary by type: + tables: 1 to add, 4 to modify + +Tables: + ~ case_comments + - case_comments_case_id_fkey (constraint) + + case_comments_case_id_fkey (constraint) + + case_files + + case_files_case_id_fkey (constraint) + ~ case_notes + - case_notes_case_id_fkey (constraint) + + case_notes_case_id_fkey (constraint) + ~ case_tags + - case_tags_case_id_fkey (constraint) + + case_tags_case_id_fkey (constraint) + ~ cases + - cases_id_key (constraint) + + cases_pkey (constraint) + +DDL to be executed: +-------------------------------------------------- + +CREATE TABLE IF NOT EXISTS case_files ( + id integer, + case_id integer, + CONSTRAINT case_files_pkey PRIMARY KEY (id) +); + +ALTER TABLE case_comments DROP CONSTRAINT case_comments_case_id_fkey; + +ALTER TABLE case_notes DROP CONSTRAINT case_notes_case_id_fkey; + +ALTER TABLE case_tags DROP CONSTRAINT case_tags_case_id_fkey; + +ALTER TABLE cases DROP CONSTRAINT cases_id_key; + +ALTER TABLE cases +ADD CONSTRAINT cases_pkey PRIMARY KEY (id); + +ALTER TABLE case_comments +ADD CONSTRAINT case_comments_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID; + +ALTER TABLE case_comments VALIDATE CONSTRAINT case_comments_case_id_fkey; + +ALTER TABLE case_files +ADD CONSTRAINT case_files_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id); + +ALTER TABLE case_notes +ADD CONSTRAINT case_notes_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) NOT VALID; + +ALTER TABLE case_notes VALIDATE CONSTRAINT case_notes_case_id_fkey; + +ALTER TABLE case_tags +ADD CONSTRAINT case_tags_case_id_fkey FOREIGN KEY (case_id) REFERENCES cases (id) ON DELETE CASCADE NOT VALID; + +ALTER TABLE case_tags VALIDATE CONSTRAINT case_tags_case_id_fkey;