diff --git a/README.md b/README.md
index e7a8db452..5946a44c2 100644
--- a/README.md
+++ b/README.md
@@ -150,6 +150,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J
| **MySQL table maintenance** | `ANALYZE` · `CHECK` · `CHECKSUM` · `OPTIMIZE` · `REPAIR TABLE` |
| **PostgreSQL maintenance** | `VACUUM` · `ANALYZE` · `CLUSTER` · `CHECKPOINT` |
| **MySQL locks** | `LOCK TABLES` · `UNLOCK TABLES` · `LOCK INSTANCE FOR BACKUP` · `UNLOCK INSTANCE` |
+| **MySQL server administration** | `FLUSH` · `KILL` · `RESET PERSIST` · `HANDLER … OPEN`/`READ`/`CLOSE` |
| **MySQL account management** | `CREATE`/`ALTER`/`RENAME USER` · `CREATE ROLE` · `SET ROLE` · `SET DEFAULT ROLE` · `SET PASSWORD` |
| **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` |
| **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` |
diff --git a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
index c5ef37b5d..02b1c0b9c 100644
--- a/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
+++ b/src/main/java/net/sf/jsqlparser/parser/RoutineBodyBoundary.java
@@ -248,7 +248,7 @@ private void acceptStatementToken(Token token, Token next) {
if (statementStart) {
statementKind = kind;
}
- if (statementKind == K_DECLARE && keyword(token, "HANDLER")) {
+ if (statementKind == K_DECLARE && token.kind == K_HANDLER) {
handlerAction = true;
}
statementStart = false;
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 dc3d137e3..f69ff2e46 100644
--- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
+++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java
@@ -28,6 +28,8 @@
import net.sf.jsqlparser.statement.ShowStatement;
import net.sf.jsqlparser.statement.TableMaintenanceStatement;
import net.sf.jsqlparser.statement.UseStatement;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
import net.sf.jsqlparser.statement.alter.Alter;
import net.sf.jsqlparser.statement.alter.AlterEventTrigger;
import net.sf.jsqlparser.statement.alter.sequence.AlterSequence;
@@ -51,6 +53,7 @@
import net.sf.jsqlparser.statement.drop.Drop;
import net.sf.jsqlparser.statement.execute.Execute;
import net.sf.jsqlparser.statement.grant.Grant;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
import net.sf.jsqlparser.statement.merge.Merge;
@@ -490,6 +493,27 @@ public enum Feature {
*/
tableMaintenance,
+ /**
+ * MySQL "FLUSH" statement is allowed
+ *
+ * @see FlushStatement
+ */
+ flush,
+
+ /**
+ * MySQL "KILL" statement is allowed
+ *
+ * @see KillStatement
+ */
+ kill,
+
+ /**
+ * MySQL "HANDLER ... OPEN", "HANDLER ... READ" and "HANDLER ... CLOSE" statements are allowed
+ *
+ * @see HandlerStatement
+ */
+ handler,
+
/**
* PostgreSQL "VACUUM" statement is allowed
*
@@ -930,6 +954,12 @@ public enum Feature {
* @see ResetStatement
*/
reset,
+ /**
+ * MySQL "RESET PERSIST" statement is allowed
+ *
+ * @see ResetStatement#isPersist()
+ */
+ resetPersist,
/**
* @see Pivot
*/
diff --git a/src/main/java/net/sf/jsqlparser/statement/ResetStatement.java b/src/main/java/net/sf/jsqlparser/statement/ResetStatement.java
index 6d40879f8..a9cc1744e 100644
--- a/src/main/java/net/sf/jsqlparser/statement/ResetStatement.java
+++ b/src/main/java/net/sf/jsqlparser/statement/ResetStatement.java
@@ -11,9 +11,18 @@
import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+/**
+ * PostgreSQL {@code RESET configuration_parameter}, {@code RESET ALL} and {@code RESET TIME ZONE},
+ * and MySQL {@code RESET PERSIST [[IF EXISTS] system_var_name]}, which {@link #isPersist()} marks.
+ *
+ * @see PostgreSQL RESET
+ * @see MySQL RESET PERSIST
+ */
public final class ResetStatement extends ASTNodeAccessImpl implements Statement {
private String name = "";
+ private boolean persist;
+ private boolean ifExists;
public ResetStatement() {
// empty constructor
@@ -27,6 +36,10 @@ public void add(String name) {
this.name = name;
}
+ /**
+ * Returns the parameter or, for RESET PERSIST, the system variable; null when RESET PERSIST
+ * names none and removes every persisted variable.
+ */
public String getName() {
return name;
}
@@ -35,10 +48,52 @@ public void setName(String name) {
this.name = name;
}
+ /** Whether this is MySQL RESET PERSIST, which removes persisted global system variables. */
+ public boolean isPersist() {
+ return persist;
+ }
+
+ public void setPersist(boolean persist) {
+ this.persist = persist;
+ }
+
+ public ResetStatement withPersist(boolean persist) {
+ setPersist(persist);
+ return this;
+ }
+
+ /** Whether RESET PERSIST has IF EXISTS before the variable name. */
+ public boolean isIfExists() {
+ return ifExists;
+ }
+
+ public void setIfExists(boolean ifExists) {
+ this.ifExists = ifExists;
+ }
+
+ public ResetStatement withIfExists(boolean ifExists) {
+ setIfExists(ifExists);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append("RESET ");
+ if (!persist) {
+ return builder.append(name);
+ }
+ builder.append("PERSIST");
+ if (ifExists) {
+ builder.append(" IF EXISTS");
+ }
+ if (name != null && !name.isEmpty()) {
+ builder.append(' ').append(name);
+ }
+ return builder;
+ }
+
@Override
public String toString() {
- StringBuilder b = new StringBuilder("RESET ").append(name);
- return b.toString();
+ return appendTo(new StringBuilder()).toString();
}
@Override
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
index df67903b0..eba66018a 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java
@@ -98,6 +98,9 @@
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.load.LoadDataStatement;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -783,6 +786,40 @@ public Void visit(TableMaintenanceStatement statement, S context) {
return null;
}
+ @Override
+ public Void visit(FlushStatement flush, S context) {
+ // a server operation like ALTER SYSTEM FLUSH; WITH READ LOCK and FOR EXPORT also lock
+ analysis.claimTopLevel();
+ analysis.certain(StmtFeature.MODIFIES_SESSION);
+ if (flush.getTableLock() != null) {
+ analysis.certain(StmtFeature.MODIFIES_TRANSACTION);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visit(KillStatement kill, S context) {
+ // ends another session or its statement, like ALTER SYSTEM KILL SESSION
+ analysis.claimTopLevel();
+ analysis.certain(StmtFeature.MODIFIES_SESSION);
+ return null;
+ }
+
+ @Override
+ public Void visit(HandlerStatement handler, S context) {
+ if (handler.getAction() != HandlerStatement.Action.READ) {
+ // OPEN and CLOSE change the handlers of the session
+ analysis.claimTopLevel();
+ analysis.certain(StmtFeature.MODIFIES_SESSION);
+ return null;
+ }
+ if (analysis.claimTopLevel()) {
+ analysis.certain(StmtFeature.RETURNS_RESULT_SET);
+ }
+ analysis.certain(StmtFeature.READS_DATA);
+ return super.visit(handler, context);
+ }
+
@Override
public Void visit(VacuumStatement vacuum, S context) {
// reclaims storage and, with ANALYZE, rewrites statistics; FULL rewrites the tables
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
index 4063637f7..6a9aa337b 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java
@@ -90,6 +90,9 @@
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -376,6 +379,30 @@ default void visit(TableMaintenanceStatement statement) {
this.visit(statement, null);
}
+ default T visit(FlushStatement flush, S context) {
+ return null;
+ }
+
+ default void visit(FlushStatement flush) {
+ this.visit(flush, null);
+ }
+
+ default T visit(KillStatement kill, S context) {
+ return null;
+ }
+
+ default void visit(KillStatement kill) {
+ this.visit(kill, null);
+ }
+
+ default T visit(HandlerStatement handler, S context) {
+ return null;
+ }
+
+ default void visit(HandlerStatement handler) {
+ this.visit(handler, null);
+ }
+
default T visit(VacuumStatement vacuum, S context) {
return null;
}
diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
index 9cb08129e..906f812de 100644
--- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
+++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java
@@ -93,6 +93,9 @@
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.insert.InsertConflictAction;
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -723,6 +726,24 @@ public T visit(TableMaintenanceStatement statement, S context) {
return null;
}
+ @Override
+ public T visit(FlushStatement flush, S context) {
+ return null;
+ }
+
+ @Override
+ public T visit(KillStatement kill, S context) {
+ expressionVisitor.visitExpression(kill.getProcesslistId(), context);
+ return null;
+ }
+
+ @Override
+ public T visit(HandlerStatement handler, S context) {
+ expressionVisitor.visitExpression(handler.getKeyValues(), context);
+ expressionVisitor.visitExpression(handler.getWhere(), context);
+ return null;
+ }
+
@Override
public T visit(VacuumStatement vacuum, S context) {
return null;
diff --git a/src/main/java/net/sf/jsqlparser/statement/admin/FlushOption.java b/src/main/java/net/sf/jsqlparser/statement/admin/FlushOption.java
new file mode 100644
index 000000000..b3945aef6
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/admin/FlushOption.java
@@ -0,0 +1,97 @@
+/*-
+ * #%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.admin;
+
+import java.util.Locale;
+import java.util.Objects;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+
+/**
+ * One flush option of MySQL {@code FLUSH}, such as {@code BINARY LOGS} or
+ * {@code RELAY LOGS FOR CHANNEL 'c1'}.
+ *
+ * @see MySQL FLUSH
+ */
+public class FlushOption extends ASTNodeAccessImpl {
+ private Kind kind;
+ private StringValue channel;
+
+ /** The flush options of MySQL 8.4. */
+ public enum Kind {
+ BINARY_LOGS, ENGINE_LOGS, ERROR_LOGS, GENERAL_LOGS, LOGS, OPTIMIZER_COSTS, PRIVILEGES, RELAY_LOGS, SLOW_LOGS, STATUS, USER_RESOURCES;
+
+ /** Returns the SQL spelling, for example {@code BINARY LOGS}. */
+ public String getKeyword() {
+ return this == OPTIMIZER_COSTS || this == USER_RESOURCES ? name()
+ : name().replace('_', ' ');
+ }
+
+ /**
+ * Returns the option spelled by the keyword, ignoring case, or null.
+ *
+ * @param keyword the words of the option separated by one space, such as
+ * {@code binary logs}
+ */
+ public static Kind fromKeyword(String keyword) {
+ String upper = keyword == null ? null : keyword.toUpperCase(Locale.ROOT);
+ for (Kind kind : values()) {
+ if (kind.getKeyword().equals(upper)) {
+ return kind;
+ }
+ }
+ return null;
+ }
+ }
+
+ public FlushOption(Kind kind) {
+ setKind(kind);
+ }
+
+ public Kind getKind() {
+ return kind;
+ }
+
+ public void setKind(Kind kind) {
+ this.kind = Objects.requireNonNull(kind);
+ }
+
+ public FlushOption withKind(Kind kind) {
+ setKind(kind);
+ return this;
+ }
+
+ /** Returns the channel of {@code RELAY LOGS FOR CHANNEL 'name'}, or null. */
+ public StringValue getChannel() {
+ return channel;
+ }
+
+ public void setChannel(StringValue channel) {
+ this.channel = channel;
+ }
+
+ public FlushOption withChannel(StringValue channel) {
+ setChannel(channel);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append(kind.getKeyword());
+ if (channel != null) {
+ builder.append(" FOR CHANNEL ").append(channel);
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/admin/FlushStatement.java b/src/main/java/net/sf/jsqlparser/statement/admin/FlushStatement.java
new file mode 100644
index 000000000..95957fe4d
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/admin/FlushStatement.java
@@ -0,0 +1,181 @@
+/*-
+ * #%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.admin;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+import net.sf.jsqlparser.statement.TableMaintenanceStatement;
+
+/**
+ * MySQL {@code FLUSH}, either with a list of {@link FlushOption flush options} or as
+ * {@code FLUSH TABLES}, which may name tables and lock them.
+ * Examples:
+ * FLUSH LOCAL BINARY LOGS, RELAY LOGS FOR CHANNEL 'c1'
+ * FLUSH TABLES t, u WITH READ LOCK
+ *
+ * Without options the statement is {@code FLUSH TABLES}: {@link #isFlushTables()} derives the form
+ * from {@link #getOptions()}, and the tables, the spelling and the lock apply only to that form.
+ *
+ * @see MySQL FLUSH
+ */
+public class FlushStatement extends ASTNodeAccessImpl implements Statement {
+ private TableMaintenanceStatement.Modifier modifier;
+ private final List options = new ArrayList<>();
+ private boolean tablesKeyword = true;
+ private final List tables = new ArrayList<>();
+ private TableLock tableLock;
+
+ /** What {@code FLUSH TABLES} does to the tables after flushing them. */
+ public enum TableLock {
+ /** {@code WITH READ LOCK}: the named tables, or every table, stay read locked. */
+ WITH_READ_LOCK("WITH READ LOCK"),
+ /** {@code FOR EXPORT}: the named tables are made ready to copy their files. */
+ FOR_EXPORT("FOR EXPORT");
+
+ private final String keyword;
+
+ TableLock(String keyword) {
+ this.keyword = keyword;
+ }
+
+ public String getKeyword() {
+ return keyword;
+ }
+ }
+
+ /** Returns {@code NO_WRITE_TO_BINLOG} or its synonym {@code LOCAL}, or null. */
+ public TableMaintenanceStatement.Modifier getModifier() {
+ return modifier;
+ }
+
+ public void setModifier(TableMaintenanceStatement.Modifier modifier) {
+ this.modifier = modifier;
+ }
+
+ public FlushStatement withModifier(TableMaintenanceStatement.Modifier modifier) {
+ setModifier(modifier);
+ return this;
+ }
+
+ /** Returns the flush options in source order; empty for {@code FLUSH TABLES}. */
+ public List getOptions() {
+ return Collections.unmodifiableList(options);
+ }
+
+ public void setOptions(Collection options) {
+ this.options.clear();
+ if (options != null) {
+ this.options.addAll(options);
+ }
+ }
+
+ public FlushStatement withOptions(Collection options) {
+ setOptions(options);
+ return this;
+ }
+
+ public FlushStatement addOptions(FlushOption... options) {
+ Collections.addAll(this.options, options);
+ return this;
+ }
+
+ /** Whether the statement is {@code FLUSH TABLE} or {@code FLUSH TABLES}: it has no options. */
+ public boolean isFlushTables() {
+ return options.isEmpty();
+ }
+
+ /** Whether {@code FLUSH TABLES} is spelled with {@code TABLES} instead of {@code TABLE}. */
+ public boolean isTablesKeyword() {
+ return tablesKeyword;
+ }
+
+ public void setTablesKeyword(boolean tablesKeyword) {
+ this.tablesKeyword = tablesKeyword;
+ }
+
+ public FlushStatement withTablesKeyword(boolean tablesKeyword) {
+ setTablesKeyword(tablesKeyword);
+ return this;
+ }
+
+ /** Returns the tables of {@code FLUSH TABLES}; empty when it flushes every table. */
+ public List getTables() {
+ return Collections.unmodifiableList(tables);
+ }
+
+ public void setTables(Collection extends Table> tables) {
+ this.tables.clear();
+ if (tables != null) {
+ this.tables.addAll(tables);
+ }
+ }
+
+ public FlushStatement withTables(Collection extends Table> tables) {
+ setTables(tables);
+ return this;
+ }
+
+ public FlushStatement addTables(Table... tables) {
+ Collections.addAll(this.tables, tables);
+ return this;
+ }
+
+ /** Returns the lock of {@code FLUSH TABLES}, or null. */
+ public TableLock getTableLock() {
+ return tableLock;
+ }
+
+ public void setTableLock(TableLock tableLock) {
+ this.tableLock = tableLock;
+ }
+
+ public FlushStatement withTableLock(TableLock tableLock) {
+ setTableLock(tableLock);
+ return this;
+ }
+
+ public StringBuilder appendTo(StringBuilder builder) {
+ builder.append("FLUSH");
+ if (modifier != null) {
+ builder.append(' ').append(modifier);
+ }
+ if (!options.isEmpty()) {
+ for (int i = 0; i < options.size(); i++) {
+ builder.append(i > 0 ? ", " : " ");
+ options.get(i).appendTo(builder);
+ }
+ return builder;
+ }
+ builder.append(tablesKeyword ? " TABLES" : " TABLE");
+ for (int i = 0; i < tables.size(); i++) {
+ builder.append(i > 0 ? ", " : " ").append(tables.get(i).getFullyQualifiedName());
+ }
+ if (tableLock != null) {
+ builder.append(' ').append(tableLock.getKeyword());
+ }
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/admin/KillStatement.java b/src/main/java/net/sf/jsqlparser/statement/admin/KillStatement.java
new file mode 100644
index 000000000..79d13dad8
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/admin/KillStatement.java
@@ -0,0 +1,89 @@
+/*-
+ * #%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.admin;
+
+import java.util.function.Consumer;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+
+/**
+ * MySQL {@code KILL [CONNECTION | QUERY] processlist_id}, which ends a connection or the statement
+ * it runs. MySQL takes any expression for the identifier, such as {@code @id} or {@code 1 + 2}.
+ *
+ * @see MySQL KILL
+ */
+public class KillStatement extends ASTNodeAccessImpl implements Statement {
+ private Kind kind;
+ private Expression processlistId;
+
+ /** What KILL ends. Without either keyword, KILL ends the connection. */
+ public enum Kind {
+ CONNECTION, QUERY
+ }
+
+ public KillStatement() {
+ // empty constructor
+ }
+
+ public KillStatement(Kind kind, Expression processlistId) {
+ this.kind = kind;
+ this.processlistId = processlistId;
+ }
+
+ /** Returns {@code CONNECTION} or {@code QUERY}, or null when neither keyword was given. */
+ public Kind getKind() {
+ return kind;
+ }
+
+ public void setKind(Kind kind) {
+ this.kind = kind;
+ }
+
+ public KillStatement withKind(Kind kind) {
+ setKind(kind);
+ return this;
+ }
+
+ public Expression getProcesslistId() {
+ return processlistId;
+ }
+
+ public void setProcesslistId(Expression processlistId) {
+ this.processlistId = processlistId;
+ }
+
+ public KillStatement withProcesslistId(Expression processlistId) {
+ setProcesslistId(processlistId);
+ return this;
+ }
+
+ /** Shares the statement syntax with deparsers, which render the identifier expression. */
+ public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) {
+ builder.append("KILL ");
+ if (kind != null) {
+ builder.append(kind).append(' ');
+ }
+ expressionRenderer.accept(processlistId);
+ return builder;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ return appendTo(builder, builder::append).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.visit(this, context);
+ }
+}
diff --git a/src/main/java/net/sf/jsqlparser/statement/handler/HandlerStatement.java b/src/main/java/net/sf/jsqlparser/statement/handler/HandlerStatement.java
new file mode 100644
index 000000000..f70be46b3
--- /dev/null
+++ b/src/main/java/net/sf/jsqlparser/statement/handler/HandlerStatement.java
@@ -0,0 +1,277 @@
+/*-
+ * #%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.handler;
+
+import java.util.Objects;
+import java.util.function.Consumer;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.parser.ASTNodeAccessImpl;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementVisitor;
+import net.sf.jsqlparser.statement.select.Limit;
+import net.sf.jsqlparser.statement.select.Offset;
+
+/**
+ * MySQL {@code HANDLER}, which opens a table for direct row access, reads rows from it and closes
+ * it again.
+ * Examples:
+ * HANDLER db.t OPEN AS h
+ * HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3
+ * HANDLER h READ i1 NEXT
+ * HANDLER h READ FIRST
+ * HANDLER h CLOSE
+ *
+ * {@link #getTable()} is the table that OPEN opens, with the handler name as its alias when one is
+ * given. READ and CLOSE name the handler, which is that alias or the unqualified table name, so
+ * their {@link #getTable()} holds the handler name rather than a table reference.
+ * A READ through an index either compares the index with a list of key values
+ * ({@link #getKeyComparison()} and {@link #getKeyValues()}) or moves in index order
+ * ({@link #getDirection()}). A READ without an index moves in storage order: FIRST or NEXT.
+ *
+ * @see MySQL HANDLER
+ */
+public class HandlerStatement extends ASTNodeAccessImpl implements Statement {
+ private Action action = Action.OPEN;
+ private Table table;
+ private String indexName;
+ private KeyComparison keyComparison;
+ private ExpressionList> keyValues;
+ private Direction direction;
+ private Expression where;
+ private Limit limit;
+ private Offset offset;
+
+ public enum Action {
+ OPEN, READ, CLOSE
+ }
+
+ /** Where a READ continues: FIRST and NEXT, and through an index also PREV and LAST. */
+ public enum Direction {
+ FIRST, NEXT, PREV, LAST
+ }
+
+ /** How a READ through an index compares the index with the key values. */
+ public enum KeyComparison {
+ /** {@code =} */
+ EQUALS("="),
+ /** {@code <=} */
+ LESS_THAN_OR_EQUALS("<="),
+ /** {@code >=} */
+ GREATER_THAN_OR_EQUALS(">="),
+ /** {@code <} */
+ LESS_THAN("<"),
+ /** {@code >} */
+ GREATER_THAN(">");
+
+ private final String operator;
+
+ KeyComparison(String operator) {
+ this.operator = operator;
+ }
+
+ public String getOperator() {
+ return operator;
+ }
+ }
+
+ public HandlerStatement() {
+ // empty constructor
+ }
+
+ public HandlerStatement(Action action, Table table) {
+ setAction(action);
+ this.table = table;
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public void setAction(Action action) {
+ this.action = Objects.requireNonNull(action);
+ }
+
+ public HandlerStatement withAction(Action action) {
+ setAction(action);
+ return this;
+ }
+
+ /**
+ * Returns the table that OPEN opens, whose alias is the handler name, or the handler that READ
+ * and CLOSE use.
+ */
+ public Table getTable() {
+ return table;
+ }
+
+ public void setTable(Table table) {
+ this.table = table;
+ }
+
+ public HandlerStatement withTable(Table table) {
+ setTable(table);
+ return this;
+ }
+
+ /** Returns the name of the handler: the alias that OPEN gives, or else the table name. */
+ public String getHandlerName() {
+ if (table == null) {
+ return null;
+ }
+ Alias alias = table.getAlias();
+ return alias != null ? alias.getName() : table.getName();
+ }
+
+ /** Returns the index a READ uses, or null when it reads in storage order. */
+ public String getIndexName() {
+ return indexName;
+ }
+
+ public void setIndexName(String indexName) {
+ this.indexName = indexName;
+ }
+
+ public HandlerStatement withIndexName(String indexName) {
+ setIndexName(indexName);
+ return this;
+ }
+
+ /** Returns how a READ compares the index with {@link #getKeyValues()}, or null. */
+ public KeyComparison getKeyComparison() {
+ return keyComparison;
+ }
+
+ public void setKeyComparison(KeyComparison keyComparison) {
+ this.keyComparison = keyComparison;
+ }
+
+ public HandlerStatement withKeyComparison(KeyComparison keyComparison) {
+ setKeyComparison(keyComparison);
+ return this;
+ }
+
+ /** Returns the parenthesized key values a READ compares the index with, or null. */
+ public ExpressionList> getKeyValues() {
+ return keyValues;
+ }
+
+ public void setKeyValues(ExpressionList> keyValues) {
+ this.keyValues = keyValues;
+ }
+
+ public HandlerStatement withKeyValues(ExpressionList> keyValues) {
+ setKeyValues(keyValues);
+ return this;
+ }
+
+ /** Returns where a READ continues, or null when it compares key values. */
+ public Direction getDirection() {
+ return direction;
+ }
+
+ public void setDirection(Direction direction) {
+ this.direction = direction;
+ }
+
+ public HandlerStatement withDirection(Direction direction) {
+ setDirection(direction);
+ return this;
+ }
+
+ public Expression getWhere() {
+ return where;
+ }
+
+ public void setWhere(Expression where) {
+ this.where = where;
+ }
+
+ public HandlerStatement withWhere(Expression where) {
+ setWhere(where);
+ return this;
+ }
+
+ public Limit getLimit() {
+ return limit;
+ }
+
+ public void setLimit(Limit limit) {
+ this.limit = limit;
+ }
+
+ public HandlerStatement withLimit(Limit limit) {
+ setLimit(limit);
+ return this;
+ }
+
+ /** Returns the OFFSET of {@code LIMIT row_count OFFSET offset}, or null. */
+ public Offset getOffset() {
+ return offset;
+ }
+
+ public void setOffset(Offset offset) {
+ this.offset = offset;
+ }
+
+ public HandlerStatement withOffset(Offset offset) {
+ setOffset(offset);
+ return this;
+ }
+
+ /** Shares the statement syntax with deparsers, which render the WHERE and key values. */
+ public StringBuilder appendTo(StringBuilder builder, Consumer expressionRenderer) {
+ builder.append("HANDLER ").append(table.getFullyQualifiedName()).append(' ')
+ .append(action);
+ if (action == Action.OPEN) {
+ if (table.getAlias() != null) {
+ builder.append(table.getAlias());
+ }
+ } else if (action == Action.READ) {
+ appendReadTo(builder, expressionRenderer);
+ }
+ return builder;
+ }
+
+ private void appendReadTo(StringBuilder builder, Consumer expressionRenderer) {
+ if (indexName != null) {
+ builder.append(' ').append(indexName);
+ }
+ if (keyComparison != null) {
+ builder.append(' ').append(keyComparison.getOperator()).append(' ');
+ expressionRenderer.accept(keyValues);
+ } else {
+ builder.append(' ').append(direction);
+ }
+ if (where != null) {
+ builder.append(" WHERE ");
+ expressionRenderer.accept(where);
+ }
+ if (limit != null) {
+ builder.append(limit);
+ }
+ if (offset != null) {
+ builder.append(offset);
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder builder = new StringBuilder();
+ return appendTo(builder, builder::append).toString();
+ }
+
+ @Override
+ public T accept(StatementVisitor statementVisitor, S context) {
+ return statementVisitor.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 e1cf00f3f..11484a068 100644
--- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
+++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java
@@ -194,6 +194,9 @@
import net.sf.jsqlparser.statement.insert.OracleMultiInsertClause;
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
import net.sf.jsqlparser.statement.load.LoadDataStatement;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -1732,6 +1735,35 @@ public void visit(TableMaintenanceStatement statement) {
StatementVisitor.super.visit(statement);
}
+ @Override
+ public Void visit(FlushStatement flush, S context) {
+ flush.getTables().forEach(table -> visit(table, context));
+ return null;
+ }
+
+ @Override
+ public Void visit(KillStatement kill, S context) {
+ if (kill.getProcesslistId() != null) {
+ kill.getProcesslistId().accept(this, context);
+ }
+ return null;
+ }
+
+ @Override
+ public Void visit(HandlerStatement handler, S context) {
+ if (handler.getAction() == HandlerStatement.Action.OPEN) {
+ visit(handler.getTable(), context);
+ }
+ // READ and CLOSE name a handler, which may be an alias that OPEN gave the table
+ if (handler.getKeyValues() != null) {
+ handler.getKeyValues().accept(this, context);
+ }
+ if (handler.getWhere() != null) {
+ handler.getWhere().accept(this, context);
+ }
+ return null;
+ }
+
@Override
public Void visit(VacuumStatement vacuum, S context) {
// the column lists name columns of these tables, not tables
@@ -2040,7 +2072,7 @@ public void visit(SetStatement set) {
@Override
public Void visit(ResetStatement reset, S context) {
- throwUnsupported(reset);
+ // RESET names a configuration parameter or a system variable, never a table
return null;
}
diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ResetStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ResetStatementDeParser.java
index 22eaca518..b65eabfcc 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/ResetStatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/ResetStatementDeParser.java
@@ -24,8 +24,7 @@ public ResetStatementDeParser(ExpressionVisitor expressionVisitor
@Override
public void deParse(ResetStatement set) {
- builder.append("RESET ");
- builder.append(set.getName());
+ set.appendTo(builder);
}
public ExpressionVisitor getExpressionVisitor() {
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 0e9adaba3..19777b82f 100644
--- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
+++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java
@@ -121,6 +121,9 @@
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -393,6 +396,22 @@ public StringBuilder visit(TableMaintenanceStatement statement, S context) {
return builder;
}
+ @Override
+ public StringBuilder visit(FlushStatement flush, S context) {
+ return flush.appendTo(builder);
+ }
+
+ @Override
+ public StringBuilder visit(KillStatement kill, S context) {
+ return kill.appendTo(builder, id -> id.accept(expressionDeParser, context));
+ }
+
+ @Override
+ public StringBuilder visit(HandlerStatement handler, S context) {
+ return handler.appendTo(builder,
+ expression -> expression.accept(expressionDeParser, context));
+ }
+
@Override
public StringBuilder visit(VacuumStatement vacuum, S context) {
return vacuum.appendTo(builder);
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
index 5ca009b15..c85cc390e 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MySqlVersion.java
@@ -79,6 +79,14 @@ public enum MySqlVersion implements Version {
// https://dev.mysql.com/doc/refman/8.0/en/table-maintenance-statements.html
Feature.analyze, Feature.tableMaintenance,
+ // https://dev.mysql.com/doc/refman/8.0/en/flush.html
+ // https://dev.mysql.com/doc/refman/8.0/en/kill.html
+ // https://dev.mysql.com/doc/refman/8.0/en/reset-persist.html
+ Feature.flush, Feature.kill, Feature.resetPersist,
+
+ // https://dev.mysql.com/doc/refman/8.0/en/handler.html
+ Feature.handler,
+
// https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html
Feature.lock, Feature.unlockTables,
diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ResetStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ResetStatementValidator.java
index 3a4efb95b..83d3b80cb 100644
--- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ResetStatementValidator.java
+++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ResetStatementValidator.java
@@ -18,7 +18,7 @@ public class ResetStatementValidator extends AbstractValidator {
@Override
public void validate(ResetStatement reset) {
for (ValidationCapability c : getCapabilities()) {
- validateFeature(c, Feature.reset);
+ validateFeature(c, reset.isPersist() ? Feature.resetPersist : Feature.reset);
}
}
}
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 ec1b85f29..9b87405ce 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
@@ -125,6 +125,9 @@
import net.sf.jsqlparser.statement.insert.Insert;
import net.sf.jsqlparser.statement.insert.InsertBulk;
import net.sf.jsqlparser.statement.insert.ParenthesedInsert;
+import net.sf.jsqlparser.statement.admin.FlushStatement;
+import net.sf.jsqlparser.statement.admin.KillStatement;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import net.sf.jsqlparser.statement.lock.BackupLockStatement;
import net.sf.jsqlparser.statement.lock.LockStatement;
import net.sf.jsqlparser.statement.lock.UnlockStatement;
@@ -649,6 +652,35 @@ public Void visit(TableMaintenanceStatement statement, S context) {
return null;
}
+ @Override
+ public Void visit(FlushStatement flush, S context) {
+ validateFeature(Feature.flush);
+ for (Table table : flush.getTables()) {
+ validateName(NamedObject.table, table.getFullyQualifiedName());
+ }
+ return null;
+ }
+
+ @Override
+ public Void visit(KillStatement kill, S context) {
+ validateFeature(Feature.kill);
+ validateOptionalExpression(kill.getProcesslistId());
+ return null;
+ }
+
+ @Override
+ public Void visit(HandlerStatement handler, S context) {
+ validateFeature(Feature.handler);
+ if (handler.getAction() == HandlerStatement.Action.OPEN) {
+ Table table = handler.getTable();
+ validateNameWithAlias(NamedObject.table, table.getFullyQualifiedName(),
+ ValidationUtil.getAlias(table.getAlias()));
+ }
+ validateOptionalExpression(handler.getKeyValues());
+ validateOptionalExpression(handler.getWhere());
+ return null;
+ }
+
@Override
public Void visit(VacuumStatement vacuum, S context) {
getValidator(VacuumValidator.class).validate(vacuum);
diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
index 4cf410188..187818085 100644
--- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
+++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt
@@ -103,6 +103,8 @@ import net.sf.jsqlparser.statement.role.*;
import net.sf.jsqlparser.statement.imprt.*;
import net.sf.jsqlparser.statement.export.*;
import net.sf.jsqlparser.statement.lock.*;
+import net.sf.jsqlparser.statement.admin.*;
+import net.sf.jsqlparser.statement.handler.HandlerStatement;
import java.util.*;
import java.util.AbstractMap.SimpleEntry;
import java.util.concurrent.CancellationException;
@@ -176,6 +178,14 @@ public class CCJSqlParser extends AbstractJSqlParser {
return isPostgreSqlMaintenanceAhead();
case K_UNLOCK:
return isUnlockTablesAhead() || isBackupLockAhead();
+ case K_CHECK: case K_CHECKSUM: case K_OPTIMIZE: case K_REPAIR:
+ return isTableMaintenanceAhead();
+ case K_FLUSH:
+ return isFlushAhead();
+ case K_KILL:
+ return isKillAhead();
+ case K_HANDLER:
+ return isHandlerAhead();
case K_IMPORT: case K_EXPORT:
return Dialect.EXASOL.name().equals(getAsString(Feature.dialect))
|| next.kind == K_EXPORT && getToken(2).kind == K_DATA;
@@ -2570,6 +2580,122 @@ public class CCJSqlParser extends AbstractJSqlParser {
&& isMySqlLockDialect(false);
}
+ /** MySQL's FLUSH, KILL, HANDLER and RESET PERSIST parse without a dialect and with MySQL's. */
+ private boolean isMySqlServerAdminDialect() {
+ String dialect = getAsString(Feature.dialect);
+ return dialect == null || Dialect.MYSQL.name().equals(dialect);
+ }
+
+ /**
+ * Whether MySQL FLUSH starts here: FLUSH, an optional NO_WRITE_TO_BINLOG or LOCAL, and then
+ * TABLE, TABLES or the first word of a flush option. Other FLUSH forms, such as the FLUSH
+ * HOSTS that MySQL 8.4 removed, remain unsupported statements.
+ */
+ private boolean isFlushAhead() {
+ if (getToken(1).kind != K_FLUSH || !isMySqlServerAdminDialect()) {
+ return false;
+ }
+ int modifier = getToken(2).kind;
+ Token next = getToken(modifier == K_NO_WRITE_TO_BINLOG || modifier == K_LOCAL ? 3 : 2);
+ if (next.kind == K_TABLE || next.kind == K_TABLES) {
+ return true;
+ }
+ if (next.kind != S_IDENTIFIER && next.kind != K_BINARY && next.kind != K_ENGINE
+ && next.kind != K_ERROR) {
+ return false;
+ }
+ for (FlushOption.Kind kind : FlushOption.Kind.values()) {
+ if (kind.getKeyword().split(" ")[0].equalsIgnoreCase(next.image)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Whether MySQL KILL [CONNECTION | QUERY] processlist_id starts here. */
+ private boolean isKillAhead() {
+ int next = getToken(2).kind;
+ return getToken(1).kind == K_KILL && next != EOF && next != ST_SEMICOLON
+ && isMySqlServerAdminDialect();
+ }
+
+ /** Whether MySQL HANDLER starts here: a table or handler name, then OPEN, READ or CLOSE. */
+ private boolean isHandlerAhead() {
+ if (getToken(1).kind != K_HANDLER || !isMySqlServerAdminDialect()) {
+ return false;
+ }
+ int offset = 2;
+ while (isIdentifierishNameToken(getToken(offset)) && ".".equals(getToken(offset + 1).image)) {
+ offset += 2;
+ }
+ int action = getToken(offset + 1).kind;
+ return isIdentifierishNameToken(getToken(offset))
+ && (action == K_OPEN || action == K_READ || action == K_CLOSE);
+ }
+
+ /**
+ * Whether HANDLER ... READ continues with an index name: a comparison or FIRST, NEXT, PREV or
+ * LAST follows the name. Otherwise FIRST or NEXT reads in storage order.
+ */
+ private boolean isHandlerIndexAhead() {
+ Token next = getToken(2);
+ switch (next.kind) {
+ case K_FIRST: case K_NEXT: case K_LAST:
+ case OP_GREATERTHANEQUALS: case OP_MINORTHANEQUALS:
+ return isIdentifierishNameToken(getToken(1));
+ default:
+ return isIdentifierishNameToken(getToken(1)) && ("=".equals(next.image)
+ || "<".equals(next.image) || ">".equals(next.image)
+ || next.kind == S_IDENTIFIER && "PREV".equalsIgnoreCase(next.image));
+ }
+ }
+
+ /**
+ * Whether MySQL RESET PERSIST starts here. Without a dialect only a variable name, which may
+ * have a component prefix, or IF EXISTS after PERSIST tells it from PostgreSQL's RESET of a
+ * parameter named persist, which a bare RESET PERSIST keeps.
+ */
+ private boolean isResetPersistAhead() {
+ if (getToken(1).kind != K_RESET || !isWordSequenceAhead(2, "PERSIST")) {
+ return false;
+ }
+ String dialect = getAsString(Feature.dialect);
+ if (Dialect.MYSQL.name().equals(dialect)) {
+ return true;
+ }
+ Token next = getToken(3);
+ int end = getToken(4).kind;
+ return dialect == null && (next.kind == K_IF && end == K_EXISTS
+ || isIdentifierishNameToken(next)
+ && (end == EOF || end == ST_SEMICOLON || ".".equals(getToken(4).image)));
+ }
+
+ /** Whether one of the expressions holds a subquery. */
+ private static boolean containsSubquery(Expression... expressions) {
+ boolean[] found = new boolean[1];
+ ExpressionVisitorAdapter finder = new ExpressionVisitorAdapter() {
+ @Override
+ public Void visit(Select select, S context) {
+ found[0] = true;
+ return null;
+ }
+ };
+ for (Expression expression : expressions) {
+ if (expression != null) {
+ expression.accept(finder, null);
+ }
+ }
+ return found[0];
+ }
+
+ private static FlushOption.Kind flushOptionKind(String keyword) throws ParseException {
+ FlushOption.Kind kind = FlushOption.Kind.fromKeyword(keyword);
+ if (kind == null) {
+ throw new ParseException("Unknown FLUSH option: " + keyword);
+ }
+ return kind;
+ }
+
/**
* Whether NO_WRITE_TO_BINLOG or LOCAL modifies a MySQL partition operation. Followed by a
* comma, INTO or the end of the statement, the word remains a partition name.
@@ -4331,6 +4457,7 @@ String NonReservedWordPart1() :
| tk=
| tk=
| tk=
+ | tk=
| tk=
| tk=
| tk=
@@ -5217,6 +5344,12 @@ Statement SingleStatement() :
|
LOOKAHEAD({ isTableMaintenanceAhead() }) stm = TableMaintenanceStatement()
|
+ LOOKAHEAD({ isFlushAhead() }) stm = FlushStatement()
+ |
+ LOOKAHEAD({ isKillAhead() }) stm = KillStatement()
+ |
+ LOOKAHEAD({ isHandlerAhead() }) stm = HandlerStatement()
+ |
LOOKAHEAD({ isReindexAhead() }) stm = Reindex()
|
LOOKAHEAD({ getToken(1).kind == K_SECURITY && "LABEL".equalsIgnoreCase(getToken(2).image) })
@@ -5255,6 +5388,8 @@ Statement SingleStatement() :
|
stm = Set()
|
+ LOOKAHEAD({ isResetPersistAhead() }) stm = ResetPersist()
+ |
stm = Reset()
|
stm = Show()
@@ -6910,6 +7045,33 @@ ResetStatement Reset(): {
{ reset = new ResetStatement(name); return reset; }
}
+// https://dev.mysql.com/doc/refman/8.4/en/reset-persist.html
+ResetStatement ResetPersist():
+{
+ ResetStatement reset = new ResetStatement((String) null).withPersist(true);
+ String name;
+ String part;
+ Token prefix;
+}
+{
+ ContextualKeyword("PERSIST")
+ [
+ // MySQL reserves IF, so IF without EXISTS starts the next statement
+ LOOKAHEAD({ getToken(1).kind == K_IF ? getToken(2).kind == K_EXISTS
+ : isIdentifierishNameToken(getToken(1)) })
+ [ LOOKAHEAD(2) { reset.setIfExists(true); } ]
+ // MySQL's persisted_variable_ident: ident, ident.ident or DEFAULT.ident
+ (
+ LOOKAHEAD(2) prefix= "." part=RelObjectName() { name = prefix.image + "." + part; }
+ |
+ LOOKAHEAD({ getToken(1).kind != K_DEFAULT })
+ name=RelObjectName() [ "." part=RelObjectName() { name = name + "." + part; } ]
+ )
+ { reset.setName(name); }
+ ]
+ { return reset; }
+}
+
RenameTableStatement RenameTableStatement(): {
RenameTableStatement renameTableStatement;
Table oldName;
@@ -19857,7 +20019,7 @@ TableMaintenanceStatement TableMaintenanceStatement():
TableMaintenanceStatement statement = new TableMaintenanceStatement();
TableMaintenanceStatement.Operation operation = null;
TableMaintenanceStatement.Option option = null;
- Token modifier = null;
+ TableMaintenanceStatement.Modifier modifier;
Token word = null;
Table table = null;
}
@@ -19872,11 +20034,7 @@ TableMaintenanceStatement TableMaintenanceStatement():
[
LOOKAHEAD({ operation.allowsModifier()
&& (getToken(1).kind == K_NO_WRITE_TO_BINLOG || getToken(1).kind == K_LOCAL) })
- ( modifier= | modifier= )
- {
- statement.setModifier(TableMaintenanceStatement.Modifier.valueOf(
- modifier.image.toUpperCase(Locale.ROOT)));
- }
+ modifier=BinlogModifier() { statement.setModifier(modifier); }
]
( | { statement.setTablesKeyword(true); } )
table=Table() { statement.addTables(table); }
@@ -19899,6 +20057,199 @@ TableMaintenanceStatement TableMaintenanceStatement():
}
}
+/** MySQL's opt_no_write_to_binlog: NO_WRITE_TO_BINLOG or its synonym LOCAL. */
+TableMaintenanceStatement.Modifier BinlogModifier():
+{ Token modifier; }
+{
+ ( modifier= | modifier= )
+ { return TableMaintenanceStatement.Modifier.valueOf(modifier.image.toUpperCase(Locale.ROOT)); }
+}
+
+/**
+ * MySQL FLUSH [NO_WRITE_TO_BINLOG | LOCAL] followed by flush options or by
+ * {TABLE | TABLES} [tbl_name [, tbl_name] ...] [WITH READ LOCK | FOR EXPORT].
+ * https://dev.mysql.com/doc/refman/8.4/en/flush.html
+ */
+FlushStatement FlushStatement():
+{
+ FlushStatement statement = new FlushStatement();
+ TableMaintenanceStatement.Modifier modifier;
+ FlushOption option;
+ Table table;
+}
+{
+
+ [ modifier=BinlogModifier() { statement.setModifier(modifier); } ]
+ (
+ ( { statement.setTablesKeyword(false); } | )
+ [
+ table=Table() { statement.addTables(table); }
+ ( "," table=Table() { statement.addTables(table); } )*
+ ]
+ [
+ // WITH without READ LOCK starts the next statement
+ LOOKAHEAD({ getToken(1).kind == K_WITH && getToken(2).kind == K_READ
+ || getToken(1).kind == K_FOR })
+ (
+
+ { statement.setTableLock(FlushStatement.TableLock.WITH_READ_LOCK); }
+ |
+ {
+ requireDdlSyntax(!statement.getTables().isEmpty(), "FOR EXPORT requires a table list");
+ statement.setTableLock(FlushStatement.TableLock.FOR_EXPORT);
+ }
+ )
+ ]
+ |
+ option=FlushOption() { statement.addOptions(option); }
+ ( "," option=FlushOption() { statement.addOptions(option); } )*
+ )
+ { return statement; }
+}
+
+/** A flush option of one word, or of a word and LOGS. Only RELAY LOGS names a channel. */
+FlushOption FlushOption():
+{
+ Token first = getToken(1);
+ Token word;
+ Token channel;
+ FlushOption option;
+}
+{
+ (
+ LOOKAHEAD({ isWordSequenceAhead(2, "LOGS") })
+ ( word= | word= | word= | word= )
+ ContextualKeyword("LOGS")
+ { option = new FlushOption(flushOptionKind(word.image + " LOGS")); }
+ |
+ word= { option = new FlushOption(flushOptionKind(word.image)); }
+ )
+ [
+ ContextualKeyword("CHANNEL") channel=
+ {
+ requireDdlSyntax(option.getKind() == FlushOption.Kind.RELAY_LOGS,
+ "Only RELAY LOGS takes FOR CHANNEL");
+ option.setChannel(stringValue(channel));
+ }
+ ]
+ { linkAST(option, first); return option; }
+}
+
+// https://dev.mysql.com/doc/refman/8.4/en/kill.html
+KillStatement KillStatement():
+{
+ KillStatement statement = new KillStatement();
+ Expression id;
+}
+{
+
+ [
+ LOOKAHEAD({ getToken(1).kind == K_QUERY || isKeywordAhead("CONNECTION") })
+ (
+ { statement.setKind(KillStatement.Kind.QUERY); }
+ |
+ ContextualKeyword("CONNECTION") { statement.setKind(KillStatement.Kind.CONNECTION); }
+ )
+ ]
+ id=Expression() { statement.setProcesslistId(id); }
+ { return statement; }
+}
+
+/**
+ * MySQL HANDLER tbl_name OPEN [[AS] alias], HANDLER name READ ... and HANDLER name CLOSE. READ
+ * and CLOSE take the handler name, which is unqualified.
+ * https://dev.mysql.com/doc/refman/8.4/en/handler.html
+ */
+HandlerStatement HandlerStatement():
+{
+ HandlerStatement statement = new HandlerStatement();
+ Table table;
+ String name;
+ boolean useAs = false;
+}
+{
+
+ (
+ LOOKAHEAD({ getToken(2).kind != K_READ && getToken(2).kind != K_CLOSE })
+ table=Table()
+ [
+ [ { useAs = true; } ]
+ name=RelObjectName() { table.setAlias(new Alias(name, useAs)); }
+ ]
+ |
+ name=RelObjectName() { table = linkToLastToken(createTable(Arrays.asList(name))); }
+ (
+ { statement.setAction(HandlerStatement.Action.CLOSE); }
+ |
+ { statement.setAction(HandlerStatement.Action.READ); }
+ HandlerRead(statement)
+ )
+ )
+ { statement.setTable(table); return statement; }
+}
+
+void HandlerRead(HandlerStatement statement):
+{
+ String index;
+ ExpressionList> values;
+ Expression expression;
+ Limit limit;
+}
+{
+ (
+ LOOKAHEAD({ isHandlerIndexAhead() })
+ index=RelObjectName() { statement.setIndexName(index); }
+ (
+ HandlerKeyComparison(statement) values=ParenthesedExpressionList()
+ {
+ requireDdlSyntax(!values.isEmpty(), "HANDLER READ requires key values");
+ statement.setKeyValues(values);
+ }
+ |
+ { statement.setDirection(HandlerStatement.Direction.FIRST); }
+ | { statement.setDirection(HandlerStatement.Direction.NEXT); }
+ | ContextualKeyword("PREV") { statement.setDirection(HandlerStatement.Direction.PREV); }
+ | { statement.setDirection(HandlerStatement.Direction.LAST); }
+ )
+ |
+ { statement.setDirection(HandlerStatement.Direction.FIRST); }
+ |
+ { statement.setDirection(HandlerStatement.Direction.NEXT); }
+ )
+ [ expression=WhereClause() { statement.setWhere(expression); } ]
+ [
+ limit=LimitWithOffset() { statement.setLimit(limit); }
+ [
+ expression=Expression()
+ {
+ requireDdlSyntax(limit.getOffset() == null, "LIMIT offset, row_count takes no OFFSET");
+ statement.setOffset(new Offset().withOffset(expression));
+ }
+ ]
+ ]
+ {
+ // MySQL parses HANDLER READ with subqueries disallowed, like PURGE
+ requireDdlSyntax(!containsSubquery(statement.getKeyValues(), statement.getWhere(),
+ statement.getLimit() == null ? null : statement.getLimit().getOffset(),
+ statement.getLimit() == null ? null : statement.getLimit().getRowCount(),
+ statement.getOffset() == null ? null : statement.getOffset().getOffset()),
+ "HANDLER READ takes no subquery");
+ }
+}
+
+void HandlerKeyComparison(HandlerStatement statement):
+{ HandlerStatement.KeyComparison comparison; }
+{
+ (
+ "=" { comparison = HandlerStatement.KeyComparison.EQUALS; }
+ | { comparison = HandlerStatement.KeyComparison.LESS_THAN_OR_EQUALS; }
+ | { comparison = HandlerStatement.KeyComparison.GREATER_THAN_OR_EQUALS; }
+ | "<" { comparison = HandlerStatement.KeyComparison.LESS_THAN; }
+ | ">" { comparison = HandlerStatement.KeyComparison.GREATER_THAN; }
+ )
+ { statement.setKeyComparison(comparison); }
+}
+
/**
* MySQL ANALYZE [NO_WRITE_TO_BINLOG | LOCAL] TABLE t, ... [histogram] and, with the PostgreSQL
* dialect or without a dialect, PostgreSQL {ANALYZE | ANALYSE} [(options) | VERBOSE] [target, ...].
@@ -22321,14 +22672,10 @@ AlterExpressionPartition AddPartition():
/** Parses MySQL's opt_no_write_to_binlog of a partition operation. */
void PartitionBinlogModifier(AlterExpressionPartition alterExp):
{
- Token tk;
+ TableMaintenanceStatement.Modifier modifier;
}
{
- ( tk= | tk= )
- {
- alterExp.setModifier(TableMaintenanceStatement.Modifier.valueOf(
- tk.image.toUpperCase(Locale.ROOT)));
- }
+ modifier=BinlogModifier() { alterExp.setModifier(modifier); }
}
/**
@@ -26248,10 +26595,10 @@ ForeignDataWrapperFunctions ForeignDataWrapperFunctions():
{ name = null; }
(
- ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; }
+ ( { handler = true; }
| ContextualKeyword("VALIDATOR") { handler = false; } )
|
- ( LOOKAHEAD({ isKeywordAhead("HANDLER") }) ContextualKeyword("HANDLER") { handler = true; }
+ ( { handler = true; }
| ContextualKeyword("VALIDATOR") { handler = false; } )
name=AccessQualifiedName()
)
@@ -26355,7 +26702,7 @@ CreateAccessMethod CreateAccessMethod():
{ statement.setName(name); }
( { statement.setType(CreateAccessMethod.Type.TABLE); }
| { statement.setType(CreateAccessMethod.Type.INDEX); } )
- ContextualKeyword("HANDLER") name=AccessQualifiedName()
+ name=AccessQualifiedName()
{ statement.setHandler(name); return statement; }
}
diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst
index e0ff2d8a4..e9aa7363e 100644
--- a/src/site/sphinx/index.rst
+++ b/src/site/sphinx/index.rst
@@ -164,6 +164,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad
- ``VACUUM`` · ``ANALYZE`` · ``CLUSTER`` · ``CHECKPOINT``
* - **MySQL locks**
- ``LOCK TABLES`` · ``UNLOCK TABLES`` · ``LOCK INSTANCE FOR BACKUP`` · ``UNLOCK INSTANCE``
+ * - **MySQL server administration**
+ - ``FLUSH`` · ``KILL`` · ``RESET PERSIST`` · ``HANDLER … OPEN``/``READ``/``CLOSE``
* - **MySQL account management**
- ``CREATE``/``ALTER``/``RENAME USER`` · ``CREATE ROLE`` · ``SET ROLE`` · ``SET DEFAULT ROLE`` · ``SET PASSWORD``
* - **Tablespaces**
diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst
index 93bee3339..91da91378 100644
--- a/src/site/sphinx/usage.rst
+++ b/src/site/sphinx/usage.rst
@@ -2484,7 +2484,8 @@ records the ``TABLES`` spelling. ``getOptions()`` keeps the trailing options in
order. CHECK accepts ``FOR UPGRADE``, ``QUICK``, ``FAST``, ``MEDIUM``, ``EXTENDED`` and
``CHANGED``; CHECKSUM accepts one of ``QUICK`` and ``EXTENDED``; REPAIR accepts ``QUICK``,
``EXTENDED`` and ``USE_FRM``. Other combinations are rejected, and the option words and
-``CHECKSUM`` remain ordinary identifiers elsewhere.
+``CHECKSUM`` remain ordinary identifiers elsewhere. With unsupported statements enabled, these
+statements are parsed rather than captured as unsupported statements.
.. code-block:: java
@@ -2606,6 +2607,70 @@ Table visitors report the locked tables but not their aliases. Validators report
three, MariaDB the first two, and PostgreSQL and Oracle ``lock``. Feature analysis reports
``MODIFIES_TRANSACTION``.
+MySQL server administration statements
+--------------------------------------
+
+With ``Dialect.MYSQL`` and without a dialect, these MySQL 8.4 statements parse to typed
+statements:
+
+* `FLUSH `_ parses to
+ ``FlushStatement``. ``getModifier()`` returns ``NO_WRITE_TO_BINLOG`` or ``LOCAL``, the
+ ``TableMaintenanceStatement.Modifier`` of OPTIMIZE, REPAIR and the partition operations.
+ ``getOptions()`` keeps the flush options in source order. Each is a ``FlushOption`` whose
+ ``getKind()`` is one of MySQL 8.4's options, from ``BINARY_LOGS`` to ``USER_RESOURCES``,
+ and whose ``getChannel()`` holds the string literal of ``RELAY LOGS FOR CHANNEL 'name'``.
+ Without options the statement is
+ ``FLUSH {TABLE | TABLES} [tbl_name, ...] [WITH READ LOCK | FOR EXPORT]``
+ (``isFlushTables()``), with ``getTables()``, ``isTablesKeyword()`` and ``getTableLock()``.
+ As in MySQL, that form takes no flush option, ``FOR EXPORT`` requires a table list, only
+ ``RELAY LOGS`` names a channel, and ``FLUSH HOSTS``, which MySQL 8.4 removed, is rejected;
+ with unsupported statements enabled it remains an ``UnsupportedStatement``.
+* `KILL [CONNECTION | QUERY] processlist_id
+ `_ parses to ``KillStatement``.
+ ``getKind()`` is ``CONNECTION``, ``QUERY`` or null when neither is given, and
+ ``getProcesslistId()`` is an expression, since MySQL accepts ``@id`` or ``1 + 2`` there.
+* `RESET PERSIST [[IF EXISTS] system_var_name]
+ `_ extends ``ResetStatement``
+ with ``isPersist()`` and ``isIfExists()``; ``getName()`` is the variable as written, with
+ a component or ``DEFAULT`` prefix such as ``validate_password.length``, or null when the
+ statement names none. Without a dialect only a variable name or ``IF EXISTS`` after
+ ``PERSIST`` selects this form, so a bare ``RESET PERSIST`` still resets a parameter named
+ ``persist``, as it does in the other dialects.
+* `HANDLER `_ parses to
+ ``HandlerStatement``, whose ``getAction()`` is ``OPEN``, ``READ`` or ``CLOSE``.
+ ``getTable()`` is the table that OPEN opens, with the handler name as its alias, or the
+ unqualified handler name that READ and CLOSE take; ``getHandlerName()`` returns the name in
+ both cases. A READ has ``getIndexName()`` and either ``getKeyComparison()`` (``=``,
+ ``<=``, ``>=``, ``<`` or ``>``) with ``getKeyValues()``, or ``getDirection()``
+ (``FIRST``, ``NEXT``, ``PREV`` or ``LAST``, and only ``FIRST`` or ``NEXT`` without an
+ index), followed by ``getWhere()``, ``getLimit()`` and ``getOffset()``. As in MySQL, a
+ READ takes no subquery.
+
+.. code-block:: java
+
+ FlushStatement flush = (FlushStatement) CCJSqlParserUtil.parse(
+ "FLUSH LOCAL TABLES t1, t2 WITH READ LOCK");
+ flush.getTables(); // [t1, t2]
+ flush.getTableLock(); // WITH_READ_LOCK
+
+ HandlerStatement read = (HandlerStatement) CCJSqlParserUtil.parse(
+ "HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3");
+ read.getKeyComparison(); // GREATER_THAN_OR_EQUALS
+ read.getKeyValues(); // (1, 2)
+
+Other dialects keep rejecting these statements. ``HANDLER`` becomes a non-reserved keyword
+and remains usable as a name, like ``FLUSH``, ``KILL`` and the option words.
+
+Table visitors report the tables of ``FLUSH TABLES`` and the table that ``HANDLER ... OPEN``
+opens, but not the handler name of READ and CLOSE, which may be an alias. They find the
+tables of subqueries in the KILL expression, and they now return no table for every
+``RESET`` statement instead of throwing. Validators report the
+``flush``, ``kill``, ``handler`` and ``resetPersist`` features, which the MySQL capabilities
+enable; PostgreSQL's ``RESET`` keeps ``reset``. Feature analysis reports
+``MODIFIES_SESSION`` for FLUSH, KILL and HANDLER OPEN and CLOSE, adds
+``MODIFIES_TRANSACTION`` for the table locks of FLUSH TABLES, and reports ``READS_DATA`` and
+``RETURNS_RESULT_SET`` for HANDLER READ.
+
MySQL account management statements
-----------------------------------
diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
index 25dd26d38..a6be61c1c 100644
--- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
+++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java
@@ -147,6 +147,13 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException {
"CLUSTER VERBOSE i ON t",
"ANALYSE (\"verbose\", PARALLEL -1) t",
"CHECKPOINT",
+ "FLUSH LOCAL TABLES db1.t, s WITH READ LOCK",
+ "FLUSH ERROR LOGS, RELAY LOGS FOR CHANNEL 'c1', STATUS",
+ "KILL QUERY 1 + 2",
+ "HANDLER db1.t OPEN AS h",
+ "HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3 OFFSET 1",
+ "HANDLER h CLOSE",
+ "RESET PERSIST IF EXISTS max_connections",
"DROP CAST IF EXISTS (int[] AS public.t) CASCADE",
"DROP OPERATOR public.=== (integer, integer), ~ (NONE, bigint)",
"DROP TRANSFORM FOR public.hstore LANGUAGE plpython3u"}) {
diff --git a/src/test/java/net/sf/jsqlparser/statement/ResetStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ResetStatementTest.java
index b3372ff73..6839c0a54 100644
--- a/src/test/java/net/sf/jsqlparser/statement/ResetStatementTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/ResetStatementTest.java
@@ -11,11 +11,32 @@
import net.sf.jsqlparser.JSQLParserException;
import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+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;
public class ResetStatementTest {
+ private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL);
+ private static final Consumer NO_DIALECT = parser -> {
+ };
@Test
public void tesResetTZ() throws JSQLParserException {
@@ -37,4 +58,129 @@ public void testObject() {
assertEquals("somethingElse", resetStatement.getName());
}
+ @ParameterizedTest
+ @ValueSource(strings = {"RESET PERSIST", "RESET PERSIST max_connections",
+ "RESET PERSIST IF EXISTS max_connections", "RESET PERSIST `max_connections`",
+ "RESET PERSIST a.b", "RESET PERSIST `a`.`b`", "RESET PERSIST DEFAULT.x",
+ "RESET PERSIST IF EXISTS validate_password.length",
+ "RESET PERSIST IF EXISTS DEFAULT.x"})
+ public void testResetPersistInMySql(String sql) throws JSQLParserException {
+ ResetStatement reset = (ResetStatement) assertSqlCanBeParsedAndDeparsed(sql, false, MYSQL);
+ assertTrue(reset.isPersist());
+ assertEquals(sql, deparse(reset));
+ }
+
+ @Test
+ public void testResetPersistState() throws JSQLParserException {
+ ResetStatement all = (ResetStatement) CCJSqlParserUtil.parse("reset persist", MYSQL);
+ assertTrue(all.isPersist());
+ assertFalse(all.isIfExists());
+ assertNull(all.getName());
+ assertEquals("RESET PERSIST", all.toString());
+
+ ResetStatement one = (ResetStatement) CCJSqlParserUtil
+ .parse("RESET PERSIST IF EXISTS max_connections", MYSQL);
+ assertTrue(one.isPersist());
+ assertTrue(one.isIfExists());
+ assertEquals("max_connections", one.getName());
+
+ // a component variable keeps its prefix in the name
+ ResetStatement component = (ResetStatement) CCJSqlParserUtil
+ .parse("RESET PERSIST IF EXISTS `validate_password`.length", MYSQL);
+ assertEquals("`validate_password`.length", component.getName());
+ assertEquals("DEFAULT.x",
+ ((ResetStatement) CCJSqlParserUtil.parse("RESET PERSIST DEFAULT.x", MYSQL))
+ .getName());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"RESET PERSIST max_connections",
+ "RESET PERSIST IF EXISTS max_connections", "RESET PERSIST validate_password.length",
+ "RESET PERSIST DEFAULT.x"})
+ public void testResetPersistWithANameWithoutDialect(String sql) throws JSQLParserException {
+ ResetStatement reset =
+ (ResetStatement) assertSqlCanBeParsedAndDeparsed(sql, false, NO_DIALECT);
+ assertTrue(reset.isPersist());
+ assertEquals(sql.substring(sql.lastIndexOf(' ') + 1), reset.getName());
+ }
+
+ @Test
+ public void testBareResetPersistKeepsItsReadingOutsideMySql() throws JSQLParserException {
+ // PostgreSQL resets a parameter named persist; only MySQL reads the PERSIST keyword alone
+ for (Consumer dialect : List.of(NO_DIALECT,
+ (Consumer) parser -> parser.withDialect(Dialect.POSTGRESQL),
+ (Consumer) parser -> parser.withDialect(Dialect.MARIADB))) {
+ ResetStatement reset =
+ (ResetStatement) assertSqlCanBeParsedAndDeparsed("RESET persist", false,
+ dialect);
+ assertFalse(reset.isPersist());
+ assertEquals("persist", reset.getName());
+ }
+ ResetStatement script = (ResetStatement) CCJSqlParserUtil
+ .parseStatements("RESET persist; SELECT 1", NO_DIALECT).get(0);
+ assertFalse(script.isPersist());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"RESET PERSIST max_connections",
+ "RESET PERSIST IF EXISTS max_connections", "RESET PERSIST a.b"})
+ public void testOtherDialectsKeepRejectingResetPersistOfAName(String sql) {
+ for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MARIADB, Dialect.ORACLE)) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)));
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"RESET PERSIST IF EXISTS", "RESET PERSIST IF max_connections",
+ "RESET PERSIST a.b.c", "RESET PERSIST a.", "RESET PERSIST DEFAULT",
+ "RESET PERSIST a b", "RESET PERSIST 'max_connections'",
+ "RESET PERSIST max_connections, wait_timeout"})
+ public void testRejectsResetPersistFormsMySqlRejects(String sql) {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, dialect),
+ sql);
+ }
+ }
+
+ @Test
+ public void testResetPersistEndsTheStatement() throws JSQLParserException {
+ List statements = CCJSqlParserUtil
+ .parseStatements("RESET PERSIST; RESET PERSIST IF EXISTS x; SELECT 1", MYSQL)
+ .stream().map(Object::toString).collect(Collectors.toList());
+ assertEquals(List.of("RESET PERSIST", "RESET PERSIST IF EXISTS x", "SELECT 1"),
+ statements);
+ }
+
+ @Test
+ public void testFindsNoTables() throws JSQLParserException {
+ for (String sql : new String[] {"RESET ALL", "RESET work_mem", "RESET PERSIST x"}) {
+ assertThat(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse(sql)))
+ .isEmpty();
+ }
+ }
+
+ @Test
+ public void testNodeSpansTheStatement() throws JSQLParserException {
+ String sql = "RESET PERSIST IF EXISTS max_connections";
+ Node node = ((ASTNodeAccess) CCJSqlParserUtil.parse(sql, MYSQL)).getASTNode();
+ assertEquals(sql, sql.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1));
+ }
+
+ @Test
+ public void testBuildsResetPersistInCode() {
+ ResetStatement reset = new ResetStatement().withPersist(true);
+ assertEquals("RESET PERSIST", reset.toString());
+ reset.withIfExists(true).setName("x");
+ assertEquals("RESET PERSIST IF EXISTS x", deparse(reset));
+ reset.setPersist(false);
+ assertEquals("RESET x", reset.toString());
+ }
+
+ private static String deparse(ResetStatement reset) {
+ StringBuilder builder = new StringBuilder();
+ reset.accept(new StatementDeParser(builder), null);
+ return builder.toString();
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java
index 0f7ad33af..77e6084c9 100644
--- a/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java
+++ b/src/test/java/net/sf/jsqlparser/statement/TableMaintenanceStatementTest.java
@@ -213,4 +213,16 @@ void reportsStatementFeatures() throws JSQLParserException {
assertThat(repair.getCertain()).contains(StmtFeature.MODIFIES_SCHEMA);
assertThat(repair.getUncertain()).contains(StmtFeature.MODIFIES_DATA);
}
+
+ @Test
+ void isAStatementStartForUnsupportedStatements() throws JSQLParserException {
+ Statements statements = CCJSqlParserUtil.parseStatements(
+ "FOO; CHECK TABLE t; OPTIMIZE TABLE t; CHECKSUM TABLE t; REPAIR TABLE t; CHECK x",
+ parser -> parser.withUnsupportedStatements(true));
+ assertEquals(List.of("UnsupportedStatement", "TableMaintenanceStatement",
+ "TableMaintenanceStatement", "TableMaintenanceStatement",
+ "TableMaintenanceStatement", "UnsupportedStatement"),
+ statements.stream().map(statement -> statement.getClass().getSimpleName())
+ .collect(Collectors.toList()));
+ }
}
diff --git a/src/test/java/net/sf/jsqlparser/statement/admin/MySqlFlushStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/admin/MySqlFlushStatementTest.java
new file mode 100644
index 000000000..79566e6dd
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/admin/MySqlFlushStatementTest.java
@@ -0,0 +1,300 @@
+/*-
+ * #%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.admin;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.assertj.core.api.Assertions.assertThat;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.StringValue;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.StatementFeatures;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.TableMaintenanceStatement.Modifier;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
+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 MySqlFlushStatementTest {
+ private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL);
+ private static final Consumer NO_DIALECT = parser -> {
+ };
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "FLUSH TABLES",
+ "FLUSH TABLE",
+ "FLUSH TABLES WITH READ LOCK",
+ "FLUSH TABLES t, s WITH READ LOCK",
+ "FLUSH TABLES t FOR EXPORT",
+ "FLUSH TABLE t",
+ "FLUSH TABLES db1.t, `u`",
+ "FLUSH LOCAL TABLES t FOR EXPORT",
+ "FLUSH NO_WRITE_TO_BINLOG TABLE t WITH READ LOCK",
+ "FLUSH PRIVILEGES",
+ "FLUSH LOCAL BINARY LOGS",
+ "FLUSH NO_WRITE_TO_BINLOG LOGS",
+ "FLUSH ERROR LOGS, SLOW LOGS, STATUS",
+ "FLUSH RELAY LOGS",
+ "FLUSH RELAY LOGS FOR CHANNEL 'c1'",
+ "FLUSH LOGS, RELAY LOGS FOR CHANNEL 'c1'",
+ "FLUSH OPTIMIZER_COSTS",
+ "FLUSH USER_RESOURCES",
+ "FLUSH ENGINE LOGS, GENERAL LOGS",
+ "FLUSH BINARY LOGS, ENGINE LOGS, ERROR LOGS, GENERAL LOGS, LOGS, OPTIMIZER_COSTS, "
+ + "PRIVILEGES, RELAY LOGS, SLOW LOGS, STATUS, USER_RESOURCES"})
+ void parsesMySqlFormsWithAndWithoutDialect(String sql) throws JSQLParserException {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect);
+ assertInstanceOf(FlushStatement.class, statement);
+ assertEquals(sql, deparse(statement));
+ assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString());
+ }
+ }
+
+ @Test
+ void keepsTheOptionsInSourceOrder() throws JSQLParserException {
+ FlushStatement flush =
+ parse("flush local binary logs, relay logs for channel 'c1', status");
+ assertEquals(Modifier.LOCAL, flush.getModifier());
+ assertFalse(flush.isFlushTables());
+ assertEquals(List.of(FlushOption.Kind.BINARY_LOGS, FlushOption.Kind.RELAY_LOGS,
+ FlushOption.Kind.STATUS),
+ flush.getOptions().stream().map(FlushOption::getKind)
+ .collect(Collectors.toList()));
+ assertNull(flush.getOptions().get(0).getChannel());
+ assertEquals("c1", flush.getOptions().get(1).getChannel().getValue());
+ assertTrue(flush.getTables().isEmpty());
+ assertNull(flush.getTableLock());
+ assertEquals("FLUSH LOCAL BINARY LOGS, RELAY LOGS FOR CHANNEL 'c1', STATUS",
+ flush.toString());
+ }
+
+ @Test
+ void keepsTheTablesAndTheirLock() throws JSQLParserException {
+ FlushStatement flush = parse("FLUSH NO_WRITE_TO_BINLOG TABLES db1.t, s WITH READ LOCK");
+ assertTrue(flush.isFlushTables());
+ assertTrue(flush.isTablesKeyword());
+ assertEquals(Modifier.NO_WRITE_TO_BINLOG, flush.getModifier());
+ assertEquals(List.of("db1.t", "s"), flush.getTables().stream()
+ .map(Table::getFullyQualifiedName).collect(Collectors.toList()));
+ assertEquals(FlushStatement.TableLock.WITH_READ_LOCK, flush.getTableLock());
+
+ FlushStatement export = parse("FLUSH TABLE t FOR EXPORT");
+ assertFalse(export.isTablesKeyword());
+ assertNull(export.getModifier());
+ assertEquals(FlushStatement.TableLock.FOR_EXPORT, export.getTableLock());
+
+ FlushStatement all = parse("FLUSH TABLES");
+ assertTrue(all.getTables().isEmpty());
+ assertNull(all.getTableLock());
+ assertTrue(all.getOptions().isEmpty());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "FLUSH",
+ "FLUSH LOCAL",
+ "FLUSH HOSTS",
+ "FLUSH QUERY CACHE",
+ "FLUSH DES_KEY_FILE",
+ "FLUSH LOGS,",
+ "FLUSH BINARY",
+ "FLUSH STATUS LOGS",
+ "FLUSH LOCAL NO_WRITE_TO_BINLOG LOGS",
+ "FLUSH TABLES FOR EXPORT",
+ "FLUSH TABLES t WITH READ LOCK, PRIVILEGES",
+ "FLUSH PRIVILEGES, TABLES",
+ "FLUSH TABLES t WITH READ",
+ "FLUSH TABLES t FOR UPDATE",
+ "FLUSH TABLES t WITH READ LOCK FOR EXPORT",
+ "FLUSH RELAY LOGS FOR CHANNEL c1",
+ "FLUSH RELAY LOGS FOR CHANNEL `c1`",
+ "FLUSH RELAY LOGS FOR CHANNEL",
+ "FLUSH BINARY LOGS FOR CHANNEL 'c1'",
+ "FLUSH TABLES t,"})
+ void rejectsFormsMySqlRejects(String sql) {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, dialect),
+ sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"FLUSH TABLES", "FLUSH PRIVILEGES", "FLUSH LOCAL LOGS"})
+ void otherDialectsKeepRejectingFlush(String sql) {
+ for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MARIADB, Dialect.ORACLE,
+ Dialect.SQLSERVER)) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)),
+ sql + " " + dialect);
+ }
+ }
+
+ @Test
+ void oracleAlterSystemFlushIsUnchanged() throws JSQLParserException {
+ Statement statement = assertSqlCanBeParsedAndDeparsed("ALTER SYSTEM FLUSH SHARED_POOL",
+ true, parser -> parser.withDialect(Dialect.ORACLE));
+ assertInstanceOf(AlterSystemStatement.class, statement);
+ }
+
+ @Test
+ void wordsRemainUsableAsNames() throws JSQLParserException {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertSqlCanBeParsedAndDeparsed(
+ "SELECT flush, logs, relay, slow, general, privileges, status, channel FROM flush",
+ true, dialect);
+ assertSqlCanBeParsedAndDeparsed("SELECT * FROM t AS flush", true, dialect);
+ }
+ }
+
+ @Test
+ void endsTheStatementsAroundIt() throws JSQLParserException {
+ Statements statements = CCJSqlParserUtil.parseStatements(
+ "SELECT 1; FLUSH TABLES WITH READ LOCK; FLUSH LOGS; UNLOCK TABLES", MYSQL);
+ assertEquals(List.of("PlainSelect", "FlushStatement", "FlushStatement",
+ "UnlockStatement"), names(statements));
+ assertEquals("FLUSH TABLES WITH READ LOCK", statements.get(1).toString());
+
+ // WITH without READ LOCK starts the next statement
+ Statements next = CCJSqlParserUtil.parseStatements(
+ "SELECT 1; FLUSH TABLES t\nWITH x AS (SELECT 1) SELECT * FROM x", MYSQL);
+ assertEquals(List.of("PlainSelect", "FlushStatement", "PlainSelect"), names(next));
+ assertEquals("FLUSH TABLES t", next.get(1).toString());
+ }
+
+ @Test
+ void isAStatementStartForUnsupportedStatements() throws JSQLParserException {
+ Statements statements = CCJSqlParserUtil.parseStatements(
+ "FOO BAR; FLUSH TABLES; BAZ; FLUSH HOSTS",
+ parser -> parser.withUnsupportedStatements(true));
+ assertEquals(List.of("UnsupportedStatement", "FlushStatement", "UnsupportedStatement",
+ "UnsupportedStatement"), names(statements));
+ assertEquals("FLUSH HOSTS", statements.get(3).toString());
+
+ Statements postgres = CCJSqlParserUtil.parseStatements("FLUSH TABLES",
+ parser -> parser.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(true));
+ assertInstanceOf(UnsupportedStatement.class, postgres.get(0));
+ }
+
+ @Test
+ void findsTheFlushedTables() throws JSQLParserException {
+ assertThat(new TablesNamesFinder<>()
+ .getTables(CCJSqlParserUtil.parse("FLUSH TABLES db1.t, s WITH READ LOCK")))
+ .containsExactlyInAnyOrder("db1.t", "s");
+ assertThat(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse("FLUSH TABLES")))
+ .isEmpty();
+ assertThat(new TablesNamesFinder<>()
+ .getTables(CCJSqlParserUtil.parse("FLUSH PRIVILEGES, LOGS"))).isEmpty();
+ }
+
+ @Test
+ void reportsSessionFeaturesAndTheLock() throws JSQLParserException {
+ StatementFeatures logs = CCJSqlParserUtil.parse("FLUSH LOGS").getFeatures();
+ assertThat(logs.getCertain()).containsExactly(StmtFeature.MODIFIES_SESSION);
+
+ for (String sql : new String[] {"FLUSH TABLES WITH READ LOCK",
+ "FLUSH TABLE t FOR EXPORT"}) {
+ assertThat(CCJSqlParserUtil.parse(sql).getFeatures().getCertain())
+ .containsExactlyInAnyOrder(StmtFeature.MODIFIES_SESSION,
+ StmtFeature.MODIFIES_TRANSACTION);
+ }
+ }
+
+ @Test
+ void nodesSpanTheStatementTablesAndOptions() throws JSQLParserException {
+ String sql = "FLUSH LOCAL TABLES db1.t, s WITH READ LOCK";
+ FlushStatement flush = parse(sql);
+ assertEquals(sql, text(sql, flush));
+ assertEquals("db1.t", text(sql, flush.getTables().get(0)));
+ assertEquals("s", text(sql, flush.getTables().get(1)));
+
+ sql = "FLUSH ERROR LOGS, RELAY LOGS FOR CHANNEL 'c1', STATUS";
+ flush = parse(sql);
+ assertEquals(sql, text(sql, flush));
+ assertEquals("ERROR LOGS", text(sql, flush.getOptions().get(0)));
+ assertEquals("RELAY LOGS FOR CHANNEL 'c1'", text(sql, flush.getOptions().get(1)));
+ assertEquals("'c1'", text(sql, flush.getOptions().get(1).getChannel()));
+ assertEquals("STATUS", text(sql, flush.getOptions().get(2)));
+ }
+
+ @Test
+ void buildsStatementsInCode() {
+ FlushStatement flush = new FlushStatement();
+ assertTrue(flush.isFlushTables());
+ assertEquals("FLUSH TABLES", flush.toString());
+ flush.withTablesKeyword(false).addTables(new Table("db", "t"))
+ .withTableLock(FlushStatement.TableLock.FOR_EXPORT)
+ .withModifier(Modifier.NO_WRITE_TO_BINLOG);
+ assertEquals("FLUSH NO_WRITE_TO_BINLOG TABLE db.t FOR EXPORT", flush.toString());
+ assertEquals(flush.toString(), deparse(flush));
+
+ FlushOption relay = new FlushOption(FlushOption.Kind.RELAY_LOGS)
+ .withChannel(new StringValue("c1"));
+ FlushStatement options = new FlushStatement()
+ .withOptions(List.of(new FlushOption(FlushOption.Kind.OPTIMIZER_COSTS)))
+ .addOptions(relay);
+ assertFalse(options.isFlushTables());
+ assertEquals("FLUSH OPTIMIZER_COSTS, RELAY LOGS FOR CHANNEL 'c1'", options.toString());
+ assertEquals(options.toString(), deparse(options));
+ options.setOptions(null);
+ options.setTables(List.of(new Table("t")));
+ assertEquals("FLUSH TABLES t", options.toString());
+ options.setTables(null);
+ assertEquals("FLUSH TABLES", options.toString());
+
+ assertEquals(FlushOption.Kind.SLOW_LOGS, FlushOption.Kind.fromKeyword("slow logs"));
+ assertNull(FlushOption.Kind.fromKeyword("HOSTS"));
+ assertNull(FlushOption.Kind.fromKeyword(null));
+ assertThrows(NullPointerException.class, () -> relay.setKind(null));
+ assertEquals("ENGINE LOGS", new FlushOption(FlushOption.Kind.ENGINE_LOGS).toString());
+ }
+
+ private static FlushStatement parse(String sql) throws JSQLParserException {
+ return (FlushStatement) assertSqlCanBeParsedAndDeparsed(sql, true, MYSQL);
+ }
+
+ private static List names(Statements statements) {
+ return statements.stream().map(statement -> statement.getClass().getSimpleName())
+ .collect(Collectors.toList());
+ }
+
+ static String deparse(Statement statement) {
+ StringBuilder builder = new StringBuilder();
+ statement.accept(new StatementDeParser(builder), null);
+ return builder.toString();
+ }
+
+ static String text(String sql, ASTNodeAccess access) {
+ Node node = access.getASTNode();
+ return sql.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1);
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/admin/MySqlKillStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/admin/MySqlKillStatementTest.java
new file mode 100644
index 000000000..2fe79d9ed
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/admin/MySqlKillStatementTest.java
@@ -0,0 +1,182 @@
+/*-
+ * #%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.admin;
+
+import static net.sf.jsqlparser.statement.admin.MySqlFlushStatementTest.deparse;
+import static net.sf.jsqlparser.statement.admin.MySqlFlushStatementTest.text;
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Function;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.UserVariable;
+import net.sf.jsqlparser.expression.operators.arithmetic.Addition;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.alter.AlterSystemStatement;
+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 MySqlKillStatementTest {
+ private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL);
+ private static final Consumer NO_DIALECT = parser -> {
+ };
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "KILL 123",
+ "KILL QUERY 123",
+ "KILL CONNECTION 123",
+ "KILL CONNECTION 1 + 2",
+ "KILL @x",
+ "KILL QUERY @id",
+ "KILL CONNECTION_ID()",
+ "KILL ?",
+ "KILL connection + 1"})
+ void parsesMySqlFormsWithAndWithoutDialect(String sql) throws JSQLParserException {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, dialect);
+ assertInstanceOf(KillStatement.class, statement);
+ assertEquals(statement.toString(), deparse(statement));
+ assertEquals(statement.toString(),
+ CCJSqlParserUtil.parse(statement.toString(), dialect).toString());
+ }
+ }
+
+ @Test
+ void keepsWhatItEndsAndTheIdentifierExpression() throws JSQLParserException {
+ KillStatement kill = parse("KILL 123");
+ assertNull(kill.getKind());
+ assertEquals(123L, ((LongValue) kill.getProcesslistId()).getValue());
+
+ assertEquals(KillStatement.Kind.QUERY, parse("kill query 123").getKind());
+ assertEquals(KillStatement.Kind.CONNECTION, parse("KILL CONNECTION 123").getKind());
+ assertInstanceOf(Addition.class, parse("KILL CONNECTION 1 + 2").getProcesslistId());
+ assertInstanceOf(UserVariable.class, parse("KILL @x").getProcesslistId());
+
+ // CONNECTION is the keyword only before the identifier, as in MySQL's grammar
+ KillStatement function = parse("KILL CONNECTION_ID()");
+ assertNull(function.getKind());
+ assertInstanceOf(Function.class, function.getProcesslistId());
+ KillStatement plus = parse("KILL connection + 1");
+ assertEquals(KillStatement.Kind.CONNECTION, plus.getKind());
+ assertEquals("KILL CONNECTION +1", plus.toString());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "KILL",
+ "KILL QUERY",
+ "KILL CONNECTION",
+ "KILL QUERY CONNECTION 1",
+ "KILL 1 2",
+ "KILL 1, 2",
+ "KILL USER u"})
+ void rejectsFormsMySqlRejects(String sql) {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, dialect),
+ sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"KILL 1", "KILL QUERY 1"})
+ void otherDialectsKeepRejectingKill(String sql) {
+ for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MARIADB, Dialect.ORACLE,
+ Dialect.SQLSERVER)) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)),
+ sql + " " + dialect);
+ }
+ }
+
+ @Test
+ void oracleAlterSystemKillSessionIsUnchanged() throws JSQLParserException {
+ assertInstanceOf(AlterSystemStatement.class, assertSqlCanBeParsedAndDeparsed(
+ "ALTER SYSTEM KILL SESSION '13, 8'", true,
+ parser -> parser.withDialect(Dialect.ORACLE)));
+ }
+
+ @Test
+ void wordsRemainUsableAsNames() throws JSQLParserException {
+ // MySQL 8.4 reserves KILL, but the parser keeps accepting it as a name, as before
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertSqlCanBeParsedAndDeparsed("SELECT kill, query, connection FROM kill", true,
+ dialect);
+ }
+ }
+
+ @Test
+ void endsTheStatementsAroundIt() throws JSQLParserException {
+ Statements statements =
+ CCJSqlParserUtil.parseStatements("SELECT 1; KILL QUERY 5; KILL 6", MYSQL);
+ assertEquals(List.of("PlainSelect", "KillStatement", "KillStatement"),
+ statements.stream().map(statement -> statement.getClass().getSimpleName())
+ .collect(Collectors.toList()));
+
+ Statements unsupported = CCJSqlParserUtil.parseStatements("FOO; KILL 5",
+ parser -> parser.withUnsupportedStatements(true));
+ assertInstanceOf(UnsupportedStatement.class, unsupported.get(0));
+ assertInstanceOf(KillStatement.class, unsupported.get(1));
+ }
+
+ @Test
+ void findsTablesOnlyInASubquery() throws JSQLParserException {
+ assertThat(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse("KILL 5")))
+ .isEmpty();
+ assertThat(new TablesNamesFinder<>()
+ .getTables(CCJSqlParserUtil.parse("KILL (SELECT MAX(id) FROM t)")))
+ .containsExactly("t");
+ assertThat(new TablesNamesFinder<>().getTables(new KillStatement())).isEmpty();
+ }
+
+ @Test
+ void reportsSessionFeatures() throws JSQLParserException {
+ assertThat(CCJSqlParserUtil.parse("KILL QUERY 5").getFeatures().getCertain())
+ .containsExactly(StmtFeature.MODIFIES_SESSION);
+ }
+
+ @Test
+ void nodesSpanTheStatementAndTheIdentifier() throws JSQLParserException {
+ String sql = "KILL CONNECTION 1 + 2";
+ KillStatement kill = parse(sql);
+ assertEquals(sql, text(sql, kill));
+ assertEquals("1 + 2", text(sql, kill.getProcesslistId()));
+ }
+
+ @Test
+ void buildsStatementsInCode() {
+ KillStatement kill = new KillStatement().withProcesslistId(new LongValue(5));
+ assertEquals("KILL 5", kill.toString());
+ kill.withKind(KillStatement.Kind.QUERY);
+ assertEquals("KILL QUERY 5", deparse(kill));
+ assertEquals("KILL CONNECTION @x", new KillStatement(KillStatement.Kind.CONNECTION,
+ new UserVariable("x")).toString());
+ }
+
+ private static KillStatement parse(String sql) throws JSQLParserException {
+ return (KillStatement) assertSqlCanBeParsedAndDeparsed(sql, true, MYSQL);
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/statement/handler/MySqlHandlerStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/handler/MySqlHandlerStatementTest.java
new file mode 100644
index 000000000..60708d425
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/statement/handler/MySqlHandlerStatementTest.java
@@ -0,0 +1,340 @@
+/*-
+ * #%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.handler;
+
+import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed;
+import static org.assertj.core.api.Assertions.assertThat;
+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.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.expression.Alias;
+import net.sf.jsqlparser.expression.LongValue;
+import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
+import net.sf.jsqlparser.expression.operators.relational.GreaterThan;
+import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
+import net.sf.jsqlparser.parser.ASTNodeAccess;
+import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect;
+import net.sf.jsqlparser.parser.CCJSqlParser;
+import net.sf.jsqlparser.parser.CCJSqlParserUtil;
+import net.sf.jsqlparser.parser.Node;
+import net.sf.jsqlparser.schema.Column;
+import net.sf.jsqlparser.schema.Table;
+import net.sf.jsqlparser.statement.Statement;
+import net.sf.jsqlparser.statement.Statements;
+import net.sf.jsqlparser.statement.StmtFeature;
+import net.sf.jsqlparser.statement.UnsupportedStatement;
+import net.sf.jsqlparser.statement.create.accessmethod.CreateAccessMethod;
+import net.sf.jsqlparser.statement.handler.HandlerStatement.Action;
+import net.sf.jsqlparser.statement.handler.HandlerStatement.Direction;
+import net.sf.jsqlparser.statement.handler.HandlerStatement.KeyComparison;
+import net.sf.jsqlparser.statement.select.Limit;
+import net.sf.jsqlparser.statement.select.Offset;
+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 MySqlHandlerStatementTest {
+ private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL);
+ private static final Consumer NO_DIALECT = parser -> {
+ };
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "HANDLER t OPEN",
+ "HANDLER t OPEN AS h",
+ "HANDLER db1.t OPEN h2",
+ "HANDLER `db1`.`t` OPEN AS `h`",
+ "HANDLER h READ FIRST",
+ "HANDLER h READ NEXT LIMIT 10",
+ "HANDLER h READ FIRST WHERE a > 1 LIMIT 5",
+ "HANDLER h READ i1 = (1)",
+ "HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3",
+ "HANDLER h READ i1 <= (1)",
+ "HANDLER h READ i1 < (1)",
+ "HANDLER h READ i1 > ('a', 2)",
+ "HANDLER h READ `PRIMARY` = (1, DEFAULT)",
+ "HANDLER h READ i1 FIRST",
+ "HANDLER h READ i1 NEXT",
+ "HANDLER h READ i1 PREV",
+ "HANDLER h READ i1 LAST WHERE a IS NULL LIMIT 2, 5",
+ "HANDLER h READ i1 NEXT LIMIT 5 OFFSET 2",
+ "HANDLER h READ first NEXT",
+ "HANDLER `read` READ NEXT",
+ "HANDLER h CLOSE"})
+ void parsesMySqlFormsWithAndWithoutDialect(String sql) throws JSQLParserException {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect);
+ assertInstanceOf(HandlerStatement.class, statement);
+ assertEquals(sql, deparse(statement));
+ assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString());
+ }
+ }
+
+ @Test
+ void opensATableUnderItsHandlerName() throws JSQLParserException {
+ HandlerStatement open = parse("HANDLER db1.t OPEN AS h");
+ assertEquals(Action.OPEN, open.getAction());
+ assertEquals("db1.t", open.getTable().getFullyQualifiedName());
+ assertEquals("h", open.getTable().getAlias().getName());
+ assertTrue(open.getTable().getAlias().isUseAs());
+ assertEquals("h", open.getHandlerName());
+
+ HandlerStatement withoutAs = parse("HANDLER t OPEN h2");
+ assertFalse(withoutAs.getTable().getAlias().isUseAs());
+ assertEquals("h2", withoutAs.getHandlerName());
+
+ HandlerStatement plain = parse("HANDLER t OPEN");
+ assertNull(plain.getTable().getAlias());
+ assertEquals("t", plain.getHandlerName());
+ }
+
+ @Test
+ void readsThroughAnIndexWithKeyValues() throws JSQLParserException {
+ HandlerStatement read = parse("HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3");
+ assertEquals(Action.READ, read.getAction());
+ assertEquals("h", read.getHandlerName());
+ assertEquals("i1", read.getIndexName());
+ assertEquals(KeyComparison.GREATER_THAN_OR_EQUALS, read.getKeyComparison());
+ assertInstanceOf(ParenthesedExpressionList.class, read.getKeyValues());
+ assertEquals(List.of("1", "2"), read.getKeyValues().stream().map(Object::toString)
+ .collect(Collectors.toList()));
+ assertNull(read.getDirection());
+ assertInstanceOf(GreaterThan.class, read.getWhere());
+ assertEquals("3", read.getLimit().getRowCount().toString());
+ assertNull(read.getOffset());
+
+ for (KeyComparison comparison : KeyComparison.values()) {
+ HandlerStatement compared =
+ parse("HANDLER h READ i1 " + comparison.getOperator() + " (1)");
+ assertEquals(comparison, compared.getKeyComparison());
+ }
+ }
+
+ @Test
+ void readsInIndexOrStorageOrder() throws JSQLParserException {
+ HandlerStatement index = parse("HANDLER h READ i1 PREV");
+ assertEquals("i1", index.getIndexName());
+ assertEquals(Direction.PREV, index.getDirection());
+ assertNull(index.getKeyComparison());
+ assertNull(index.getKeyValues());
+
+ HandlerStatement storage = parse("HANDLER h READ next WHERE a > 1");
+ assertNull(storage.getIndexName());
+ assertEquals(Direction.NEXT, storage.getDirection());
+
+ // a direction after the word makes it the name of an index, as in MySQL's grammar
+ HandlerStatement named = parse("HANDLER h READ first NEXT");
+ assertEquals("first", named.getIndexName());
+ assertEquals(Direction.NEXT, named.getDirection());
+
+ HandlerStatement offset = parse("HANDLER h READ i1 LAST LIMIT 5 OFFSET 2");
+ assertEquals(Direction.LAST, offset.getDirection());
+ assertEquals("5", offset.getLimit().getRowCount().toString());
+ assertEquals("2", offset.getOffset().getOffset().toString());
+
+ HandlerStatement comma = parse("HANDLER h READ FIRST LIMIT 2, 5");
+ assertEquals("2", comma.getLimit().getOffset().toString());
+ assertNull(comma.getOffset());
+ }
+
+ @Test
+ void closesAHandler() throws JSQLParserException {
+ HandlerStatement close = parse("HANDLER h CLOSE");
+ assertEquals(Action.CLOSE, close.getAction());
+ assertEquals("h", close.getHandlerName());
+ assertEquals("h", close.getTable().getName());
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {
+ "HANDLER",
+ "HANDLER t",
+ "HANDLER t OPEN AS",
+ "HANDLER t OPEN h AS x",
+ "HANDLER t OPEN AS h(a)",
+ "HANDLER t OPEN AS 'h'",
+ "HANDLER db1.t CLOSE",
+ "HANDLER db1.t READ FIRST",
+ "HANDLER h READ",
+ "HANDLER h READ PREV",
+ "HANDLER h READ LAST",
+ "HANDLER h READ i1",
+ "HANDLER h READ i1 = 1",
+ "HANDLER h READ i1 = ()",
+ "HANDLER h READ i1 != (1)",
+ "HANDLER h READ i1 <> (1)",
+ "HANDLER h READ i1 FIRST NEXT",
+ "HANDLER h READ FIRST OFFSET 2",
+ "HANDLER h READ FIRST LIMIT 2, 5 OFFSET 1",
+ "HANDLER h READ FIRST WHERE",
+ "HANDLER h READ FIRST LIMIT 5 WHERE a > 1",
+ "HANDLER h CLOSE h",
+ "HANDLER h OPEN AS x CLOSE",
+ // MySQL parses HANDLER READ with subqueries disallowed
+ "HANDLER h READ FIRST WHERE a IN (SELECT b FROM s)",
+ "HANDLER h READ FIRST WHERE a = (SELECT 1)",
+ "HANDLER h READ FIRST WHERE EXISTS (SELECT 1)",
+ "HANDLER h READ FIRST WHERE a IN (TABLE s)",
+ "HANDLER h READ FIRST WHERE f((SELECT 1)) > 0",
+ "HANDLER h READ i1 = ((SELECT 1))",
+ "HANDLER h READ FIRST LIMIT (SELECT 1)"})
+ void rejectsFormsMySqlRejects(String sql) {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, dialect),
+ sql);
+ }
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"HANDLER t OPEN", "HANDLER h READ FIRST", "HANDLER h CLOSE"})
+ void otherDialectsKeepRejectingHandler(String sql) {
+ for (Dialect dialect : List.of(Dialect.POSTGRESQL, Dialect.MARIADB, Dialect.ORACLE,
+ Dialect.SQLSERVER)) {
+ assertThrows(JSQLParserException.class,
+ () -> CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)),
+ sql + " " + dialect);
+ }
+ }
+
+ @Test
+ void wordsRemainUsableAsNames() throws JSQLParserException {
+ for (Consumer dialect : List.of(NO_DIALECT, MYSQL)) {
+ assertSqlCanBeParsedAndDeparsed("SELECT handler, prev FROM handler", true, dialect);
+ assertSqlCanBeParsedAndDeparsed("SELECT handler.a FROM db.handler AS handler", true,
+ dialect);
+ assertSqlCanBeParsedAndDeparsed("SELECT * FROM t handler", true, dialect);
+ assertSqlCanBeParsedAndDeparsed("CREATE TABLE handler (handler INT)", true, dialect);
+ assertSqlCanBeParsedAndDeparsed("HANDLER handler OPEN AS handler", true, dialect);
+ }
+ CreateAccessMethod accessMethod = (CreateAccessMethod) assertSqlCanBeParsedAndDeparsed(
+ "CREATE ACCESS METHOD heap2 TYPE TABLE HANDLER heap_tableam_handler", true,
+ parser -> parser.withDialect(Dialect.POSTGRESQL));
+ assertEquals("heap_tableam_handler", accessMethod.getHandler());
+ }
+
+ @Test
+ void endsTheStatementsAroundIt() throws JSQLParserException {
+ Statements statements = CCJSqlParserUtil.parseStatements(
+ "HANDLER t OPEN AS h; HANDLER h READ FIRST; HANDLER h CLOSE; SELECT 1", MYSQL);
+ assertEquals(List.of("HandlerStatement", "HandlerStatement", "HandlerStatement",
+ "PlainSelect"), names(statements));
+
+ Statements unsupported = CCJSqlParserUtil.parseStatements(
+ "FOO; HANDLER t OPEN; BAR; HANDLER t CLOSE",
+ parser -> parser.withUnsupportedStatements(true));
+ assertEquals(List.of("UnsupportedStatement", "HandlerStatement", "UnsupportedStatement",
+ "HandlerStatement"), names(unsupported));
+
+ Statements postgres = CCJSqlParserUtil.parseStatements("HANDLER t OPEN",
+ parser -> parser.withDialect(Dialect.POSTGRESQL).withUnsupportedStatements(true));
+ assertInstanceOf(UnsupportedStatement.class, postgres.get(0));
+ }
+
+ @Test
+ void findsTheOpenedTableButNotTheHandlerName() throws JSQLParserException {
+ assertThat(new TablesNamesFinder<>()
+ .getTables(CCJSqlParserUtil.parse("HANDLER db1.t OPEN AS h")))
+ .containsExactly("db1.t");
+ // READ and CLOSE name the handler, which may be the alias that OPEN gave the table
+ assertThat(new TablesNamesFinder<>()
+ .getTables(CCJSqlParserUtil.parse("HANDLER h READ i1 = (1) WHERE a > 1")))
+ .isEmpty();
+ assertThat(new TablesNamesFinder<>().getTables(CCJSqlParserUtil.parse("HANDLER h CLOSE")))
+ .isEmpty();
+ }
+
+ @Test
+ void reportsReadsAndSessionChanges() throws JSQLParserException {
+ assertThat(CCJSqlParserUtil.parse("HANDLER h READ FIRST").getFeatures().getCertain())
+ .containsExactlyInAnyOrder(StmtFeature.READS_DATA,
+ StmtFeature.RETURNS_RESULT_SET);
+ for (String sql : new String[] {"HANDLER t OPEN", "HANDLER h CLOSE"}) {
+ assertThat(CCJSqlParserUtil.parse(sql).getFeatures().getCertain())
+ .containsExactly(StmtFeature.MODIFIES_SESSION);
+ }
+ }
+
+ @Test
+ void nodesSpanTheStatementAndItsParts() throws JSQLParserException {
+ String sql = "HANDLER db1.t OPEN AS h";
+ HandlerStatement open = parse(sql);
+ assertEquals(sql, text(sql, open));
+ assertEquals("db1.t", text(sql, open.getTable()));
+
+ sql = "HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3";
+ HandlerStatement read = parse(sql);
+ assertEquals(sql, text(sql, read));
+ assertEquals("h", text(sql, read.getTable()));
+ assertEquals("(1, 2)", text(sql, read.getKeyValues()));
+ assertEquals("a > 0", text(sql, read.getWhere()));
+ assertEquals("LIMIT 3", text(sql, read.getLimit()));
+ }
+
+ @Test
+ void buildsStatementsInCode() {
+ Table table = new Table("db", "t");
+ table.setAlias(new Alias("h"));
+ HandlerStatement open = new HandlerStatement(Action.OPEN, table);
+ assertEquals("HANDLER db.t OPEN AS h", open.toString());
+ assertEquals("h", open.getHandlerName());
+ assertNull(new HandlerStatement().getHandlerName());
+ assertEquals(Action.OPEN, new HandlerStatement().getAction());
+
+ HandlerStatement read = new HandlerStatement().withAction(Action.READ)
+ .withTable(new Table("h")).withIndexName("i1")
+ .withKeyComparison(KeyComparison.EQUALS)
+ .withKeyValues(new ParenthesedExpressionList<>(new LongValue(1)))
+ .withWhere(new GreaterThan(new Column("a"), new LongValue(0)))
+ .withLimit(new Limit().withRowCount(new LongValue(5)))
+ .withOffset(new Offset().withOffset(new LongValue(2)));
+ assertEquals("HANDLER h READ i1 = (1) WHERE a > 0 LIMIT 5 OFFSET 2", read.toString());
+ assertEquals(read.toString(), deparse(read));
+
+ read.withKeyComparison(null).withKeyValues((ExpressionList>) null)
+ .withDirection(Direction.FIRST).withIndexName(null).withWhere(null)
+ .withLimit(null).withOffset(null);
+ assertEquals("HANDLER h READ FIRST", read.toString());
+
+ HandlerStatement close = new HandlerStatement(Action.CLOSE, new Table("h"));
+ assertEquals("HANDLER h CLOSE", deparse(close));
+ assertThrows(NullPointerException.class, () -> close.setAction(null));
+ }
+
+ private static HandlerStatement parse(String sql) throws JSQLParserException {
+ return (HandlerStatement) assertSqlCanBeParsedAndDeparsed(sql, true, MYSQL);
+ }
+
+ private static List names(Statements statements) {
+ return statements.stream().map(statement -> statement.getClass().getSimpleName())
+ .collect(Collectors.toList());
+ }
+
+ private static String deparse(Statement statement) {
+ StringBuilder builder = new StringBuilder();
+ statement.accept(new StatementDeParser(builder), null);
+ return builder.toString();
+ }
+
+ private static String text(String sql, ASTNodeAccess access) {
+ Node node = access.getASTNode();
+ return sql.substring(node.jjtGetFirstToken().absoluteBegin - 1,
+ node.jjtGetLastToken().absoluteEnd - 1);
+ }
+}
diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlServerAdminValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlServerAdminValidatorTest.java
new file mode 100644
index 000000000..430e66782
--- /dev/null
+++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlServerAdminValidatorTest.java
@@ -0,0 +1,63 @@
+/*-
+ * #%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.util.validation.validator;
+
+import java.util.stream.Stream;
+import net.sf.jsqlparser.JSQLParserException;
+import net.sf.jsqlparser.parser.feature.Feature;
+import net.sf.jsqlparser.util.validation.ValidationTestAsserts;
+import net.sf.jsqlparser.util.validation.feature.DatabaseType;
+import net.sf.jsqlparser.util.validation.feature.FeaturesAllowed;
+import net.sf.jsqlparser.util.validation.feature.MySqlVersion;
+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.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+public class MySqlServerAdminValidatorTest extends ValidationTestAsserts {
+
+ static Stream statements() {
+ return Stream.of(
+ Arguments.of("FLUSH TABLES t WITH READ LOCK", Feature.flush),
+ Arguments.of("FLUSH LOCAL BINARY LOGS, RELAY LOGS FOR CHANNEL 'c1'",
+ Feature.flush),
+ Arguments.of("KILL QUERY 5", Feature.kill),
+ Arguments.of("HANDLER db1.t OPEN AS h", Feature.handler),
+ Arguments.of("HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3", Feature.handler),
+ Arguments.of("HANDLER h CLOSE", Feature.handler),
+ Arguments.of("RESET PERSIST IF EXISTS max_connections", Feature.resetPersist));
+ }
+
+ @ParameterizedTest
+ @MethodSource("statements")
+ public void testAllowedForMySql(String sql, Feature feature) throws JSQLParserException {
+ validateNoErrors(sql, 1, MySqlVersion.V8_0, DatabaseType.MYSQL);
+ }
+
+ @ParameterizedTest
+ @MethodSource("statements")
+ public void testNotAllowedForSelect(String sql, Feature feature) {
+ validateNotAllowed(sql, 1, 1, FeaturesAllowed.SELECT, feature);
+ }
+
+ @ParameterizedTest
+ @MethodSource("statements")
+ public void testNotAllowedForPostgresql(String sql, Feature feature)
+ throws JSQLParserException {
+ validate(sql, 1, 1, PostgresqlVersion.V14);
+ }
+
+ @Test
+ public void testPostgresqlResetStaysAllowed() throws JSQLParserException {
+ validateNoErrors("RESET ALL", 1, PostgresqlVersion.V14, DatabaseType.POSTGRESQL);
+ validate("RESET ALL", 1, 1, MySqlVersion.V8_0);
+ }
+}