-
Notifications
You must be signed in to change notification settings - Fork 29
fix: topological sort for modified tables' constraint dependencies #249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -510,3 +510,146 @@ func topologicallySortFunctions(functions []*ir.Function) []*ir.Function { | |
|
|
||
| return sortedFunctions | ||
| } | ||
|
|
||
| // topologicallySortModifiedTables sorts modified tables based on constraint dependencies | ||
| // Tables with added UNIQUE/PK constraints that are referenced by other tables' added FKs | ||
| // will come before those tables | ||
| func topologicallySortModifiedTables(tableDiffs []*tableDiff) []*tableDiff { | ||
|
Comment on lines
+514
to
+517
|
||
| if len(tableDiffs) <= 1 { | ||
| return tableDiffs | ||
| } | ||
|
|
||
| // Build maps for efficient lookup | ||
| tableDiffMap := make(map[string]*tableDiff) | ||
| var insertionOrder []string | ||
| for _, td := range tableDiffs { | ||
| key := td.Table.Schema + "." + td.Table.Name | ||
| tableDiffMap[key] = td | ||
| insertionOrder = append(insertionOrder, key) | ||
| } | ||
|
|
||
| // Build dependency graph based on added constraints | ||
| inDegree := make(map[string]int) | ||
| adjList := make(map[string][]string) | ||
|
|
||
| // Initialize | ||
| for key := range tableDiffMap { | ||
| inDegree[key] = 0 | ||
| adjList[key] = []string{} | ||
| } | ||
|
|
||
| // Build edges: if tableA adds a FK to tableB's newly-added UNIQUE/PK, add edge tableB -> tableA | ||
| for keyA, tdA := range tableDiffMap { | ||
| // Look at FK constraints being added to tableA | ||
| for _, fkConstraint := range tdA.AddedConstraints { | ||
| if fkConstraint.Type != ir.ConstraintTypeForeignKey { | ||
| continue | ||
| } | ||
|
|
||
| // Build referenced table key | ||
| referencedSchema := fkConstraint.ReferencedSchema | ||
| if referencedSchema == "" { | ||
| referencedSchema = tdA.Table.Schema | ||
| } | ||
| keyB := referencedSchema + "." + fkConstraint.ReferencedTable | ||
|
|
||
| // Check if referenced table exists in our modified tables set | ||
| tdB, exists := tableDiffMap[keyB] | ||
| if !exists || keyA == keyB { | ||
| continue | ||
| } | ||
|
|
||
| // Check if tableB is adding a UNIQUE or PK constraint that matches the FK reference | ||
| for _, constraint := range tdB.AddedConstraints { | ||
| if constraint.Type != ir.ConstraintTypeUnique && constraint.Type != ir.ConstraintTypePrimaryKey { | ||
| continue | ||
| } | ||
|
|
||
| // Check if this constraint matches the FK's referenced columns | ||
| if constraintMatchesFKReference(constraint, fkConstraint) { | ||
| // Add edge: tableB (with new UNIQUE/PK) -> tableA (with new FK) | ||
| adjList[keyB] = append(adjList[keyB], keyA) | ||
| inDegree[keyA]++ | ||
| break | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Kahn's algorithm with deterministic cycle breaking | ||
| var queue []string | ||
| var result []string | ||
| processed := make(map[string]bool, len(tableDiffMap)) | ||
|
|
||
| // Seed queue with nodes that have no incoming edges | ||
| for key, degree := range inDegree { | ||
| if degree == 0 { | ||
| queue = append(queue, key) | ||
| } | ||
| } | ||
| sort.Strings(queue) | ||
|
|
||
| for len(result) < len(tableDiffMap) { | ||
| if len(queue) == 0 { | ||
| // Cycle detected: pick the next unprocessed table using original insertion order | ||
| next := nextInOrder(insertionOrder, processed) | ||
| if next == "" { | ||
| break | ||
| } | ||
| queue = append(queue, next) | ||
| inDegree[next] = 0 | ||
| } | ||
|
Comment on lines
+592
to
+601
|
||
|
|
||
| current := queue[0] | ||
| queue = queue[1:] | ||
| if processed[current] { | ||
| continue | ||
| } | ||
| processed[current] = true | ||
| result = append(result, current) | ||
|
|
||
| neighbors := append([]string(nil), adjList[current]...) | ||
| sort.Strings(neighbors) | ||
|
|
||
| for _, neighbor := range neighbors { | ||
| inDegree[neighbor]-- | ||
| if inDegree[neighbor] <= 0 && !processed[neighbor] { | ||
| queue = append(queue, neighbor) | ||
| sort.Strings(queue) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Convert result back to tableDiff slice | ||
| sortedTableDiffs := make([]*tableDiff, 0, len(result)) | ||
| for _, key := range result { | ||
| sortedTableDiffs = append(sortedTableDiffs, tableDiffMap[key]) | ||
| } | ||
|
|
||
| return sortedTableDiffs | ||
| } | ||
|
|
||
| // constraintMatchesFKReference checks if a UNIQUE/PK constraint matches the columns | ||
| // referenced by a foreign key constraint. | ||
| // In PostgreSQL, composite foreign keys must reference columns in the same order as they | ||
| // appear in the referenced unique/primary key constraint. | ||
| // For example, FK (col1, col2) can only reference UNIQUE (col1, col2), not UNIQUE (col2, col1). | ||
| func constraintMatchesFKReference(uniqueConstraint, fkConstraint *ir.Constraint) bool { | ||
|
Comment on lines
+632
to
+637
|
||
| // Must have same number of columns | ||
| if len(uniqueConstraint.Columns) != len(fkConstraint.ReferencedColumns) { | ||
| return false | ||
| } | ||
|
|
||
| // Sort both constraint columns by position to ensure order-preserving comparison | ||
| uniqueCols := sortConstraintColumnsByPosition(uniqueConstraint.Columns) | ||
| refCols := sortConstraintColumnsByPosition(fkConstraint.ReferencedColumns) | ||
|
|
||
| // Check if columns match in the same order (position by position) | ||
| for i := 0; i < len(uniqueCols); i++ { | ||
| if uniqueCols[i].Name != refCols[i].Name { | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| return true | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,8 @@ | ||
| ALTER TABLE employees | ||
| ADD CONSTRAINT employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE; | ||
| ALTER TABLE z_companies | ||
| ADD CONSTRAINT z_companies_company_id_name_key UNIQUE (company_id, company_name); | ||
|
|
||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES z_companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE; | ||
|
|
||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_name_fkey FOREIGN KEY (company_id, company_name) REFERENCES z_companies (company_id, company_name) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,31 @@ | ||
| CREATE TABLE public.companies ( | ||
| -- Test Case: Adding foreign keys with NOT VALID (online migration) | ||
| -- Scenario 1: FK referencing existing constraint (z_companies_pkey) | ||
| -- Scenario 2: FK referencing newly-added constraint (tests cross-table constraint ordering bug #248) | ||
| -- | ||
| -- Table names are intentionally chosen to test cross-table constraint ordering: | ||
| -- - z_companies (referenced table) comes AFTER a_employees alphabetically | ||
| -- - Without proper topological sorting, a_employees FK would be added before z_companies UNIQUE constraint | ||
| -- - This reproduces bug #248: "ERROR: there is no unique constraint matching given keys for referenced table" | ||
|
|
||
| CREATE TABLE public.z_companies ( | ||
| tenant_id integer NOT NULL, | ||
| company_id integer NOT NULL, | ||
| company_name text NOT NULL, | ||
| CONSTRAINT companies_pkey PRIMARY KEY (tenant_id, company_id) | ||
| CONSTRAINT z_companies_pkey PRIMARY KEY (tenant_id, company_id), | ||
| -- New UNIQUE constraint added in same migration as FK that references it | ||
| CONSTRAINT z_companies_company_id_name_key UNIQUE (company_id, company_name) | ||
| ); | ||
|
|
||
| CREATE TABLE public.employees ( | ||
| CREATE TABLE public.a_employees ( | ||
| id integer NOT NULL, | ||
| employee_number text NOT NULL, | ||
| first_name text NOT NULL, | ||
| last_name text NOT NULL, | ||
| tenant_id integer NOT NULL, | ||
| company_id integer NOT NULL, | ||
| CONSTRAINT employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES public.companies(tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE | ||
| company_name text NOT NULL, | ||
| -- Scenario 1: FK to existing PK (no cross-table dependency issue) | ||
| CONSTRAINT a_employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES public.z_companies(tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE, | ||
| -- Scenario 2: FK to newly-added UNIQUE constraint (requires correct ordering: UNIQUE before VALIDATE) | ||
| CONSTRAINT a_employees_company_name_fkey FOREIGN KEY (company_id, company_name) REFERENCES public.z_companies(company_id, company_name) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,25 @@ | ||
| CREATE TABLE public.companies ( | ||
| -- Test Case: Adding foreign keys with NOT VALID (online migration) | ||
| -- Scenario 1: FK referencing existing constraint (z_companies_pkey) | ||
| -- Scenario 2: FK referencing newly-added constraint (tests cross-table constraint ordering bug #248) | ||
| -- | ||
| -- Table names are intentionally chosen to test cross-table constraint ordering: | ||
| -- - z_companies (referenced table) comes AFTER a_employees alphabetically | ||
| -- - Without proper topological sorting, a_employees FK would be added before z_companies UNIQUE constraint | ||
| -- - This reproduces bug #248: "ERROR: there is no unique constraint matching given keys for referenced table" | ||
|
|
||
| CREATE TABLE public.z_companies ( | ||
| tenant_id integer NOT NULL, | ||
| company_id integer NOT NULL, | ||
| company_name text NOT NULL, | ||
| CONSTRAINT companies_pkey PRIMARY KEY (tenant_id, company_id) | ||
| CONSTRAINT z_companies_pkey PRIMARY KEY (tenant_id, company_id) | ||
| ); | ||
|
|
||
| CREATE TABLE public.employees ( | ||
| CREATE TABLE public.a_employees ( | ||
| id integer NOT NULL, | ||
| employee_number text NOT NULL, | ||
| first_name text NOT NULL, | ||
| last_name text NOT NULL, | ||
| tenant_id integer NOT NULL, | ||
| company_id integer NOT NULL | ||
| company_id integer NOT NULL, | ||
| company_name text NOT NULL | ||
| ); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,12 @@ | ||
| ALTER TABLE employees | ||
| ADD CONSTRAINT employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
| ALTER TABLE z_companies | ||
| ADD CONSTRAINT z_companies_company_id_name_key UNIQUE (company_id, company_name); | ||
|
|
||
| ALTER TABLE employees VALIDATE CONSTRAINT employees_company_fkey; | ||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES z_companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
|
|
||
| ALTER TABLE a_employees VALIDATE CONSTRAINT a_employees_company_fkey; | ||
|
|
||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_name_fkey FOREIGN KEY (company_id, company_name) REFERENCES z_companies (company_id, company_name) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
|
|
||
| ALTER TABLE a_employees VALIDATE CONSTRAINT a_employees_company_name_fkey; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,27 @@ | ||
| Plan: 1 to modify. | ||
| Plan: 2 to modify. | ||
|
|
||
| Summary by type: | ||
| tables: 1 to modify | ||
| tables: 2 to modify | ||
|
|
||
| Tables: | ||
| ~ employees | ||
| + employees_company_fkey (constraint) | ||
| ~ a_employees | ||
| + a_employees_company_fkey (constraint) | ||
| + a_employees_company_name_fkey (constraint) | ||
| ~ z_companies | ||
| + z_companies_company_id_name_key (constraint) | ||
|
|
||
| DDL to be executed: | ||
| -------------------------------------------------- | ||
|
|
||
| ALTER TABLE employees | ||
| ADD CONSTRAINT employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
| ALTER TABLE z_companies | ||
| ADD CONSTRAINT z_companies_company_id_name_key UNIQUE (company_id, company_name); | ||
|
|
||
| ALTER TABLE employees VALIDATE CONSTRAINT employees_company_fkey; | ||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_fkey FOREIGN KEY (tenant_id, company_id) REFERENCES z_companies (tenant_id, company_id) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
|
|
||
| ALTER TABLE a_employees VALIDATE CONSTRAINT a_employees_company_fkey; | ||
|
|
||
| ALTER TABLE a_employees | ||
| ADD CONSTRAINT a_employees_company_name_fkey FOREIGN KEY (company_id, company_name) REFERENCES z_companies (company_id, company_name) ON UPDATE CASCADE ON DELETE RESTRICT DEFERRABLE NOT VALID; | ||
|
|
||
| ALTER TABLE a_employees VALIDATE CONSTRAINT a_employees_company_name_fkey; |
Uh oh!
There was an error while loading. Please reload this page.