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
23 changes: 1 addition & 22 deletions ir/normalize.go
Original file line number Diff line number Diff line change
Expand Up @@ -819,28 +819,7 @@ func normalizeExpressionParentheses(expr string) string {
expr = fmt.Sprintf("(%s)", expr)
}

// Step 2: Remove unnecessary parentheses around function calls within the expression
// Specifically targets patterns like (function_name(...)) -> function_name(...)
// This pattern looks for:
// \( - opening parenthesis
// ([a-zA-Z_][a-zA-Z0-9_]*) - function name (captured)
// \( - opening parenthesis for function call
// ([^)]*) - function arguments (captured, non-greedy to avoid matching nested parens)
// \) - closing parenthesis for function call
// \) - closing parenthesis around the whole function
functionParensRegex := regexp.MustCompile(`\(([a-zA-Z_][a-zA-Z0-9_]*\([^)]*\))\)`)

// Replace (function(...)) with function(...)
// Keep applying until no more matches to handle nested cases
for {
original := expr
expr = functionParensRegex.ReplaceAllString(expr, "$1")
if expr == original {
break
}
}

// Step 3: Normalize redundant type casts in function arguments
// Step 2: Normalize redundant type casts in function arguments
// Pattern: 'text'::text -> 'text' (removing redundant text cast from literals)
// IMPORTANT: Do NOT match when followed by [] (array cast is semantically significant)
// e.g., '{nested,key}'::text[] must be preserved as-is
Expand Down
69 changes: 69 additions & 0 deletions ir/normalize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,75 @@ func TestStripSchemaFromReturnType(t *testing.T) {
}
}

func TestNormalizeExpressionParentheses(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "simple expression gets wrapped",
input: "tenant_id = 1",
expected: "(tenant_id = 1)",
},
{
name: "already parenthesized",
input: "(tenant_id = 1)",
expected: "(tenant_id = 1)",
},
{
name: "nested function call preserved",
input: "(id IN ( SELECT unnest(get_user_assigned_projects()) AS unnest))",
expected: "(id IN ( SELECT unnest(get_user_assigned_projects()) AS unnest))",
},
{
name: "simple function call preserved",
input: "(has_scope('admin'))",
expected: "(has_scope('admin'))",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizeExpressionParentheses(tt.input)
if result != tt.expected {
t.Errorf("normalizeExpressionParentheses(%q) = %q, want %q", tt.input, result, tt.expected)
}
})
}
}
Comment on lines +324 to +360
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing test case for type-cast pattern

The new TestNormalizeExpressionParentheses tests verify the fix for nested function calls and basic wrapping, but there's no test case for the expression pattern that motivated the old regex — the type-cast form (current_setting('app.current_tenant'))::integer.

Adding a case like this would document that the cast suffix is preserved and guard against a future regression if someone tries to "clean up" the function by re-adding a similar regex:

{
    name:     "function call with type cast preserved",
    input:    "(tenant_id = (current_setting('app.current_tenant'))::integer)",
    expected: "(tenant_id = (current_setting('app.current_tenant'))::integer)",
},


func TestNormalizePolicyExpression(t *testing.T) {
tests := []struct {
name string
expr string
tableSchema string
expected string
}{
{
name: "nested function call in policy expression",
expr: "(id IN ( SELECT unnest(get_user_assigned_projects()) AS unnest))",
tableSchema: "public",
expected: "(id IN ( SELECT unnest(get_user_assigned_projects()) AS unnest))",
},
{
name: "schema-qualified nested function call",
expr: "(id IN ( SELECT unnest(public.get_user_assigned_projects()) AS unnest))",
tableSchema: "public",
expected: "(id IN ( SELECT unnest(get_user_assigned_projects()) AS unnest))",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := normalizePolicyExpression(tt.expr, tt.tableSchema)
if result != tt.expected {
t.Errorf("normalizePolicyExpression(%q, %q) = %q, want %q", tt.expr, tt.tableSchema, result, tt.expected)
}
})
}
}

func TestNormalizeCheckClause(t *testing.T) {
tests := []struct {
name string
Expand Down
4 changes: 2 additions & 2 deletions testdata/diff/create_policy/add_policy/diff.sql
Original file line number Diff line number Diff line change
@@ -1,7 +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 "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);
CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = (current_setting('app.current_tenant'))::integer);
4 changes: 2 additions & 2 deletions testdata/diff/create_policy/add_policy/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"path": "public.orders.orders_user_access"
},
{
"sql": "CREATE POLICY \"UserPolicy\" 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"
Expand All @@ -45,7 +45,7 @@
"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);",
"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"
Expand Down
4 changes: 2 additions & 2 deletions testdata/diff/create_policy/add_policy/plan.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ 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 "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);
CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = (current_setting('app.current_tenant'))::integer);
4 changes: 2 additions & 2 deletions testdata/diff/create_policy/add_policy/plan.txt
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ 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 "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);
CREATE POLICY user_tenant_isolation ON users FOR UPDATE TO PUBLIC USING (tenant_id = (current_setting('app.current_tenant'))::integer);
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/alter_policy_roles/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "a24989791c9ba13d2d21f70cd0405e0e509c55341872b552ab87a0c79d238ddd"
"hash": "a5fcd005530df97b095b020e1a8c2fb12333baf056c3a2318bc34f2157893877"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/alter_policy_using/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "e7aac12e5d350ee9d014b756c838017ddd0c17180b7baa33300f4a773c4c89b1"
"hash": "b23d70f93fe88b9bfcf8d05f46e3faa60d54cf469813e2ead871e9c9d9231ab7"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/disable_rls/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "a24989791c9ba13d2d21f70cd0405e0e509c55341872b552ab87a0c79d238ddd"
"hash": "a5fcd005530df97b095b020e1a8c2fb12333baf056c3a2318bc34f2157893877"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/drop_policy/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "a24989791c9ba13d2d21f70cd0405e0e509c55341872b552ab87a0c79d238ddd"
"hash": "a5fcd005530df97b095b020e1a8c2fb12333baf056c3a2318bc34f2157893877"
},
"groups": [
{
Expand Down
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/force_rls/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "5e4ac40b64a7ec01d1f41af5c61ba420960e98b8ac166d757ee424ea3f10474f"
"hash": "f26c6614d22a712e9314414c386efca5bb723c12064c63e173483f718716f193"
},
"groups": [
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE projects ADD COLUMN description text;
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
CREATE FUNCTION get_user_assigned_projects() RETURNS integer[]
LANGUAGE sql STABLE AS $$ SELECT ARRAY[1, 2, 3] $$;

CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT
);

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Policy unchanged, but table has new column
CREATE POLICY project_access ON projects
FOR SELECT
TO PUBLIC
USING (id IN (SELECT unnest(get_user_assigned_projects()) AS unnest));
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
CREATE FUNCTION get_user_assigned_projects() RETURNS integer[]
LANGUAGE sql STABLE AS $$ SELECT ARRAY[1, 2, 3] $$;

CREATE TABLE projects (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL
);

ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

CREATE POLICY project_access ON projects
FOR SELECT
TO PUBLIC
USING (id IN (SELECT unnest(get_user_assigned_projects()) AS unnest));
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": "1.0.0",
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "37d3ebdd423c5db41ba042431c6328705f50593fe2c8f5163de497aa4eee08b8"
},
"groups": [
{
"steps": [
{
"sql": "ALTER TABLE projects ADD COLUMN description text;",
"type": "table.column",
"operation": "create",
"path": "public.projects.description"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE projects ADD COLUMN description text;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Plan: 1 to modify.

Summary by type:
tables: 1 to modify

Tables:
~ projects
+ description (column)

DDL to be executed:
--------------------------------------------------

ALTER TABLE projects ADD COLUMN description text;
2 changes: 1 addition & 1 deletion testdata/diff/create_policy/remove_force_rls/plan.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"pgschema_version": "1.8.0",
"created_at": "1970-01-01T00:00:00Z",
"source_fingerprint": {
"hash": "0d3cfe3e8283f19f2e5ea37cfc7f2cbab91b543bf77aac99a2a912a17bc6e3f1"
"hash": "6d556186d2ea5db5c7aebfd47e9a7d91430bbed6f2c9052964d2d17424b6984d"
},
"groups": [
{
Expand Down
Loading