diff --git a/internal/diff/constraint.go b/internal/diff/constraint.go index ddf97cb8..8e3e52d5 100644 --- a/internal/diff/constraint.go +++ b/internal/diff/constraint.go @@ -8,7 +8,7 @@ import ( ) // generateConstraintSQL generates constraint definition for inline table constraints -func generateConstraintSQL(constraint *ir.Constraint, _ string) string { +func generateConstraintSQL(constraint *ir.Constraint, targetSchema string) string { // Helper function to get column names from ConstraintColumn array getColumnNames := func(columns []*ir.ConstraintColumn) []string { var names []string @@ -27,10 +27,12 @@ func generateConstraintSQL(constraint *ir.Constraint, _ string) string { return fmt.Sprintf("CONSTRAINT %s UNIQUE (%s)", constraint.Name, strings.Join(getColumnNames(constraint.Columns), ", ")) case ir.ConstraintTypeForeignKey: // Always include CONSTRAINT name to preserve explicit FK names + // Use QualifyEntityNameWithQuotes to add schema qualifier when referencing tables in other schemas + qualifiedRefTable := ir.QualifyEntityNameWithQuotes(constraint.ReferencedSchema, constraint.ReferencedTable, targetSchema) stmt := fmt.Sprintf("CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)", constraint.Name, strings.Join(getColumnNames(constraint.Columns), ", "), - ir.QuoteIdentifier(constraint.ReferencedTable), strings.Join(getColumnNames(constraint.ReferencedColumns), ", ")) + qualifiedRefTable, strings.Join(getColumnNames(constraint.ReferencedColumns), ", ")) // Only add ON UPDATE/DELETE if they are not the default "NO ACTION" if constraint.UpdateRule != "" && constraint.UpdateRule != "NO ACTION" { stmt += fmt.Sprintf(" ON UPDATE %s", constraint.UpdateRule) diff --git a/internal/diff/identifier_quote_test.go b/internal/diff/identifier_quote_test.go index 4275497c..c5064884 100644 --- a/internal/diff/identifier_quote_test.go +++ b/internal/diff/identifier_quote_test.go @@ -44,6 +44,7 @@ func TestGenerateConstraintSQL_WithQuoting(t *testing.T) { Columns: []*ir.ConstraintColumn{ {Name: "userId", Position: 1}, }, + ReferencedSchema: "public", ReferencedTable: "users", ReferencedColumns: []*ir.ConstraintColumn{ {Name: "id", Position: 1}, @@ -65,11 +66,33 @@ func TestGenerateConstraintSQL_WithQuoting(t *testing.T) { }, want: `CONSTRAINT test_unique_lower UNIQUE (email, username)`, }, + { + name: "FOREIGN KEY with cross-schema reference", + constraint: &ir.Constraint{ + Name: "test_cross_schema_fk", + Type: ir.ConstraintTypeForeignKey, + Columns: []*ir.ConstraintColumn{ + {Name: "category_id", Position: 1}, + }, + ReferencedSchema: "public", + ReferencedTable: "categories", + ReferencedColumns: []*ir.ConstraintColumn{ + {Name: "id", Position: 1}, + }, + IsValid: true, + }, + want: `CONSTRAINT test_cross_schema_fk FOREIGN KEY (category_id) REFERENCES public.categories (id)`, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := generateConstraintSQL(tt.constraint, "public") + // Use "tenant1" as target schema for cross-schema FK test, "public" for others + targetSchema := "public" + if tt.name == "FOREIGN KEY with cross-schema reference" { + targetSchema = "tenant1" + } + got := generateConstraintSQL(tt.constraint, targetSchema) if got != tt.want { t.Errorf("generateConstraintSQL() = %q, want %q", got, tt.want) } diff --git a/testdata/dump/tenant/pgschema.sql b/testdata/dump/tenant/pgschema.sql index 09a92de1..df235535 100644 --- a/testdata/dump/tenant/pgschema.sql +++ b/testdata/dump/tenant/pgschema.sql @@ -35,9 +35,11 @@ CREATE TABLE IF NOT EXISTS posts ( title varchar(200) NOT NULL, content text, author_id integer, + category_id integer NOT NULL, status public.status DEFAULT 'active', created_at timestamp DEFAULT now(), CONSTRAINT posts_pkey PRIMARY KEY (id), - CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES users (id) + CONSTRAINT posts_author_id_fkey FOREIGN KEY (author_id) REFERENCES users (id), + CONSTRAINT posts_category_id_fkey FOREIGN KEY (category_id) REFERENCES public.categories (id) ); diff --git a/testdata/dump/tenant/public.sql b/testdata/dump/tenant/public.sql index 6357ebf5..c4985867 100644 --- a/testdata/dump/tenant/public.sql +++ b/testdata/dump/tenant/public.sql @@ -1,2 +1,11 @@ CREATE TYPE public.user_role AS ENUM ('admin', 'user'); CREATE TYPE public.status AS ENUM ('active', 'inactive'); + +-- +-- Shared table in public schema that tenant schemas will reference +-- +CREATE TABLE public.categories ( + id SERIAL PRIMARY KEY, + name varchar(100) NOT NULL UNIQUE, + description text +);