From c4a8a59eec44e30a968f6006c56f89df0d14282e Mon Sep 17 00:00:00 2001 From: minleejae Date: Fri, 25 Sep 2026 22:45:01 +0900 Subject: [PATCH] feat: support PostgreSQL rewrite rules with reusable statement actions Signed-off-by: minleejae --- .../sf/jsqlparser/parser/feature/Feature.java | 2 + .../statement/StatementVisitor.java | 18 ++ .../statement/create/rule/CreateRule.java | 170 ++++++++++++++++++ .../statement/notify/NotifyStatement.java | 64 +++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 16 ++ .../util/deparser/StatementDeParser.java | 15 ++ .../validation/feature/FeaturesAllowed.java | 4 +- .../validation/feature/PostgresqlVersion.java | 2 + .../validator/StatementValidator.java | 18 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 59 ++++++ .../statement/create/PostgreSqlRuleTest.java | 128 +++++++++++++ .../TableStatementValidatorTest.java | 6 +- 12 files changed, 496 insertions(+), 6 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/notify/NotifyStatement.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java diff --git a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java index e09ae85535..86a3b31420 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -580,6 +580,8 @@ public enum Feature { */ createSequence, /** Publication and subscription definitions. */ + createRule, notifyStatement, + createPublication, alterPublication, createSubscription, alterSubscription, /** * Structured type, domain and extension statements. diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 03755d652b..6cc3a55695 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.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -733,4 +735,20 @@ default T visit(AlterStatistics statement, S context) { default void visit(AlterStatistics statement) { visit(statement, null); } + + default T visit(CreateRule statement, S context) { + return null; + } + + default void visit(CreateRule statement) { + visit(statement, null); + } + + default T visit(NotifyStatement statement, S context) { + return null; + } + + default void visit(NotifyStatement statement) { + visit(statement, null); + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java b/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java new file mode 100644 index 0000000000..44e1ef134a --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/create/rule/CreateRule.java @@ -0,0 +1,170 @@ +/*- + * #%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.rule; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL rewrite rule whose actions reuse ordinary statement AST nodes. */ +public class CreateRule implements Statement { + public enum Event { + SELECT, INSERT, UPDATE, DELETE + } + public enum Behavior { + ALSO, INSTEAD + } + + private String name; + private boolean orReplace; + private Event event; + private Table table; + private Expression whereExpression; + private Behavior behavior; + private boolean useParentheses; + private List actions = new ArrayList<>(); + + public String getName() { + return name; + } + + public void setName(String value) { + name = value; + } + + public boolean isOrReplace() { + return orReplace; + } + + public void setOrReplace(boolean value) { + orReplace = value; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event value) { + event = value; + } + + public Table getTable() { + return table; + } + + public void setTable(Table value) { + table = value; + } + + public Expression getWhereExpression() { + return whereExpression; + } + + public void setWhereExpression(Expression value) { + whereExpression = value; + } + + public Behavior getBehavior() { + return behavior; + } + + public void setBehavior(Behavior value) { + behavior = value; + } + + public boolean isUseParentheses() { + return useParentheses; + } + + public void setUseParentheses(boolean value) { + useParentheses = value; + } + + public List getActions() { + return actions; + } + + public void setActions(List value) { + actions = value; + } + + public boolean isNothing() { + return actions.isEmpty() && !useParentheses; + } + + public void setNothing(boolean nothing) { + if (nothing) { + actions.clear(); + useParentheses = false; + } + } + + public void visitExpressions(Consumer visitor) { + if (whereExpression != null) { + visitor.accept(whereExpression); + } + } + + public void visitTables(Consumer visitor) { + if (table != null) { + visitor.accept(table); + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer expressions, + Consumer statements) { + sql.append("CREATE "); + if (orReplace) { + sql.append("OR REPLACE "); + } + sql.append("RULE ").append(name).append(" AS ON ").append(event).append(" TO ") + .append(table); + if (whereExpression != null) { + sql.append(" WHERE "); + expressions.accept(whereExpression); + } + sql.append(" DO "); + if (behavior != null) { + sql.append(behavior).append(' '); + } + if (isNothing()) { + return sql.append("NOTHING"); + } + boolean grouped = useParentheses || actions.size() != 1; + if (grouped) { + sql.append('('); + } + for (int i = 0; i < actions.size(); i++) { + if (i > 0) { + sql.append("; "); + } + statements.accept(actions.get(i)); + } + if (grouped) { + sql.append(')'); + } + return sql; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append, sql::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/notify/NotifyStatement.java b/src/main/java/net/sf/jsqlparser/statement/notify/NotifyStatement.java new file mode 100644 index 0000000000..ff8e6dcc1e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/notify/NotifyStatement.java @@ -0,0 +1,64 @@ +/*- + * #%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.notify; + +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; + +/** PostgreSQL NOTIFY, usable independently and in CREATE RULE actions. */ +public class NotifyStatement implements Statement { + private String channel; + private StringValue payload; + + public String getChannel() { + return channel; + } + + public void setChannel(String value) { + channel = value; + } + + public StringValue getPayload() { + return payload; + } + + public void setPayload(StringValue value) { + payload = value; + } + + public void visitExpressions(Consumer visitor) { + if (payload != null) { + visitor.accept(payload); + } + } + + public StringBuilder appendTo(StringBuilder sql, Consumer printer) { + sql.append("NOTIFY ").append(channel); + if (payload != null) { + sql.append(", "); + printer.accept(payload); + } + return sql; + } + + @Override + public String toString() { + StringBuilder sql = new StringBuilder(); + return appendTo(sql, sql::append).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 9d21d83770..7f71d8b256 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.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -2968,4 +2970,18 @@ public Void visit(CreateStatistics statement, S context) { public Void visit(AlterStatistics statement, S context) { return null; } + + @Override + public Void visit(CreateRule statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + statement.visitTables(table -> visit(table, context)); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public Void visit(NotifyStatement statement, S context) { + statement.visitExpressions(expression -> expression.accept(this, context)); + return 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 d949c3e623..9e09c3f1a4 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.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.alter.AlterPolicy; import net.sf.jsqlparser.statement.drop.DropPolicy; import net.sf.jsqlparser.statement.create.statistics.CreateStatistics; @@ -853,4 +855,17 @@ public StringBuilder visit(CreateStatistics statement, S context) { public StringBuilder visit(AlterStatistics statement, S context) { return statement.appendTo(builder); } + + @Override + public StringBuilder visit(CreateRule statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context), + action -> action.accept(this, context)); + } + + @Override + public StringBuilder visit(NotifyStatement statement, S context) { + return statement.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java index 08e137e54f..cbfd9efc46 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/FeaturesAllowed.java @@ -101,6 +101,7 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all "CREATE" {@link Feature}'s */ public static final FeaturesAllowed CREATE = new FeaturesAllowed("CREATE", Feature.createIndex, + Feature.createRule, Feature.createSchema, Feature.createSequence, Feature.createTable, Feature.createTableUnlogged, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, @@ -157,7 +158,8 @@ public class FeaturesAllowed implements FeatureSetValidation, ModifyableFeatureS * all DML {@link Feature}'s */ public static final FeaturesAllowed DML = - new FeaturesAllowed("DML").add(SELECT, INSERT, UPDATE, DELETE, MERGE) + new FeaturesAllowed("DML", Feature.notifyStatement) + .add(SELECT, INSERT, UPDATE, DELETE, MERGE) .unmodifyable(); /** * all DDL {@link Feature}'s diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java index e5a9b43f13..3d5f042239 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/PostgresqlVersion.java @@ -31,6 +31,7 @@ public enum PostgresqlVersion implements Version { Feature.exprLike, Feature.exprSimilarTo, // https://www.postgresql.org/docs/current/sql-select.html Feature.select, + Feature.tableStatement, Feature.selectGroupBy, Feature.function, Feature.tableFunction, Feature.lateralSubSelect, Feature.selectHaving, // https://www.postgresql.org/docs/current/queries-table-expressions.html#QUERIES-GROUPING-SETS @@ -97,6 +98,7 @@ public enum PostgresqlVersion implements Version { // https://www.postgresql.org/docs/current/sql-createsequence.html Feature.createSequence, + Feature.createRule, Feature.notifyStatement, Feature.createPublication, Feature.alterPublication, Feature.createSubscription, Feature.alterSubscription, Feature.createType, Feature.alterType, 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 0b8d2765d5..2331397e8a 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 @@ -9,6 +9,8 @@ */ package net.sf.jsqlparser.util.validation.validator; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.oracle.OracleBlock; import net.sf.jsqlparser.statement.oracle.OracleAssignment; @@ -1024,4 +1026,20 @@ public Void visit(OracleNullStatement statement, S context) { return null; } + + @Override + public Void visit(CreateRule statement, S context) { + validateFeature(Feature.createRule); + statement.visitExpressions(this::validateOptionalExpression); + statement.visitTables(this::validateOptionalFromItem); + statement.getActions().forEach(action -> action.accept(this, context)); + return null; + } + + @Override + public Void visit(NotifyStatement statement, S context) { + validateFeature(Feature.notifyStatement); + statement.visitExpressions(this::validateOptionalExpression); + 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 4b505dab0d..5a4fd27a56 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -49,6 +49,8 @@ import net.sf.jsqlparser.schema.*; import net.sf.jsqlparser.statement.*; import net.sf.jsqlparser.statement.analyze.*; import net.sf.jsqlparser.statement.alter.*; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.notify.NotifyStatement; import net.sf.jsqlparser.statement.alter.schema.AlterSchema; import net.sf.jsqlparser.statement.alter.database.AlterDatabase; import net.sf.jsqlparser.statement.alter.sequence.*; @@ -2533,6 +2535,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3314,6 +3317,8 @@ Statement SingleStatement() : | stm = Comment() | + LOOKAHEAD({ isKeywordAhead("NOTIFY") }) stm = NotifyStatement() + | stm = Describe() | stm = Explain() @@ -19650,6 +19655,8 @@ Statement Create(): { [ { isUsingOrReplace = true; } ] ( + LOOKAHEAD({ isKeywordAhead("RULE") }) statement = CreateRule(isUsingOrReplace) + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() @@ -20952,3 +20959,55 @@ AlterStatistics AlterStatistics(): statement.setStatistics(target.intValue()); } ) ) ) { return statement; } } + +CreateRule CreateRule(boolean orReplace): +{ + CreateRule statement = new CreateRule(); String name; Table table; + Token event; Expression condition; Statement action; +} +{ + ContextualKeyword("RULE") name=RelObjectName() + { statement.setName(name); statement.setOrReplace(orReplace); } + ( event= | event= | event= | event= ) + { statement.setEvent(CreateRule.Event.valueOf(event.image.toUpperCase(java.util.Locale.ROOT))); } + table=Table() { statement.setTable(table); } + [ condition=Expression() { statement.setWhereExpression(condition); } ] + + [ LOOKAHEAD({ isKeywordAhead("ALSO") || isKeywordAhead("INSTEAD") }) + ( LOOKAHEAD({ isKeywordAhead("ALSO") }) ContextualKeyword("ALSO") { statement.setBehavior(CreateRule.Behavior.ALSO); } + | AccessKeyword("INSTEAD") { statement.setBehavior(CreateRule.Behavior.INSTEAD); } ) ] + ( + { statement.setNothing(true); } + | LOOKAHEAD(Select()) action=Select() { statement.getActions().add(action); } + | LOOKAHEAD("(") "(" { statement.setUseParentheses(true); } + [ LOOKAHEAD({ getToken(1).kind != ST_SEMICOLON && !")".equals(getToken(1).image) }) + action=PostgreSqlRuleAction() { statement.getActions().add(action); } ] + ( + [ LOOKAHEAD({ getToken(1).kind != ST_SEMICOLON && !")".equals(getToken(1).image) }) + action=PostgreSqlRuleAction() { statement.getActions().add(action); } ] )* + ")" + | action=PostgreSqlRuleAction() { statement.getActions().add(action); } + ) + { return statement; } +} + +Statement PostgreSqlRuleAction(): +{ Statement statement; } +{ + statement=SingleStatement() + { + requireDdlSyntax(statement instanceof Select || statement instanceof TableStatement || statement instanceof Insert + || statement instanceof Update || statement instanceof Delete + || statement instanceof NotifyStatement, + "A rule action must be SELECT, INSERT, UPDATE, DELETE or NOTIFY"); + return statement; + } +} + +NotifyStatement NotifyStatement(): +{ NotifyStatement statement = new NotifyStatement(); String channel; Token payload; } +{ + channel=RelObjectName() { statement.setChannel(channel); } + [ "," payload= { statement.setPayload(new StringValue(payload.image)); } ] + { return statement; } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java new file mode 100644 index 0000000000..e82a0e8db4 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/PostgreSqlRuleTest.java @@ -0,0 +1,128 @@ +/*- + * #%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.List; +import java.util.Set; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.create.rule.CreateRule; +import net.sf.jsqlparser.statement.insert.Insert; +import net.sf.jsqlparser.statement.notify.NotifyStatement; +import net.sf.jsqlparser.util.TablesNamesFinder; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import net.sf.jsqlparser.util.validation.Validation; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class PostgreSqlRuleTest { + @ParameterizedTest + @ValueSource(strings = { + "CREATE RULE r AS ON INSERT TO t DO ALSO NOTIFY ddl_probe", + "CREATE OR REPLACE RULE r AS ON UPDATE TO app.t WHERE NEW.id > 0 DO INSTEAD NOTHING", + "CREATE RULE r AS ON DELETE TO t DO NOTHING", + "CREATE RULE r AS ON INSERT TO t DO INSERT INTO log SELECT NEW.id", + "CREATE RULE r AS ON UPDATE TO t DO UPDATE log SET id=NEW.id WHERE id=OLD.id", + "CREATE RULE r AS ON DELETE TO t DO DELETE FROM log WHERE id=OLD.id", + "CREATE RULE \"_RETURN\" AS ON SELECT TO v DO INSTEAD SELECT * FROM t", + "CREATE RULE r AS ON INSERT TO t DO ALSO (NOTIFY ch, 'updated'; INSERT INTO log VALUES (1);)", + "CREATE RULE r AS ON INSERT TO t DO (;;;NOTIFY ch;;)", + "CREATE RULE r AS ON INSERT TO t DO ()", + "CREATE RULE r AS ON INSERT TO t DO (;)", + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1 UNION ALL SELECT 2)", + "CREATE RULE r AS ON INSERT TO t DO (SELECT 1) UNION ALL SELECT 2", + "CREATE RULE r AS ON INSERT TO t DO TABLE log", + "CREATE RULE r AS ON INSERT TO t DO ((SELECT 1) UNION ALL SELECT 2; NOTIFY ch)", + "CREATE RULE r AS ON INSERT TO t DO WITH x AS (SELECT 1) INSERT INTO log SELECT * FROM x", + "CREATE RULE r AS ON INSERT TO t DO NOTIFY \"Channel\", 'semi;colon'", + "NOTIFY ch", "NOTIFY \"Channel\", 'it''s ready'"}) + void ruleActionsAndStatementBoundaries(String sql) throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse(sql); + assertTrue(statement instanceof CreateRule || statement instanceof NotifyStatement); + roundTrip(statement); + assertEquals(2, CCJSqlParserUtil.parseStatements(sql + "; SELECT 1").size()); + assertTrue(new Validation(List.of(PostgresqlVersion.V14), sql) + .validate().isEmpty()); + } + + @Test + void actionAndPredicateMutationsUseTheExistingAst() throws JSQLParserException { + CreateRule rule = (CreateRule) CCJSqlParserUtil.parse( + "CREATE RULE r AS ON INSERT TO app.t WHERE NEW.id > 0 DO (INSERT INTO log SELECT NEW.id; NOTIFY ch, 'original')"); + assertEquals(Set.of("app.t", "log"), new TablesNamesFinder().getTables(rule)); + rule.setWhereExpression(CCJSqlParserUtil.parseCondExpression("NEW.id > 10")); + Insert insert = (Insert) rule.getActions().get(0); + insert.getTable().setName("audit_log"); + NotifyStatement notify = (NotifyStatement) rule.getActions().get(1); + notify.setPayload(new StringValue("changed")); + assertTrue(rule.toString().contains("NEW.id > 10")); + assertTrue(rule.toString().contains("INSERT INTO audit_log")); + assertTrue(rule.toString().contains("'changed'")); + assertEquals(Set.of("app.t", "audit_log"), new TablesNamesFinder().getTables(rule)); + roundTrip(rule); + rule.setNothing(true); + assertTrue(rule.getActions().isEmpty()); + assertTrue(rule.toString().endsWith("DO NOTHING")); + roundTrip(rule); + } + + @Test + void nestedActionsUseCustomExpressionDeparser() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse( + "CREATE RULE r AS ON INSERT TO t DO (INSERT INTO log VALUES ('before'); NOTIFY ch, 'before')"); + StringBuilder output = new StringBuilder(); + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append("'after'"); + } + }; + SelectDeParser selects = new SelectDeParser(expressions, output); + expressions.setBuilder(output); + expressions.setSelectVisitor(selects); + statement.accept(new StatementDeParser(expressions, selects, output)); + assertFalse(output.toString().contains("'before'")); + assertTrue(output.toString().contains("VALUES ('after')")); + assertTrue(output.toString().contains("NOTIFY ch, 'after'")); + assertTrue(new TablesNamesFinder().getTables(CCJSqlParserUtil.parse("NOTIFY table_name")) + .isEmpty()); + } + + @Test + void notifyRemainsUsableAsAnIdentifier() throws JSQLParserException { + roundTrip(CCJSqlParserUtil.parse("SELECT notify FROM notify")); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE RULE r AS ON TRUNCATE TO t DO NOTHING", + "CREATE RULE r AS ON INSERT TO t DO", "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch", + "CREATE RULE r AS ON INSERT TO t DO CREATE TABLE x(id int)", + "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch NOTIFY ch2)", + "CREATE RULE r AS ON INSERT TO t DO (NOTIFY ch; DROP TABLE t)", + "CREATE RULE r AS ON INSERT TO t DO ALSO INSTEAD NOTHING", + "NOTIFY", "NOTIFY ch, 123", "NOTIFY ch, 'a' || 'b'", "NOTIFY app.ch"}) + void rejectsInvalidActionsAndPayloads(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql)); + } + + private static void roundTrip(Statement statement) throws JSQLParserException { + StringBuilder output = new StringBuilder(); + statement.accept(new StatementDeParser(output)); + assertEquals(statement.toString(), output.toString()); + assertEquals(output.toString(), CCJSqlParserUtil.parse(output.toString()).toString()); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/TableStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/TableStatementValidatorTest.java index c71b8448eb..24d50a1060 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/TableStatementValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/TableStatementValidatorTest.java @@ -9,7 +9,6 @@ */ package net.sf.jsqlparser.util.validation.validator; -import java.util.Arrays; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.util.validation.ValidationTestAsserts; @@ -23,16 +22,13 @@ public class TableStatementValidatorTest extends ValidationTestAsserts { @Test public void testValidationSelectAllowed() throws JSQLParserException { String sql = "TABLE columns ORDER BY column_name LIMIT 10 OFFSET 10"; - validateNoErrors(sql, 1, MySqlVersion.V8_0); + validateNoErrors(sql, 1, MySqlVersion.V8_0, PostgresqlVersion.V14); } @Test public void testValidationSelectNotAllowed() throws JSQLParserException { String sql = "TABLE columns ORDER BY column_name LIMIT 10 OFFSET 10"; validateNotAllowed(sql, 1, 1, FeaturesAllowed.DDL, Feature.select, Feature.tableStatement); - - validateNotSupported(sql, 1, 1, Arrays.asList( - PostgresqlVersion.V14), Feature.tableStatement); } }