From cba137dec2453d6ac2733fb0a436e9d77c0c42c9 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Wed, 25 Mar 2026 19:17:43 -0700 Subject: [PATCH 1/5] fix: defer all policies to after all tables are created (#373) Policies may reference other tables in USING/WITH CHECK expressions (e.g., subqueries), and those tables may not exist yet when the policy's parent table is created. Always defer policy creation to after all tables exist, rather than only deferring policies that reference new functions. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/diff/diff.go | 11 +----- internal/diff/table.go | 17 ++------- .../diff.sql | 15 ++++++++ .../new.sql | 27 +++++++++++++ .../old.sql | 1 + .../plan.json | 38 +++++++++++++++++++ .../plan.sql | 15 ++++++++ .../plan.txt | 29 ++++++++++++++ 8 files changed, 131 insertions(+), 22 deletions(-) create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/diff.sql create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/new.sql create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/old.sql create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/plan.json create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/plan.sql create mode 100644 testdata/diff/dependency/issue_373_policy_references_other_table/plan.txt diff --git a/internal/diff/diff.go b/internal/diff/diff.go index e2827070..ed7c70f9 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -1528,13 +1528,6 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto key := fmt.Sprintf("%s.%s", tableDiff.Table.Schema, tableDiff.Table.Name) existingTables[key] = true } - var shouldDeferPolicy func(*ir.RLSPolicy) bool - if len(newFunctionLookup) > 0 { - shouldDeferPolicy = func(policy *ir.RLSPolicy) bool { - return policyReferencesNewFunction(policy, newFunctionLookup) - } - } - // Create default privileges BEFORE tables so auto-grants apply to new tables generateCreateDefaultPrivilegesSQL(d.addedDefaultPrivileges, targetSchema, collector) @@ -1552,7 +1545,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto } // Create tables WITHOUT function/domain dependencies first (functions may reference these) - deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables, shouldDeferPolicy) + deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables) // Build view lookup - needed for detecting functions that depend on views newViewLookup := buildViewLookup(d.addedViews) @@ -1583,7 +1576,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto generateCreateProceduresSQL(d.addedProcedures, targetSchema, collector) // Create tables WITH function/domain dependencies (now that functions and deferred domains exist) - deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables, shouldDeferPolicy) + deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables) // Add deferred foreign key constraints from BOTH batches AFTER all tables are created // This ensures FK references to tables in the second batch (function-dependent tables) work correctly diff --git a/internal/diff/table.go b/internal/diff/table.go index bf5acd02..eda88652 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -393,7 +393,6 @@ func generateCreateTablesSQL( targetSchema string, collector *diffCollector, existingTables map[string]bool, - shouldDeferPolicy func(*ir.RLSPolicy) bool, ) ([]*ir.RLSPolicy, []*deferredConstraint) { var deferredPolicies []*ir.RLSPolicy var deferredConstraints []*deferredConstraint @@ -474,21 +473,13 @@ func generateCreateTablesSQL( generateRLSChangesSQL(rlsChanges, targetSchema, collector) } - // Collect policies that can run immediately; defer only those that depend on new helper functions + // Defer all policies to after all tables are created. + // Policies may reference other tables in USING/WITH CHECK expressions (e.g., subqueries), + // and those tables may not exist yet at this point. (#373) if len(table.Policies) > 0 { - var inlinePolicies []*ir.RLSPolicy policyNames := sortedKeys(table.Policies) for _, name := range policyNames { - policy := table.Policies[name] - if shouldDeferPolicy != nil && shouldDeferPolicy(policy) { - deferredPolicies = append(deferredPolicies, policy) - } else { - inlinePolicies = append(inlinePolicies, policy) - } - } - - if len(inlinePolicies) > 0 { - generateCreatePoliciesSQL(inlinePolicies, targetSchema, collector) + deferredPolicies = append(deferredPolicies, table.Policies[name]) } } diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/diff.sql b/testdata/diff/dependency/issue_373_policy_references_other_table/diff.sql new file mode 100644 index 00000000..8603680e --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/diff.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS manager ( + id SERIAL, + user_id uuid NOT NULL +); + +ALTER TABLE manager ENABLE ROW LEVEL SECURITY; + +CREATE TABLE IF NOT EXISTS project_manager ( + id SERIAL, + project_id integer NOT NULL, + manager_id integer NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +CREATE POLICY employee_manager_select ON manager FOR SELECT TO PUBLIC USING (id IN ( SELECT pam.manager_id FROM project_manager pam WHERE ((pam.project_id IN ( SELECT unnest(ARRAY[1, 2, 3]) AS unnest)) AND (pam.is_deleted = false)))); diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/new.sql b/testdata/diff/dependency/issue_373_policy_references_other_table/new.sql new file mode 100644 index 00000000..3bd421e8 --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/new.sql @@ -0,0 +1,27 @@ +CREATE TABLE project_manager ( + id SERIAL, + project_id int NOT NULL, + manager_id int NOT NULL, + is_deleted boolean NOT NULL DEFAULT false +); + +CREATE TABLE manager ( + id SERIAL, + user_id uuid NOT NULL +); + +ALTER TABLE manager ENABLE ROW LEVEL SECURITY; + +CREATE POLICY employee_manager_select ON manager + FOR SELECT + TO PUBLIC + USING ( + id IN ( + SELECT pam.manager_id + FROM project_manager pam + WHERE pam.project_id IN ( + SELECT unnest(ARRAY[1, 2, 3]) + ) + AND pam.is_deleted = false + ) + ); \ No newline at end of file diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/old.sql b/testdata/diff/dependency/issue_373_policy_references_other_table/old.sql new file mode 100644 index 00000000..68cc3483 --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/old.sql @@ -0,0 +1 @@ +-- Empty schema (no tables) \ No newline at end of file diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/plan.json b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.json new file mode 100644 index 00000000..10c9160b --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.json @@ -0,0 +1,38 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.7.4", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "groups": [ + { + "steps": [ + { + "sql": "CREATE TABLE IF NOT EXISTS manager (\n id SERIAL,\n user_id uuid NOT NULL\n);", + "type": "table", + "operation": "create", + "path": "public.manager" + }, + { + "sql": "ALTER TABLE manager ENABLE ROW LEVEL SECURITY;", + "type": "table.rls", + "operation": "alter", + "path": "public.manager" + }, + { + "sql": "CREATE TABLE IF NOT EXISTS project_manager (\n id SERIAL,\n project_id integer NOT NULL,\n manager_id integer NOT NULL,\n is_deleted boolean DEFAULT false NOT NULL\n);", + "type": "table", + "operation": "create", + "path": "public.project_manager" + }, + { + "sql": "CREATE POLICY employee_manager_select ON manager FOR SELECT TO PUBLIC USING (id IN ( SELECT pam.manager_id FROM project_manager pam WHERE ((pam.project_id IN ( SELECT unnest(ARRAY[1, 2, 3]) AS unnest)) AND (pam.is_deleted = false))));", + "type": "table.policy", + "operation": "create", + "path": "public.manager.employee_manager_select" + } + ] + } + ] +} diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/plan.sql b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.sql new file mode 100644 index 00000000..8603680e --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS manager ( + id SERIAL, + user_id uuid NOT NULL +); + +ALTER TABLE manager ENABLE ROW LEVEL SECURITY; + +CREATE TABLE IF NOT EXISTS project_manager ( + id SERIAL, + project_id integer NOT NULL, + manager_id integer NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +CREATE POLICY employee_manager_select ON manager FOR SELECT TO PUBLIC USING (id IN ( SELECT pam.manager_id FROM project_manager pam WHERE ((pam.project_id IN ( SELECT unnest(ARRAY[1, 2, 3]) AS unnest)) AND (pam.is_deleted = false)))); diff --git a/testdata/diff/dependency/issue_373_policy_references_other_table/plan.txt b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.txt new file mode 100644 index 00000000..fa03ed88 --- /dev/null +++ b/testdata/diff/dependency/issue_373_policy_references_other_table/plan.txt @@ -0,0 +1,29 @@ +Plan: 2 to add. + +Summary by type: + tables: 2 to add + +Tables: + + manager + + employee_manager_select (policy) + ~ manager (rls) + + project_manager + +DDL to be executed: +-------------------------------------------------- + +CREATE TABLE IF NOT EXISTS manager ( + id SERIAL, + user_id uuid NOT NULL +); + +ALTER TABLE manager ENABLE ROW LEVEL SECURITY; + +CREATE TABLE IF NOT EXISTS project_manager ( + id SERIAL, + project_id integer NOT NULL, + manager_id integer NOT NULL, + is_deleted boolean DEFAULT false NOT NULL +); + +CREATE POLICY employee_manager_select ON manager FOR SELECT TO PUBLIC USING (id IN ( SELECT pam.manager_id FROM project_manager pam WHERE ((pam.project_id IN ( SELECT unnest(ARRAY[1, 2, 3]) AS unnest)) AND (pam.is_deleted = false)))); From c888b59740a4b38873c3d11c1914a79d1efd76e5 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Wed, 25 Mar 2026 19:25:58 -0700 Subject: [PATCH 2/5] fix: sort deferred policies by (schema, table, name) and update doc comment Address review feedback: - Sort policies by (schema, table, name) for deterministic ordering when policies from multiple tables are collected together - Update generateCreateTablesSQL doc comment to reflect that all policies are now deferred Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/diff/policy.go | 10 +++++++++- internal/diff/table.go | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/diff/policy.go b/internal/diff/policy.go index fa46e023..5e4006bc 100644 --- a/internal/diff/policy.go +++ b/internal/diff/policy.go @@ -10,10 +10,18 @@ import ( // generateCreatePoliciesSQL generates CREATE POLICY statements func generateCreatePoliciesSQL(policies []*ir.RLSPolicy, targetSchema string, collector *diffCollector) { - // Sort policies by name for consistent ordering + // Sort policies by (schema, table, name) for deterministic ordering across tables. + // Policy names are only unique per table, so sorting by name alone is insufficient + // when policies from multiple tables are collected together (#373). sortedPolicies := make([]*ir.RLSPolicy, len(policies)) copy(sortedPolicies, policies) sort.Slice(sortedPolicies, func(i, j int) bool { + if sortedPolicies[i].Schema != sortedPolicies[j].Schema { + return sortedPolicies[i].Schema < sortedPolicies[j].Schema + } + if sortedPolicies[i].Table != sortedPolicies[j].Table { + return sortedPolicies[i].Table < sortedPolicies[j].Table + } return sortedPolicies[i].Name < sortedPolicies[j].Name }) diff --git a/internal/diff/table.go b/internal/diff/table.go index eda88652..5d1fc9ca 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -383,9 +383,9 @@ type deferredConstraint struct { constraint *ir.Constraint } -// generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes, policies, and RLS. -// Policies that reference newly added helper functions are collected for deferred creation after -// dependent functions/procedures have been created, while all other policies are emitted inline. +// generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes and RLS settings. +// All policies are deferred for creation after all tables exist, since policies may reference +// other tables in USING/WITH CHECK expressions (#373). // It returns deferred policies and foreign key constraints that should be applied after dependent objects exist. // Tables are assumed to be pre-sorted in topological order for dependency-aware creation. func generateCreateTablesSQL( From 3d1af174690dc0064833c5f3983bd14f6b1ce1e4 Mon Sep 17 00:00:00 2001 From: tianzhou Date: Wed, 25 Mar 2026 20:14:58 -0700 Subject: [PATCH 3/5] fix: only defer policies that reference other new tables (#373) Instead of deferring all policies (which changed dump output ordering), selectively defer only policies whose USING/WITH CHECK expressions reference another new table being created in the same batch. Policies that only reference their own table or existing tables remain inline. Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/diff/diff.go | 48 ++++++++++++++++++++++++++++++++++++++++-- internal/diff/table.go | 24 +++++++++++++++------ 2 files changed, 63 insertions(+), 9 deletions(-) diff --git a/internal/diff/diff.go b/internal/diff/diff.go index ed7c70f9..3be2d0ad 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -1528,6 +1528,23 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto key := fmt.Sprintf("%s.%s", tableDiff.Table.Schema, tableDiff.Table.Name) existingTables[key] = true } + // Build lookup of all new table names for policy deferral (#373). + // Policies that reference other new tables must be deferred until all tables exist. + newTableLookup := make(map[string]struct{}, len(d.addedTables)) + for _, table := range d.addedTables { + newTableLookup[table.Name] = struct{}{} + newTableLookup[fmt.Sprintf("%s.%s", table.Schema, table.Name)] = struct{}{} + } + var shouldDeferPolicy func(*ir.RLSPolicy) bool + if len(newFunctionLookup) > 0 || len(newTableLookup) > 0 { + shouldDeferPolicy = func(policy *ir.RLSPolicy) bool { + if policyReferencesNewFunction(policy, newFunctionLookup) { + return true + } + return policyReferencesOtherNewTable(policy, newTableLookup) + } + } + // Create default privileges BEFORE tables so auto-grants apply to new tables generateCreateDefaultPrivilegesSQL(d.addedDefaultPrivileges, targetSchema, collector) @@ -1545,7 +1562,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto } // Create tables WITHOUT function/domain dependencies first (functions may reference these) - deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables) + deferredPolicies1, deferredConstraints1 := generateCreateTablesSQL(tablesWithoutDeps, targetSchema, collector, existingTables, shouldDeferPolicy) // Build view lookup - needed for detecting functions that depend on views newViewLookup := buildViewLookup(d.addedViews) @@ -1576,7 +1593,7 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto generateCreateProceduresSQL(d.addedProcedures, targetSchema, collector) // Create tables WITH function/domain dependencies (now that functions and deferred domains exist) - deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables) + deferredPolicies2, deferredConstraints2 := generateCreateTablesSQL(tablesWithDeps, targetSchema, collector, existingTables, shouldDeferPolicy) // Add deferred foreign key constraints from BOTH batches AFTER all tables are created // This ensures FK references to tables in the second batch (function-dependent tables) work correctly @@ -2021,6 +2038,33 @@ func policyReferencesNewFunction(policy *ir.RLSPolicy, newFunctions map[string]s return false } +// policyReferencesOtherNewTable determines if a policy's USING or WITH CHECK expressions +// reference any newly added table other than the policy's own table (#373). +func policyReferencesOtherNewTable(policy *ir.RLSPolicy, newTables map[string]struct{}) bool { + if len(newTables) == 0 || policy == nil { + return false + } + + for _, expr := range []string{policy.Using, policy.WithCheck} { + if expr == "" { + continue + } + exprLower := strings.ToLower(expr) + for tableName := range newTables { + // Skip the policy's own table + if strings.EqualFold(tableName, policy.Table) || + strings.EqualFold(tableName, fmt.Sprintf("%s.%s", policy.Schema, policy.Table)) { + continue + } + // Check if the expression contains a reference to this table name + if strings.Contains(exprLower, strings.ToLower(tableName)) { + return true + } + } + } + return false +} + // tableUsesDeferredDomain determines if a table uses any deferred domain types in its columns. func tableUsesDeferredDomain(table *ir.Table, deferredDomains map[string]struct{}) bool { if len(deferredDomains) == 0 || table == nil { diff --git a/internal/diff/table.go b/internal/diff/table.go index 5d1fc9ca..7172c3ba 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -383,9 +383,9 @@ type deferredConstraint struct { constraint *ir.Constraint } -// generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes and RLS settings. -// All policies are deferred for creation after all tables exist, since policies may reference -// other tables in USING/WITH CHECK expressions (#373). +// generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes, policies, and RLS. +// Policies that reference other new tables in the same batch (via USING/WITH CHECK expressions) +// are deferred for creation after all tables exist (#373). All other policies are emitted inline. // It returns deferred policies and foreign key constraints that should be applied after dependent objects exist. // Tables are assumed to be pre-sorted in topological order for dependency-aware creation. func generateCreateTablesSQL( @@ -393,6 +393,7 @@ func generateCreateTablesSQL( targetSchema string, collector *diffCollector, existingTables map[string]bool, + shouldDeferPolicy func(*ir.RLSPolicy) bool, ) ([]*ir.RLSPolicy, []*deferredConstraint) { var deferredPolicies []*ir.RLSPolicy var deferredConstraints []*deferredConstraint @@ -473,13 +474,22 @@ func generateCreateTablesSQL( generateRLSChangesSQL(rlsChanges, targetSchema, collector) } - // Defer all policies to after all tables are created. - // Policies may reference other tables in USING/WITH CHECK expressions (e.g., subqueries), - // and those tables may not exist yet at this point. (#373) + // Collect policies: defer those that reference other new tables or new functions (#373), + // emit the rest inline with their parent table. if len(table.Policies) > 0 { + var inlinePolicies []*ir.RLSPolicy policyNames := sortedKeys(table.Policies) for _, name := range policyNames { - deferredPolicies = append(deferredPolicies, table.Policies[name]) + policy := table.Policies[name] + if shouldDeferPolicy != nil && shouldDeferPolicy(policy) { + deferredPolicies = append(deferredPolicies, policy) + } else { + inlinePolicies = append(inlinePolicies, policy) + } + } + + if len(inlinePolicies) > 0 { + generateCreatePoliciesSQL(inlinePolicies, targetSchema, collector) } } From bf1d939e6fea49342fe2b0bfb1adadd0c3eece0b Mon Sep 17 00:00:00 2001 From: tianzhou Date: Wed, 25 Mar 2026 20:46:17 -0700 Subject: [PATCH 4/5] fix: update comment to say "same migration" instead of "same batch" Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/diff/table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/diff/table.go b/internal/diff/table.go index 7172c3ba..4559e519 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -384,7 +384,7 @@ type deferredConstraint struct { } // generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes, policies, and RLS. -// Policies that reference other new tables in the same batch (via USING/WITH CHECK expressions) +// Policies that reference other new tables in the same migration (via USING/WITH CHECK expressions) // are deferred for creation after all tables exist (#373). All other policies are emitted inline. // It returns deferred policies and foreign key constraints that should be applied after dependent objects exist. // Tables are assumed to be pre-sorted in topological order for dependency-aware creation. From ba8c29577cbd044b51f11518cc97d8f0664b7f9d Mon Sep 17 00:00:00 2001 From: tianzhou Date: Wed, 25 Mar 2026 22:55:20 -0700 Subject: [PATCH 5/5] fix: use qualified table names in lookup and update doc comment - Store only fully-qualified (schema.table) keys in newTableLookup to avoid cross-schema name collisions - Update generateCreateTablesSQL doc comment to mention both table and function deferral conditions Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/diff/diff.go | 20 ++++++++++++-------- internal/diff/table.go | 5 +++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/internal/diff/diff.go b/internal/diff/diff.go index 3be2d0ad..66331d49 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -1528,12 +1528,11 @@ func (d *ddlDiff) generateCreateSQL(targetSchema string, collector *diffCollecto key := fmt.Sprintf("%s.%s", tableDiff.Table.Schema, tableDiff.Table.Name) existingTables[key] = true } - // Build lookup of all new table names for policy deferral (#373). + // Build lookup of all new table names (qualified) for policy deferral (#373). // Policies that reference other new tables must be deferred until all tables exist. newTableLookup := make(map[string]struct{}, len(d.addedTables)) for _, table := range d.addedTables { - newTableLookup[table.Name] = struct{}{} - newTableLookup[fmt.Sprintf("%s.%s", table.Schema, table.Name)] = struct{}{} + newTableLookup[fmt.Sprintf("%s.%s", strings.ToLower(table.Schema), strings.ToLower(table.Name))] = struct{}{} } var shouldDeferPolicy func(*ir.RLSPolicy) bool if len(newFunctionLookup) > 0 || len(newTableLookup) > 0 { @@ -2040,24 +2039,29 @@ func policyReferencesNewFunction(policy *ir.RLSPolicy, newFunctions map[string]s // policyReferencesOtherNewTable determines if a policy's USING or WITH CHECK expressions // reference any newly added table other than the policy's own table (#373). +// newTables keys are fully qualified (schema.table) to avoid cross-schema collisions. func policyReferencesOtherNewTable(policy *ir.RLSPolicy, newTables map[string]struct{}) bool { if len(newTables) == 0 || policy == nil { return false } + ownQualified := fmt.Sprintf("%s.%s", strings.ToLower(policy.Schema), strings.ToLower(policy.Table)) + for _, expr := range []string{policy.Using, policy.WithCheck} { if expr == "" { continue } exprLower := strings.ToLower(expr) - for tableName := range newTables { + for qualifiedName := range newTables { // Skip the policy's own table - if strings.EqualFold(tableName, policy.Table) || - strings.EqualFold(tableName, fmt.Sprintf("%s.%s", policy.Schema, policy.Table)) { + if qualifiedName == ownQualified { continue } - // Check if the expression contains a reference to this table name - if strings.Contains(exprLower, strings.ToLower(tableName)) { + // Extract the unqualified table name for substring matching. + // Policy expressions may use unqualified or qualified references. + parts := strings.SplitN(qualifiedName, ".", 2) + tableName := parts[len(parts)-1] + if strings.Contains(exprLower, tableName) { return true } } diff --git a/internal/diff/table.go b/internal/diff/table.go index 4559e519..20d6cdf2 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -384,8 +384,9 @@ type deferredConstraint struct { } // generateCreateTablesSQL generates CREATE TABLE statements with co-located indexes, policies, and RLS. -// Policies that reference other new tables in the same migration (via USING/WITH CHECK expressions) -// are deferred for creation after all tables exist (#373). All other policies are emitted inline. +// Policies that reference other new tables in the same migration or newly added functions +// (via USING/WITH CHECK expressions) are deferred for creation after all tables and functions +// exist (#373). All other policies are emitted inline. // It returns deferred policies and foreign key constraints that should be applied after dependent objects exist. // Tables are assumed to be pre-sorted in topological order for dependency-aware creation. func generateCreateTablesSQL(