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 8e57b1990..ae0290458 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -582,6 +582,8 @@ public enum Feature { /** Publication and subscription definitions. */ createForeignDataWrapper, alterForeignDataWrapper, createServer, alterServer, createUserMapping, alterUserMapping, + 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 bc94336ae..066a45783 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -15,6 +15,8 @@ import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.alter.AlterUserMapping; +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; @@ -787,4 +789,20 @@ default T visit(AlterUserMapping statement, S context) { default void visit(AlterUserMapping 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 000000000..44e1ef134 --- /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 000000000..ff8e6dcc1 --- /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 baa385f62..486de0ee7 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -15,6 +15,8 @@ import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.alter.AlterUserMapping; +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; @@ -3010,4 +3012,18 @@ public Void visit(AlterUserMapping statement, S context) { statement.visitExpressions(expression -> expression.accept(this, 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 8516e51dc..e4608311f 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -15,6 +15,8 @@ import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.alter.AlterUserMapping; +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; @@ -874,4 +876,17 @@ public StringBuilder visit(AlterUserMapping statement, S context) { return statement.appendTo(builder, expression -> expression.accept(expressionDeParser, context)); } + + @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 3c74b26c8..93456ee95 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.createForeignDataWrapper, Feature.createServer, Feature.createUserMapping, Feature.createTableUnlogged, @@ -159,7 +160,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 37a523e41..7a5f8b716 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 @@ -100,6 +101,7 @@ public enum PostgresqlVersion implements Version { Feature.createForeignDataWrapper, Feature.alterForeignDataWrapper, Feature.createServer, Feature.alterServer, Feature.createUserMapping, Feature.alterUserMapping, + 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 534c2eb3a..7c428c73b 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 @@ -15,6 +15,8 @@ import net.sf.jsqlparser.statement.alter.AlterServer; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; import net.sf.jsqlparser.statement.alter.AlterUserMapping; +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; @@ -1072,4 +1074,20 @@ public Void visit(AlterUserMapping statement, S context) { statement.visitExpressions(this::validateOptionalExpression); 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 d07c9e0fa..1ef3a074d 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -52,6 +52,8 @@ import net.sf.jsqlparser.statement.alter.*; import net.sf.jsqlparser.statement.create.fdw.*; import net.sf.jsqlparser.statement.create.server.CreateServer; import net.sf.jsqlparser.statement.create.usermapping.CreateUserMapping; +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.*; @@ -2558,6 +2560,7 @@ String NonReservedWord() : | tk= | tk= | tk= + | tk= | tk= | tk= | tk= @@ -3339,6 +3342,8 @@ Statement SingleStatement() : | stm = Comment() | + LOOKAHEAD({ isKeywordAhead("NOTIFY") }) stm = NotifyStatement() + | stm = Describe() | stm = Explain() @@ -19716,6 +19721,8 @@ Statement Create(): | LOOKAHEAD({ getToken(1).kind == K_USER && "MAPPING".equalsIgnoreCase(getToken(2).image) }) { requireDdlSyntax(!isUsingOrReplace, "OR REPLACE is not supported for foreign-data objects"); } statement = CreateUserMapping() | + LOOKAHEAD({ isKeywordAhead("RULE") }) statement = CreateRule(isUsingOrReplace) + | LOOKAHEAD({ isKeywordAhead("STATISTICS") }) statement = CreateStatistics() | LOOKAHEAD({ isPostgreSqlRoleAhead() }) statement = CreateRole() @@ -21147,3 +21154,55 @@ AlterUserMapping AlterUserMapping(): ContextualKeyword("SERVER") name=RelObjectName() { statement.setServer(name); } options=ForeignDataOptions(true) { statement.setOptions(options); 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 000000000..e82a0e8db --- /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 c71b8448e..24d50a106 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); } }