Skip to content

Commit ff2bc8d

Browse files
authored
feat(parser): enhance PostgreSQL CREATE POLICY support (#2549)
1 parent 03b8fc4 commit ff2bc8d

10 files changed

Lines changed: 271 additions & 11 deletions

File tree

src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,9 @@ public <S> T visit(LockStatement lock, S context) {
329329

330330
@Override
331331
public <S> T visit(CreatePolicy createPolicy, S context) {
332+
fromItemVisitor.visitFromItem(createPolicy.getTable(), context);
333+
expressionVisitor.visitExpression(createPolicy.getUsingExpression(), context);
334+
expressionVisitor.visitExpression(createPolicy.getWithCheckExpression(), context);
332335

333336
return null;
334337
}

src/main/java/net/sf/jsqlparser/statement/create/policy/CreatePolicy.java

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@
2020
/**
2121
* PostgreSQL CREATE POLICY statement for Row Level Security (RLS).
2222
*
23-
* Syntax: CREATE POLICY name ON table_name [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] [ TO
24-
* { role_name | PUBLIC | CURRENT_USER | SESSION_USER } [, ...] ] [ USING ( using_expression ) ] [
25-
* WITH CHECK ( check_expression ) ]
23+
* Syntax: CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ] [ FOR { ALL | SELECT
24+
* | INSERT | UPDATE | DELETE } ] [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER |
25+
* SESSION_USER } [, ...] ] [ USING ( using_expression ) ] [ WITH CHECK ( check_expression ) ]
2626
*/
2727
public class CreatePolicy implements Statement {
2828

2929
private String policyName;
3030
private Table table;
31-
private String command; // ALL, SELECT, INSERT, UPDATE, DELETE
31+
private PolicyMode policyMode;
32+
private PolicyCommand policyCommand;
3233
private List<String> roles = new ArrayList<>();
3334
private Expression usingExpression;
3435
private Expression withCheckExpression;
@@ -51,15 +52,71 @@ public CreatePolicy setTable(Table table) {
5152
return this;
5253
}
5354

55+
/**
56+
* Returns the explicitly specified policy mode, or {@code null} when the {@code AS} clause was
57+
* omitted.
58+
*
59+
* @return the explicitly specified policy mode
60+
*/
61+
public PolicyMode getPolicyMode() {
62+
return policyMode;
63+
}
64+
65+
public CreatePolicy setPolicyMode(PolicyMode policyMode) {
66+
this.policyMode = policyMode;
67+
return this;
68+
}
69+
70+
/**
71+
* Returns the effective PostgreSQL policy mode, including the default for an omitted {@code AS}
72+
* clause.
73+
*
74+
* @return the explicit policy mode, or {@link PolicyMode#PERMISSIVE}
75+
*/
76+
public PolicyMode getEffectivePolicyMode() {
77+
return policyMode != null ? policyMode : PolicyMode.PERMISSIVE;
78+
}
79+
80+
/**
81+
* Returns the explicitly specified command as a string for backwards compatibility.
82+
*
83+
* @return the explicitly specified command, or {@code null} when the {@code FOR} clause was
84+
* omitted
85+
*/
5486
public String getCommand() {
55-
return command;
87+
return policyCommand != null ? policyCommand.name() : null;
5688
}
5789

5890
public CreatePolicy setCommand(String command) {
59-
this.command = command;
91+
this.policyCommand = command != null ? PolicyCommand.from(command) : null;
92+
return this;
93+
}
94+
95+
/**
96+
* Returns the explicitly specified command, or {@code null} when the {@code FOR} clause was
97+
* omitted.
98+
*
99+
* @return the explicitly specified command
100+
*/
101+
public PolicyCommand getPolicyCommand() {
102+
return policyCommand;
103+
}
104+
105+
public CreatePolicy setPolicyCommand(PolicyCommand policyCommand) {
106+
this.policyCommand = policyCommand;
60107
return this;
61108
}
62109

110+
/**
111+
* Returns the effective PostgreSQL command, including the default for an omitted {@code FOR}
112+
* clause.
113+
*
114+
* @return the explicit command, or {@link PolicyCommand#ALL}
115+
*/
116+
public PolicyCommand getEffectivePolicyCommand() {
117+
return policyCommand != null ? policyCommand : PolicyCommand.ALL;
118+
}
119+
63120
public List<String> getRoles() {
64121
return roles;
65122
}
@@ -104,8 +161,12 @@ public String toString() {
104161
builder.append(" ON ");
105162
builder.append(table.toString());
106163

107-
if (command != null) {
108-
builder.append(" FOR ").append(command);
164+
if (policyMode != null) {
165+
builder.append(" AS ").append(policyMode);
166+
}
167+
168+
if (policyCommand != null) {
169+
builder.append(" FOR ").append(policyCommand);
109170
}
110171

111172
if (roles != null && !roles.isEmpty()) {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.policy;
11+
12+
import java.util.Locale;
13+
14+
/**
15+
* Commands to which a PostgreSQL policy can apply.
16+
*/
17+
public enum PolicyCommand {
18+
ALL, SELECT, INSERT, UPDATE, DELETE;
19+
20+
public static PolicyCommand from(String command) {
21+
return Enum.valueOf(PolicyCommand.class, command.toUpperCase(Locale.ROOT));
22+
}
23+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.statement.create.policy;
11+
12+
import java.util.Locale;
13+
14+
/**
15+
* PostgreSQL policy evaluation mode.
16+
*/
17+
public enum PolicyMode {
18+
PERMISSIVE, RESTRICTIVE;
19+
20+
public static PolicyMode from(String mode) {
21+
return Enum.valueOf(PolicyMode.class, mode.toUpperCase(Locale.ROOT));
22+
}
23+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.util.deparser;
11+
12+
import net.sf.jsqlparser.expression.ExpressionVisitor;
13+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
14+
15+
public class CreatePolicyDeParser extends AbstractDeParser<CreatePolicy> {
16+
17+
private final ExpressionVisitor<StringBuilder> expressionVisitor;
18+
19+
public CreatePolicyDeParser(StringBuilder builder) {
20+
super(builder);
21+
ExpressionDeParser expressionDeParser = new ExpressionDeParser();
22+
expressionDeParser.setBuilder(builder);
23+
this.expressionVisitor = expressionDeParser;
24+
}
25+
26+
public CreatePolicyDeParser(ExpressionVisitor<StringBuilder> expressionVisitor,
27+
StringBuilder builder) {
28+
super(builder);
29+
this.expressionVisitor = expressionVisitor;
30+
}
31+
32+
@Override
33+
public void deParse(CreatePolicy createPolicy) {
34+
builder.append("CREATE POLICY ").append(createPolicy.getPolicyName());
35+
builder.append(" ON ").append(createPolicy.getTable());
36+
37+
if (createPolicy.getPolicyMode() != null) {
38+
builder.append(" AS ").append(createPolicy.getPolicyMode());
39+
}
40+
41+
if (createPolicy.getPolicyCommand() != null) {
42+
builder.append(" FOR ").append(createPolicy.getPolicyCommand());
43+
}
44+
45+
if (createPolicy.getRoles() != null && !createPolicy.getRoles().isEmpty()) {
46+
builder.append(" TO ").append(String.join(", ", createPolicy.getRoles()));
47+
}
48+
49+
if (createPolicy.getUsingExpression() != null) {
50+
builder.append(" USING (");
51+
createPolicy.getUsingExpression().accept(expressionVisitor, null);
52+
builder.append(")");
53+
}
54+
55+
if (createPolicy.getWithCheckExpression() != null) {
56+
builder.append(" WITH CHECK (");
57+
createPolicy.getWithCheckExpression().accept(expressionVisitor, null);
58+
builder.append(")");
59+
}
60+
}
61+
}

src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ public <S> StringBuilder visit(LockStatement lock, S context) {
558558

559559
@Override
560560
public <S> StringBuilder visit(CreatePolicy createPolicy, S context) {
561-
builder.append(createPolicy.toString());
561+
new CreatePolicyDeParser(expressionDeParser, builder).deParse(createPolicy);
562562
return builder;
563563
}
564564
}

src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,9 @@ public void visit(Export export) {
650650

651651
@Override
652652
public <S> Void visit(CreatePolicy createPolicy, S context) {
653-
// TODO: not yet implemented
653+
validateOptionalFromItem(createPolicy.getTable());
654+
validateOptionalExpression(createPolicy.getUsingExpression());
655+
validateOptionalExpression(createPolicy.getWithCheckExpression());
654656
return null;
655657
}
656658

src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15131,6 +15131,7 @@ CreatePolicy CreatePolicy() #CreatePolicy:
1513115131
CreatePolicy createPolicy = new CreatePolicy();
1513215132
String policyName;
1513315133
Table table;
15134+
String policyMode = null;
1513415135
Token commandToken = null;
1513515136
String roleName;
1513615137
Expression usingExpr = null;
@@ -15140,14 +15141,20 @@ CreatePolicy CreatePolicy() #CreatePolicy:
1514015141
<K_POLICY> policyName=RelObjectName() { createPolicy.setPolicyName(policyName); }
1514115142
<K_ON> table=Table() { createPolicy.setTable(table); }
1514215143

15144+
[ <K_AS>
15145+
LOOKAHEAD({ isKeywordAhead("PERMISSIVE") || isKeywordAhead("RESTRICTIVE") })
15146+
policyMode=RelObjectName()
15147+
{ createPolicy.setPolicyMode(PolicyMode.from(policyMode)); }
15148+
]
15149+
1514315150
[ <K_FOR>
1514415151
( commandToken=<K_ALL>
1514515152
| commandToken=<K_SELECT>
1514615153
| commandToken=<K_INSERT>
1514715154
| commandToken=<K_UPDATE>
1514815155
| commandToken=<K_DELETE>
1514915156
)
15150-
{ createPolicy.setCommand(commandToken.image); }
15157+
{ createPolicy.setPolicyCommand(PolicyCommand.from(commandToken.image)); }
1515115158
]
1515215159

1515315160
[ <K_TO>

src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
1414
import net.sf.jsqlparser.statement.Statement;
1515
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
16+
import net.sf.jsqlparser.statement.create.policy.PolicyCommand;
17+
import net.sf.jsqlparser.statement.create.policy.PolicyMode;
1618
import org.junit.jupiter.api.Test;
1719

1820
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
@@ -33,6 +35,10 @@ public void testCreatePolicyBasic() throws JSQLParserException {
3335
CreatePolicy policy = (CreatePolicy) stmt;
3436
assertEquals("policy_name", policy.getPolicyName());
3537
assertEquals("table_name", policy.getTable().getName());
38+
assertNull(policy.getPolicyMode());
39+
assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode());
40+
assertNull(policy.getPolicyCommand());
41+
assertEquals(PolicyCommand.ALL, policy.getEffectivePolicyCommand());
3642
}
3743

3844
@Test
@@ -55,6 +61,7 @@ public void testCreatePolicyWithForClause() throws JSQLParserException {
5561

5662
CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
5763
assertEquals("SELECT", policy.getCommand());
64+
assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand());
5865
}
5966

6067
@Test
@@ -65,9 +72,40 @@ public void testCreatePolicyWithAllCommands() throws JSQLParserException {
6572
assertSqlCanBeParsedAndDeparsed(sql, true);
6673
CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
6774
assertEquals(cmd, policy.getCommand());
75+
assertEquals(PolicyCommand.from(cmd), policy.getPolicyCommand());
6876
}
6977
}
7078

79+
@Test
80+
public void testCreateRestrictivePolicy() throws JSQLParserException {
81+
String sql = "CREATE POLICY tenant_policy ON users AS RESTRICTIVE FOR SELECT TO app "
82+
+ "USING (tenant_id = current_user)";
83+
assertSqlCanBeParsedAndDeparsed(sql, true);
84+
85+
CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
86+
assertEquals(PolicyMode.RESTRICTIVE, policy.getPolicyMode());
87+
assertEquals(PolicyMode.RESTRICTIVE, policy.getEffectivePolicyMode());
88+
assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand());
89+
assertEquals("app", policy.getRoles().get(0));
90+
assertNotNull(policy.getUsingExpression());
91+
}
92+
93+
@Test
94+
public void testCreateExplicitPermissivePolicy() throws JSQLParserException {
95+
String sql = "CREATE POLICY tenant_policy ON users AS PERMISSIVE";
96+
assertSqlCanBeParsedAndDeparsed(sql, true);
97+
98+
CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql);
99+
assertEquals(PolicyMode.PERMISSIVE, policy.getPolicyMode());
100+
assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode());
101+
}
102+
103+
@Test
104+
public void testCreatePolicyRejectsUnknownMode() {
105+
assertThrows(JSQLParserException.class,
106+
() -> CCJSqlParserUtil.parse("CREATE POLICY p ON t AS UNKNOWN"));
107+
}
108+
71109
@Test
72110
public void testCreatePolicyWithSingleRole() throws JSQLParserException {
73111
String sql = "CREATE POLICY policy1 ON table1 TO role1";
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*-
2+
* #%L
3+
* JSQLParser library
4+
* %%
5+
* Copyright (C) 2004 - 2026 JSQLParser
6+
* %%
7+
* Dual licensed under GNU LGPL 2.1 or Apache License 2.0
8+
* #L%
9+
*/
10+
package net.sf.jsqlparser.util.deparser;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
14+
import net.sf.jsqlparser.JSQLParserException;
15+
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
16+
import net.sf.jsqlparser.schema.Column;
17+
import net.sf.jsqlparser.statement.create.policy.CreatePolicy;
18+
19+
import org.junit.jupiter.api.Test;
20+
21+
public class CreatePolicyDeParserTest {
22+
23+
@Test
24+
public void testUseExternalExpressionDeParser() throws JSQLParserException {
25+
StringBuilder builder = new StringBuilder();
26+
ExpressionDeParser expressionDeParser = new ExpressionDeParser() {
27+
@Override
28+
public <S> StringBuilder visit(Column column, S context) {
29+
getBuilder().append('"').append(column.getColumnName()).append('"');
30+
return getBuilder();
31+
}
32+
};
33+
expressionDeParser.setBuilder(builder);
34+
35+
CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(
36+
"CREATE POLICY tenant_policy ON users AS RESTRICTIVE USING (tenant_id = owner_id)");
37+
new CreatePolicyDeParser(expressionDeParser, builder).deParse(policy);
38+
39+
assertEquals("CREATE POLICY tenant_policy ON users AS RESTRICTIVE "
40+
+ "USING (\"tenant_id\" = \"owner_id\")", builder.toString());
41+
}
42+
}

0 commit comments

Comments
 (0)