diff --git a/internal/diff/diff.go b/internal/diff/diff.go index b5c1f3a8..e3a7cc3d 100644 --- a/internal/diff/diff.go +++ b/internal/diff/diff.go @@ -2382,17 +2382,15 @@ func policyReferencesOtherNewTable(policy *ir.RLSPolicy, newTables map[string]st if expr == "" { continue } - exprLower := strings.ToLower(expr) for qualifiedName := range newTables { // Skip the policy's own table if qualifiedName == ownQualified { continue } - // 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) { + if containsIdentifier(expr, tableName) || containsIdentifier(expr, qualifiedName) { return true } } diff --git a/internal/diff/policy_dependency_test.go b/internal/diff/policy_dependency_test.go index 60d202f8..ceed20c7 100644 --- a/internal/diff/policy_dependency_test.go +++ b/internal/diff/policy_dependency_test.go @@ -59,3 +59,27 @@ func TestPolicyReferencesNewFunction_BuiltInIgnored(t *testing.T) { t.Fatalf("expected policy referencing only built-in functions to remain inline") } } + +func TestPolicyReferencesOtherNewTable_IdentifierAware(t *testing.T) { + lookup := map[string]struct{}{ + "public.orders": {}, + "public.users": {}, + "public.user": {}, + } + + if !policyReferencesOtherNewTable(&ir.RLSPolicy{ + Schema: "public", + Table: "orders", + Using: "EXISTS (SELECT 1 FROM users u WHERE u.id = orders.user_id)", + }, lookup) { + t.Fatalf("expected policy referencing another new table to be deferred") + } + + if policyReferencesOtherNewTable(&ir.RLSPolicy{ + Schema: "public", + Table: "orders", + Using: "orders_archive.user_id = 1", + }, lookup) { + t.Fatalf("did not expect substring-only matches inside longer identifiers") + } +} diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/diff.sql b/testdata/diff/dependency/policy_identifier_substring_false_positive/diff.sql new file mode 100644 index 00000000..26bd18f4 --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/diff.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS orders ( + id integer NOT NULL, + user_name text NOT NULL +); + +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; + +CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER); + +CREATE TABLE IF NOT EXISTS "user" ( + id integer NOT NULL +); diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/new.sql b/testdata/diff/dependency/policy_identifier_substring_false_positive/new.sql new file mode 100644 index 00000000..f89a59ea --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/new.sql @@ -0,0 +1,15 @@ +CREATE TABLE orders ( + id int NOT NULL, + user_name text NOT NULL +); + +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; + +CREATE POLICY orders_current_user_scope ON orders + FOR SELECT + TO PUBLIC + USING (user_name = CURRENT_USER); + +CREATE TABLE "user" ( + id int NOT NULL +); diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/old.sql b/testdata/diff/dependency/policy_identifier_substring_false_positive/old.sql new file mode 100644 index 00000000..abe02221 --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/old.sql @@ -0,0 +1 @@ +-- Empty schema (no tables) diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.json b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.json new file mode 100644 index 00000000..d8d4ac37 --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.json @@ -0,0 +1,38 @@ +{ + "version": "1.0.0", + "pgschema_version": "1.11.0", + "created_at": "1970-01-01T00:00:00Z", + "source_fingerprint": { + "hash": "965b1131737c955e24c7f827c55bd78e4cb49a75adfd04229e0ba297376f5085" + }, + "groups": [ + { + "steps": [ + { + "sql": "CREATE TABLE IF NOT EXISTS orders (\n id integer NOT NULL,\n user_name text NOT NULL\n);", + "type": "table", + "operation": "create", + "path": "public.orders" + }, + { + "sql": "ALTER TABLE orders ENABLE ROW LEVEL SECURITY;", + "type": "table.rls", + "operation": "alter", + "path": "public.orders" + }, + { + "sql": "CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER);", + "type": "table.policy", + "operation": "create", + "path": "public.orders.orders_current_user_scope" + }, + { + "sql": "CREATE TABLE IF NOT EXISTS \"user\" (\n id integer NOT NULL\n);", + "type": "table", + "operation": "create", + "path": "public.user" + } + ] + } + ] +} diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.sql b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.sql new file mode 100644 index 00000000..26bd18f4 --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS orders ( + id integer NOT NULL, + user_name text NOT NULL +); + +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; + +CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER); + +CREATE TABLE IF NOT EXISTS "user" ( + id integer NOT NULL +); diff --git a/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.txt b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.txt new file mode 100644 index 00000000..94007acb --- /dev/null +++ b/testdata/diff/dependency/policy_identifier_substring_false_positive/plan.txt @@ -0,0 +1,26 @@ +Plan: 2 to add. + +Summary by type: + tables: 2 to add + +Tables: + + orders + + orders_current_user_scope (policy) + ~ orders (rls) + + user + +DDL to be executed: +-------------------------------------------------- + +CREATE TABLE IF NOT EXISTS orders ( + id integer NOT NULL, + user_name text NOT NULL +); + +ALTER TABLE orders ENABLE ROW LEVEL SECURITY; + +CREATE POLICY orders_current_user_scope ON orders FOR SELECT TO PUBLIC USING (user_name = CURRENT_USER); + +CREATE TABLE IF NOT EXISTS "user" ( + id integer NOT NULL +);