Skip to content

feat: parse MySQL EXPLAIN options, prepared statements and DO - #2898

Open
minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-explain-prepare-do-20261105
Open

minleejae wants to merge 1 commit into
JSQLParser:masterfrom
minleejae:feat/mysql-explain-prepare-do-20261105

Conversation

@minleejae

Copy link
Copy Markdown
Contributor

Purpose

MySQL scripts and tools use EXPLAIN with an output format, prepared statements and DO. On master (4a970a34e) these fail with Dialect.MYSQL and without a dialect, for example:

  • EXPLAIN FORMAT=JSON SELECT * FROM t, EXPLAIN FORMAT=TREE UPDATE t SET a = 1, EXPLAIN ANALYZE FORMAT=TREE SELECT * FROM t
  • EXPLAIN FORMAT=JSON INTO @plan SELECT * FROM t and EXPLAIN FOR CONNECTION 7
  • DESC FORMAT=JSON SELECT …, DESCRIBE UPDATE t SET a = 1, EXPLAIN REPLACE INTO t VALUES (1)
  • DESCRIBE t a and DESCRIBE t 'a%'
  • PREPARE s FROM 'SELECT ?', PREPARE s FROM @sql, EXECUTE s USING @a, @b, DROP PREPARE s
  • DO SLEEP(0), DO 1, @a := 2, DO (SELECT 1)

MySQL 8.4 references: EXPLAIN, prepared statements, PREPARE, EXECUTE, DEALLOCATE PREPARE, DO.

Current design (As-is)

  • ExplainStatement holds a keyword (EXPLAIN, SUMMARIZE), an option list (ANALYZE, FORMAT, … with PostgreSQL's parenthesized form) and either the explained statement or a table. Its unparenthesized options are PostgreSQL's legacy keywords: FORMAT JSON parses, FORMAT = JSON does not. There is no INTO, FOR CONNECTION or FOR SCHEMA, and REPLACE cannot be explained.
  • DescribeStatement describes a table or, as in DuckDB, a query. DESC/DESCRIBE cannot take an explain option or a DML statement, and neither statement takes a column after the table.
  • PrepareStatement is the PREPARE name [(types)] AS statement of PostgreSQL and DuckDB. Execute is the EXEC/EXECUTE/CALL of several dialects with an argument list; it has no USING. DeallocateStatement is DEALLOCATE [PREPARE] name.
  • DoStatement is PostgreSQL's DO [LANGUAGE l] 'code', read only with Dialect.POSTGRESQL; MySQL's DO expr, … has no reading.
  • TablesNamesFinder reports no table for EXPLAIN t, while it reports t for DESCRIBE t.

Target design (To-be)

The existing models are extended; MySQL's forms are read with Dialect.MYSQL, Dialect.MARIADB and, where noted, without a dialect. Other dialects are unchanged.

  • EXPLAIN, DESCRIBE, DESC follow MySQL 8.4's explain_stmt: {EXPLAIN | DESCRIBE | DESC} then ANALYZE [FORMAT = name] or [FORMAT = name] [INTO @var], then FOR CONNECTION id or [FOR {SCHEMA | DATABASE} name] and a SELECT, TABLE, VALUES, INSERT, REPLACE, UPDATE or DELETE statement. They parse to ExplainStatement, and getKeyword() keeps the spelling.
    • ANALYZE and FORMAT are ExplainStatement.Options, as for PostgreSQL. Option.isUseEqual() marks MySQL's FORMAT = name, so getOption(OptionType.FORMAT).getValue() reads the format of both products. The format is a keyword choice: ExplainStatement.Format (TEXT, XML, JSON, YAML, TRADITIONAL, TREE), read with getFormat(). FORMAT = name takes MySQL's TRADITIONAL, JSON and TREE; as in MySQL the name is case-insensitive and may be quoted (FORMAT='json'), and it is rendered as the enum name.
    • New state: getIntoVariable() (UserVariable), getConnectionId() (LongValue, or HexValue for 0x1F), getSchemaName() with isSchemaKeyword(), and getColumnPattern() for EXPLAIN t col. getTargetKind() (STATEMENT, TABLE, CONNECTION) is derived from the target, and the target setters keep them exclusive.
    • EXPLAIN SELECT, EXPLAIN ANALYZE SELECT, EXPLAIN t, DESCRIBE SELECT … and DESCRIBE t keep their readings and models. The MySQL path is taken only for tokens the generic one does not read (FORMAT =, INTO, FOR, REPLACE, and for DESC/DESCRIBE also ANALYZE, INSERT, UPDATE, DELETE, TABLE, VALUES), and a single word after DESC, such as DESC replace, remains a table name.
    • The explained statement is read by one ExplainableStatement() production, shared with the generic EXPLAIN.
    • toString() and StatementDeParser share appendTo. DescribeStatement does too; its toString() now renders the table as the deparser did (Table.toString(), which keeps a time-travel clause).
  • {DESCRIBE | DESC | EXPLAIN} table {column | 'pattern'} (MySQL's opt_describe_column) adds getColumnPattern() to DescribeStatement and ExplainStatement: a Column, a StringValue or a hex string. The column must end the statement, and a statement keyword on the next line, as in DESC t + newline + COMMIT, still starts the next statement.
  • PREPARE name FROM {'text' | @var} extends PrepareStatement: getSource() is the StringValue or UserVariable, getStatement() is null, and setting one clears the other. The text is not parsed.
  • EXECUTE name USING @a, @b adds Execute.getUsingVariables() (ExpressionList<UserVariable>, as MySqlSelectIntoClause holds its variables). Only EXECUTE takes it, and only user variables, as in MySQL.
  • DROP PREPARE name parses to DeallocateStatement with getKeyword() DROP (DeallocateStatement.Keyword); isUsingPrepareKeyword() is true for it.
  • DO expr [, expr] … extends DoStatement with getSelectItems(). MySQL's do_stmt is DO select_item_list, so the items reuse SelectItemsList() and may carry aliases (DO 1 AS a); getCode() is null.
  • MySQL's user variables of EXPLAIN … INTO, EXECUTE … USING and PREPARE … FROM reuse MySqlIntoVariable() of SELECT … INTO, which now also links the quoted form (@'v') to its source range.
  • Without a dialect the new forms are read unless another product reads the same text differently:
    • DESCRIBE TABLE t and DESCRIBE x y need the MySQL dialects, because Snowflake's DESCRIBE TABLE t and DESCRIBE VIEW v name an object kind.
    • DO before a string or LANGUAGE stays PostgreSQL's code block, which needs Dialect.POSTGRESQL; DO SLEEP(0) is MySQL's.
  • Wiring:
    • StatementDeParser uses the models' appendTo for EXPLAIN, DESCRIBE, PREPARE and DO; ExecuteDeParser renders USING through Execute.appendTo. StatementVisitorAdapter visits the USING variables, the PREPARE source and the DO items.
    • TablesNamesFinder reports the table of EXPLAIN t (until now none) and the tables of subqueries in MySQL's DO. PREPARE … FROM raises UnsupportedOperationException, like EXECUTE, CALL and PostgreSQL's DO, since the tables of the statement text are unknown; EXECUTE keeps raising it for the same reason.
    • Validation: Feature.executeUsing is reported for USING. MySqlVersion and MariaDbVersion add prepare, executeExecute, executeUsing and doStatement.
    • Feature analysis: EXPLAIN … INTO reports MODIFIES_SESSION instead of RETURNS_RESULT_SET; MySQL's DO analyses its expressions (a subquery reads data, a function is unresolved) instead of reporting OPAQUE.
    • AST nodes (fix: give every parsed object a node spanning its source #2870): the new objects get the node of their tokens, and ASTNodeCoverageTest walks the new forms.
    • The usage guide, README and index list the statements.
  • No new keyword tokens; the public signatures are unchanged.

Validation

  • 129 new tests: MySqlExplainTest 82, MySqlPreparedStatementTest 27 and 20 in DoStatementTest; ASTNodeCoverageTest walks the new forms. They fail on master.
    • Every form: parse, toString(), StatementDeParser and reparse with Dialect.MYSQL and, where read there, without a dialect and with Dialect.MARIADB, plus the AST fields (options, Format, useEqual, target kind, INTO variable, connection id, schema, column pattern, source, USING variables, keyword, select items).
    • Rejected forms: unknown formats (FORMAT = XML), ANALYZE … INTO, misplaced INTO and ANALYZE, @@v, FOR CONNECTION -1/1.5/@c, FOR SCHEMA s FOR CONNECTION 1, options before a table, two columns, PREPARE s FROM SELECT 1, PREPARE s FROM 'a' 'b', PREPARE s (INT) FROM …, EXECUTE s USING 1, CALL p USING @a, DROP PREPARE, DO, DO 1,, DO 1 FROM t.
    • Kept readings: EXPLAIN ANALYZE SELECT, EXPLAIN t, DESC SELECT …, DESC analyze/replace/format as tables, the generic EXPLAIN FORMAT JSON, Oracle's EXPLAIN PLAN FOR, PostgreSQL's EXPLAIN (FORMAT JSON), PREPARE … AS, DEALLOCATE s and DO $$…$$; PostgreSQL, Oracle, SQL Server and H2 reject the new forms.
    • Statement boundaries in scripts, builders, visitors, table discovery, validation capabilities and statement features.
    • DoStatementTest.gatesDialectAndRejectsMalformedWrappers asserted that every dialect but PostgreSQL rejects DO $$BEGIN NULL; END$$. The MySQL dialects now read it as DO of a string, because the lexer reads $$…$$ as a string in every dialect; the test asserts that reading for them.
  • The statements of the tests were run against MySQL 8.4.11 (126 forms) and PostgreSQL 18.3 (14 forms). Every form the parser accepts with Dialect.MYSQL is accepted by the server's grammar, and every form it rejects is rejected while the server parses it; the PostgreSQL results all agree. The exceptions:
    • Readings kept from master, which MySQL rejects: DESC analyze, DESC replace and EXPLAIN replace (table names), the generic EXPLAIN FORMAT JSON …, DEALLOCATE s, EXECUTE s (1, 2) and PREPARE p AS SELECT 1.
    • DO $$BEGIN NULL; END$$ parses with Dialect.MYSQL because the lexer reads $$…$$ as a string in every dialect, as SELECT $$…$$ already does on master.
    • MySQL rejects unknown format names (FORMAT = XML, TEXT, 'LONG') with error 1791 rather than 1064 while parsing (EXPLAIN FORMAT = XML FROM also reports 1791), and so does this parser. EXPLAIN INTO @v FORMAT = JSON … stops at error 6006 before the server reads FORMAT; FORMAT after INTO is a syntax error (EXPLAIN FORMAT=JSON INTO @v FORMAT = JSON SELECT 1 gives 1064), and the parser rejects it.
    • PREPARE … FROM @var was run in one session with the variable set, since an unset variable makes the server prepare the text NULL.
  • Java 17 offline Gradle spotlessApply cleanTest check jar: 12,768 tests, 12,743 passed, 25 skipped, zero failures/errors. The grammar choice conflict check, Checkstyle, PMD and SpotBugs pass.
  • Compared with master (4a970a34e) on a corpus of 188 MySQL 8.4 and 166 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: EXPLAIN FORMAT=JSON SELECT * FROM t, EXPLAIN FORMAT=TREE UPDATE t SET a = 1, EXPLAIN FOR CONNECTION 1, DESCRIBE t a, PREPARE s FROM 'SELECT ?', EXECUTE s USING @a, @b, DROP PREPARE s and DO SLEEP(0); without a dialect all of them but DESCRIBE t a.
    • 0 regressed and 0 changed in status, statement class or rendering. Table discovery of EXPLAIN t changes from no table to t.
  • Compared with master (4a970a34e) on a corpus of 635 MySQL 8.4 and PostgreSQL 18 DDL/DCL statements, each accepted by the server, plus variants, scripts and the keyword sweeps: 0 regressed, 0 changed.

AI assistance: prepared with Claude Code.

MySQL's EXPLAIN FORMAT = name, EXPLAIN ANALYZE FORMAT = name,
EXPLAIN ... INTO @var, EXPLAIN FOR CONNECTION and FOR SCHEMA, their
DESCRIBE and DESC spellings, DESCRIBE t col, PREPARE ... FROM,
EXECUTE ... USING, DROP PREPARE and DO expr failed to parse.

Extend the existing models. ExplainStatement keeps ANALYZE and FORMAT as
options, marks FORMAT = name with useEqual and reads it as a Format enum,
and gains the INTO variable, the connection id, the schema, a column
pattern and a derived TargetKind. DescribeStatement gains the column
pattern, PrepareStatement the FROM source, Execute the USING variables,
DeallocateStatement a DROP keyword and DoStatement MySQL's select-list
items. One ExplainableStatement production reads the explained statement
for both EXPLAIN paths, and the user variables reuse MySqlIntoVariable.

The forms are read with the MySQL and MariaDB dialects and without a
dialect, except DESCRIBE TABLE t, DESCRIBE t col and DO before a string,
which other products read differently. Other dialects are unchanged.
Wire the deparser, visitors, table finder (EXPLAIN t now reports t,
PREPARE ... FROM raises like EXECUTE), validation (prepare,
executeExecute, executeUsing and doStatement in the MySQL and MariaDB
capabilities) and feature analysis.

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