Skip to content

Commit 3ea9cc5

Browse files
committed
fix(db diff): filter inherited constraints for partitioned tables
when a foreign key references a partitioned table, postgresql automatically creates inherited constraints with numeric suffixes (fkey1, fkey2, etc.) for each partition. migra treats these as independent constraints and generates drop/add statements for them, which fails because postgresql forbids dropping inherited constraints directly. this change filters out alter table statements that operate on constraints matching the pattern fkey followed by digits, as these are always postgresql auto-generated inherited constraints. legitimate user-defined constraints (like users_id_fkey without numeric suffix) are preserved. the fix is applied to both the migra and pgadmin diff paths to ensure consistent behavior across all diff methods. closes #4562
1 parent 03a9671 commit 3ea9cc5

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

internal/db/diff/diff.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config
3535
if err != nil {
3636
return err
3737
}
38+
out = filterInheritedConstraints(out)
3839
branch := keys.GetGitBranch(fsys)
3940
fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n")
4041
if err := SaveDiff(out, file, fsys); err != nil {
@@ -89,6 +90,29 @@ func findDropStatements(out string) []string {
8990
return drops
9091
}
9192

93+
var inheritedConstraintPattern = regexp.MustCompile(`(?i)alter\s+table\s+[^;]+\s+(drop|add)\s+constraint\s+"[^"]*fkey\d+"`)
94+
95+
func filterInheritedConstraints(out string) string {
96+
lines, err := parser.SplitAndTrim(strings.NewReader(out))
97+
if err != nil {
98+
return out
99+
}
100+
var filtered []string
101+
for _, line := range lines {
102+
if !inheritedConstraintPattern.MatchString(line) {
103+
filtered = append(filtered, line)
104+
}
105+
}
106+
if len(filtered) == 0 {
107+
return ""
108+
}
109+
result := strings.Join(filtered, ";\n\n") + ";"
110+
if strings.HasSuffix(out, "\n") {
111+
result += "\n"
112+
}
113+
return result
114+
}
115+
92116
func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) {
93117
// Disable background workers in shadow database
94118
config := start.NewContainerConfig("-c", "max_worker_processes=0")

internal/db/diff/diff_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,68 @@ func TestDropStatements(t *testing.T) {
345345
assert.Equal(t, []string{"drop table t", "alter table t drop column c"}, drops)
346346
}
347347

348+
func TestFilterInheritedConstraints(t *testing.T) {
349+
t.Run("filters inherited fkey constraints", func(t *testing.T) {
350+
input := `alter table "public"."users" drop constraint "users_avatar_id_fkey1";
351+
alter table "public"."users" drop constraint "users_avatar_id_fkey2";
352+
alter table "public"."users" add constraint "users_avatar_id_fkey1" FOREIGN KEY (avatar_id) REFERENCES photos_avatars(id);
353+
create table test();`
354+
result := filterInheritedConstraints(input)
355+
assert.Equal(t, "create table test();", result)
356+
})
357+
358+
t.Run("preserves non-inherited constraints", func(t *testing.T) {
359+
input := `alter table "public"."users" drop constraint "users_avatar_id_fkey";
360+
alter table "public"."users" add constraint "users_avatar_id_fkey" FOREIGN KEY (avatar_id) REFERENCES photos(id);`
361+
result := filterInheritedConstraints(input)
362+
assert.Contains(t, result, `alter table "public"."users" drop constraint "users_avatar_id_fkey"`)
363+
assert.Contains(t, result, `alter table "public"."users" add constraint "users_avatar_id_fkey" FOREIGN KEY (avatar_id) REFERENCES photos(id)`)
364+
})
365+
366+
t.Run("returns empty string when all statements filtered", func(t *testing.T) {
367+
input := `alter table "public"."users" drop constraint "users_fkey1";
368+
alter table "public"."users" drop constraint "users_fkey2";`
369+
result := filterInheritedConstraints(input)
370+
assert.Equal(t, "", result)
371+
})
372+
373+
t.Run("handles empty input", func(t *testing.T) {
374+
result := filterInheritedConstraints("")
375+
assert.Equal(t, "", result)
376+
})
377+
378+
t.Run("handles mixed statements with partitioned table constraints", func(t *testing.T) {
379+
input := `create table accounts(id text primary key);
380+
alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey1";
381+
alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey14";
382+
alter table "public"."companies" drop constraint "companies_logo_id_fkey1";
383+
create index idx_test on test(id);`
384+
result := filterInheritedConstraints(input)
385+
assert.Contains(t, result, "create table accounts(id text primary key)")
386+
assert.Contains(t, result, "create index idx_test on test(id)")
387+
assert.NotContains(t, result, "fkey1")
388+
assert.NotContains(t, result, "fkey14")
389+
})
390+
391+
t.Run("filters exact bug report pattern", func(t *testing.T) {
392+
input := `alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey1";
393+
alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey2";
394+
alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey3";
395+
alter table "public"."users" add constraint "users_avatar_id_avatar_bucket_fkey1" FOREIGN KEY (avatar_id, avatar_bucket) REFERENCES photos_avatars(id, bucket) ON DELETE SET NULL;
396+
alter table "public"."users" add constraint "users_avatar_id_avatar_bucket_fkey2" FOREIGN KEY (avatar_id, avatar_bucket) REFERENCES photos_brands(id, bucket) ON DELETE SET NULL;`
397+
result := filterInheritedConstraints(input)
398+
assert.Equal(t, "", result)
399+
})
400+
401+
t.Run("preserves legitimate constraint operations", func(t *testing.T) {
402+
input := `alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey";
403+
alter table "public"."users" add constraint "users_avatar_id_avatar_bucket_fkey" FOREIGN KEY (avatar_id, avatar_bucket) REFERENCES photos(id, bucket) ON DELETE SET NULL;`
404+
result := filterInheritedConstraints(input)
405+
assert.Contains(t, result, "users_avatar_id_avatar_bucket_fkey")
406+
assert.NotContains(t, result, "fkey1")
407+
})
408+
}
409+
348410
func TestLoadSchemas(t *testing.T) {
349411
expected := []string{
350412
filepath.Join(utils.SchemasDir, "comment", "model.sql"),

internal/db/diff/pgadmin.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func RunPgAdmin(ctx context.Context, schema []string, file string, config pgconn
4444
return err
4545
}
4646

47-
return SaveDiff(output, file, fsys)
47+
return SaveDiff(filterInheritedConstraints(output), file, fsys)
4848
}
4949

5050
var output string

0 commit comments

Comments
 (0)