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