From 5cd5c42f49574798141655984b0de7f0bcc4f621 Mon Sep 17 00:00:00 2001 From: 7ttp <117663341+7ttp@users.noreply.github.com> Date: Sat, 6 Dec 2025 16:27:31 +0530 Subject: [PATCH] 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 --- internal/db/diff/diff.go | 24 ++++++++++++++ internal/db/diff/diff_test.go | 62 +++++++++++++++++++++++++++++++++++ internal/db/diff/pgadmin.go | 2 +- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/internal/db/diff/diff.go b/internal/db/diff/diff.go index af19faa5aa..bc8b1eeab8 100644 --- a/internal/db/diff/diff.go +++ b/internal/db/diff/diff.go @@ -35,6 +35,7 @@ func Run(ctx context.Context, schema []string, file string, config pgconn.Config if err != nil { return err } + out = filterInheritedConstraints(out) branch := keys.GetGitBranch(fsys) fmt.Fprintln(os.Stderr, "Finished "+utils.Aqua("supabase db diff")+" on branch "+utils.Aqua(branch)+".\n") if err := SaveDiff(out, file, fsys); err != nil { @@ -89,6 +90,29 @@ func findDropStatements(out string) []string { return drops } +var inheritedConstraintPattern = regexp.MustCompile(`(?i)alter\s+table\s+[^;]+\s+(drop|add)\s+constraint\s+"[^"]*fkey\d+"`) + +func filterInheritedConstraints(out string) string { + lines, err := parser.SplitAndTrim(strings.NewReader(out)) + if err != nil { + return out + } + var filtered []string + for _, line := range lines { + if !inheritedConstraintPattern.MatchString(line) { + filtered = append(filtered, line) + } + } + if len(filtered) == 0 { + return "" + } + result := strings.Join(filtered, ";\n\n") + ";" + if strings.HasSuffix(out, "\n") { + result += "\n" + } + return result +} + func CreateShadowDatabase(ctx context.Context, port uint16) (string, error) { // Disable background workers in shadow database config := start.NewContainerConfig("-c", "max_worker_processes=0") diff --git a/internal/db/diff/diff_test.go b/internal/db/diff/diff_test.go index 3c21a83cc2..9895104efb 100644 --- a/internal/db/diff/diff_test.go +++ b/internal/db/diff/diff_test.go @@ -345,6 +345,68 @@ func TestDropStatements(t *testing.T) { assert.Equal(t, []string{"drop table t", "alter table t drop column c"}, drops) } +func TestFilterInheritedConstraints(t *testing.T) { + t.Run("filters inherited fkey constraints", func(t *testing.T) { + input := `alter table "public"."users" drop constraint "users_avatar_id_fkey1"; +alter table "public"."users" drop constraint "users_avatar_id_fkey2"; +alter table "public"."users" add constraint "users_avatar_id_fkey1" FOREIGN KEY (avatar_id) REFERENCES photos_avatars(id); +create table test();` + result := filterInheritedConstraints(input) + assert.Equal(t, "create table test();", result) + }) + + t.Run("preserves non-inherited constraints", func(t *testing.T) { + input := `alter table "public"."users" drop constraint "users_avatar_id_fkey"; +alter table "public"."users" add constraint "users_avatar_id_fkey" FOREIGN KEY (avatar_id) REFERENCES photos(id);` + result := filterInheritedConstraints(input) + assert.Contains(t, result, `alter table "public"."users" drop constraint "users_avatar_id_fkey"`) + assert.Contains(t, result, `alter table "public"."users" add constraint "users_avatar_id_fkey" FOREIGN KEY (avatar_id) REFERENCES photos(id)`) + }) + + t.Run("returns empty string when all statements filtered", func(t *testing.T) { + input := `alter table "public"."users" drop constraint "users_fkey1"; +alter table "public"."users" drop constraint "users_fkey2";` + result := filterInheritedConstraints(input) + assert.Equal(t, "", result) + }) + + t.Run("handles empty input", func(t *testing.T) { + result := filterInheritedConstraints("") + assert.Equal(t, "", result) + }) + + t.Run("handles mixed statements with partitioned table constraints", func(t *testing.T) { + input := `create table accounts(id text primary key); +alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey1"; +alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey14"; +alter table "public"."companies" drop constraint "companies_logo_id_fkey1"; +create index idx_test on test(id);` + result := filterInheritedConstraints(input) + assert.Contains(t, result, "create table accounts(id text primary key)") + assert.Contains(t, result, "create index idx_test on test(id)") + assert.NotContains(t, result, "fkey1") + assert.NotContains(t, result, "fkey14") + }) + + t.Run("filters exact bug report pattern", func(t *testing.T) { + input := `alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey1"; +alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey2"; +alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey3"; +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; +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;` + result := filterInheritedConstraints(input) + assert.Equal(t, "", result) + }) + + t.Run("preserves legitimate constraint operations", func(t *testing.T) { + input := `alter table "public"."users" drop constraint "users_avatar_id_avatar_bucket_fkey"; +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;` + result := filterInheritedConstraints(input) + assert.Contains(t, result, "users_avatar_id_avatar_bucket_fkey") + assert.NotContains(t, result, "fkey1") + }) +} + func TestLoadSchemas(t *testing.T) { expected := []string{ filepath.Join(utils.SchemasDir, "comment", "model.sql"), diff --git a/internal/db/diff/pgadmin.go b/internal/db/diff/pgadmin.go index cb983ebe3c..1ad31d13e9 100644 --- a/internal/db/diff/pgadmin.go +++ b/internal/db/diff/pgadmin.go @@ -44,7 +44,7 @@ func RunPgAdmin(ctx context.Context, schema []string, file string, config pgconn return err } - return SaveDiff(output, file, fsys) + return SaveDiff(filterInheritedConstraints(output), file, fsys) } var output string