diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java index c018be76bc..52972cf7ed 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/AlterEvent.java @@ -32,7 +32,9 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - StringBuilder builder = new StringBuilder("ALTER EVENT ").append(getEvent()); + StringBuilder builder = new StringBuilder("ALTER "); + appendDefinerAndEvent(builder); + builder.append(getEvent()); appendScheduleAndCompletion(builder); if (renameTo != null) { builder.append(" RENAME TO ").append(renameTo); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java b/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java index 2cefc5d9e4..6b355f49d1 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/CreateEvent.java @@ -31,7 +31,8 @@ public T accept(StatementVisitor statementVisitor, S context) { @Override public String toString() { - StringBuilder builder = new StringBuilder("CREATE EVENT "); + StringBuilder builder = new StringBuilder("CREATE "); + appendDefinerAndEvent(builder); if (ifNotExists) { builder.append("IF NOT EXISTS "); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java index bd66e95f22..77427d33fd 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatement.java @@ -11,11 +11,13 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.MySqlDefiner; import net.sf.jsqlparser.statement.Statement; /** Common structured properties of MySQL {@code CREATE EVENT} and {@code ALTER EVENT}. */ public abstract class EventStatement implements Statement { + private MySqlDefiner definer; private Table event; private EventSchedule schedule; private Boolean onCompletionPreserve; @@ -23,6 +25,14 @@ public abstract class EventStatement implements Statement { private StringValue comment; private Statement body; + public MySqlDefiner getDefiner() { + return definer; + } + + public void setDefiner(MySqlDefiner definer) { + this.definer = definer; + } + public Table getEvent() { return event; } @@ -75,6 +85,14 @@ public void setBody(Statement body) { this.body = body; } + /** Appends the optional DEFINER clause, followed by EVENT, after CREATE or ALTER. */ + protected void appendDefinerAndEvent(StringBuilder builder) { + if (definer != null) { + builder.append("DEFINER = ").append(definer).append(' '); + } + builder.append("EVENT "); + } + protected void appendScheduleAndCompletion(StringBuilder builder) { if (schedule != null) { builder.append(" ON SCHEDULE ").append(schedule); diff --git a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java index 8418267e24..cec7cd2ef3 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/event/EventStatus.java @@ -10,7 +10,8 @@ package net.sf.jsqlparser.statement.create.event; public enum EventStatus { - ENABLE("ENABLE"), DISABLE("DISABLE"), DISABLE_ON_SLAVE("DISABLE ON SLAVE"); + ENABLE("ENABLE"), DISABLE("DISABLE"), DISABLE_ON_SLAVE("DISABLE ON SLAVE"), DISABLE_ON_REPLICA( + "DISABLE ON REPLICA"); private final String sql; diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index f2bf3b59cb..b0b7d515f3 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -17330,7 +17330,13 @@ EventStatus MySqlEventStatus(): { status = EventStatus.ENABLE; } | { status = EventStatus.DISABLE; } - [ LOOKAHEAD(2) { status = EventStatus.DISABLE_ON_SLAVE; } ] + [ LOOKAHEAD({ getToken(1).kind == K_ON && (getToken(2).kind == K_SLAVE + || "REPLICA".equalsIgnoreCase(getToken(2).image)) }) + ( + { status = EventStatus.DISABLE_ON_SLAVE; } + | + ContextualKeyword("REPLICA") { status = EventStatus.DISABLE_ON_REPLICA; } + ) ] ) { return status; } } @@ -17344,8 +17350,11 @@ CreateEvent CreateEvent(): EventStatus status = null; Token comment = null; Statement body = null; + MySqlDefiner definer = null; } { + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) + definer=MySqlRoutineDefiner() { createEvent.setDefiner(definer); } ] [ LOOKAHEAD(3) { createEvent.setIfNotExists(true); } ] event=Table() { createEvent.setEvent(event); } @@ -17358,7 +17367,7 @@ CreateEvent CreateEvent(): [ LOOKAHEAD({ getToken(1).kind == K_COMMENT }) comment= { createEvent.setComment(stringValue(comment)); } ] - body=SingleStatement() { createEvent.setBody(body); } + body=MySqlTriggerBody() { createEvent.setBody(body); } { return createEvent; } } @@ -17372,8 +17381,11 @@ AlterEvent AlterEvent(): EventStatus status = null; Token comment = null; Statement body = null; + MySqlDefiner definer = null; } { + [ LOOKAHEAD({ isKeywordAhead("DEFINER") }) + definer=MySqlRoutineDefiner() { alterEvent.setDefiner(definer); } ] event=Table() { alterEvent.setEvent(event); } [ LOOKAHEAD({ getToken(1).kind == K_ON && getToken(2).kind == K_SCHEDULE }) schedule=MySqlEventSchedule() { alterEvent.setSchedule(schedule); } ] @@ -17387,7 +17399,7 @@ AlterEvent AlterEvent(): alterEvent.setComment(stringValue(comment)); } ] [ LOOKAHEAD({ getToken(1).kind == K_DO }) - body=SingleStatement() { alterEvent.setBody(body); } ] + body=MySqlTriggerBody() { alterEvent.setBody(body); } ] { return alterEvent; } } @@ -17472,9 +17484,9 @@ CreateTrigger.Order MySqlTriggerOrder(): } /** - * MySQL trigger statement. BEGIN ... END with SQL statements only remains a Block. Other compound - * statements, such as those with IF or DECLARE, keep their tokens like routine bodies and use the - * same boundary tracking. The semicolon after END is left to separate statements. + * MySQL trigger or event statement. BEGIN ... END with SQL statements only remains a Block. Other + * compound statements, such as those with IF or DECLARE, keep their tokens like routine bodies and + * use the same boundary tracking. The semicolon after END is left to separate statements. */ Statement MySqlTriggerBody(): { @@ -21824,6 +21836,8 @@ Statement Alter(): | LOOKAHEAD({ isPostgreSqlEventTriggerAhead(true) }) statement = AlterEventTrigger() | + LOOKAHEAD([ MySqlRoutineDefiner() ] , + { getToken(1).kind == K_EVENT || isKeywordAhead("DEFINER") }) statement = AlterEvent() | LOOKAHEAD(( | | ) ) @@ -23625,6 +23639,8 @@ Statement Create(): { requireDdlSyntax(!isUsingOrReplace, "CREATE EVENT TRIGGER does not support OR REPLACE"); } statement = CreateEventTrigger() | + LOOKAHEAD([ MySqlRoutineDefiner() ] , + { getToken(1).kind == K_EVENT || isKeywordAhead("DEFINER") }) statement = CreateEvent() | LOOKAHEAD(MySqlViewPrefix() ) statement = CreateView(isUsingOrReplace) diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java index a3e514364b..52f250afeb 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateFunctionalStatementTest.java @@ -18,10 +18,10 @@ import net.sf.jsqlparser.expression.StringValue; import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; import net.sf.jsqlparser.parser.CCJSqlParserUtil; -import net.sf.jsqlparser.statement.UnsupportedStatement; import net.sf.jsqlparser.statement.MySqlDefiner; import net.sf.jsqlparser.statement.CreateFunctionalStatement; import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.create.event.CreateEvent; import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; import net.sf.jsqlparser.util.deparser.StatementDeParser; @@ -154,9 +154,8 @@ public void definerDispatchKeepsTriggersAndViews() throws JSQLParserException { assertThat(CCJSqlParserUtil.parse("CREATE FUNCTION f() RETURNS int AS 'SELECT 1'")) .isInstanceOf(CreateFunction.class) .extracting(statement -> ((CreateFunction) statement).getDefiner()).isNull(); - // DEFINER is not modelled for EVENT, so the statement falls back like other unknown forms assertThat(CCJSqlParserUtil.parse("CREATE DEFINER = x EVENT e " - + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(UnsupportedStatement.class); + + "ON SCHEDULE EVERY 1 DAY DO SELECT 1")).isInstanceOf(CreateEvent.class); } @Test diff --git a/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java index 95f406388b..06f10422e4 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/event/EventStatementTest.java @@ -10,16 +10,33 @@ package net.sf.jsqlparser.statement.create.event; import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static net.sf.jsqlparser.test.TestUtils.buildSqlString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.Block; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.delete.Delete; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.Update; +import net.sf.jsqlparser.util.TablesNamesFinder; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; class EventStatementTest { @@ -92,4 +109,88 @@ void parsesCompletionPolicy() throws JSQLParserException { assertFalse(event.getOnCompletionPreserve()); assertEquals(EventStatus.ENABLE, event.getStatus()); } + + @ParameterizedTest + @ValueSource(strings = { + "CREATE DEFINER = `root`@`localhost` EVENT `e_daily` ON SCHEDULE EVERY 1 DAY " + + "STARTS CURRENT_TIMESTAMP + INTERVAL 6 HOUR ON COMPLETION NOT PRESERVE " + + "ENABLE COMMENT 'Saves total number of sessions then clears the table' " + + "DO BEGIN INSERT INTO site_activity.totals (time, total) " + + "SELECT CURRENT_TIMESTAMP, COUNT(*) FROM site_activity.sessions; " + + "DELETE FROM site_activity.sessions; END", + "CREATE DEFINER = 'admin'@'%' EVENT IF NOT EXISTS ev ON SCHEDULE EVERY 1 HOUR " + + "DO SELECT 1", + "CREATE DEFINER = CURRENT_USER() EVENT ev ON SCHEDULE AT CURRENT_TIMESTAMP " + + "DISABLE ON REPLICA DO SELECT 1", + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY DO BEGIN END", + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY " + + "DO BEGIN IF @a > 0 THEN DELETE FROM t1; END IF; END", + "ALTER DEFINER = CURRENT_USER EVENT ev ON COMPLETION PRESERVE RENAME TO db2.ev2 " + + "ENABLE COMMENT 'moved' DO SELECT 2", + "ALTER EVENT ev DISABLE ON REPLICA", + "ALTER EVENT ev DISABLE ON SLAVE", + "ALTER EVENT ev DO BEGIN DELETE FROM t1; DELETE FROM t2; END"}) + void parsesMySqlEventForms(String sql) throws JSQLParserException { + for (Consumer consumer : List.>of( + parser -> parser.withDialect(Dialect.MYSQL), parser -> { + })) { + // a statement block prints one statement per line + Statement event = assertSqlCanBeParsedAndDeparsed(sql, true, consumer); + assertInstanceOf(EventStatement.class, event); + Statements script = CCJSqlParserUtil.parseStatements(sql + ";\nSELECT 1", consumer); + assertEquals(2, script.size()); + assertEquals(buildSqlString(sql, true), + buildSqlString(script.get(0).toString(), true)); + } + } + + @Test + void parsesDefinerAsShowCreateEventPrintsIt() throws JSQLParserException { + CreateEvent event = assertInstanceOf(CreateEvent.class, CCJSqlParserUtil.parse( + "CREATE DEFINER=`root`@`localhost` EVENT `ev` ON SCHEDULE EVERY 1 DAY " + + "STARTS '2026-01-01 00:00:00' ON COMPLETION NOT PRESERVE ENABLE " + + "DO DELETE FROM t1")); + + assertEquals("root", event.getDefiner().getUser().getValue()); + assertEquals("localhost", event.getDefiner().getHost().getValue()); + assertInstanceOf(Delete.class, event.getBody()); + assertEquals("CREATE DEFINER = `root`@`localhost` EVENT `ev` ON SCHEDULE EVERY 1 DAY " + + "STARTS '2026-01-01 00:00:00' ON COMPLETION NOT PRESERVE ENABLE " + + "DO DELETE FROM t1", event.toString()); + + AlterEvent altered = assertInstanceOf(AlterEvent.class, CCJSqlParserUtil.parse( + "ALTER DEFINER = 'admin'@'%' EVENT ev ENABLE")); + assertEquals("admin", altered.getDefiner().getUser().getValue()); + assertEquals("%", altered.getDefiner().getHost().getValue()); + assertEquals(EventStatus.ENABLE, altered.getStatus()); + } + + @Test + void keepsStatementBlockBodiesStructured() throws JSQLParserException { + CreateEvent event = assertInstanceOf(CreateEvent.class, CCJSqlParserUtil.parse( + "CREATE EVENT ev ON SCHEDULE EVERY 1 DAY " + + "DO BEGIN DELETE FROM a; DELETE FROM b; END", + parser -> parser.withDialect(Dialect.MYSQL))); + + Block body = assertInstanceOf(Block.class, event.getBody()); + assertEquals(2, body.getStatements().size()); + assertInstanceOf(Delete.class, body.getStatements().get(0)); + assertEquals(Set.of("a", "b"), + new HashSet<>(new TablesNamesFinder<>().getTables((Statement) event))); + + AlterEvent altered = assertInstanceOf(AlterEvent.class, CCJSqlParserUtil.parse( + "ALTER EVENT ev DO BEGIN IF @a > 0 THEN DELETE FROM t1; END IF; END")); + assertTrue(altered.getBody() instanceof UnsupportedStatement); + } + + @Test + void parsesDisableOnReplica() throws JSQLParserException { + AlterEvent event = assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT ev DISABLE ON REPLICA")); + assertEquals(EventStatus.DISABLE_ON_REPLICA, event.getStatus()); + + event = assertInstanceOf(AlterEvent.class, + CCJSqlParserUtil.parse("ALTER EVENT ev DISABLE ON SLAVE")); + assertEquals(EventStatus.DISABLE_ON_SLAVE, event.getStatus()); + } }