From 6fca434cc0c8ec2d15eec1a6cf4629c2338f7772 Mon Sep 17 00:00:00 2001 From: Minjae Lee Date: Sat, 5 Sep 2026 11:19:25 +0900 Subject: [PATCH] feat(parser): enhance PostgreSQL CREATE POLICY support --- .../statement/StatementVisitorAdapter.java | 3 + .../statement/create/policy/CreatePolicy.java | 77 +++++++++++++++++-- .../create/policy/PolicyCommand.java | 23 ++++++ .../statement/create/policy/PolicyMode.java | 23 ++++++ .../util/deparser/CreatePolicyDeParser.java | 61 +++++++++++++++ .../util/deparser/StatementDeParser.java | 2 +- .../validator/StatementValidator.java | 4 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 9 ++- .../statement/create/CreatePolicyTest.java | 38 +++++++++ .../deparser/CreatePolicyDeParserTest.java | 42 ++++++++++ 10 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyCommand.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyMode.java create mode 100644 src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java create mode 100644 src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index bc72947e9..ad4065126 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -329,6 +329,9 @@ public T visit(LockStatement lock, S context) { @Override public T visit(CreatePolicy createPolicy, S context) { + fromItemVisitor.visitFromItem(createPolicy.getTable(), context); + expressionVisitor.visitExpression(createPolicy.getUsingExpression(), context); + expressionVisitor.visitExpression(createPolicy.getWithCheckExpression(), context); return null; } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/policy/CreatePolicy.java b/src/main/java/net/sf/jsqlparser/statement/create/policy/CreatePolicy.java index 7c11636aa..2158e7efa 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/policy/CreatePolicy.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/policy/CreatePolicy.java @@ -20,15 +20,16 @@ /** * PostgreSQL CREATE POLICY statement for Row Level Security (RLS). * - * Syntax: CREATE POLICY name ON table_name [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ] [ TO - * { role_name | PUBLIC | CURRENT_USER | SESSION_USER } [, ...] ] [ USING ( using_expression ) ] [ - * WITH CHECK ( check_expression ) ] + * Syntax: CREATE POLICY name ON table_name [ AS { PERMISSIVE | RESTRICTIVE } ] [ FOR { ALL | SELECT + * | INSERT | UPDATE | DELETE } ] [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | + * SESSION_USER } [, ...] ] [ USING ( using_expression ) ] [ WITH CHECK ( check_expression ) ] */ public class CreatePolicy implements Statement { private String policyName; private Table table; - private String command; // ALL, SELECT, INSERT, UPDATE, DELETE + private PolicyMode policyMode; + private PolicyCommand policyCommand; private List roles = new ArrayList<>(); private Expression usingExpression; private Expression withCheckExpression; @@ -51,15 +52,71 @@ public CreatePolicy setTable(Table table) { return this; } + /** + * Returns the explicitly specified policy mode, or {@code null} when the {@code AS} clause was + * omitted. + * + * @return the explicitly specified policy mode + */ + public PolicyMode getPolicyMode() { + return policyMode; + } + + public CreatePolicy setPolicyMode(PolicyMode policyMode) { + this.policyMode = policyMode; + return this; + } + + /** + * Returns the effective PostgreSQL policy mode, including the default for an omitted {@code AS} + * clause. + * + * @return the explicit policy mode, or {@link PolicyMode#PERMISSIVE} + */ + public PolicyMode getEffectivePolicyMode() { + return policyMode != null ? policyMode : PolicyMode.PERMISSIVE; + } + + /** + * Returns the explicitly specified command as a string for backwards compatibility. + * + * @return the explicitly specified command, or {@code null} when the {@code FOR} clause was + * omitted + */ public String getCommand() { - return command; + return policyCommand != null ? policyCommand.name() : null; } public CreatePolicy setCommand(String command) { - this.command = command; + this.policyCommand = command != null ? PolicyCommand.from(command) : null; + return this; + } + + /** + * Returns the explicitly specified command, or {@code null} when the {@code FOR} clause was + * omitted. + * + * @return the explicitly specified command + */ + public PolicyCommand getPolicyCommand() { + return policyCommand; + } + + public CreatePolicy setPolicyCommand(PolicyCommand policyCommand) { + this.policyCommand = policyCommand; return this; } + /** + * Returns the effective PostgreSQL command, including the default for an omitted {@code FOR} + * clause. + * + * @return the explicit command, or {@link PolicyCommand#ALL} + */ + public PolicyCommand getEffectivePolicyCommand() { + return policyCommand != null ? policyCommand : PolicyCommand.ALL; + } + public List getRoles() { return roles; } @@ -104,8 +161,12 @@ public String toString() { builder.append(" ON "); builder.append(table.toString()); - if (command != null) { - builder.append(" FOR ").append(command); + if (policyMode != null) { + builder.append(" AS ").append(policyMode); + } + + if (policyCommand != null) { + builder.append(" FOR ").append(policyCommand); } if (roles != null && !roles.isEmpty()) { diff --git a/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyCommand.java b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyCommand.java new file mode 100644 index 000000000..b59f75464 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyCommand.java @@ -0,0 +1,23 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.policy; + +import java.util.Locale; + +/** + * Commands to which a PostgreSQL policy can apply. + */ +public enum PolicyCommand { + ALL, SELECT, INSERT, UPDATE, DELETE; + + public static PolicyCommand from(String command) { + return Enum.valueOf(PolicyCommand.class, command.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyMode.java b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyMode.java new file mode 100644 index 000000000..86b56b090 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyMode.java @@ -0,0 +1,23 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.policy; + +import java.util.Locale; + +/** + * PostgreSQL policy evaluation mode. + */ +public enum PolicyMode { + PERMISSIVE, RESTRICTIVE; + + public static PolicyMode from(String mode) { + return Enum.valueOf(PolicyMode.class, mode.toUpperCase(Locale.ROOT)); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java new file mode 100644 index 000000000..a470990bf --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java @@ -0,0 +1,61 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.util.deparser; + +import net.sf.jsqlparser.expression.ExpressionVisitor; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; + +public class CreatePolicyDeParser extends AbstractDeParser { + + private final ExpressionVisitor expressionVisitor; + + public CreatePolicyDeParser(StringBuilder builder) { + super(builder); + ExpressionDeParser expressionDeParser = new ExpressionDeParser(); + expressionDeParser.setBuilder(builder); + this.expressionVisitor = expressionDeParser; + } + + public CreatePolicyDeParser(ExpressionVisitor expressionVisitor, + StringBuilder builder) { + super(builder); + this.expressionVisitor = expressionVisitor; + } + + @Override + public void deParse(CreatePolicy createPolicy) { + builder.append("CREATE POLICY ").append(createPolicy.getPolicyName()); + builder.append(" ON ").append(createPolicy.getTable()); + + if (createPolicy.getPolicyMode() != null) { + builder.append(" AS ").append(createPolicy.getPolicyMode()); + } + + if (createPolicy.getPolicyCommand() != null) { + builder.append(" FOR ").append(createPolicy.getPolicyCommand()); + } + + if (createPolicy.getRoles() != null && !createPolicy.getRoles().isEmpty()) { + builder.append(" TO ").append(String.join(", ", createPolicy.getRoles())); + } + + if (createPolicy.getUsingExpression() != null) { + builder.append(" USING ("); + createPolicy.getUsingExpression().accept(expressionVisitor, null); + builder.append(")"); + } + + if (createPolicy.getWithCheckExpression() != null) { + builder.append(" WITH CHECK ("); + createPolicy.getWithCheckExpression().accept(expressionVisitor, null); + builder.append(")"); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java index 0b23109ec..c00ad7cbd 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -558,7 +558,7 @@ public StringBuilder visit(LockStatement lock, S context) { @Override public StringBuilder visit(CreatePolicy createPolicy, S context) { - builder.append(createPolicy.toString()); + new CreatePolicyDeParser(expressionDeParser, builder).deParse(createPolicy); return builder; } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 30ae5f6ab..185760962 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -650,7 +650,9 @@ public void visit(Export export) { @Override public Void visit(CreatePolicy createPolicy, S context) { - // TODO: not yet implemented + validateOptionalFromItem(createPolicy.getTable()); + validateOptionalExpression(createPolicy.getUsingExpression()); + validateOptionalExpression(createPolicy.getWithCheckExpression()); return null; } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 06a77c92f..8caefdb5c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -15131,6 +15131,7 @@ CreatePolicy CreatePolicy() #CreatePolicy: CreatePolicy createPolicy = new CreatePolicy(); String policyName; Table table; + String policyMode = null; Token commandToken = null; String roleName; Expression usingExpr = null; @@ -15140,6 +15141,12 @@ CreatePolicy CreatePolicy() #CreatePolicy: policyName=RelObjectName() { createPolicy.setPolicyName(policyName); } table=Table() { createPolicy.setTable(table); } + [ + LOOKAHEAD({ isKeywordAhead("PERMISSIVE") || isKeywordAhead("RESTRICTIVE") }) + policyMode=RelObjectName() + { createPolicy.setPolicyMode(PolicyMode.from(policyMode)); } + ] + [ ( commandToken= | commandToken= @@ -15147,7 +15154,7 @@ CreatePolicy CreatePolicy() #CreatePolicy: | commandToken= | commandToken= ) - { createPolicy.setCommand(commandToken.image); } + { createPolicy.setPolicyCommand(PolicyCommand.from(commandToken.image)); } ] [ diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java index 829efd2c7..90fed32e1 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreatePolicyTest.java @@ -13,6 +13,8 @@ import net.sf.jsqlparser.parser.CCJSqlParserUtil; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.policy.PolicyCommand; +import net.sf.jsqlparser.statement.create.policy.PolicyMode; import org.junit.jupiter.api.Test; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; @@ -33,6 +35,10 @@ public void testCreatePolicyBasic() throws JSQLParserException { CreatePolicy policy = (CreatePolicy) stmt; assertEquals("policy_name", policy.getPolicyName()); assertEquals("table_name", policy.getTable().getName()); + assertNull(policy.getPolicyMode()); + assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode()); + assertNull(policy.getPolicyCommand()); + assertEquals(PolicyCommand.ALL, policy.getEffectivePolicyCommand()); } @Test @@ -55,6 +61,7 @@ public void testCreatePolicyWithForClause() throws JSQLParserException { CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); assertEquals("SELECT", policy.getCommand()); + assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand()); } @Test @@ -65,9 +72,40 @@ public void testCreatePolicyWithAllCommands() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed(sql, true); CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); assertEquals(cmd, policy.getCommand()); + assertEquals(PolicyCommand.from(cmd), policy.getPolicyCommand()); } } + @Test + public void testCreateRestrictivePolicy() throws JSQLParserException { + String sql = "CREATE POLICY tenant_policy ON users AS RESTRICTIVE FOR SELECT TO app " + + "USING (tenant_id = current_user)"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); + assertEquals(PolicyMode.RESTRICTIVE, policy.getPolicyMode()); + assertEquals(PolicyMode.RESTRICTIVE, policy.getEffectivePolicyMode()); + assertEquals(PolicyCommand.SELECT, policy.getPolicyCommand()); + assertEquals("app", policy.getRoles().get(0)); + assertNotNull(policy.getUsingExpression()); + } + + @Test + public void testCreateExplicitPermissivePolicy() throws JSQLParserException { + String sql = "CREATE POLICY tenant_policy ON users AS PERMISSIVE"; + assertSqlCanBeParsedAndDeparsed(sql, true); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse(sql); + assertEquals(PolicyMode.PERMISSIVE, policy.getPolicyMode()); + assertEquals(PolicyMode.PERMISSIVE, policy.getEffectivePolicyMode()); + } + + @Test + public void testCreatePolicyRejectsUnknownMode() { + assertThrows(JSQLParserException.class, + () -> CCJSqlParserUtil.parse("CREATE POLICY p ON t AS UNKNOWN")); + } + @Test public void testCreatePolicyWithSingleRole() throws JSQLParserException { String sql = "CREATE POLICY policy1 ON table1 TO role1"; diff --git a/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java b/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java new file mode 100644 index 000000000..41428e00b --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParserTest.java @@ -0,0 +1,42 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2026 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.util.deparser; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; + +import org.junit.jupiter.api.Test; + +public class CreatePolicyDeParserTest { + + @Test + public void testUseExternalExpressionDeParser() throws JSQLParserException { + StringBuilder builder = new StringBuilder(); + ExpressionDeParser expressionDeParser = new ExpressionDeParser() { + @Override + public StringBuilder visit(Column column, S context) { + getBuilder().append('"').append(column.getColumnName()).append('"'); + return getBuilder(); + } + }; + expressionDeParser.setBuilder(builder); + + CreatePolicy policy = (CreatePolicy) CCJSqlParserUtil.parse( + "CREATE POLICY tenant_policy ON users AS RESTRICTIVE USING (tenant_id = owner_id)"); + new CreatePolicyDeParser(expressionDeParser, builder).deParse(policy); + + assertEquals("CREATE POLICY tenant_policy ON users AS RESTRICTIVE " + + "USING (\"tenant_id\" = \"owner_id\")", builder.toString()); + } +}