diff --git a/internal/diff/policy.go b/internal/diff/policy.go index 48a8a14b..094fb374 100644 --- a/internal/diff/policy.go +++ b/internal/diff/policy.go @@ -62,7 +62,7 @@ func generatePolicySQL(policy *ir.RLSPolicy, targetSchema string) string { // Only include table name without schema if it's in the target schema tableName := getTableNameWithSchema(policy.Schema, policy.Table, targetSchema) - policyStmt := fmt.Sprintf("CREATE POLICY %s ON %s", policy.Name, tableName) + policyStmt := fmt.Sprintf("CREATE POLICY %s ON %s", ir.QuoteIdentifier(policy.Name), tableName) // Add command type if specified if policy.Command != ir.PolicyCommandAll { @@ -104,7 +104,7 @@ func generateAlterPolicySQL(old, new *ir.RLSPolicy, targetSchema string) string withCheckChange := old.WithCheck != new.WithCheck // Build ALTER POLICY statement with all changes - alterStmt := fmt.Sprintf("ALTER POLICY %s ON %s", new.Name, tableName) + alterStmt := fmt.Sprintf("ALTER POLICY %s ON %s", ir.QuoteIdentifier(new.Name), tableName) // Add TO clause if roles changed if roleChange { diff --git a/internal/diff/table.go b/internal/diff/table.go index c68f0a20..61fd4359 100644 --- a/internal/diff/table.go +++ b/internal/diff/table.go @@ -915,7 +915,7 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector // Drop policies - already sorted by the Diff operation for _, policy := range td.DroppedPolicies { tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema) - sql := fmt.Sprintf("DROP POLICY IF EXISTS %s ON %s;", policy.Name, tableName) + sql := fmt.Sprintf("DROP POLICY IF EXISTS %s ON %s;", ir.QuoteIdentifier(policy.Name), tableName) context := &diffContext{ Type: DiffTypeTablePolicy, @@ -1006,7 +1006,7 @@ func (td *tableDiff) generateAlterTableStatements(targetSchema string, collector if needsRecreate(policyDiff.Old, policyDiff.New) { tableName := getTableNameWithSchema(td.Table.Schema, td.Table.Name, targetSchema) // Drop and recreate policy for modification - sql := fmt.Sprintf("DROP POLICY IF EXISTS %s ON %s;", policyDiff.Old.Name, tableName) + sql := fmt.Sprintf("DROP POLICY IF EXISTS %s ON %s;", ir.QuoteIdentifier(policyDiff.Old.Name), tableName) context := &diffContext{ Type: DiffTypeTablePolicy, diff --git a/testdata/diff/create_policy/add_policy/diff.sql b/testdata/diff/create_policy/add_policy/diff.sql index 85da810c..59ba9531 100644 --- a/testdata/diff/create_policy/add_policy/diff.sql +++ b/testdata/diff/create_policy/add_policy/diff.sql @@ -1 +1,4 @@ -CREATE POLICY user_tenant_isolation ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); +CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); +CREATE POLICY "my-policy" ON users FOR INSERT TO PUBLIC WITH CHECK ((role)::text = 'user'); +CREATE POLICY "select" ON users FOR SELECT TO PUBLIC USING (true); +CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); diff --git a/testdata/diff/create_policy/add_policy/new.sql b/testdata/diff/create_policy/add_policy/new.sql index 88cf2213..1956708d 100644 --- a/testdata/diff/create_policy/add_policy/new.sql +++ b/testdata/diff/create_policy/add_policy/new.sql @@ -1,13 +1,33 @@ CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, - tenant_id INTEGER NOT NULL + tenant_id INTEGER NOT NULL, + role VARCHAR(50) NOT NULL ); --- RLS is enabled with new policy +-- RLS is enabled with multiple policies demonstrating quoting scenarios ALTER TABLE users ENABLE ROW LEVEL SECURITY; -CREATE POLICY user_tenant_isolation ON users +-- Policy with reserved word name (requires quoting) +CREATE POLICY "select" ON users + FOR SELECT + TO PUBLIC + USING (true); + +-- Policy with mixed case name (requires quoting to preserve case) +CREATE POLICY "UserPolicy" ON users FOR ALL TO PUBLIC - USING (tenant_id = current_setting('app.current_tenant')::INTEGER); \ No newline at end of file + USING (tenant_id = current_setting('app.current_tenant')::INTEGER); + +-- Policy with special character in name (requires quoting) +CREATE POLICY "my-policy" ON users + FOR INSERT + TO PUBLIC + WITH CHECK (role = 'user'); + +-- Policy with regular snake_case name (no quoting needed) +CREATE POLICY user_tenant_isolation ON users + FOR UPDATE + TO PUBLIC + USING (tenant_id = current_setting('app.current_tenant')::INTEGER); diff --git a/testdata/diff/create_policy/add_policy/old.sql b/testdata/diff/create_policy/add_policy/old.sql index 2a0cac27..0a0b39a1 100644 --- a/testdata/diff/create_policy/add_policy/old.sql +++ b/testdata/diff/create_policy/add_policy/old.sql @@ -1,8 +1,9 @@ CREATE TABLE users ( id SERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, - tenant_id INTEGER NOT NULL + tenant_id INTEGER NOT NULL, + role VARCHAR(50) NOT NULL ); -- RLS is enabled but no policies exist yet -ALTER TABLE users ENABLE ROW LEVEL SECURITY; \ No newline at end of file +ALTER TABLE users ENABLE ROW LEVEL SECURITY; diff --git a/testdata/diff/create_policy/add_policy/plan.json b/testdata/diff/create_policy/add_policy/plan.json index 0da89393..816b4bd0 100644 --- a/testdata/diff/create_policy/add_policy/plan.json +++ b/testdata/diff/create_policy/add_policy/plan.json @@ -3,13 +3,31 @@ "pgschema_version": "1.5.0", "created_at": "1970-01-01T00:00:00Z", "source_fingerprint": { - "hash": "fb10f588a3287058ca5ba2bf9491dabe163e388b42043ebc878fadca3ef22c5c" + "hash": "dbb8387a973c070eb3a87e82500e6c4342a795d3271927271e1831dd101e2920" }, "groups": [ { "steps": [ { - "sql": "CREATE POLICY user_tenant_isolation ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);", + "sql": "CREATE POLICY \"UserPolicy\" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);", + "type": "table.policy", + "operation": "create", + "path": "public.users.UserPolicy" + }, + { + "sql": "CREATE POLICY \"my-policy\" ON users FOR INSERT TO PUBLIC WITH CHECK ((role)::text = 'user');", + "type": "table.policy", + "operation": "create", + "path": "public.users.my-policy" + }, + { + "sql": "CREATE POLICY \"select\" ON users FOR SELECT TO PUBLIC USING (true);", + "type": "table.policy", + "operation": "create", + "path": "public.users.select" + }, + { + "sql": "CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);", "type": "table.policy", "operation": "create", "path": "public.users.user_tenant_isolation" diff --git a/testdata/diff/create_policy/add_policy/plan.sql b/testdata/diff/create_policy/add_policy/plan.sql index 85da810c..ecd9291d 100644 --- a/testdata/diff/create_policy/add_policy/plan.sql +++ b/testdata/diff/create_policy/add_policy/plan.sql @@ -1 +1,7 @@ -CREATE POLICY user_tenant_isolation ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); +CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); + +CREATE POLICY "my-policy" ON users FOR INSERT TO PUBLIC WITH CHECK ((role)::text = 'user'); + +CREATE POLICY "select" ON users FOR SELECT TO PUBLIC USING (true); + +CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); diff --git a/testdata/diff/create_policy/add_policy/plan.txt b/testdata/diff/create_policy/add_policy/plan.txt index deef922b..22e99bac 100644 --- a/testdata/diff/create_policy/add_policy/plan.txt +++ b/testdata/diff/create_policy/add_policy/plan.txt @@ -5,9 +5,18 @@ Summary by type: Tables: ~ users + + UserPolicy (policy) + + my-policy (policy) + + select (policy) + user_tenant_isolation (policy) DDL to be executed: -------------------------------------------------- -CREATE POLICY user_tenant_isolation ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); +CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer); + +CREATE POLICY "my-policy" ON users FOR INSERT TO PUBLIC WITH CHECK ((role)::text = 'user'); + +CREATE POLICY "select" ON users FOR SELECT TO PUBLIC USING (true); + +CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);