From e7bc1be4d9712e752b7036ffc7a4821869cdc91e Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 29 Oct 2026 10:45:39 +0900 Subject: [PATCH] fix: parse MySQL events as SHOW CREATE EVENT prints them SHOW CREATE EVENT prints DEFINER=user@host before EVENT, and MySQL 8.4 prints DISABLE ON REPLICA for an event that is disabled on replicas. Event bodies are often BEGIN ... END blocks. CREATE and ALTER EVENT with a DEFINER fell back to UnsupportedStatement, and DISABLE ON REPLICA and a BEGIN ... END body failed. CREATE EVENT and ALTER EVENT now read DEFINER with the production that stored routines use and keep it in EventStatement.getDefiner(). The body reuses MySqlTriggerBody, so a block of SQL statements is a Block and other compound statements keep their tokens, as in triggers. EventStatus gains DISABLE_ON_REPLICA; DISABLE ON SLAVE still gives DISABLE_ON_SLAVE. Signed-off-by: minleejae --- .../statement/create/event/AlterEvent.java | 4 +- .../statement/create/event/CreateEvent.java | 3 +- .../create/event/EventStatement.java | 18 ++++ .../statement/create/event/EventStatus.java | 3 +- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 28 +++-- .../create/CreateFunctionalStatementTest.java | 5 +- .../create/event/EventStatementTest.java | 101 ++++++++++++++++++ 7 files changed, 150 insertions(+), 12 deletions(-) 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()); + } }