From e32c7081dcb61ca38a51c4124d90b4f6b5a758ef Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 5 Nov 2026 13:10:58 +0900 Subject: [PATCH] feat: parse MySQL EXPLAIN options, prepared statements and DO 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 --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 5 + .../statement/DeallocateStatement.java | 33 +- .../statement/DescribeStatement.java | 45 ++- .../sf/jsqlparser/statement/DoStatement.java | 53 ++- .../statement/ExplainStatement.java | 199 +++++++++- .../statement/PrepareStatement.java | 42 ++- .../statement/StatementFeatureVisitor.java | 12 +- .../statement/StatementVisitorAdapter.java | 4 +- .../jsqlparser/statement/execute/Execute.java | 32 ++ .../sf/jsqlparser/util/TablesNamesFinder.java | 18 +- .../util/deparser/StatementDeParser.java | 28 +- .../validation/feature/MariaDbVersion.java | 7 + .../util/validation/feature/MySqlVersion.java | 6 + .../validator/ExecuteValidator.java | 2 + .../validator/StatementValidator.java | 4 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 309 ++++++++++++++-- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 55 +++ .../parser/ASTNodeCoverageTest.java | 10 +- .../jsqlparser/statement/DoStatementTest.java | 127 ++++++- .../statement/MySqlExplainTest.java | 346 ++++++++++++++++++ .../statement/MySqlPreparedStatementTest.java | 220 +++++++++++ 23 files changed, 1477 insertions(+), 83 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/MySqlExplainTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/MySqlPreparedStatementTest.java diff --git a/README.md b/README.md index e7a8db4521..7272abda26 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | | **Routine changes** | `ALTER FUNCTION` · `ALTER PROCEDURE` · `ALTER ROUTINE` · PostgreSQL `ALTER TRIGGER` | | **MySQL metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` | +| **MySQL utility statements** | `EXPLAIN`/`DESCRIBE [ANALYZE] FORMAT = …` · `EXPLAIN FOR CONNECTION` · `DESCRIBE t col` · `PREPARE … FROM` · `EXECUTE … USING` · `DROP PREPARE` · `DO` | | **PostgreSQL administration** | `CREATE`/`ALTER EVENT TRIGGER` · `REINDEX` · `SECURITY LABEL` | | **Informix constraints** | `ALTER TABLE … ADD CONSTRAINT` with trailing constraint names for primary, unique, foreign and check constraints; enable with `parser.withDialect(Dialect.INFORMIX)` | | **Salesforce SOQL** | `INCLUDES` · `EXCLUDES` | 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 447245f4aa..2b7bc6ea9a 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -537,6 +537,11 @@ public enum Feature { */ executeStatementImmediate, + /** + * MySQL "EXECUTE name USING @variable, ..." of a prepared statement is allowed + * + * @see Execute#getUsingVariables() + */ executeUsing, /** * "PREPARE" and "DEALLOCATE" of a prepared statement are allowed diff --git a/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java b/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java index 58b0fe34cd..e4624f7f89 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/DeallocateStatement.java @@ -12,9 +12,14 @@ import net.sf.jsqlparser.parser.ASTNodeAccessImpl; /** - * {@code DEALLOCATE [PREPARE] name}, which drops a statement prepared by {@link PrepareStatement}. + * {@code DEALLOCATE [PREPARE] name}, or MySQL's {@code DROP PREPARE name}, which drops a statement + * prepared by {@link PrepareStatement}. + * + * @see MySQL DEALLOCATE + * PREPARE */ public class DeallocateStatement extends ASTNodeAccessImpl implements Statement { + private Keyword keyword = Keyword.DEALLOCATE; private String name; private boolean usingPrepareKeyword; @@ -37,8 +42,23 @@ public DeallocateStatement withName(String name) { return this; } + /** The leading keyword, {@code DEALLOCATE} unless MySQL's {@code DROP} was written. */ + public Keyword getKeyword() { + return keyword; + } + + public void setKeyword(Keyword keyword) { + this.keyword = keyword == null ? Keyword.DEALLOCATE : keyword; + } + + public DeallocateStatement withKeyword(Keyword keyword) { + setKeyword(keyword); + return this; + } + + /** Whether {@code PREPARE} follows the keyword; {@code DROP} always takes it. */ public boolean isUsingPrepareKeyword() { - return usingPrepareKeyword; + return usingPrepareKeyword || keyword == Keyword.DROP; } public void setUsingPrepareKeyword(boolean usingPrepareKeyword) { @@ -51,8 +71,8 @@ public DeallocateStatement withUsingPrepareKeyword(boolean usingPrepareKeyword) } public StringBuilder appendTo(StringBuilder builder) { - builder.append("DEALLOCATE "); - if (usingPrepareKeyword) { + builder.append(keyword).append(' '); + if (isUsingPrepareKeyword()) { builder.append("PREPARE "); } builder.append(name); @@ -68,4 +88,9 @@ public String toString() { public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + + /** The keyword that starts the statement. */ + public enum Keyword { + DEALLOCATE, DROP + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java b/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java index 2efedba2d1..ad0cadd549 100644 --- a/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/DescribeStatement.java @@ -9,15 +9,23 @@ */ package net.sf.jsqlparser.statement; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.select.Select; +/** + * {@code DESCRIBE} or {@code DESC} of a table, or, in DuckDB, of a query's result. + * + * @see MySQL DESCRIBE + */ public class DescribeStatement extends ASTNodeAccessImpl implements Statement { private Table table; private Select select; private String describeType; + private Expression columnPattern; public DescribeStatement() { // empty constructor @@ -49,10 +57,43 @@ public DescribeStatement withSelect(Select select) { return this; } + /** + * The MySQL column name ({@link net.sf.jsqlparser.schema.Column}) or wildcard string that + * limits the described columns to those it matches, or null. + */ + public Expression getColumnPattern() { + return columnPattern; + } + + public void setColumnPattern(Expression columnPattern) { + this.columnPattern = columnPattern; + } + + public DescribeStatement withColumnPattern(Expression columnPattern) { + setColumnPattern(columnPattern); + return this; + } + @Override public String toString() { - return this.describeType + " " - + (select != null ? select.toString() : table.getFullyQualifiedName()); + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append, builder::append).toString(); + } + + /** Shared by SQL rendering and statement deparsers. */ + public StringBuilder appendTo(StringBuilder builder, Consumer