Skip to content

feat: parse MySQL FLUSH, KILL, RESET PERSIST and HANDLER - #2901

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-server-admin-20261106
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-server-admin-20261106

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

MySQL administration scripts flush, kill and reset, and some applications read tables through HANDLER. On master these statements fail, for example:

FLUSH TABLES t, s WITH READ LOCK;
FLUSH LOCAL BINARY LOGS, RELAY LOGS FOR CHANNEL 'c1';
KILL QUERY 123;
RESET PERSIST IF EXISTS max_connections;
HANDLER db1.t OPEN AS h;
HANDLER h READ i1 >= (1, 2) WHERE a > 0 LIMIT 3;
HANDLER h CLOSE;

FLUSH, KILL and HANDLER fail in every dialect. RESET PERSIST alone parses as a reset of a parameter named PERSIST, RESET PERSIST max_connections fails, and TablesNamesFinder throws UnsupportedOperationException for every RESET.

Syntax reference: MySQL 8.4 FLUSH, KILL, RESET PERSIST, HANDLER.

Current design (As-is)

  • FLUSH and KILL are non-reserved tokens that only Oracle's ALTER SYSTEM FLUSH … and ALTER SYSTEM KILL SESSION … use. No statement starts with them.
  • HANDLER is a contextual word of PostgreSQL's CREATE FOREIGN DATA WRAPPER … [NO] HANDLER and CREATE ACCESS METHOD … HANDLER, and the routine body scanner recognizes MySQL's DECLARE … HANDLER by it.
  • ResetStatement models PostgreSQL's RESET name, RESET ALL and RESET TIME ZONE as one name string, and TablesNamesFinder.visit(ResetStatement) throws.
  • isStatementStartAhead() lacks CHECK, CHECKSUM, OPTIMIZE and REPAIR, so with unsupported statements enabled CHECK TABLE t becomes an UnsupportedStatement.
  • NO_WRITE_TO_BINLOG | LOCAL is TableMaintenanceStatement.Modifier, which OPTIMIZE/REPAIR TABLE and the MySQL partition operations share. Each parses it with its own copy of the same alternative.

Target design (To-be)

  • FlushStatement (net.sf.jsqlparser.statement.admin):
    • getModifier() is the shared TableMaintenanceStatement.Modifier; the new BinlogModifier() production now parses it for FLUSH, OPTIMIZE/REPAIR and the partition operations.
    • getOptions() keeps the flush options in source order. Each is a FlushOption with getKind(), one of MySQL 8.4's options (BINARY_LOGS, ENGINE_LOGS, ERROR_LOGS, GENERAL_LOGS, LOGS, OPTIMIZER_COSTS, PRIVILEGES, RELAY_LOGS, SLOW_LOGS, STATUS, USER_RESOURCES), and getChannel(), the string literal of RELAY LOGS FOR CHANNEL 'name'.
    • Without options the statement is FLUSH {TABLE | TABLES} [tbl_name [, tbl_name] …] [WITH READ LOCK | FOR EXPORT]: isFlushTables() is derived from the empty option list, with getTables(), isTablesKeyword() and getTableLock() (WITH_READ_LOCK, FOR_EXPORT).
    • As in MySQL 8.4's grammar, the TABLES form takes no flush option, FOR EXPORT requires a table list, only RELAY LOGS names a channel and the channel is a string literal. FLUSH HOSTS, which MySQL 8.4 removed, is rejected.
  • KillStatement: getKind() is CONNECTION, QUERY or null when neither keyword is given, and getProcesslistId() is an Expression: MySQL's grammar takes KILL [CONNECTION | QUERY] expr, so KILL @id and KILL CONNECTION 1 + 2 parse. As there, CONNECTION and QUERY are keywords directly after KILL (KILL connection + 1 kills connection 1), while KILL CONNECTION_ID() calls the function.
  • RESET PERSIST [[IF EXISTS] system_var_name] extends ResetStatement with isPersist() and isIfExists(). getName() is the variable as written, or null when none is named; as in MySQL's persisted_variable_ident it may carry a component or DEFAULT prefix (RESET PERSIST IF EXISTS validate_password.length, RESET PERSIST DEFAULT.x), while a three-part name is rejected. toString() and ResetStatementDeParser share the new appendTo(). A RESET PERSIST production is dispatched before the existing Reset(), which is unchanged.
  • HandlerStatement (net.sf.jsqlparser.statement.handler): getAction() is OPEN, READ or CLOSE.
    • getTable() is the table that OPEN opens, with the handler name as its alias, or the handler name that READ and CLOSE take. MySQL takes an unqualified name there, so HANDLER db1.t CLOSE is rejected. getHandlerName() returns the alias or the table name.
    • A READ through an index has getIndexName() and either getKeyComparison() (=, <=, >=, <, >) with getKeyValues(), a parenthesized list that may hold DEFAULT as MySQL's grammar allows, or getDirection() (FIRST, NEXT, PREV, LAST). Without an index only FIRST and NEXT read in storage order. A word followed by a direction is an index name, as in MySQL (HANDLER h READ first NEXT).
    • getWhere() reuses WhereClause(), and getLimit() reuses LimitWithOffset(); LIMIT n OFFSET m keeps its OFFSET in getOffset().
    • MySQL parses HANDLER … READ with subqueries disallowed and reports one as a syntax error, so a subquery in the key values, the condition or LIMIT is rejected (HANDLER h READ FIRST WHERE a IN (SELECT b FROM s)).
    • toString() and the deparser share appendTo(builder, expressionRenderer), so the deparser renders the key values and the condition with its ExpressionDeParser.
  • Dialects: the new forms parse with Dialect.MYSQL and without a dialect; the other dialects, MariaDB included, keep rejecting them. Without a dialect a bare RESET PERSIST keeps resetting a parameter named persist, since PostgreSQL reads it that way; a variable name or IF EXISTS after PERSIST selects the MySQL form.
  • Grammar:
    • HANDLER becomes a non-reserved keyword, like CHECKSUM (feat: parse MySQL CHECKSUM TABLE and histogram update options #2883), and remains usable as a name. The foreign data wrapper, access method and routine body uses of the word read the new token.
    • isStatementStartAhead() recognizes the new statements, so they end a preceding statement and are not captured as unsupported. It now also recognizes CHECK, CHECKSUM, OPTIMIZE and REPAIR TABLE, which were parsed alone but captured as UnsupportedStatements when unsupported statements were enabled. FLUSH counts as a start only when TABLE, TABLES or a flush option word follows, so FLUSH HOSTS or MariaDB's FLUSH QUERY CACHE remain UnsupportedStatements when unsupported statements are enabled. HANDLER needs a name and then OPEN, READ or CLOSE.
    • No reserved word and no grammar choice conflict is added.
  • Wiring:
    • StatementVisitor defaults, StatementVisitorAdapter (visits the KILL expression and the key values and condition of a READ), StatementDeParser.
    • TablesNamesFinder reports the tables of FLUSH TABLES and the table that HANDLER … OPEN opens, not the handler name of READ and CLOSE, which may be an alias. It finds the tables of a subquery in the KILL expression, which MySQL rejects only when it runs the statement, and returns no table instead of throwing for every RESET.
    • Validation: new features flush, kill, handler and resetPersist, enabled in MySqlVersion. A PostgreSQL RESET keeps reset.
    • Feature analysis: MODIFIES_SESSION for FLUSH, KILL and HANDLER … OPEN/CLOSE, plus MODIFIES_TRANSACTION when FLUSH TABLES locks tables; READS_DATA and RETURNS_RESULT_SET for HANDLER … READ.
    • The usage guide, README and index list the statements.

Public signatures are retained. Behavior changes: TablesNamesFinder no longer throws for RESET, and with Dialect.MYSQL a bare RESET PERSIST now has isPersist() and a null getName() instead of the name PERSIST; its rendering is unchanged.

Validation

  • 196 new tests: MySqlFlushStatementTest 53, MySqlHandlerStatementTest 64, MySqlKillStatementTest 26, MySqlServerAdminValidatorTest 22, 30 in ResetStatementTest and 1 in TableMaintenanceStatementTest; ASTNodeCoverageTest walks seven of the statements.
    • 80 statements, each checked against toString() and the deparser and parsed again, with Dialect.MYSQL and without a dialect, plus the AST fields: modifier, option kinds and channel, tables and lock, kill kind and expression, RESET PERSIST name and IF EXISTS, handler action, table, alias and handler name, index, key comparison and values, direction, WHERE, LIMIT and OFFSET.
    • 65 rejected forms, among them FLUSH HOSTS, FLUSH TABLES FOR EXPORT, FLUSH TABLES t WITH READ LOCK, PRIVILEGES, FLUSH RELAY LOGS FOR CHANNEL c1, KILL QUERY, RESET PERSIST IF EXISTS, RESET PERSIST a.b.c, HANDLER t OPEN h AS x, HANDLER db1.t CLOSE, HANDLER h READ PREV, HANDLER h READ i1 <> (1) and HANDLER h READ FIRST WHERE EXISTS (SELECT 1).
    • The other dialects (PostgreSQL, MariaDB, Oracle, SQL Server) keep rejecting the statements; a bare RESET persist keeps its PostgreSQL reading without a dialect and with the PostgreSQL and MariaDB dialects. Oracle's ALTER SYSTEM FLUSH and ALTER SYSTEM KILL SESSION, PostgreSQL's CREATE ACCESS METHOD … HANDLER and MySQL's DECLARE CONTINUE HANDLER in routine bodies are unchanged, and flush, kill, handler and the option words remain usable as names. MySQL 8.4 reserves KILL, but kill stays accepted as a name, as on master.
    • Statement boundaries in scripts, unsupported statements around the new ones (FLUSH HOSTS stays unsupported), node spans, builders, table discovery, validation capabilities and statement features.
    • On master (12548b9da) 78 of the 80 statements fail; RESET PERSIST alone parses there as a reset of a parameter named PERSIST.
  • Of 155 statements run on MySQL 8.4.11, the server accepts 88. The parser accepts all of them, and master rejects 80. Some fail only when run, for example an unknown handler, table or thread id, a replica channel that does not exist, RESET PERSIST of a variable that is not persisted, or KILL (SELECT MAX(id) FROM t), whose subquery MySQL does not support yet. The server reports a syntax error for the other 67, and the parser rejects 66 of them, including a subquery anywhere in a HANDLER … READ, RESET PERSIST a.b.c and FLUSH HOSTS. The exception is SELECT kill, query, connection FROM kill: MySQL 8.4 reserves KILL, but the parser keeps accepting kill as a name, as master does. PostgreSQL 18.6 rejects FLUSH, KILL, HANDLER and RESET PERSIST max_connections, and Dialect.POSTGRESQL rejects them too; RESET persist keeps its PostgreSQL reading.
  • Java 17 Gradle check: 12,993 tests, 0 failures, 25 skipped. Grammar ambiguity, formatting (Spotless ratchet against upstream/master), Checkstyle, PMD, SpotBugs and JaCoCo pass.
  • Compared with master (12548b9da) on a corpus of MySQL 8.4 and PostgreSQL 18 statements other than DDL/DCL, each accepted by the server, with the MySQL or PostgreSQL dialect and without a dialect:
    • Fixed with Dialect.MYSQL and without a dialect: HANDLER t OPEN AS h, HANDLER h READ FIRST WHERE a > 1 LIMIT 5, HANDLER h CLOSE, RESET PERSIST max_connections, FLUSH TABLES WITH READ LOCK, FLUSH TABLES t FOR EXPORT, FLUSH PRIVILEGES, FLUSH LOCAL BINARY LOGS, KILL QUERY 123 and KILL CONNECTION 123. Each renders identically to the source.
    • 0 regressed and 0 changed.
  • Compared with master (12548b9da) on the DDL/DCL survey corpora of MySQL 8.4 and PostgreSQL 18 statements, plus variants, scripts and the keyword sweeps: 0 regressed, 0 changed.

AI assistance: prepared with Claude Code.

FLUSH, KILL and HANDLER failed in every dialect, RESET PERSIST alone
reset a parameter named PERSIST and RESET PERSIST name failed.

FlushStatement keeps the binlog modifier, the flush options in source
order (FlushOption, with the channel of RELAY LOGS) or the FLUSH TABLES
form with its tables and WITH READ LOCK or FOR EXPORT. KillStatement
keeps CONNECTION or QUERY and the identifier expression. ResetStatement
gains isPersist() and isIfExists(), and the variable name may have a
component or DEFAULT prefix. HandlerStatement keeps OPEN with the table
and its alias, READ with the index, key comparison and values or
direction, WHERE and LIMIT, and CLOSE; as in MySQL, a READ takes no
subquery. The forms parse with the MySQL dialect and without one; other
dialects keep rejecting them. HANDLER becomes a non-reserved keyword,
and TablesNamesFinder no longer throws for RESET.

The new statements and CHECK, CHECKSUM, OPTIMIZE and REPAIR TABLE now
start a statement, so unsupported statement capture no longer takes
them.

Signed-off-by: minleejae <mmj9808@gmail.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant