diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index ca8a3777f..2c7a89876 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.alter.AlterRelation; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; @@ -697,4 +699,20 @@ default T visit(AlterTablespaceMove statement, S context) { default void visit(AlterTablespaceMove statement) { visit(statement, null); } + + default T visit(AlterPolicy statement, S context) { + return null; + } + + default void visit(AlterPolicy statement) { + visit(statement, null); + } + + default T visit(DropPolicy statement, S context) { + return null; + } + + default void visit(DropPolicy statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/alter/AlterPolicy.java b/src/main/java/net/sf/jsqlparser/statement/alter/AlterPolicy.java new file mode 100644 index 000000000..6cb23c086 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/alter/AlterPolicy.java @@ -0,0 +1,83 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.alter; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.create.policy.PolicyOptions; + +public class AlterPolicy implements Statement { + private String policyName; + private net.sf.jsqlparser.schema.Table table; + private String newName; + private PolicyOptions options = new PolicyOptions(); + + + public String getPolicyName() { + return policyName; + } + + public AlterPolicy setPolicyName(String policyName) { + this.policyName = policyName; + return this; + } + + public net.sf.jsqlparser.schema.Table getTable() { + return table; + } + + public AlterPolicy setTable(net.sf.jsqlparser.schema.Table table) { + this.table = table; + return this; + } + + + public String getNewName() { + return newName; + } + + public void setNewName(String newName) { + this.newName = newName; + if (newName != null) { + options = new PolicyOptions(); + } + } + + public PolicyOptions getOptions() { + return options; + } + + public void setOptions(PolicyOptions options) { + this.options = java.util.Objects.requireNonNull(options); + newName = null; + } + + public StringBuilder appendTo(StringBuilder sql, + java.util.function.Consumer printer) { + sql.append("ALTER POLICY ").append(policyName).append(" ON ").append(table); + if (newName != null) { + sql.append(" RENAME TO ").append(newName); + } else { + options.appendTo(sql, printer); + } + return sql; + } + + @Override + public String toString() { + StringBuilder b = new StringBuilder(); + return appendTo(b, b::append).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} 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 2158e7efa..caf424359 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 @@ -14,7 +14,7 @@ import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; -import java.util.ArrayList; +import java.util.function.Consumer; import java.util.List; /** @@ -30,9 +30,16 @@ public class CreatePolicy implements Statement { private Table table; private PolicyMode policyMode; private PolicyCommand policyCommand; - private List roles = new ArrayList<>(); - private Expression usingExpression; - private Expression withCheckExpression; + private PolicyOptions options = new PolicyOptions(); + + public PolicyOptions getOptions() { + return options; + } + + public CreatePolicy setOptions(PolicyOptions options) { + this.options = java.util.Objects.requireNonNull(options); + return this; + } public String getPolicyName() { return policyName; @@ -118,34 +125,34 @@ public PolicyCommand getEffectivePolicyCommand() { } public List getRoles() { - return roles; + return options.getRoles(); } public CreatePolicy setRoles(List roles) { - this.roles = roles; + options.setRoles(roles); return this; } public CreatePolicy addRole(String role) { - this.roles.add(role); + options.getRoles().add(role); return this; } public Expression getUsingExpression() { - return usingExpression; + return options.getUsingExpression(); } public CreatePolicy setUsingExpression(Expression usingExpression) { - this.usingExpression = usingExpression; + options.setUsingExpression(usingExpression); return this; } public Expression getWithCheckExpression() { - return withCheckExpression; + return options.getWithCheckExpression(); } public CreatePolicy setWithCheckExpression(Expression withCheckExpression) { - this.withCheckExpression = withCheckExpression; + options.setWithCheckExpression(withCheckExpression); return this; } @@ -154,39 +161,22 @@ public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } - @Override - public String toString() { - StringBuilder builder = new StringBuilder("CREATE POLICY "); - builder.append(policyName); - builder.append(" ON "); - builder.append(table.toString()); + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("CREATE POLICY ").append(policyName).append(" ON ").append(table); if (policyMode != null) { builder.append(" AS ").append(policyMode); } - if (policyCommand != null) { builder.append(" FOR ").append(policyCommand); } + options.appendTo(builder, printer); + return builder; + } - if (roles != null && !roles.isEmpty()) { - builder.append(" TO "); - for (int i = 0; i < roles.size(); i++) { - if (i > 0) { - builder.append(", "); - } - builder.append(roles.get(i)); - } - } - - if (usingExpression != null) { - builder.append(" USING (").append(usingExpression.toString()).append(")"); - } - - if (withCheckExpression != null) { - builder.append(" WITH CHECK (").append(withCheckExpression.toString()).append(")"); - } - - return builder.toString(); + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyOptions.java b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyOptions.java new file mode 100644 index 000000000..53c9d6087 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/policy/PolicyOptions.java @@ -0,0 +1,75 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.create.policy; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** Roles and predicates shared by PostgreSQL CREATE and ALTER POLICY. */ +public class PolicyOptions implements Serializable { + private List roles = new ArrayList<>(); + private Expression usingExpression; + private Expression withCheckExpression; + + public List getRoles() { + return roles; + } + + public PolicyOptions setRoles(List roles) { + this.roles = roles; + return this; + } + + public Expression getUsingExpression() { + return usingExpression; + } + + public PolicyOptions setUsingExpression(Expression usingExpression) { + this.usingExpression = usingExpression; + return this; + } + + public Expression getWithCheckExpression() { + return withCheckExpression; + } + + public PolicyOptions setWithCheckExpression(Expression withCheckExpression) { + this.withCheckExpression = withCheckExpression; + return this; + } + + public void visitExpressions(Consumer visitor) { + if (usingExpression != null) { + visitor.accept(usingExpression); + } + if (withCheckExpression != null) { + visitor.accept(withCheckExpression); + } + } + + public void appendTo(StringBuilder sql, Consumer printer) { + if (roles != null && !roles.isEmpty()) { + sql.append(" TO ").append(String.join(", ", roles)); + } + if (usingExpression != null) { + sql.append(" USING ("); + printer.accept(usingExpression); + sql.append(')'); + } + if (withCheckExpression != null) { + sql.append(" WITH CHECK ("); + printer.accept(withCheckExpression); + sql.append(')'); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java b/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java new file mode 100644 index 000000000..f9500f771 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/drop/DropPolicy.java @@ -0,0 +1,82 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2025 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ +package net.sf.jsqlparser.statement.drop; + +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +public class DropPolicy implements Statement { + private String policyName; + private net.sf.jsqlparser.schema.Table table; + private boolean ifExists = false; + private Behavior behavior; + + public String getPolicyName() { + return policyName; + } + + public DropPolicy setPolicyName(String policyName) { + this.policyName = policyName; + return this; + } + + public net.sf.jsqlparser.schema.Table getTable() { + return table; + } + + public DropPolicy setTable(net.sf.jsqlparser.schema.Table table) { + this.table = table; + return this; + } + + public boolean isIfExists() { + return ifExists; + } + + public DropPolicy setIfExists(boolean ifExists) { + this.ifExists = ifExists; + return this; + } + + public Behavior getBehavior() { + return behavior; + } + + public DropPolicy setBehavior(Behavior behavior) { + this.behavior = behavior; + return this; + } + + public enum Behavior { + CASCADE, RESTRICT + } + + public StringBuilder appendTo(StringBuilder sql) { + sql.append("DROP POLICY "); + if (ifExists) { + sql.append("IF EXISTS "); + } + sql.append(policyName).append(" ON ").append(table); + if (behavior != null) { + sql.append(' ').append(behavior); + } + return sql; + } + + @Override + public String toString() { + return appendTo(new StringBuilder()).toString(); + } + + @Override + public T accept(StatementVisitor visitor, S context) { + return visitor.visit(this, context); + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index aead296b0..1ed6a928f 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.alter.AlterRelation; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; @@ -2939,4 +2941,17 @@ public Void visit(AlterDatabase statement, S context) { public Void visit(AlterTablespaceMove statement, S context) { return null; } + + @Override + public Void visit(AlterPolicy statement, S context) { + visit(statement.getTable(), context); + statement.getOptions().visitExpressions(expression -> expression.accept(this, context)); + return null; + } + + @Override + public Void visit(DropPolicy statement, S context) { + visit(statement.getTable(), context); + return null; + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java index a470990bf..4d604494e 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreatePolicyDeParser.java @@ -31,31 +31,6 @@ public CreatePolicyDeParser(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(")"); - } + createPolicy.appendTo(builder, expression -> expression.accept(expressionVisitor, null)); } } 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 f50ecd351..889d4ac3d 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.deparser; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.alter.AlterRelation; import net.sf.jsqlparser.statement.alter.AlterTablespaceMove; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; @@ -827,4 +829,15 @@ public StringBuilder visit(AlterDatabase statement, S context) { public StringBuilder visit(AlterTablespaceMove statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(AlterPolicy statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + + @Override + public StringBuilder visit(DropPolicy statement, S context) { + return statement.appendTo(builder); + } } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 0c985bcff..5985243b2 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3270,6 +3270,8 @@ Statement SingleStatement() : | stm = Create() | + LOOKAHEAD( ) stm = DropPolicy() + | stm = Drop() | stm = Analyze() @@ -18265,6 +18267,8 @@ Statement Alter(): ( ( + LOOKAHEAD() statement = AlterPolicy() + | LOOKAHEAD({ isKeywordAhead("ROLE") || isKeywordAhead("USER") || isKeywordAhead("GROUP") }) statement = AlterRole() | @@ -20200,9 +20204,7 @@ CreatePolicy CreatePolicy() #CreatePolicy: Table table; String policyMode = null; Token commandToken = null; - String roleName; - Expression usingExpr = null; - Expression checkExpr = null; + PolicyOptions options; } { policyName=RelObjectName() { createPolicy.setPolicyName(policyName); } @@ -20224,14 +20226,7 @@ CreatePolicy CreatePolicy() #CreatePolicy: { createPolicy.setPolicyCommand(PolicyCommand.from(commandToken.image)); } ] - [ - roleName=RelObjectName() { createPolicy.addRole(roleName); } - ( "," roleName=RelObjectName() { createPolicy.addRole(roleName); } )* - ] - - [ "(" usingExpr=Expression() ")" { createPolicy.setUsingExpression(usingExpr); } ] - - [ LOOKAHEAD(2) "(" checkExpr=Expression() ")" { createPolicy.setWithCheckExpression(checkExpr); } ] + options=PolicyOptions() { createPolicy.setOptions(options); } { @@ -20239,6 +20234,35 @@ CreatePolicy CreatePolicy() #CreatePolicy: } } + +PolicyOptions PolicyOptions(): +{ PolicyOptions options = new PolicyOptions(); String role; Expression expression; } +{ + [ role=RelObjectName() { options.getRoles().add(role); } + ( "," role=RelObjectName() { options.getRoles().add(role); } )* ] + [ "(" expression=Expression() ")" { options.setUsingExpression(expression); } ] + [ LOOKAHEAD(2) "(" expression=Expression() ")" { options.setWithCheckExpression(expression); } ] + { return options; } +} +AlterPolicy AlterPolicy(): +{ AlterPolicy statement = new AlterPolicy(); String name; Table table; PolicyOptions options; } +{ + name=RelObjectName() { statement.setPolicyName(name); } + table=Table() { statement.setTable(table); } + ( name=RelObjectName() { statement.setNewName(name); } + | options=PolicyOptions() { statement.setOptions(options); } ) + { return statement; } +} +DropPolicy DropPolicy(): +{ DropPolicy statement = new DropPolicy(); String name; Table table; Token behavior; } +{ + [ LOOKAHEAD(2) { statement.setIfExists(true); } ] + name=RelObjectName() { statement.setPolicyName(name); } + table=Table() { statement.setTable(table); } + [ (behavior= | behavior=) { statement.setBehavior(DropPolicy.Behavior.valueOf(behavior.image.toUpperCase(Locale.ROOT))); } ] + { return statement; } +} + UnsupportedStatement UnsupportedStatement(): { List tokens = new LinkedList(); diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java new file mode 100644 index 000000000..e7059cb4d --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlPolicyDdlTest.java @@ -0,0 +1,111 @@ +/*- + * #%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; + +import static org.junit.jupiter.api.Assertions.*; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.alter.AlterPolicy; +import net.sf.jsqlparser.statement.create.policy.CreatePolicy; +import net.sf.jsqlparser.statement.create.policy.PolicyOptions; +import net.sf.jsqlparser.statement.drop.DropPolicy; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlPolicyDdlTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE POLICY pol ON t", + "CREATE POLICY pol ON t AS RESTRICTIVE FOR SELECT TO PUBLIC USING(id > 0)", + "CREATE POLICY pol ON t FOR INSERT TO CURRENT_USER WITH CHECK(id>0)", + "CREATE POLICY pol ON t FOR UPDATE TO CURRENT_ROLE, SESSION_USER USING(id>0) WITH CHECK(id<100)", + "ALTER POLICY pol ON t RENAME TO pol2", "ALTER POLICY pol ON t TO PUBLIC", + "ALTER POLICY pol ON t TO CURRENT_USER, SESSION_USER", + "ALTER POLICY pol ON t USING(id>1)", + "ALTER POLICY pol ON t WITH CHECK(id<10)", + "ALTER POLICY pol ON t TO CURRENT_ROLE USING(id>0) WITH CHECK(id<100)", + "DROP POLICY pol ON t", "DROP POLICY IF EXISTS pol ON t", + "DROP POLICY pol ON t CASCADE", + "DROP POLICY IF EXISTS pol ON t RESTRICT"}) + void statementsAndFollowingBoundariesRoundTrip(String sql) throws JSQLParserException { + assertRoundTrip(parse(sql)); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1", + p -> p.withDialect(Dialect.POSTGRESQL)).size()); + } + + @Test + void createAndAlterShareRolesAndPredicates() throws JSQLParserException { + CreatePolicy create = (CreatePolicy) parse("CREATE POLICY pol ON t TO PUBLIC USING(id>0)"); + AlterPolicy alter = (AlterPolicy) parse("ALTER POLICY pol ON t TO PUBLIC USING(id>0)"); + assertEquals(create.getRoles(), alter.getOptions().getRoles()); + assertEquals(create.getUsingExpression().toString(), + alter.getOptions().getUsingExpression().toString()); + alter.getOptions().setUsingExpression(CCJSqlParserUtil.parseCondExpression("id > 10")); + alter.setPolicyName("new_policy"); + alter.setTable(new Table("new_table")); + assertEquals("ALTER POLICY new_policy ON new_table TO PUBLIC USING (id > 10)", + alter.toString()); + assertRoundTrip(alter); + alter.setNewName("renamed"); + assertEquals("ALTER POLICY new_policy ON new_table RENAME TO renamed", alter.toString()); + alter.setOptions(new PolicyOptions() + .setUsingExpression(CCJSqlParserUtil.parseCondExpression("id > 1"))); + assertNull(alter.getNewName()); + assertRoundTrip(alter); + } + + @Test + void predicatesAndTargetsParticipateInTraversal() throws JSQLParserException { + String sql = "ALTER POLICY pol ON target USING (EXISTS (SELECT 1 FROM permissions))" + + " WITH CHECK (EXISTS (SELECT 1 FROM allowed_values))"; + assertEquals(Set.of("target", "permissions", "allowed_values"), + TablesNamesFinder.findTables(sql)); + assertEquals(Set.of("target"), TablesNamesFinder.findTables("DROP POLICY pol ON target")); + assertRoundTrip(parse(sql)); + } + + @Test + void dropPolicyNamesAndBehaviorAreMutable() throws JSQLParserException { + DropPolicy drop = (DropPolicy) parse("DROP POLICY IF EXISTS pol ON t CASCADE"); + assertTrue(drop.isIfExists()); + drop.setPolicyName("new_pol").setTable(new Table("new_t")) + .setBehavior(DropPolicy.Behavior.RESTRICT); + assertEquals("DROP POLICY IF EXISTS new_pol ON new_t RESTRICT", drop.toString()); + assertRoundTrip(drop); + } + + @Test + void createOnlyOptionsAndMalformedDropAreRejected() { + for (String sql : new String[] {"ALTER POLICY pol ON t AS RESTRICTIVE", + "ALTER POLICY pol ON t FOR SELECT", + "ALTER POLICY pol ON t RENAME TO pol2 USING(id>0)", "DROP POLICY pol", + "DROP POLICY pol ON t, u"}) { + assertThrows(JSQLParserException.class, () -> parse(sql)); + } + } + + private static Statement parse(String sql) throws JSQLParserException { + return CCJSqlParserUtil.parse(sql, p -> p.withDialect(Dialect.POSTGRESQL)); + } + + private static void assertRoundTrip(Statement statement) throws JSQLParserException { + StringBuilder builder = new StringBuilder(); + statement.accept(new StatementDeParser(builder), null); + assertEquals(statement.toString(), builder.toString()); + assertEquals(statement.toString(), parse(builder.toString()).toString()); + } +}