Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions internal/diff/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ func generatePolicySQL(policy *ir.RLSPolicy, targetSchema string) string {

// Add USING clause if present
if policy.Using != "" {
policyStmt += fmt.Sprintf(" USING %s", policy.Using)
policyStmt += fmt.Sprintf(" USING %s", ensureParentheses(policy.Using))
}

// Add WITH CHECK clause if present
if policy.WithCheck != "" {
policyStmt += fmt.Sprintf(" WITH CHECK %s", policy.WithCheck)
policyStmt += fmt.Sprintf(" WITH CHECK %s", ensureParentheses(policy.WithCheck))
}

return policyStmt + ";"
Expand Down Expand Up @@ -142,12 +142,12 @@ func generateAlterPolicySQL(old, new *ir.RLSPolicy, targetSchema string) string

// Add USING clause if it changed
if usingChange {
alterStmt += fmt.Sprintf(" USING %s", new.Using)
alterStmt += fmt.Sprintf(" USING %s", ensureParentheses(new.Using))
}

// Add WITH CHECK clause if it changed
if withCheckChange {
alterStmt += fmt.Sprintf(" WITH CHECK %s", new.WithCheck)
alterStmt += fmt.Sprintf(" WITH CHECK %s", ensureParentheses(new.WithCheck))
}

// If no changes detected, this shouldn't happen
Expand All @@ -158,6 +158,20 @@ func generateAlterPolicySQL(old, new *ir.RLSPolicy, targetSchema string) string
return alterStmt + ";"
}

// ensureParentheses wraps an expression in parentheses if not already wrapped.
// PostgreSQL's CREATE POLICY and ALTER POLICY syntax requires USING (expr) and WITH CHECK (expr).
// pg_get_expr may return expressions without outer parentheses (e.g., simple function calls).
func ensureParentheses(expr string) string {
if expr == "" {
return expr
}
expr = strings.TrimSpace(expr)
if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") {
return expr
}
return "(" + expr + ")"
}

// roleListsEqualCaseInsensitive compares two role lists for equality (case-insensitive)
// PostgreSQL role names are case-insensitive by default
func roleListsEqualCaseInsensitive(oldRoles, newRoles []string) bool {
Expand Down
1 change: 1 addition & 0 deletions testdata/diff/create_policy/add_policy/diff.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_user_access ON orders FOR SELECT TO PUBLIC USING (user_id IN ( SELECT users.id FROM users));
CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);
CREATE POLICY admin_only ON users FOR DELETE TO PUBLIC USING (is_admin());
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);
10 changes: 10 additions & 0 deletions testdata/diff/create_policy/add_policy/new.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,22 @@ CREATE TABLE orders (
total NUMERIC(10,2)
);

-- Function to check if user is admin (for Issue #259)
CREATE FUNCTION is_admin() RETURNS boolean LANGUAGE sql AS $$ SELECT true $$;

-- RLS is enabled with multiple policies demonstrating quoting scenarios
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

-- RLS on orders with policy referencing users table (Issue #224)
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- Policy with function call in USING clause (Issue #259)
-- Tests that parentheses are correctly preserved around function calls
CREATE POLICY admin_only ON users
FOR DELETE
TO PUBLIC
USING (is_admin());

-- Policy with reserved word name (requires quoting)
CREATE POLICY "select" ON users
FOR SELECT
Expand Down
3 changes: 3 additions & 0 deletions testdata/diff/create_policy/add_policy/old.sql
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,8 @@ CREATE TABLE orders (
total NUMERIC(10,2)
);

-- Function to check if user is admin (for Issue #259)
CREATE FUNCTION is_admin() RETURNS boolean LANGUAGE sql AS $$ SELECT true $$;

-- RLS is enabled but no policies exist yet
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
8 changes: 7 additions & 1 deletion testdata/diff/create_policy/add_policy/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.6.1",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "9323772d9678bd1630383ff088214914f1c01c427086930540c96be45e4be387"
"hash": "261f1966678419184a6d17b5ad2c09d590b77fff9dbe01fd56849d53f3079c8e"
},
"groups": [
{
Expand All @@ -26,6 +26,12 @@
"operation": "create",
"path": "public.users.UserPolicy"
},
{
"sql": "CREATE POLICY admin_only ON users FOR DELETE TO PUBLIC USING (is_admin());",
"type": "table.policy",
"operation": "create",
"path": "public.users.admin_only"
},
{
"sql": "CREATE POLICY \"my-policy\" ON users FOR INSERT TO PUBLIC WITH CHECK ((role)::text = 'user');",
"type": "table.policy",
Expand Down
2 changes: 2 additions & 0 deletions testdata/diff/create_policy/add_policy/plan.sql
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ CREATE POLICY orders_user_access ON orders FOR SELECT TO PUBLIC USING (user_id I

CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);

CREATE POLICY admin_only ON users FOR DELETE TO PUBLIC USING (is_admin());

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);
Expand Down
3 changes: 3 additions & 0 deletions testdata/diff/create_policy/add_policy/plan.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Tables:
+ orders (rls)
~ users
+ UserPolicy (policy)
+ admin_only (policy)
+ my-policy (policy)
+ select (policy)
+ user_tenant_isolation (policy)
Expand All @@ -22,6 +23,8 @@ CREATE POLICY orders_user_access ON orders FOR SELECT TO PUBLIC USING (user_id I

CREATE POLICY "UserPolicy" ON users TO PUBLIC USING (tenant_id = current_setting('app.current_tenant')::integer);

CREATE POLICY admin_only ON users FOR DELETE TO PUBLIC USING (is_admin());

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);
Expand Down
Loading