From 487ad0fee2df0eac80ba88262e862791fe2fb2ac Mon Sep 17 00:00:00 2001 From: minleejae Date: Thu, 12 Nov 2026 13:32:51 +0900 Subject: [PATCH] feat: parse MySQL 8.4 SHOW statements for variables, status, grants, logs and other server metadata MySQL SHOW VARIABLES, STATUS, PROCESSLIST, GRANTS, WARNINGS, ERRORS, PROFILE, ENGINE, BINARY LOGS, BINLOG and RELAYLOG EVENTS, REPLICA STATUS, TRIGGERS, EVENTS, OPEN TABLES, routine STATUS and CODE, CHARACTER SET, COLLATION and DATABASES became UnsupportedStatement, or a ShowStatement for a single word, and SHOW INDEX FROM t FROM db failed to parse. Add MySqlShowStatement for these forms. Its Kind lists the parts each statement allows, and the values follow the MySQL 8.4 grammar: string LIKE patterns, log names and channels, unsigned limits and positions. It is read with Dialect.MYSQL and MARIADB and without a dialect; without a dialect a single word stays a ShowStatement, and the spellings removed in MySQL 8.4 (MASTER, SLAVE) are not read with Dialect.MYSQL. Share the {FROM | IN} db clause and the LIKE | WHERE filter as ShowDbSelection and ShowFilter, each read by one production, in SHOW TABLES, COLUMNS, TABLE STATUS and INDEX; the existing accessors read and write them. Extend ShowIndexStatement with EXTENDED, INDEXES and KEYS, IN, a Table, the trailing database and WHERE, and report its table in TablesNamesFinder. Let the whole-statement lookahead of the typed SHOW forms accept a WHERE filter after its first condition, so conditions with AND or OR no longer fall back to UnsupportedStatement. Signed-off-by: minleejae --- README.md | 2 +- .../sf/jsqlparser/parser/feature/Feature.java | 7 + .../statement/ShowColumnsStatement.java | 93 +- .../statement/StatementFeatureVisitor.java | 6 + .../statement/StatementVisitor.java | 9 + .../statement/StatementVisitorAdapter.java | 6 + .../statement/show/MySqlShowStatement.java | 756 ++++++++++++++++ .../statement/show/ShowDbSelection.java | 107 +++ .../jsqlparser/statement/show/ShowFilter.java | 115 +++ .../statement/show/ShowIndexStatement.java | 153 +++- .../show/ShowTableStatusStatement.java | 79 +- .../statement/show/ShowTablesStatement.java | 110 ++- .../sf/jsqlparser/util/TablesNamesFinder.java | 25 +- .../deparser/ShowIndexStatementDeParser.java | 2 +- .../util/deparser/StatementDeParser.java | 23 +- .../validation/feature/MariaDbVersion.java | 3 +- .../util/validation/feature/MySqlVersion.java | 3 +- .../MySqlShowStatementValidator.java | 31 + .../ShowColumnsStatementValidator.java | 11 +- .../ShowIndexStatementValidator.java | 6 +- .../validator/StatementValidator.java | 12 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 601 +++++++++++-- src/site/sphinx/index.rst | 2 +- src/site/sphinx/usage.rst | 107 ++- .../parser/ASTNodeCoverageTest.java | 8 + .../statement/ShowColumnsStatementTest.java | 2 +- .../statement/ShowIndexStatementTest.java | 153 +++- .../show/MySqlShowStatementTest.java | 826 ++++++++++++++++++ .../statement/show/ShowClausesTest.java | 185 ++++ .../show/ShowExpressionDeParsing.java | 45 + .../MySqlShowStatementValidatorTest.java | 79 ++ .../ShowIndexStatementValidatorTest.java | 28 +- 32 files changed, 3401 insertions(+), 194 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/show/MySqlShowStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/show/ShowDbSelection.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/show/ShowFilter.java create mode 100644 src/main/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidator.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/show/MySqlShowStatementTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/show/ShowClausesTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/show/ShowExpressionDeParsing.java create mode 100644 src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidatorTest.java diff --git a/README.md b/README.md index 0fdeacbe3a..016980535e 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **Tablespaces** | MySQL and PostgreSQL `CREATE TABLESPACE` · `ALTER TABLESPACE` | | **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 metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW INDEX`/`INDEXES`/`KEYS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` · `SHOW [GLOBAL | SESSION] VARIABLES`/`STATUS` · `SHOW [FULL] PROCESSLIST` · `SHOW GRANTS [FOR …]` · `SHOW WARNINGS`/`ERRORS` · `SHOW BINARY LOGS`/`BINLOG EVENTS`/`RELAYLOG EVENTS` · `SHOW ENGINE …` · `SHOW PROFILE` · `SHOW DATABASES`/`TRIGGERS`/`EVENTS`/`OPEN TABLES`/`CHARACTER SET`/`COLLATION` · `SHOW PROCEDURE`/`FUNCTION STATUS`/`CODE` · `SHOW REPLICA STATUS`/`REPLICAS` | | **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 7382b49e24..4f89739989 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -893,6 +893,13 @@ public enum Feature { * @see ShowCreateStatement */ showCreate, + /** + * The MySQL/MariaDB "SHOW" statements of server settings, state and object lists, such as "SHOW + * VARIABLES", "SHOW GRANTS" or "SHOW BINLOG EVENTS" + * + * @see MySqlShowStatement + */ + mySqlShow, /** * @see UseStatement */ diff --git a/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java b/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java index 5182afc659..21134c180a 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java @@ -9,9 +9,12 @@ */ 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.show.ShowDbSelection; +import net.sf.jsqlparser.statement.show.ShowFilter; import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; /** @@ -27,10 +30,8 @@ public class ShowColumnsStatement extends ASTNodeAccessImpl implements Statement private boolean fieldsKeyword; private SelectionMode tableSelectionMode = SelectionMode.FROM; private Table table; - private SelectionMode dbSelectionMode = SelectionMode.FROM; - private String dbName; - private Expression likeExpression; - private Expression whereCondition; + private ShowDbSelection dbSelection; + private ShowFilter filter; public ShowColumnsStatement() { // empty constructor @@ -97,41 +98,73 @@ public void setTableName(String tableName) { /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ public SelectionMode getDbSelectionMode() { - return dbSelectionMode; + return dbSelection == null ? SelectionMode.FROM : dbSelection.getSelectionMode(); } + /** Sets the keyword before the database name, which is kept until the name is set. */ public void setDbSelectionMode(SelectionMode dbSelectionMode) { - this.dbSelectionMode = dbSelectionMode; + dbSelection = ShowDbSelection.updateSelectionMode(dbSelection, dbSelectionMode); } /** The database of the trailing {@code FROM db} or {@code IN db} clause, or null. */ public String getDbName() { - return dbName; + return ShowDbSelection.name(dbSelection); } public void setDbName(String dbName) { - this.dbName = dbName; + dbSelection = ShowDbSelection.updateName(dbSelection, dbName); + } + + /** The trailing {@code {FROM | IN} db} clause, or null. */ + public ShowDbSelection getDbSelection() { + return dbSelection; + } + + public void setDbSelection(ShowDbSelection dbSelection) { + this.dbSelection = dbSelection; + } + + /** The {@code LIKE} or {@code WHERE} filter, or null. */ + public ShowFilter getFilter() { + return filter; + } + + public void setFilter(ShowFilter filter) { + this.filter = filter; } public Expression getLikeExpression() { - return likeExpression; + return ShowFilter.likeExpression(filter); } + /** Sets a {@code LIKE} filter, which replaces a {@code WHERE} filter; null removes it. */ public void setLikeExpression(Expression likeExpression) { - this.likeExpression = likeExpression; + filter = ShowFilter.replace(filter, ShowFilter.Kind.LIKE, likeExpression); } public Expression getWhereCondition() { - return whereCondition; + return ShowFilter.whereCondition(filter); } + /** Sets a {@code WHERE} filter, which replaces a {@code LIKE} filter; null removes it. */ public void setWhereCondition(Expression whereCondition) { - this.whereCondition = whereCondition; + filter = ShowFilter.replace(filter, ShowFilter.Kind.WHERE, whereCondition); } - @Override - public String toString() { - StringBuilder builder = new StringBuilder("SHOW"); + /** + * The table the statement reads: {@link #getTable()}, or the table in the database of the + * trailing {@code FROM db} clause, which MySQL reads as {@code db.table}. + */ + public Table getQualifiedTable() { + if (table == null || !ShowDbSelection.isPresent(dbSelection)) { + return table; + } + return new Table(dbSelection.getName(), table.getName()); + } + + /** Appends the statement, printing the filter expression with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("SHOW"); if (extended) { builder.append(" EXTENDED"); } @@ -141,17 +174,19 @@ public String toString() { builder.append(fieldsKeyword ? " FIELDS " : " COLUMNS ") .append(tableSelectionMode == null ? SelectionMode.FROM : tableSelectionMode) .append(' ').append(table); - if (dbName != null) { - builder.append(' ') - .append(dbSelectionMode == null ? SelectionMode.FROM : dbSelectionMode) - .append(' ').append(dbName); + if (ShowDbSelection.isPresent(dbSelection)) { + builder.append(' ').append(dbSelection); } - if (likeExpression != null) { - builder.append(" LIKE ").append(likeExpression); - } else if (whereCondition != null) { - builder.append(" WHERE ").append(whereCondition); + if (filter != null) { + filter.appendTo(builder.append(' '), printer); } - return builder.toString(); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } @Override @@ -208,4 +243,14 @@ public ShowColumnsStatement withWhereCondition(Expression whereCondition) { this.setWhereCondition(whereCondition); return this; } + + public ShowColumnsStatement withDbSelection(ShowDbSelection dbSelection) { + this.setDbSelection(dbSelection); + return this; + } + + public ShowColumnsStatement withFilter(ShowFilter filter) { + this.setFilter(filter); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java index be1c4cf274..1d8b878e23 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -129,6 +129,7 @@ import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; import net.sf.jsqlparser.statement.select.TableStatement; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; @@ -1065,6 +1066,11 @@ public Void visit(ShowCreateStatement showCreate, S context) { return metadataOnly(); } + @Override + public Void visit(MySqlShowStatement mySqlShow, S context) { + return metadataOnly(); + } + @Override public Void visit(DescribeStatement describe, S context) { return metadataOnly(); diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java index 2711d3a764..d99373ea88 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -102,6 +102,7 @@ import net.sf.jsqlparser.statement.reindex.Reindex; import net.sf.jsqlparser.statement.securitylabel.SecurityLabel; import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; @@ -483,6 +484,14 @@ default void visit(ShowCreateStatement showCreate) { this.visit(showCreate, null); } + default T visit(MySqlShowStatement mySqlShow, S context) { + return null; + } + + default void visit(MySqlShowStatement mySqlShow) { + this.visit(mySqlShow, null); + } + T visit(Merge merge, S context); default void visit(Merge merge) { diff --git a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java index 2d43aff589..543ead6e9c 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -117,6 +117,7 @@ import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.SelectVisitorAdapter; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; @@ -691,6 +692,11 @@ public T visit(ShowCreateStatement showCreate, S context) { return null; } + @Override + public T visit(MySqlShowStatement mySqlShow, S context) { + return null; + } + @Override public T visit(DeclareStatement declareStatement, S context) { return null; diff --git a/src/main/java/net/sf/jsqlparser/statement/show/MySqlShowStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/MySqlShowStatement.java new file mode 100644 index 0000000000..0a544b801e --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/MySqlShowStatement.java @@ -0,0 +1,756 @@ +/*- + * #%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.show; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +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.create.user.UserAccount; +import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.Offset; + +/** + * A MySQL {@code SHOW} statement that reads server settings, state or object lists, such as + * {@code SHOW GLOBAL VARIABLES LIKE 'max%'}, {@code SHOW GRANTS FOR 'app'@'%'}, + * {@code SHOW BINLOG EVENTS IN 'binlog.000001' LIMIT 10}, {@code SHOW ENGINE INNODB STATUS} or + * {@code SHOW PROFILE CPU FOR QUERY 2}. + * + *

+ * {@link #getKind()} tells which statement it is and {@link #getKeyword()} how its keywords were + * spelled. Each {@link Kind} allows a fixed set of {@link Part parts}; setting a part the kind does + * not allow throws {@link IllegalArgumentException}, and {@link #setKind(Kind)} drops the parts the + * new kind does not allow. The table and column statements ({@code SHOW TABLES}, {@code COLUMNS}, + * {@code INDEX}, {@code TABLE STATUS} and {@code CREATE}) have their own classes. + *

+ * + * @see MySQL SHOW statements + */ +public class MySqlShowStatement extends ASTNodeAccessImpl implements Statement { + + private Kind kind; + private String keyword; + private Scope scope; + private boolean full; + private boolean count; + private Table routineName; + private String engineName; + private EngineInfo engineInfo = EngineInfo.STATUS; + private ShowDbSelection dbSelection; + private ShowFilter filter; + private UserAccount account; + private final List usingRoles = new ArrayList<>(); + private StringValue logName; + private Expression position; + private Limit limit; + private Offset offset; + private StringValue channel; + private final List profileTypes = new ArrayList<>(); + private Expression profileQuery; + + public MySqlShowStatement() { + // empty constructor + } + + public MySqlShowStatement(Kind kind) { + setKind(kind); + } + + public Kind getKind() { + return kind; + } + + /** + * Sets the kind, resets the keyword to the kind's first spelling and drops the parts the new + * kind does not allow. + */ + public void setKind(Kind kind) { + this.kind = kind; + this.keyword = null; + if (kind != null) { + for (Part part : Part.values()) { + if (!kind.allows(part)) { + clear(part); + } + } + } + } + + private void clear(Part part) { + switch (part) { + case SCOPE: + scope = null; + break; + case FULL: + full = false; + break; + case COUNT: + count = false; + break; + case ROUTINE: + routineName = null; + break; + case ENGINE: + engineName = null; + engineInfo = EngineInfo.STATUS; + break; + case DATABASE: + dbSelection = null; + break; + case FILTER: + filter = null; + break; + case ACCOUNT: + account = null; + usingRoles.clear(); + break; + default: + clearRangeParts(part); + } + } + + private void clearRangeParts(Part part) { + switch (part) { + case PROFILE: + profileTypes.clear(); + profileQuery = null; + break; + case LOG_NAME: + logName = null; + break; + case POSITION: + position = null; + break; + case LIMIT: + limit = null; + offset = null; + break; + default: + channel = null; + } + } + + /** + * The keywords as written, one of {@code getKind().getKeywords()}, such as {@code SCHEMAS} for + * {@link Kind#DATABASES}; null without a kind. + */ + public String getKeyword() { + if (keyword != null || kind == null) { + return keyword; + } + return kind.getKeywords().get(0); + } + + /** + * Sets the keywords and selects the kind they spell, as {@link #setKind(Kind)} does if the kind + * changes; null resets the keywords to the first spelling of the kind. + * + * @throws IllegalArgumentException if the keywords spell no kind + */ + public void setKeyword(String keyword) { + if (keyword == null) { + this.keyword = null; + return; + } + Kind spelled = Kind.fromKeyword(keyword); + if (spelled != kind) { + setKind(spelled); + } + this.keyword = Kind.normalize(keyword); + } + + private void require(Part part, boolean present) { + if (present && kind != null && !kind.allows(part)) { + throw new IllegalArgumentException( + "SHOW " + getKeyword() + " does not take the " + part + " part"); + } + } + + /** {@code GLOBAL}, {@code SESSION} or {@code LOCAL}, or null if none was written. */ + public Scope getScope() { + return scope; + } + + public void setScope(Scope scope) { + require(Part.SCOPE, scope != null); + this.scope = scope; + } + + /** Whether {@code FULL} was written, as in {@code SHOW FULL PROCESSLIST}. */ + public boolean isFull() { + return full; + } + + public void setFull(boolean full) { + require(Part.FULL, full); + this.full = full; + } + + /** Whether the statement is {@code SHOW COUNT(*) {WARNINGS | ERRORS}}. */ + public boolean isCount() { + return count; + } + + /** Selects the {@code COUNT(*)} form, which takes no limit; {@code true} clears the limit. */ + public void setCount(boolean count) { + require(Part.COUNT, count); + this.count = count; + if (count) { + limit = null; + offset = null; + } + } + + /** The routine of {@code SHOW {PROCEDURE | FUNCTION} CODE name}, or null. */ + public Table getRoutineName() { + return routineName; + } + + public void setRoutineName(Table routineName) { + require(Part.ROUTINE, routineName != null); + this.routineName = routineName; + } + + /** + * The engine of {@code SHOW ENGINE name ...} as written, including the quotes of a quoted name; + * {@code ALL} for every engine. + */ + public String getEngineName() { + return engineName; + } + + public void setEngineName(String engineName) { + require(Part.ENGINE, engineName != null); + this.engineName = engineName; + } + + /** {@code STATUS}, {@code MUTEX} or {@code LOGS} of {@code SHOW ENGINE}; never null. */ + public EngineInfo getEngineInfo() { + return engineInfo; + } + + /** Sets what {@code SHOW ENGINE} shows; null means {@code STATUS}. */ + public void setEngineInfo(EngineInfo engineInfo) { + this.engineInfo = engineInfo == null ? EngineInfo.STATUS : engineInfo; + } + + /** The {@code {FROM | IN} db} clause, or null. */ + public ShowDbSelection getDbSelection() { + return dbSelection; + } + + public void setDbSelection(ShowDbSelection dbSelection) { + require(Part.DATABASE, dbSelection != null); + this.dbSelection = dbSelection; + } + + /** The database of the {@code {FROM | IN} db} clause, or null. */ + public String getDbName() { + return ShowDbSelection.name(dbSelection); + } + + /** The {@code LIKE} or {@code WHERE} filter, or null. */ + public ShowFilter getFilter() { + return filter; + } + + public void setFilter(ShowFilter filter) { + require(Part.FILTER, filter != null); + this.filter = filter; + } + + /** The pattern of a {@code LIKE} filter, or null. */ + public Expression getLikeExpression() { + return ShowFilter.likeExpression(filter); + } + + /** The condition of a {@code WHERE} filter, or null. */ + public Expression getWhereCondition() { + return ShowFilter.whereCondition(filter); + } + + /** + * The account or role of {@code SHOW GRANTS FOR}, without authentication, or null for the + * current account. + */ + public UserAccount getAccount() { + return account; + } + + /** Sets the account of {@code SHOW GRANTS FOR}; null also removes the {@code USING} roles. */ + public void setAccount(UserAccount account) { + require(Part.ACCOUNT, account != null); + if (account == null) { + usingRoles.clear(); + } + this.account = account; + } + + /** The roles of {@code USING}, as a read-only snapshot; empty without {@code USING}. */ + public List getUsingRoles() { + return Collections.unmodifiableList(new ArrayList<>(usingRoles)); + } + + /** Replaces the roles of {@code USING}; null or an empty list removes {@code USING}. */ + public void setUsingRoles(List usingRoles) { + require(Part.ACCOUNT, usingRoles != null && !usingRoles.isEmpty()); + this.usingRoles.clear(); + if (usingRoles != null) { + this.usingRoles.addAll(usingRoles); + } + } + + /** The log file of {@code IN 'log_name'}, or null. */ + public StringValue getLogName() { + return logName; + } + + public void setLogName(StringValue logName) { + require(Part.LOG_NAME, logName != null); + this.logName = logName; + } + + /** The position of {@code FROM pos}, a number literal, or null. */ + public Expression getPosition() { + return position; + } + + public void setPosition(Expression position) { + require(Part.POSITION, position != null); + this.position = position; + } + + /** + * The {@code LIMIT [offset,] row_count} clause, or null. {@code LIMIT row_count OFFSET offset} + * keeps its offset in {@link #getOffset()}. + */ + public Limit getLimit() { + return limit; + } + + /** Sets the limit; a limit clears the {@code COUNT(*)} form. */ + public void setLimit(Limit limit) { + require(Part.LIMIT, limit != null); + this.limit = limit; + if (limit == null) { + offset = null; + } else { + count = false; + } + } + + /** The {@code OFFSET offset} that follows {@code LIMIT row_count}, or null. */ + public Offset getOffset() { + return offset; + } + + /** + * Sets the {@code OFFSET offset} after the limit. + * + * @throws IllegalArgumentException without a limit, or with a {@code LIMIT offset, row_count} + */ + public void setOffset(Offset offset) { + require(Part.LIMIT, offset != null); + if (offset != null && (limit == null || limit.getOffset() != null)) { + throw new IllegalArgumentException("OFFSET needs a LIMIT row_count before it"); + } + this.offset = offset; + } + + /** The channel of {@code FOR CHANNEL 'channel'}, or null. */ + public StringValue getChannel() { + return channel; + } + + public void setChannel(StringValue channel) { + require(Part.CHANNEL, channel != null); + this.channel = channel; + } + + /** The types of {@code SHOW PROFILE type [, type] ...}, as a read-only snapshot. */ + public List getProfileTypes() { + return Collections.unmodifiableList(new ArrayList<>(profileTypes)); + } + + /** Replaces the types of {@code SHOW PROFILE}; null or an empty list removes them. */ + public void setProfileTypes(List profileTypes) { + require(Part.PROFILE, profileTypes != null && !profileTypes.isEmpty()); + this.profileTypes.clear(); + if (profileTypes != null) { + this.profileTypes.addAll(profileTypes); + } + } + + /** The query number of {@code SHOW PROFILE ... FOR QUERY n}, or null. */ + public Expression getProfileQuery() { + return profileQuery; + } + + public void setProfileQuery(Expression profileQuery) { + require(Part.PROFILE, profileQuery != null); + this.profileQuery = profileQuery; + } + + /** Appends the statement, printing its expressions with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("SHOW"); + if (scope != null) { + builder.append(' ').append(scope); + } + if (full) { + builder.append(" FULL"); + } + if (count) { + builder.append(" COUNT(*)"); + } + builder.append(' ').append(getKeyword()); + appendObject(builder, printer); + if (ShowDbSelection.isPresent(dbSelection)) { + builder.append(' ').append(dbSelection); + } + if (filter != null) { + filter.appendTo(builder.append(' '), printer); + } + appendAccount(builder); + appendLogRange(builder, printer); + if (channel != null) { + builder.append(" FOR CHANNEL "); + printer.accept(channel); + } + return builder; + } + + private void appendObject(StringBuilder builder, Consumer printer) { + if (kind == Kind.ENGINE) { + builder.append(' ').append(engineName).append(' ').append(engineInfo); + } else if (routineName != null) { + builder.append(' ').append(routineName); + } + for (int i = 0; i < profileTypes.size(); i++) { + builder.append(i == 0 ? " " : ", ").append(profileTypes.get(i)); + } + if (profileQuery != null) { + builder.append(" FOR QUERY "); + printer.accept(profileQuery); + } + } + + private void appendAccount(StringBuilder builder) { + if (account == null) { + return; + } + builder.append(" FOR ").append(account); + for (int i = 0; i < usingRoles.size(); i++) { + builder.append(i == 0 ? " USING " : ", ").append(usingRoles.get(i)); + } + } + + private void appendLogRange(StringBuilder builder, Consumer printer) { + if (logName != null) { + builder.append(" IN "); + printer.accept(logName); + } + if (position != null) { + builder.append(" FROM "); + printer.accept(position); + } + if (limit != null) { + builder.append(" LIMIT "); + if (limit.getOffset() != null) { + printer.accept(limit.getOffset()); + builder.append(", "); + } + printer.accept(limit.getRowCount()); + } + if (offset != null) { + builder.append(" OFFSET "); + printer.accept(offset.getOffset()); + } + } + + @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); + } + + public MySqlShowStatement withKind(Kind kind) { + this.setKind(kind); + return this; + } + + public MySqlShowStatement withKeyword(String keyword) { + this.setKeyword(keyword); + return this; + } + + public MySqlShowStatement withScope(Scope scope) { + this.setScope(scope); + return this; + } + + public MySqlShowStatement withFull(boolean full) { + this.setFull(full); + return this; + } + + public MySqlShowStatement withCount(boolean count) { + this.setCount(count); + return this; + } + + public MySqlShowStatement withRoutineName(Table routineName) { + this.setRoutineName(routineName); + return this; + } + + public MySqlShowStatement withEngineName(String engineName) { + this.setEngineName(engineName); + return this; + } + + public MySqlShowStatement withEngineInfo(EngineInfo engineInfo) { + this.setEngineInfo(engineInfo); + return this; + } + + public MySqlShowStatement withDbSelection(ShowDbSelection dbSelection) { + this.setDbSelection(dbSelection); + return this; + } + + public MySqlShowStatement withFilter(ShowFilter filter) { + this.setFilter(filter); + return this; + } + + public MySqlShowStatement withAccount(UserAccount account) { + this.setAccount(account); + return this; + } + + public MySqlShowStatement withUsingRoles(List usingRoles) { + this.setUsingRoles(usingRoles); + return this; + } + + public MySqlShowStatement withLogName(StringValue logName) { + this.setLogName(logName); + return this; + } + + public MySqlShowStatement withPosition(Expression position) { + this.setPosition(position); + return this; + } + + public MySqlShowStatement withLimit(Limit limit) { + this.setLimit(limit); + return this; + } + + public MySqlShowStatement withOffset(Offset offset) { + this.setOffset(offset); + return this; + } + + public MySqlShowStatement withChannel(StringValue channel) { + this.setChannel(channel); + return this; + } + + public MySqlShowStatement withProfileTypes(List profileTypes) { + this.setProfileTypes(profileTypes); + return this; + } + + public MySqlShowStatement withProfileQuery(Expression profileQuery) { + this.setProfileQuery(profileQuery); + return this; + } + + /** An optional or required part of a {@link MySqlShowStatement}, allowed per {@link Kind}. */ + public enum Part { + /** {@code GLOBAL}, {@code SESSION} or {@code LOCAL} before the keywords. */ + SCOPE, + /** {@code FULL} before the keywords. */ + FULL, + /** {@code COUNT(*)} before the keywords. */ + COUNT, + /** The routine name after {@code PROCEDURE CODE} or {@code FUNCTION CODE}. */ + ROUTINE, + /** + * The engine name and {@code STATUS}, {@code MUTEX} or {@code LOGS} after {@code ENGINE}. + */ + ENGINE, + /** {@code {FROM | IN} db}. */ + DATABASE, + /** {@code LIKE 'pattern'} or {@code WHERE expr}. */ + FILTER, + /** {@code FOR account [USING role [, role] ...]}. */ + ACCOUNT, + /** {@code IN 'log_name'}. */ + LOG_NAME, + /** {@code FROM pos}. */ + POSITION, + /** {@code LIMIT [offset,] row_count} or {@code LIMIT row_count OFFSET offset}. */ + LIMIT, + /** {@code FOR CHANNEL 'channel'}. */ + CHANNEL, + /** The types and {@code FOR QUERY n} of {@code SHOW PROFILE}. */ + PROFILE + } + + /** {@code GLOBAL}, {@code SESSION} or its synonym {@code LOCAL}. */ + public enum Scope { + GLOBAL, SESSION, LOCAL + } + + /** What {@code SHOW ENGINE} shows. */ + public enum EngineInfo { + STATUS, MUTEX, LOGS + } + + /** A type of information of {@code SHOW PROFILE}. */ + public enum ProfileType { + ALL, BLOCK_IO, CONTEXT_SWITCHES, CPU, IPC, MEMORY, PAGE_FAULTS, SOURCE, SWAPS; + + /** The type spelled with the keywords, such as {@code BLOCK IO}, ignoring case. */ + public static ProfileType fromKeyword(String keyword) { + return valueOf(Kind.normalize(keyword).replace(' ', '_')); + } + + @Override + public String toString() { + return name().replace('_', ' '); + } + } + + /** + * The statement. The first keyword spelling is the MySQL 8.4 one; the others are synonyms, or + * the spellings MySQL 8.4 removed ({@code MASTER} and {@code SLAVE}), which the parser reads + * with {@code Dialect.MARIADB} and without a dialect only. + */ + public enum Kind { + VARIABLES(EnumSet.of(Part.SCOPE, Part.FILTER), "VARIABLES"), + + STATUS(EnumSet.of(Part.SCOPE, Part.FILTER), "STATUS"), + + PROCESSLIST(EnumSet.of(Part.FULL), "PROCESSLIST"), + + DATABASES(EnumSet.of(Part.FILTER), "DATABASES", "SCHEMAS"), + + TRIGGERS(EnumSet.of(Part.FULL, Part.DATABASE, Part.FILTER), "TRIGGERS"), + + EVENTS(EnumSet.of(Part.DATABASE, Part.FILTER), "EVENTS"), + + OPEN_TABLES(EnumSet.of(Part.DATABASE, Part.FILTER), "OPEN TABLES"), + + PROCEDURE_STATUS(EnumSet.of(Part.FILTER), "PROCEDURE STATUS"), + + FUNCTION_STATUS(EnumSet.of(Part.FILTER), "FUNCTION STATUS"), + + PROCEDURE_CODE(EnumSet.of(Part.ROUTINE), "PROCEDURE CODE"), + + FUNCTION_CODE(EnumSet.of(Part.ROUTINE), "FUNCTION CODE"), + + CHARACTER_SET(EnumSet.of(Part.FILTER), "CHARACTER SET", "CHARSET", "CHAR SET"), + + COLLATION(EnumSet.of(Part.FILTER), "COLLATION"), + + ENGINES(EnumSet.noneOf(Part.class), "ENGINES", "STORAGE ENGINES"), + + ENGINE(EnumSet.of(Part.ENGINE), "ENGINE"), + + PLUGINS(EnumSet.noneOf(Part.class), "PLUGINS"), + + PRIVILEGES(EnumSet.noneOf(Part.class), "PRIVILEGES"), + + PROFILES(EnumSet.noneOf(Part.class), "PROFILES"), + + PROFILE(EnumSet.of(Part.PROFILE, Part.LIMIT), "PROFILE"), + + GRANTS(EnumSet.of(Part.ACCOUNT), "GRANTS"), + + WARNINGS(EnumSet.of(Part.COUNT, Part.LIMIT), "WARNINGS"), + + ERRORS(EnumSet.of(Part.COUNT, Part.LIMIT), "ERRORS"), + + BINARY_LOGS(EnumSet.noneOf(Part.class), "BINARY LOGS", "MASTER LOGS"), + + BINARY_LOG_STATUS(EnumSet.noneOf(Part.class), "BINARY LOG STATUS", "MASTER STATUS"), + + BINLOG_EVENTS(EnumSet.of(Part.LOG_NAME, Part.POSITION, Part.LIMIT), "BINLOG EVENTS"), + + RELAYLOG_EVENTS(EnumSet.of(Part.LOG_NAME, Part.POSITION, Part.LIMIT, Part.CHANNEL), + "RELAYLOG EVENTS"), + + REPLICAS(EnumSet.noneOf(Part.class), "REPLICAS", "SLAVE HOSTS"), + + REPLICA_STATUS(EnumSet.of(Part.CHANNEL), "REPLICA STATUS", "SLAVE STATUS"); + + private final Set parts; + private final List keywords; + + Kind(Set parts, String... keywords) { + this.parts = Collections.unmodifiableSet(parts); + this.keywords = Collections.unmodifiableList(Arrays.asList(keywords)); + } + + /** Whether statements of this kind may have the part. */ + public boolean allows(Part part) { + return parts.contains(part); + } + + /** The parts statements of this kind may have. */ + public Set getParts() { + return parts; + } + + /** The keyword spellings, the MySQL 8.4 spelling first, single-spaced and upper case. */ + public List getKeywords() { + return keywords; + } + + static String normalize(String keyword) { + return keyword.trim().replaceAll("\\s+", " ").toUpperCase(Locale.ROOT); + } + + /** + * The kind spelled with the keywords, compared case-insensitively. + * + * @throws IllegalArgumentException if no kind is spelled so + */ + public static Kind fromKeyword(String keyword) { + String spelling = normalize(keyword); + for (Kind kind : values()) { + if (kind.keywords.contains(spelling)) { + return kind; + } + } + throw new IllegalArgumentException("Unknown MySQL SHOW keywords: " + keyword); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowDbSelection.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowDbSelection.java new file mode 100644 index 0000000000..e06d803d02 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowDbSelection.java @@ -0,0 +1,107 @@ +/*- + * #%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.show; + +import java.io.Serializable; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; + +/** + * The {@code {FROM | IN} db} clause of a MySQL {@code SHOW} statement, which names a database. + * + *

+ * A selection without a name only keeps the selection mode for a later name; statements do not + * render it. + *

+ */ +public class ShowDbSelection implements Serializable { + + private static final long serialVersionUID = 1L; + + private SelectionMode selectionMode = SelectionMode.FROM; + private String name; + + public ShowDbSelection() { + // empty constructor + } + + /** A {@code FROM name} clause. */ + public ShowDbSelection(String name) { + this.name = name; + } + + public ShowDbSelection(SelectionMode selectionMode, String name) { + setSelectionMode(selectionMode); + this.name = name; + } + + /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getSelectionMode() { + return selectionMode; + } + + /** Sets the keyword before the database name; null means {@code FROM}. */ + public void setSelectionMode(SelectionMode selectionMode) { + this.selectionMode = selectionMode == null ? SelectionMode.FROM : selectionMode; + } + + /** The database name as written, including any quotes. */ + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + /** Whether the selection has a database name and is rendered. */ + public static boolean isPresent(ShowDbSelection selection) { + return selection != null && selection.name != null; + } + + /** The database name of the selection, or null. */ + public static String name(ShowDbSelection selection) { + return selection == null ? null : selection.name; + } + + /** + * The selection after a legacy selection mode setter: the mode is kept until a name is set. + */ + public static ShowDbSelection updateSelectionMode(ShowDbSelection selection, + SelectionMode selectionMode) { + ShowDbSelection result = selection == null ? new ShowDbSelection() : selection; + result.setSelectionMode(selectionMode); + return result; + } + + /** The selection after a legacy database name setter, keeping the selection mode. */ + public static ShowDbSelection updateName(ShowDbSelection selection, String name) { + if (selection == null && name == null) { + return null; + } + ShowDbSelection result = selection == null ? new ShowDbSelection() : selection; + result.setName(name); + return result; + } + + @Override + public String toString() { + return selectionMode + " " + name; + } + + public ShowDbSelection withSelectionMode(SelectionMode selectionMode) { + this.setSelectionMode(selectionMode); + return this; + } + + public ShowDbSelection withName(String name) { + this.setName(name); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowFilter.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowFilter.java new file mode 100644 index 0000000000..b04146cb01 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowFilter.java @@ -0,0 +1,115 @@ +/*- + * #%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.show; + +import java.io.Serializable; +import java.util.function.Consumer; +import net.sf.jsqlparser.expression.Expression; + +/** + * The {@code LIKE 'pattern'} or {@code WHERE expr} filter that ends many MySQL {@code SHOW} + * statements. + * + * @see MySQL extensions to + * SHOW statements + */ +public class ShowFilter implements Serializable { + + private static final long serialVersionUID = 1L; + + public enum Kind { + LIKE, WHERE + } + + private Kind kind = Kind.LIKE; + private Expression expression; + + public ShowFilter() { + // empty constructor + } + + public ShowFilter(Kind kind, Expression expression) { + setKind(kind); + this.expression = expression; + } + + /** A {@code LIKE pattern} filter. */ + public static ShowFilter like(Expression pattern) { + return new ShowFilter(Kind.LIKE, pattern); + } + + /** A {@code WHERE condition} filter. */ + public static ShowFilter where(Expression condition) { + return new ShowFilter(Kind.WHERE, condition); + } + + /** {@code LIKE} or {@code WHERE}; never null. */ + public Kind getKind() { + return kind; + } + + /** Sets the kind; null means {@code LIKE}. */ + public void setKind(Kind kind) { + this.kind = kind == null ? Kind.LIKE : kind; + } + + /** The pattern of a {@code LIKE} filter or the condition of a {@code WHERE} filter. */ + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + this.expression = expression; + } + + /** The pattern of a {@code LIKE} filter, or null. */ + public static Expression likeExpression(ShowFilter filter) { + return filter != null && filter.kind == Kind.LIKE ? filter.expression : null; + } + + /** The condition of a {@code WHERE} filter, or null. */ + public static Expression whereCondition(ShowFilter filter) { + return filter != null && filter.kind == Kind.WHERE ? filter.expression : null; + } + + /** + * The filter after a legacy {@code setLikeExpression} or {@code setWhereCondition} call: an + * expression replaces the filter, and null removes a filter of that kind only. + */ + public static ShowFilter replace(ShowFilter filter, Kind kind, Expression expression) { + if (expression != null) { + return new ShowFilter(kind, expression); + } + return filter != null && filter.kind == kind ? null : filter; + } + + /** Appends the filter, printing its expression with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append(kind).append(' '); + printer.accept(expression); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); + } + + public ShowFilter withKind(Kind kind) { + this.setKind(kind); + return this; + } + + public ShowFilter withExpression(Expression expression) { + this.setExpression(expression); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java index 3c9b10072d..bb9d6e9fb8 100644 --- a/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowIndexStatement.java @@ -9,37 +9,148 @@ */ package net.sf.jsqlparser.statement.show; +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.Statement; import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; /** + * A MySQL {@code SHOW [EXTENDED] {INDEX | INDEXES | KEYS} {FROM | IN} table [{FROM | IN} db] + * [WHERE expr]} statement. + * * @author Jayant Kumar Yadav + * @see MySQL SHOW INDEX */ - public class ShowIndexStatement extends ASTNodeAccessImpl implements Statement { - private String tableName; + /** The spelling of the statement keyword. */ + public enum Keyword { + INDEX, INDEXES, KEYS + } + + private boolean extended; + private Keyword keyword = Keyword.INDEX; + private SelectionMode tableSelectionMode = SelectionMode.FROM; + private Table table; + private ShowDbSelection dbSelection; + private Expression whereCondition; public ShowIndexStatement() { // empty constructor } public ShowIndexStatement(String tableName) { - this.tableName = tableName; + setTableName(tableName); + } + + public ShowIndexStatement(Table table) { + this.table = table; + } + + public boolean isExtended() { + return extended; } + public void setExtended(boolean extended) { + this.extended = extended; + } + + /** {@code INDEX}, {@code INDEXES} or {@code KEYS}, as written; never null. */ + public Keyword getKeyword() { + return keyword; + } + + /** Sets the keyword spelling; null means {@code INDEX}. */ + public void setKeyword(Keyword keyword) { + this.keyword = keyword == null ? Keyword.INDEX : keyword; + } + + /** The keyword before the table name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getTableSelectionMode() { + return tableSelectionMode; + } + + /** Sets the keyword before the table name; null means {@code FROM}. */ + public void setTableSelectionMode(SelectionMode tableSelectionMode) { + this.tableSelectionMode = + tableSelectionMode == null ? SelectionMode.FROM : tableSelectionMode; + } + + public Table getTable() { + return table; + } + + public void setTable(Table table) { + this.table = table; + } + + /** The table name as written, including any qualifier; derived from {@link #getTable()}. */ public String getTableName() { - return tableName; + return table == null ? null : table.getFullyQualifiedName(); } public void setTableName(String tableName) { - this.tableName = tableName; + this.table = tableName == null ? null : new Table(tableName); + } + + /** The trailing {@code {FROM | IN} db} clause, which names the database of the table. */ + public ShowDbSelection getDbSelection() { + return dbSelection; + } + + public void setDbSelection(ShowDbSelection dbSelection) { + this.dbSelection = dbSelection; + } + + /** The database of the trailing {@code {FROM | IN} db} clause, or null. */ + public String getDbName() { + return ShowDbSelection.name(dbSelection); + } + + public Expression getWhereCondition() { + return whereCondition; + } + + public void setWhereCondition(Expression whereCondition) { + this.whereCondition = whereCondition; + } + + /** + * The table the statement reads: {@link #getTable()}, or the table in the database of the + * trailing {@code FROM db} clause, which MySQL reads as {@code db.table}. + */ + public Table getQualifiedTable() { + if (table == null || !ShowDbSelection.isPresent(dbSelection)) { + return table; + } + return new Table(dbSelection.getName(), table.getName()); + } + + /** Appends the statement, printing the {@code WHERE} condition with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("SHOW"); + if (extended) { + builder.append(" EXTENDED"); + } + builder.append(' ').append(keyword).append(' ').append(tableSelectionMode).append(' ') + .append(table); + if (ShowDbSelection.isPresent(dbSelection)) { + builder.append(' ').append(dbSelection); + } + if (whereCondition != null) { + builder.append(" WHERE "); + printer.accept(whereCondition); + } + return builder; } @Override public String toString() { - return "SHOW INDEX FROM " + tableName; + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } @Override @@ -51,4 +162,34 @@ public ShowIndexStatement withTableName(String tableName) { this.setTableName(tableName); return this; } + + public ShowIndexStatement withTable(Table table) { + this.setTable(table); + return this; + } + + public ShowIndexStatement withExtended(boolean extended) { + this.setExtended(extended); + return this; + } + + public ShowIndexStatement withKeyword(Keyword keyword) { + this.setKeyword(keyword); + return this; + } + + public ShowIndexStatement withTableSelectionMode(SelectionMode tableSelectionMode) { + this.setTableSelectionMode(tableSelectionMode); + return this; + } + + public ShowIndexStatement withDbSelection(ShowDbSelection dbSelection) { + this.setDbSelection(dbSelection); + return this; + } + + public ShowIndexStatement withWhereCondition(Expression whereCondition) { + this.setWhereCondition(whereCondition); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java index a5126cf7ca..80688c98d5 100644 --- a/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java @@ -9,6 +9,7 @@ */ package net.sf.jsqlparser.statement.show; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; @@ -23,57 +24,79 @@ */ public class ShowTableStatusStatement extends ASTNodeAccessImpl implements Statement { - private SelectionMode selectionMode = SelectionMode.FROM; - private String dbName; - private Expression likeExpression; - private Expression whereCondition; + private ShowDbSelection dbSelection; + private ShowFilter filter; /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ public SelectionMode getSelectionMode() { - return selectionMode; + return dbSelection == null ? SelectionMode.FROM : dbSelection.getSelectionMode(); } + /** Sets the keyword before the database name, which is kept until the name is set. */ public void setSelectionMode(SelectionMode selectionMode) { - this.selectionMode = selectionMode; + dbSelection = ShowDbSelection.updateSelectionMode(dbSelection, selectionMode); } public String getDbName() { - return dbName; + return ShowDbSelection.name(dbSelection); } public void setDbName(String dbName) { - this.dbName = dbName; + dbSelection = ShowDbSelection.updateName(dbSelection, dbName); + } + + /** The {@code {FROM | IN} db} clause, or null. */ + public ShowDbSelection getDbSelection() { + return dbSelection; + } + + public void setDbSelection(ShowDbSelection dbSelection) { + this.dbSelection = dbSelection; + } + + /** The {@code LIKE} or {@code WHERE} filter, or null. */ + public ShowFilter getFilter() { + return filter; + } + + public void setFilter(ShowFilter filter) { + this.filter = filter; } public Expression getLikeExpression() { - return likeExpression; + return ShowFilter.likeExpression(filter); } + /** Sets a {@code LIKE} filter, which replaces a {@code WHERE} filter; null removes it. */ public void setLikeExpression(Expression likeExpression) { - this.likeExpression = likeExpression; + filter = ShowFilter.replace(filter, ShowFilter.Kind.LIKE, likeExpression); } public Expression getWhereCondition() { - return whereCondition; + return ShowFilter.whereCondition(filter); } + /** Sets a {@code WHERE} filter, which replaces a {@code LIKE} filter; null removes it. */ public void setWhereCondition(Expression whereCondition) { - this.whereCondition = whereCondition; + filter = ShowFilter.replace(filter, ShowFilter.Kind.WHERE, whereCondition); } - @Override - public String toString() { - StringBuilder builder = new StringBuilder("SHOW TABLE STATUS"); - if (dbName != null) { - builder.append(' ').append(selectionMode == null ? SelectionMode.FROM : selectionMode) - .append(' ').append(dbName); + /** Appends the statement, printing the filter expression with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { + builder.append("SHOW TABLE STATUS"); + if (ShowDbSelection.isPresent(dbSelection)) { + builder.append(' ').append(dbSelection); } - if (likeExpression != null) { - builder.append(" LIKE ").append(likeExpression); - } else if (whereCondition != null) { - builder.append(" WHERE ").append(whereCondition); + if (filter != null) { + filter.appendTo(builder.append(' '), printer); } - return builder.toString(); + return builder; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } @Override @@ -100,4 +123,14 @@ public ShowTableStatusStatement withWhereCondition(Expression whereCondition) { this.setWhereCondition(whereCondition); return this; } + + public ShowTableStatusStatement withDbSelection(ShowDbSelection dbSelection) { + this.setDbSelection(dbSelection); + return this; + } + + public ShowTableStatusStatement withFilter(ShowFilter filter) { + this.setFilter(filter); + return this; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java index 7dbb97b479..d368843690 100644 --- a/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowTablesStatement.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement.show; import java.util.Locale; +import java.util.function.Consumer; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; import net.sf.jsqlparser.statement.Statement; @@ -18,17 +19,21 @@ import java.util.EnumSet; /** - * A {@code SHOW TABLES} statement + * A {@code SHOW [EXTENDED] [FULL] TABLES [{FROM | IN} db] [LIKE 'pattern' | WHERE expr]} statement. * - * @see MySQL show tables + *

+ * The database and the filter are held by {@link ShowDbSelection} and {@link ShowFilter}, shared + * with the other MySQL {@code SHOW} statements; the separate selection mode, database name, + * {@code LIKE} and {@code WHERE} accessors read and write them. + *

+ * + * @see MySQL show tables */ public class ShowTablesStatement extends ASTNodeAccessImpl implements Statement { private EnumSet modifiers; - private SelectionMode selectionMode; - private String dbName; - private Expression likeExpression; - private Expression whereCondition; + private ShowDbSelection dbSelection; + private ShowFilter filter; public EnumSet getModifiers() { return modifiers; @@ -38,65 +43,87 @@ public void setModifiers(EnumSet modifiers) { this.modifiers = modifiers; } + /** {@code FROM} or {@code IN}, or null without a database. */ public SelectionMode getSelectionMode() { - return selectionMode; + return dbSelection == null ? null : dbSelection.getSelectionMode(); } + /** Sets the keyword before the database name, which is kept until the name is set. */ public void setSelectionMode(SelectionMode selectionMode) { - this.selectionMode = selectionMode; + if (selectionMode == null && !ShowDbSelection.isPresent(dbSelection)) { + dbSelection = null; + } else { + dbSelection = ShowDbSelection.updateSelectionMode(dbSelection, selectionMode); + } } public String getDbName() { - return dbName; + return ShowDbSelection.name(dbSelection); } public void setDbName(String dbName) { - this.dbName = dbName; + dbSelection = ShowDbSelection.updateName(dbSelection, dbName); + } + + /** The {@code {FROM | IN} db} clause, or null. */ + public ShowDbSelection getDbSelection() { + return dbSelection; + } + + public void setDbSelection(ShowDbSelection dbSelection) { + this.dbSelection = dbSelection; + } + + /** The {@code LIKE} or {@code WHERE} filter, or null. */ + public ShowFilter getFilter() { + return filter; + } + + public void setFilter(ShowFilter filter) { + this.filter = filter; } public Expression getLikeExpression() { - return likeExpression; + return ShowFilter.likeExpression(filter); } + /** Sets a {@code LIKE} filter, which replaces a {@code WHERE} filter; null removes it. */ public void setLikeExpression(Expression likeExpression) { - this.likeExpression = likeExpression; + filter = ShowFilter.replace(filter, ShowFilter.Kind.LIKE, likeExpression); } public Expression getWhereCondition() { - return whereCondition; + return ShowFilter.whereCondition(filter); } + /** Sets a {@code WHERE} filter, which replaces a {@code LIKE} filter; null removes it. */ public void setWhereCondition(Expression whereCondition) { - this.whereCondition = whereCondition; + filter = ShowFilter.replace(filter, ShowFilter.Kind.WHERE, whereCondition); } - @Override - public String toString() { - StringBuilder builder = new StringBuilder(); + /** Appends the statement, printing the filter expression with the given printer. */ + public StringBuilder appendTo(StringBuilder builder, Consumer printer) { builder.append("SHOW"); - - if (modifiers.contains(Modifiers.EXTENDED)) { + if (modifiers != null && modifiers.contains(Modifiers.EXTENDED)) { builder.append(" EXTENDED"); } - if (modifiers.contains(Modifiers.FULL)) { + if (modifiers != null && modifiers.contains(Modifiers.FULL)) { builder.append(" FULL"); } - builder.append(" TABLES"); - - if (dbName != null) { - builder.append(" ").append(selectionMode.name()).append(" ").append(dbName); - } - - if (likeExpression != null) { - builder.append(" ").append("LIKE").append(" ").append(likeExpression); + if (ShowDbSelection.isPresent(dbSelection)) { + builder.append(' ').append(dbSelection); } - - if (whereCondition != null) { - builder.append(" ").append("WHERE").append(" ").append(whereCondition); + if (filter != null) { + filter.appendTo(builder.append(' '), printer); } + return builder; + } - return builder.toString(); + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + return appendTo(builder, builder::append).toString(); } @Override @@ -104,6 +131,25 @@ public T accept(StatementVisitor statementVisitor, S context) { return statementVisitor.visit(this, context); } + public ShowTablesStatement withModifiers(EnumSet modifiers) { + this.setModifiers(modifiers); + return this; + } + + public ShowTablesStatement withDbSelection(ShowDbSelection dbSelection) { + this.setDbSelection(dbSelection); + return this; + } + + public ShowTablesStatement withFilter(ShowFilter filter) { + this.setFilter(filter); + return this; + } + + /** + * The keyword before a database name in the MySQL {@code SHOW} statements, shared by all of + * them. + */ public enum SelectionMode { FROM, IN; diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index f65ddf14f6..58f29c1df2 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -271,6 +271,7 @@ import net.sf.jsqlparser.statement.select.UnPivotQuery; import net.sf.jsqlparser.statement.select.Values; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; @@ -2068,11 +2069,8 @@ public void visit(ResetStatement reset) { @Override public Void visit(ShowColumnsStatement showColumnsStatement, S context) { - Table table = showColumnsStatement.getTable(); - if (table != null && showColumnsStatement.getDbName() != null) { - // MySQL: the trailing FROM db names the database of the table - table = new Table(showColumnsStatement.getDbName(), table.getName()); - } + // MySQL: the trailing FROM db names the database of the table + Table table = showColumnsStatement.getQualifiedTable(); if (table != null) { visit(table, context); } @@ -2086,7 +2084,11 @@ public void visit(ShowColumnsStatement showColumns) { @Override public Void visit(ShowIndexStatement showIndex, S context) { - throwUnsupported(showIndex); + // MySQL: the trailing FROM db names the database of the table + Table table = showIndex.getQualifiedTable(); + if (table != null) { + visit(table, context); + } return null; } @@ -2582,6 +2584,17 @@ public void visit(ShowCreateStatement showCreate) { StatementVisitor.super.visit(showCreate); } + @Override + public Void visit(MySqlShowStatement mySqlShow, S context) { + // reads server settings, state or object lists, but names no table + return null; + } + + @Override + public void visit(MySqlShowStatement mySqlShow) { + StatementVisitor.super.visit(mySqlShow); + } + @Override public Void visit(TSQLLeftJoin tsqlLeftJoin, S context) { visitBinaryExpression(tsqlLeftJoin); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ShowIndexStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ShowIndexStatementDeParser.java index 1d81d5af6d..97fec7f2ba 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ShowIndexStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ShowIndexStatementDeParser.java @@ -23,7 +23,7 @@ public ShowIndexStatementDeParser(StringBuilder buffer) { @Override public void deParse(ShowIndexStatement show) { - builder.append("SHOW INDEX FROM ").append(show.getTableName()); + show.appendTo(builder, builder::append); } } 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 41defbd120..2953583648 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -139,6 +139,7 @@ import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; import net.sf.jsqlparser.statement.select.WithItem; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; @@ -529,26 +530,26 @@ public StringBuilder visit(UseStatement use, S context) { @Override public StringBuilder visit(ShowColumnsStatement show, S context) { - new ShowColumnsStatementDeParser(builder).deParse(show); - return builder; + return show.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); } @Override public StringBuilder visit(ShowIndexStatement showIndexes, S context) { - new ShowIndexStatementDeParser(builder).deParse(showIndexes); - return builder; + return showIndexes.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); } @Override public StringBuilder visit(ShowTablesStatement showTables, S context) { - new ShowTablesStatementDeparser(builder).deParse(showTables); - return builder; + return showTables.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); } @Override public StringBuilder visit(ShowTableStatusStatement showTableStatus, S context) { - builder.append(showTableStatus); - return builder; + return showTableStatus.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); } @Override @@ -557,6 +558,12 @@ public StringBuilder visit(ShowCreateStatement showCreate, S context) { return builder; } + @Override + public StringBuilder visit(MySqlShowStatement mySqlShow, S context) { + return mySqlShow.appendTo(builder, + expression -> expression.accept(expressionDeParser, context)); + } + @Override public StringBuilder visit(Block block, S context) { return block.appendTo(builder, statements -> statements.accept(this, context)); diff --git a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java index 3b70763cfe..6d66436476 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/feature/MariaDbVersion.java @@ -148,7 +148,8 @@ public enum MariaDbVersion implements Version { Feature.showColumns, // https://mariadb.com/kb/en/show-index/ Feature.showIndex, // https://mariadb.com/kb/en/show-table-status/ Feature.showTableStatus, // https://mariadb.com/kb/en/show-create-table/ - Feature.showCreate, // https://mariadb.com/kb/en/use/ + Feature.showCreate, // https://mariadb.com/kb/en/show/ + Feature.mySqlShow, // https://mariadb.com/kb/en/use/ Feature.use, // https://mariadb.com/kb/en/grant/ Feature.grant, // https://mariadb.com/kb/en/revoke/ Feature.revoke, // https://mariadb.com/kb/en/commit/ 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 0b0eaf25e7..674077bf9d 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 @@ -144,7 +144,8 @@ public enum MySqlVersion implements Version { Feature.showColumns, // https://dev.mysql.com/doc/refman/8.0/en/show-index.html Feature.showIndex, // https://dev.mysql.com/doc/refman/8.0/en/show-table-status.html Feature.showTableStatus, // https://dev.mysql.com/doc/refman/8.0/en/show-create-table.html - Feature.showCreate, // https://dev.mysql.com/doc/refman/8.0/en/grant.html + Feature.showCreate, // https://dev.mysql.com/doc/refman/8.4/en/show.html + Feature.mySqlShow, // https://dev.mysql.com/doc/refman/8.0/en/grant.html Feature.grant, // https://dev.mysql.com/doc/refman/8.0/en/revoke.html Feature.revoke, // https://dev.mysql.com/doc/refman/8.0/en/use.html Feature.use, // https://dev.mysql.com/doc/refman/8.0/en/commit.html diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidator.java new file mode 100644 index 0000000000..07b6b8d9a9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidator.java @@ -0,0 +1,31 @@ +/*- + * #%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 net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.statement.show.MySqlShowStatement; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class MySqlShowStatementValidator extends AbstractValidator { + + @Override + public void validate(MySqlShowStatement show) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.mySqlShow); + validateOptionalName(c, NamedObject.database, show.getDbName()); + if (show.getKind() == MySqlShowStatement.Kind.PROCEDURE_CODE + && show.getRoutineName() != null) { + validateName(c, NamedObject.procedure, + show.getRoutineName().getFullyQualifiedName()); + } + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java index 4cb91dbf45..d1c0b7f7e9 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidator.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.util.validation.validator; import net.sf.jsqlparser.parser.feature.Feature; +import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.ShowColumnsStatement; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -20,11 +21,9 @@ public class ShowColumnsStatementValidator extends AbstractValidator Void visit(ShowCreateStatement showCreate, S context) { return null; } + @Override + public Void visit(MySqlShowStatement mySqlShow, S context) { + getValidator(MySqlShowStatementValidator.class).validate(mySqlShow); + return null; + } + @Override public Void visit(Block block, S context) { validateFeature(Feature.block); @@ -1029,6 +1036,11 @@ public void visit(ShowCreateStatement showCreate) { visit(showCreate, null); } + @Override + public void visit(MySqlShowStatement mySqlShow) { + visit(mySqlShow, null); + } + public void visit(Block block) { visit(block, null); } diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index e4fcc2e76e..ad0b9f6b6e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -2549,6 +2549,79 @@ public class CCJSqlParser extends AbstractJSqlParser { return token.kind != EOF && token.image != null && keyword.equalsIgnoreCase(token.image); } + /** Whether the token at the position is an identifier spelled as one of the words. */ + private boolean isAnyWordAt(int position, String... words) { + Token token = getToken(position); + if (token.kind != S_IDENTIFIER) { + return false; + } + for (String word : words) { + if (word.equalsIgnoreCase(token.image)) { + return true; + } + } + return false; + } + + /** The MySQL server SHOW statements are read with Dialect.MYSQL or MARIADB, or without one. */ + private boolean isMySqlShowDialect() { + return getAsString(Feature.dialect) == null || isMySqlDialect(); + } + + /** + * MySQL 8.4 removed SHOW MASTER STATUS, MASTER LOGS, SLAVE STATUS and SLAVE HOSTS, so they are + * read with Dialect.MARIADB or without a dialect only. + */ + private boolean isLegacyMySqlShowDialect() { + return !Dialect.MYSQL.name().equals(getAsString(Feature.dialect)); + } + + /** [GLOBAL | SESSION | LOCAL] VARIABLES or STATUS follows. */ + private boolean isShowVariablesAhead() { + int kind = getToken(1).kind; + return isAnyWordAt(kind == K_GLOBAL || kind == K_SESSION || kind == K_LOCAL ? 2 : 1, + "VARIABLES", "STATUS"); + } + + /** + * A MySQL text string: '...' or "..." without a prefix or introducer. The lexer reads "..." as + * a string with a MySQL dialect and as a quoted identifier without one. + */ + private boolean isMySqlTextStringAt(int position) { + Token token = getToken(position); + char quote = token.image == null || token.image.isEmpty() ? ' ' : token.image.charAt(0); + return token.kind == S_CHAR_LITERAL && (quote == '\'' || quote == '"') + || token.kind == S_QUOTED_IDENTIFIER && quote == '"'; + } + + /** + * Whether the lookahead of a SHOW statement has scanned the WHERE of its filter: a WHERE lies + * between the last consumed token and the scan position. + */ + private boolean isShowWhereScanned() { + Token scanned = getToken(1); + for (Token t = token.next; t != null && t != scanned; t = t.next) { + if (t.kind == K_WHERE) { + return true; + } + } + return false; + } + + /** COUNT(*) with the parenthesis right after COUNT, as the MySQL lexer requires. */ + private boolean isShowCountAhead() { + Token count = getToken(1); + Token parenthesis = getToken(2); + return count.kind == K_COUNT && "(".equals(parenthesis.image) + && parenthesis.beginLine == count.endLine + && parenthesis.beginColumn == count.endColumn + 1; + } + + /** FOR CHANNEL 'channel' follows. */ + private boolean isShowChannelAhead() { + return getToken(1).kind == K_FOR && isAnyWordAt(2, "CHANNEL") && isMySqlTextStringAt(3); + } + /** * Whether a MySQL CHECK, CHECKSUM, OPTIMIZE or REPAIR TABLE statement starts here. TABLE or * TABLES must follow, so other uses of these words are unaffected. @@ -8174,15 +8247,16 @@ Statement Show(): Statement statement; List captureRest; String name; + Token first; } { - + { first = getToken(1); } ( // A typed form must span the whole statement; other dialects' trailing clauses // (e.g. Snowflake IN SCHEMA s, ClickHouse FORMAT f) still fall back to captureRest(). LOOKAHEAD(ShowColumns() ShowStatementEnd()) statement = ShowColumns() | - LOOKAHEAD(2) statement = ShowIndex() + LOOKAHEAD(ShowIndex() ShowStatementEnd()) statement = ShowIndex() | LOOKAHEAD(2) statement = ShowTables() | @@ -8197,6 +8271,15 @@ Statement Show(): { isConfigurationParameterPhraseAhead() && isPostgreSqlUtilityDialect() }) name = ConfigurationParameterName() { statement = new ShowStatement(name); } | + LOOKAHEAD(MySqlShow() ShowStatementEnd(), { isMySqlShowDialect() }) + statement = MySqlShow() + { + if (getAsString(Feature.dialect) == null && token == first) { + // without a dialect, a single word stays a ShowStatement as before + statement = new ShowStatement(first.image); + } + } + | // any of the RDBMS specific SHOW syntax captureRest = captureRest() { @@ -8215,7 +8298,8 @@ Statement Show(): /** * Only used inside the SHOW lookaheads: a typed SHOW statement ends at a semicolon, the input end, * the ELSE or END of an enclosing IF or block, or the start of the next statement, as in - * Statements(). + * Statements(). A lookahead that has entered a WHERE filter ends there too: it sees the first + * condition only, because prattExpressionRest() reads the rest of the expression. */ void ShowStatementEnd(): {} @@ -8228,16 +8312,142 @@ void ShowStatementEnd(): | | - LOOKAHEAD({ getToken(1).kind != K_FROM && getToken(1).kind != K_PIVOT + LOOKAHEAD({ isShowWhereScanned() || getToken(1).kind != K_FROM && getToken(1).kind != K_PIVOT && getToken(1).kind != K_UNPIVOT && isStatementStartAhead() }) {} } +/** The {FROM | IN} db clause of the MySQL SHOW statements. */ +ShowDbSelection ShowDbSelection(): +{ + ShowDbSelection selection = new ShowDbSelection(); + String name; +} +{ + ( | { selection.setSelectionMode(ShowTablesStatement.SelectionMode.IN); } ) + name = RelObjectName() { selection.setName(name); } + { + return selection; + } +} + +/** + * The LIKE 'pattern' or WHERE expr filter of the MySQL SHOW statements. MySQL and MariaDB take a + * string literal as the pattern; without a dialect any simple expression is read, as before. + */ +ShowFilter ShowFilter(): +{ + ShowFilter filter; + Expression expression; +} +{ + ( + + ( + LOOKAHEAD({ isMySqlDialect() }) expression = ShowTextString() + | + LOOKAHEAD({ !isMySqlDialect() }) expression = SimpleExpression() + ) + { filter = ShowFilter.like(expression); } + | + expression = Expression() { filter = ShowFilter.where(expression); } + ) + { + return filter; + } +} + +/** A MySQL text string, '...' or "...", where the server takes no other expression. */ +StringValue ShowTextString(): +{ + Token token; + StringValue value; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == S_CHAR_LITERAL && isMySqlTextStringAt(1) }) + token = { value = stringValue(token); } + | + LOOKAHEAD({ getToken(1).kind == S_QUOTED_IDENTIFIER && isMySqlTextStringAt(1) }) + token = + { + value = linkToLastToken(new StringValue() + .withValue(token.image.substring(1, token.image.length() - 1)).setQuoteStr("\"")); + } + ) + { + return value; + } +} + +/** + * LIMIT [offset,] row_count or LIMIT row_count OFFSET offset of SHOW WARNINGS, ERRORS, PROFILE and + * BINLOG or RELAYLOG EVENTS. + */ +void ShowLimit(MySqlShowStatement statement): +{ + Limit limit = new Limit(); + Offset offset = null; + Token first; + Token last; + Expression rowCount; + Expression second; +} +{ + { first = getToken(0); } + rowCount = ShowLimitOption() { limit.setRowCount(rowCount); last = getToken(0); } + [ + "," second = ShowLimitOption() + { limit.setOffset(rowCount); limit.setRowCount(second); last = getToken(0); } + | + second = ShowLimitOption() { offset = new Offset().withOffset(second); } + ] + { + linkAST(limit, first, last); + statement.setLimit(limit); + statement.setOffset(offset); + } +} + +/** A limit value: an unsigned integer, a name (a stored program variable) or a ? parameter. */ +Expression ShowLimitOption(): +{ + Token token = getToken(1); + String name; + JdbcParameter parameter; + Expression expression; +} +{ + ( + token = { expression = linkToLastToken(new LongValue(token.image)); } + | + parameter = JdbcParameter() { expression = linked(parameter, token); } + | + LOOKAHEAD({ !isMySqlTextStringAt(1) }) name = RelObjectName() + { expression = linkToLastToken(new Column(name)); } + ) + { + return expression; + } +} + +/** FOR CHANNEL 'channel', checked by the caller's lookahead. */ +StringValue ShowChannel(): +{ + StringValue channel; +} +{ + channel = ShowTextString() + { + return channel; + } +} + // https://dev.mysql.com/doc/refman/8.4/en/show-columns.html ShowColumnsStatement ShowColumns(): { ShowColumnsStatement statement = new ShowColumnsStatement(); Table table; - String dbName; - Expression expression; + ShowDbSelection dbSelection; + ShowFilter filter; } { [ { statement.setExtended(true); } ] @@ -8249,20 +8459,8 @@ ShowColumnsStatement ShowColumns(): { { statement.setTableSelectionMode(ShowTablesStatement.SelectionMode.IN); } ) table = Table() { statement.setTable(table); } - [ - LOOKAHEAD(2) - ( - - | - { statement.setDbSelectionMode(ShowTablesStatement.SelectionMode.IN); } - ) - dbName = RelObjectName() { statement.setDbName(dbName); } - ] - [ - expression = SimpleExpression() { statement.setLikeExpression(expression); } - | - expression = Expression() { statement.setWhereCondition(expression); } - ] + [ LOOKAHEAD(2) dbSelection = ShowDbSelection() { statement.setDbSelection(dbSelection); } ] + [ filter = ShowFilter() { statement.setFilter(filter); } ] { return statement; } @@ -8271,25 +8469,13 @@ ShowColumnsStatement ShowColumns(): { // https://dev.mysql.com/doc/refman/8.4/en/show-table-status.html ShowTableStatusStatement ShowTableStatus(): { ShowTableStatusStatement statement = new ShowTableStatusStatement(); - String dbName; - Expression expression; + ShowDbSelection dbSelection; + ShowFilter filter; } { ContextualKeyword("STATUS") - [ - LOOKAHEAD(2) - ( - - | - { statement.setSelectionMode(ShowTablesStatement.SelectionMode.IN); } - ) - dbName = RelObjectName() { statement.setDbName(dbName); } - ] - [ - expression = SimpleExpression() { statement.setLikeExpression(expression); } - | - expression = Expression() { statement.setWhereCondition(expression); } - ] + [ LOOKAHEAD(2) dbSelection = ShowDbSelection() { statement.setDbSelection(dbSelection); } ] + [ filter = ShowFilter() { statement.setFilter(filter); } ] { return statement; } @@ -8344,16 +8530,312 @@ ShowCreateStatement ShowCreate(): { } } +// https://dev.mysql.com/doc/refman/8.4/en/show-index.html ShowIndexStatement ShowIndex(): { - String tableName; + ShowIndexStatement statement = new ShowIndexStatement(); + Table table; + ShowDbSelection dbSelection; + Expression condition; +} +{ + [ { statement.setExtended(true); } ] + ( + + | + { statement.setKeyword(ShowIndexStatement.Keyword.KEYS); } + | + LOOKAHEAD({ isAnyWordAt(1, "INDEXES") }) + { statement.setKeyword(ShowIndexStatement.Keyword.INDEXES); } + ) + ( + + | + { statement.setTableSelectionMode(ShowTablesStatement.SelectionMode.IN); } + ) + table = Table() { statement.setTable(table); } + [ LOOKAHEAD(2) dbSelection = ShowDbSelection() { statement.setDbSelection(dbSelection); } ] + [ condition = Expression() { statement.setWhereCondition(condition); } ] + { + return statement; + } +} + +/** + * The MySQL SHOW statements of MySqlShowStatement. Each alternative is chosen by its leading + * keywords, so a SHOW statement of another form fails the caller's lookahead at once. + */ +MySqlShowStatement MySqlShow(): +{ + MySqlShowStatement statement = new MySqlShowStatement(); + MySqlShowStatement.Scope scope = null; + boolean full = false; + Token token; + StringValue channel; + ShowDbSelection dbSelection; + ShowFilter filter; +} +{ + ( + // [GLOBAL | SESSION | LOCAL] {VARIABLES | STATUS} [LIKE 'pattern' | WHERE expr] + LOOKAHEAD({ isShowVariablesAhead() }) + [ + { scope = MySqlShowStatement.Scope.GLOBAL; } + | + { scope = MySqlShowStatement.Scope.SESSION; } + | + { scope = MySqlShowStatement.Scope.LOCAL; } + ] + token = { statement.setKeyword(token.image); statement.setScope(scope); } + [ filter = ShowFilter() { statement.setFilter(filter); } ] + | + // [FULL] PROCESSLIST + LOOKAHEAD({ isAnyWordAt(getToken(1).kind == K_FULL ? 2 : 1, "PROCESSLIST") }) + [ { full = true; } ] + { statement.setKeyword("PROCESSLIST"); statement.setFull(full); } + | + // {DATABASES | SCHEMAS | CHARSET | COLLATION} [LIKE 'pattern' | WHERE expr] + LOOKAHEAD({ isAnyWordAt(1, "DATABASES", "SCHEMAS", "CHARSET", "COLLATION") }) + token = { statement.setKeyword(token.image); } + [ filter = ShowFilter() { statement.setFilter(filter); } ] + | + // {CHARACTER | CHAR} SET [LIKE 'pattern' | WHERE expr] + LOOKAHEAD({ (getToken(1).kind == K_CHARACTER || "CHAR".equalsIgnoreCase(getToken(1).image)) + && getToken(2).kind == K_SET }) + ( + { statement.setKeyword("CHARACTER SET"); } + | + ( | ) { statement.setKeyword("CHAR SET"); } + ) + + [ filter = ShowFilter() { statement.setFilter(filter); } ] + | + // {PROCEDURE | FUNCTION} STATUS [LIKE 'pattern' | WHERE expr] and {PROCEDURE | FUNCTION} CODE name + LOOKAHEAD({ (getToken(1).kind == K_PROCEDURE || getToken(1).kind == K_FUNCTION) + && isAnyWordAt(2, "STATUS", "CODE") }) + ( token = | token = ) + ( + LOOKAHEAD({ isAnyWordAt(1, "STATUS") }) + { statement.setKeyword(token.image + " STATUS"); } + [ filter = ShowFilter() { statement.setFilter(filter); } ] + | + { statement.setKeyword(token.image + " CODE"); } + ShowRoutineName(statement) + ) + | + // {[FULL] TRIGGERS | EVENTS | OPEN TABLES} [{FROM | IN} db] [LIKE 'pattern' | WHERE expr] + LOOKAHEAD({ isAnyWordAt(1, "TRIGGERS", "EVENTS") + || getToken(1).kind == K_FULL && isAnyWordAt(2, "TRIGGERS") + || getToken(1).kind == K_OPEN && getToken(2).kind == K_TABLES }) + ( + { statement.setKeyword("OPEN TABLES"); } + | + [ { full = true; } ] + token = { statement.setKeyword(token.image); statement.setFull(full); } + ) + [ LOOKAHEAD(2) dbSelection = ShowDbSelection() { statement.setDbSelection(dbSelection); } ] + [ filter = ShowFilter() { statement.setFilter(filter); } ] + | + // ENGINES, PLUGINS, PRIVILEGES, PROFILES, REPLICAS + LOOKAHEAD({ isAnyWordAt(1, "ENGINES", "PLUGINS", "PRIVILEGES", "PROFILES", "REPLICAS") }) + token = { statement.setKeyword(token.image); } + | + // STORAGE ENGINES + LOOKAHEAD({ getToken(1).kind == K_STORAGE && isAnyWordAt(2, "ENGINES") }) + { statement.setKeyword("STORAGE ENGINES"); } + | + // BINARY LOGS and BINARY LOG STATUS + LOOKAHEAD({ getToken(1).kind == K_BINARY && (isAnyWordAt(2, "LOGS") + || getToken(2).kind == K_LOG && isAnyWordAt(3, "STATUS")) }) + + ( + { statement.setKeyword("BINARY LOG STATUS"); } + | + { statement.setKeyword("BINARY LOGS"); } + ) + | + // MASTER LOGS and MASTER STATUS, the spellings before MySQL 8.4 + LOOKAHEAD({ isLegacyMySqlShowDialect() && isAnyWordAt(1, "MASTER") + && isAnyWordAt(2, "LOGS", "STATUS") }) + token = { statement.setKeyword("MASTER " + token.image); } + | + // SLAVE HOSTS, the spelling of REPLICAS before MySQL 8.4 + LOOKAHEAD({ isLegacyMySqlShowDialect() && getToken(1).kind == K_SLAVE && isAnyWordAt(2, "HOSTS") }) + { statement.setKeyword("SLAVE HOSTS"); } + | + // REPLICA STATUS [FOR CHANNEL 'channel']; SLAVE STATUS is the spelling before MySQL 8.4 + LOOKAHEAD({ (isLegacyMySqlShowDialect() && getToken(1).kind == K_SLAVE || isAnyWordAt(1, "REPLICA")) + && isAnyWordAt(2, "STATUS") }) + ( token = | token = ) + { statement.setKeyword(token.image + " STATUS"); } + [ LOOKAHEAD({ isShowChannelAhead() }) channel = ShowChannel() { statement.setChannel(channel); } ] + | + // GRANTS [FOR account [USING role [, role] ...]] + LOOKAHEAD({ isAnyWordAt(1, "GRANTS") }) + { statement.setKeyword("GRANTS"); } + [ ShowGrantsAccount(statement) ] + | + // {WARNINGS | ERRORS} [LIMIT ...] + LOOKAHEAD({ getToken(1).kind == K_ERRORS || isAnyWordAt(1, "WARNINGS") }) + ( { statement.setKeyword("ERRORS"); } | { statement.setKeyword("WARNINGS"); } ) + [ ShowLimit(statement) ] + | + // COUNT(*) {WARNINGS | ERRORS} + LOOKAHEAD({ isShowCountAhead() }) + "(" "*" ")" + ( + { statement.setKeyword("ERRORS"); } + | + LOOKAHEAD({ isAnyWordAt(1, "WARNINGS") }) { statement.setKeyword("WARNINGS"); } + ) + { statement.setCount(true); } + | + // {BINLOG | RELAYLOG} EVENTS [IN 'log_name'] [FROM pos] [LIMIT ...] [FOR CHANNEL 'channel'], + // the channel only for RELAYLOG + LOOKAHEAD({ isAnyWordAt(1, "BINLOG", "RELAYLOG") && isAnyWordAt(2, "EVENTS") }) + ( + LOOKAHEAD({ isAnyWordAt(1, "BINLOG") }) + { statement.setKeyword("BINLOG EVENTS"); } + ShowLogRange(statement) + | + { statement.setKeyword("RELAYLOG EVENTS"); } + ShowLogRange(statement) + [ LOOKAHEAD({ isShowChannelAhead() }) channel = ShowChannel() { statement.setChannel(channel); } ] + ) + | + // ENGINE {engine_name | ALL} {STATUS | MUTEX | LOGS} + LOOKAHEAD({ getToken(1).kind == K_ENGINE }) + { statement.setKeyword("ENGINE"); } + ShowEngine(statement) + | + // PROFILE [type [, type] ...] [FOR QUERY n] [LIMIT ...] + LOOKAHEAD({ isAnyWordAt(1, "PROFILE") }) + { statement.setKeyword("PROFILE"); } + ShowProfile(statement) + ) + { + return statement; + } +} + +/** The routine of SHOW {PROCEDURE | FUNCTION} CODE: name or db.name. */ +void ShowRoutineName(MySqlShowStatement statement): +{ + Token first = getToken(1); + String name; + String routine = null; + Table table; } { - tableName = RelObjectName() + name = RelObjectName() [ "." routine = RelObjectName() ] { - return new ShowIndexStatement(tableName); + table = routine == null ? new Table(name) : new Table(name, routine); + linkAST(table, first); + statement.setRoutineName(table); } } +/** {engine_name | ALL} {STATUS | MUTEX | LOGS} of SHOW ENGINE; the engine is a name or a string. */ +void ShowEngine(MySqlShowStatement statement): +{ + Token token; + String name; +} +{ + ( + LOOKAHEAD({ getToken(1).kind == S_CHAR_LITERAL && isMySqlTextStringAt(1) }) + token = { name = token.image; } + | + // RelObjectName() also reads ALL, which shows every engine + LOOKAHEAD({ getToken(1).kind != S_CHAR_LITERAL }) name = RelObjectName() + ) + { statement.setEngineName(name); } + ( + LOOKAHEAD({ isAnyWordAt(1, "STATUS") }) + { statement.setEngineInfo(MySqlShowStatement.EngineInfo.STATUS); } + | + LOOKAHEAD({ isAnyWordAt(1, "MUTEX") }) + { statement.setEngineInfo(MySqlShowStatement.EngineInfo.MUTEX); } + | + LOOKAHEAD({ isAnyWordAt(1, "LOGS") }) + { statement.setEngineInfo(MySqlShowStatement.EngineInfo.LOGS); } + ) +} + +/** [type [, type] ...] [FOR QUERY n] [LIMIT ...] of SHOW PROFILE. */ +void ShowProfile(MySqlShowStatement statement): +{ + List types = new ArrayList(); + MySqlShowStatement.ProfileType type; + Token token; +} +{ + [ + type = ShowProfileType() { types.add(type); } + ( "," type = ShowProfileType() { types.add(type); } )* + { statement.setProfileTypes(types); } + ] + [ + LOOKAHEAD(2) token = + { statement.setProfileQuery(linkToLastToken(new LongValue(token.image))); } + ] + [ ShowLimit(statement) ] +} + +MySqlShowStatement.ProfileType ShowProfileType(): +{ + Token token; + Token second = null; +} +{ + ( + token = + | + LOOKAHEAD({ getToken(1).kind == K_BLOCK && isAnyWordAt(2, "IO") }) + token = second = + | + LOOKAHEAD({ isAnyWordAt(1, "CONTEXT") && isAnyWordAt(2, "SWITCHES") + || isAnyWordAt(1, "PAGE") && isAnyWordAt(2, "FAULTS") }) + token = second = + | + LOOKAHEAD({ isAnyWordAt(1, "CPU", "IPC", "MEMORY", "SOURCE", "SWAPS") }) + token = + ) + { + return MySqlShowStatement.ProfileType + .fromKeyword(second == null ? token.image : token.image + " " + second.image); + } +} + +/** FOR account [USING role [, role] ...] of SHOW GRANTS, read like the accounts of CREATE USER. */ +void ShowGrantsAccount(MySqlShowStatement statement): +{ + UserAccount account; + List roles; +} +{ + account = MySqlAccountName() { statement.setAccount(account); } + [ roles = MySqlAccountNameList() { statement.setUsingRoles(roles); } ] +} + +/** [IN 'log_name'] [FROM pos] [LIMIT ...] of SHOW BINLOG and RELAYLOG EVENTS. */ +void ShowLogRange(MySqlShowStatement statement): +{ + Token token; + StringValue logName; +} +{ + [ logName = ShowTextString() { statement.setLogName(logName); } ] + [ + LOOKAHEAD(2) + ( + token = { statement.setPosition(linkToLastToken(new LongValue(token.image))); } + | + token = { statement.setPosition(linkToLastToken(new DoubleValue(token.image))); } + ) + ] + [ ShowLimit(statement) ] +} + Statement RefreshMaterializedView(): { Table view = null; boolean concurrently = false; @@ -8380,37 +8862,22 @@ Statement RefreshMaterializedView(): { } } } -// https://dev.mysql.com/doc/refman/8.0/en/show-tables.html +// https://dev.mysql.com/doc/refman/8.4/en/show-tables.html ShowTablesStatement ShowTables(): { - ShowTablesStatement showTablesStatement; - EnumSet modifiers = EnumSet.noneOf(ShowTablesStatement.Modifiers.class); - ShowTablesStatement.SelectionMode selectionMode = null; - String dbName = null; - Expression likeExpression = null; - Expression whereCondition = null; + ShowTablesStatement statement = new ShowTablesStatement(); + EnumSet modifiers = EnumSet.noneOf(ShowTablesStatement.Modifiers.class); + ShowDbSelection dbSelection; + ShowFilter filter; } { - [ { modifiers.add(ShowTablesStatement.Modifiers.EXTENDED); } ] - [ { modifiers.add(ShowTablesStatement.Modifiers.FULL); } ] - - [ - LOOKAHEAD(2) ( - { selectionMode = ShowTablesStatement.SelectionMode.FROM; } - | - { selectionMode = ShowTablesStatement.SelectionMode.IN; } - ) - dbName = RelObjectName() - ] - [ ( likeExpression = SimpleExpression() | whereCondition = Expression()) ] - { - showTablesStatement = new ShowTablesStatement(); - showTablesStatement.setModifiers(modifiers); - showTablesStatement.setSelectionMode(selectionMode); - showTablesStatement.setDbName(dbName); - showTablesStatement.setLikeExpression(likeExpression); - showTablesStatement.setWhereCondition(whereCondition); - return showTablesStatement; - } + [ { modifiers.add(ShowTablesStatement.Modifiers.EXTENDED); } ] + [ { modifiers.add(ShowTablesStatement.Modifiers.FULL); } ] + { statement.setModifiers(modifiers); } + [ LOOKAHEAD(2) dbSelection = ShowDbSelection() { statement.setDbSelection(dbSelection); } ] + [ filter = ShowFilter() { statement.setFilter(filter); } ] + { + return statement; + } } Values Values(): { diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 0097242f85..45148bb53b 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -177,7 +177,7 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad * - **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`` + - ``SHOW [FULL] COLUMNS``/``FIELDS`` · ``SHOW INDEX``/``INDEXES``/``KEYS`` · ``SHOW TABLE STATUS`` · ``SHOW CREATE TABLE``/``VIEW``/``PROCEDURE``/``FUNCTION``/``TRIGGER``/``EVENT``/``DATABASE``/``USER`` · ``SHOW [GLOBAL | SESSION] VARIABLES``/``STATUS`` · ``SHOW [FULL] PROCESSLIST`` · ``SHOW GRANTS [FOR …]`` · ``SHOW WARNINGS``/``ERRORS`` · ``SHOW BINARY LOGS``/``BINLOG EVENTS``/``RELAYLOG EVENTS`` · ``SHOW ENGINE …`` · ``SHOW PROFILE`` · ``SHOW DATABASES``/``TRIGGERS``/``EVENTS``/``OPEN TABLES``/``CHARACTER SET``/``COLLATION`` · ``SHOW PROCEDURE``/``FUNCTION STATUS``/``CODE`` · ``SHOW REPLICA STATUS``/``REPLICAS`` * - **PostgreSQL administration** - ``CREATE``/``ALTER EVENT TRIGGER`` · ``REINDEX`` · ``SECURITY LABEL`` * - **Salesforce SOQL** diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 9d89430009..3838bc2eed 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -3017,15 +3017,22 @@ MySQL SHOW statements The MySQL statements that read table, column and object definitions parse to typed statements in every dialect: +* ``SHOW [EXTENDED] [FULL] TABLES [{FROM | IN} db] [LIKE 'p' | WHERE expr]`` parses to + ``ShowTablesStatement``, as before. * ``SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} t [{FROM | IN} db] [LIKE 'p' | WHERE expr]`` extends ``ShowColumnsStatement``. ``getTable()`` returns the ``Table``; ``getTableName()`` and ``setTableName(String)`` remain and are derived from it. ``isExtended()``, ``isFull()`` and ``isFieldsKeyword()`` record the modifiers and the - ``FIELDS`` spelling, ``getTableSelectionMode()`` and ``getDbSelectionMode()`` the - ``FROM``/``IN`` spelling, and ``getDbName()``, ``getLikeExpression()`` and - ``getWhereCondition()`` the optional clauses. + ``FIELDS`` spelling, and ``getTableSelectionMode()`` the ``FROM``/``IN`` spelling before + the table. +* ``SHOW [EXTENDED] {INDEX | INDEXES | KEYS} {FROM | IN} t [{FROM | IN} db] [WHERE expr]`` + extends ``ShowIndexStatement`` in the same way: ``getTable()``, ``isExtended()``, + ``getKeyword()`` (``INDEX``, ``INDEXES`` or ``KEYS``), ``getTableSelectionMode()``, + ``getDbSelection()`` and ``getWhereCondition()``. The ``String`` constructor, + ``getTableName()`` and ``setTableName(String)`` remain. ``SHOW INDEX FROM t FROM db`` + used to fail. * ``SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'p' | WHERE expr]`` parses to - ``ShowTableStatusStatement``, with the same accessors as ``ShowTablesStatement``. + ``ShowTableStatusStatement``. * ``SHOW CREATE`` parses to ``ShowCreateStatement``. ``getObjectType()`` is ``TABLE``, ``VIEW``, ``PROCEDURE``, ``FUNCTION``, ``TRIGGER``, ``EVENT``, ``DATABASE`` or ``USER``. The first six name the object with ``getName()``, a ``Table``. ``DATABASE`` uses @@ -3033,6 +3040,49 @@ statements in every dialect: ``getAccount()``, a ``UserAccount`` without authentication; ``isCurrentUser()`` and ``isCurrentUserParentheses()`` describe ``CURRENT_USER`` and ``CURRENT_USER()``. +The ``{FROM | IN} db`` clause is a ``ShowDbSelection`` (``getSelectionMode()`` and +``getName()``) and the ``LIKE 'p' | WHERE expr`` filter a ``ShowFilter`` (``getKind()`` and +``getExpression()``). ``ShowTablesStatement``, ``ShowColumnsStatement``, +``ShowTableStatusStatement`` and ``MySqlShowStatement`` return them from +``getDbSelection()`` and ``getFilter()``; ``ShowIndexStatement`` has the database clause +only. The separate accessors remain and read and write these values: +``getSelectionMode()``/``getDbSelectionMode()``, ``getDbName()``, ``getLikeExpression()`` +and ``getWhereCondition()``. A selection mode set before the database name is kept for it. +A statement holds one filter, so ``setWhereCondition`` replaces a ``LIKE`` filter and +``setLikeExpression`` a ``WHERE`` filter; ``ShowTablesStatement`` used to keep and render +both. ``ShowTablesStatement.toString()`` no longer fails without modifiers or without a +selection mode. + +The other `MySQL 8.4 SHOW statements `_ +parse to ``MySqlShowStatement``. ``getKind()`` tells which one it is and ``getKeyword()`` +how its keywords were spelled: + +* ``VARIABLES`` and ``STATUS`` take a scope and a filter; +* ``PROCESSLIST`` takes ``FULL``; +* ``DATABASES`` (also ``SCHEMAS``), ``PROCEDURE_STATUS``, ``FUNCTION_STATUS``, + ``CHARACTER_SET`` (also ``CHARSET`` and ``CHAR SET``) and ``COLLATION`` take a filter; +* ``TRIGGERS`` (with ``FULL``), ``EVENTS`` and ``OPEN_TABLES`` take a database and a filter; +* ``PROCEDURE_CODE`` and ``FUNCTION_CODE`` take the routine name, and ``ENGINE`` the engine + name and ``STATUS``, ``MUTEX`` or ``LOGS``; +* ``GRANTS`` takes ``FOR account [USING role, ...]``; +* ``WARNINGS`` and ``ERRORS`` take ``COUNT(*)`` or a limit; +* ``BINLOG_EVENTS`` takes ``IN 'log'``, ``FROM pos`` and a limit, and ``RELAYLOG_EVENTS`` + also ``FOR CHANNEL 'c'``; +* ``REPLICA_STATUS`` takes ``FOR CHANNEL 'c'``; +* ``PROFILE`` takes types such as ``CPU`` or ``BLOCK IO``, ``FOR QUERY n`` and a limit; +* ``ENGINES`` (also ``STORAGE ENGINES``), ``PLUGINS``, ``PRIVILEGES``, ``PROFILES``, + ``BINARY_LOGS``, ``BINARY_LOG_STATUS`` and ``REPLICAS`` take nothing. + +The parts have typed accessors: ``getScope()`` (``GLOBAL``, ``SESSION`` or ``LOCAL``), +``isFull()``, ``isCount()``, ``getRoutineName()`` (a ``Table``), ``getEngineName()`` and +``getEngineInfo()``, ``getDbSelection()``, ``getFilter()``, ``getAccount()`` and +``getUsingRoles()`` (``UserAccount`` values, read like the accounts of ``CREATE USER``), +``getLogName()``, ``getPosition()``, ``getLimit()`` (``LIMIT [offset,] n``) with +``getOffset()`` (``LIMIT n OFFSET offset``), ``getChannel()``, ``getProfileTypes()`` and +``getProfileQuery()``. ``Kind.allows(Part)`` tells which parts a kind takes; setting +another part throws ``IllegalArgumentException``, and ``setKind`` drops the parts the new +kind does not take. ``setKeyword("SCHEMAS")`` selects the kind together with its spelling. + .. code-block:: java ShowColumnsStatement columns = (ShowColumnsStatement) CCJSqlParserUtil.parse( @@ -3044,13 +3094,44 @@ statements in every dialect: "SHOW CREATE USER 'app'@'%'"); create.getAccount().getHost().getNotExcapedValue(); // % -A typed statement is used only when it covers the whole statement. Other dialects' -``SHOW`` forms, such as ``SHOW COLUMNS IN SCHEMA s`` or ``SHOW CREATE TABLE t FORMAT f``, -remain ``UnsupportedStatement``. + MySqlShowStatement grants = (MySqlShowStatement) CCJSqlParserUtil.parse( + "SHOW GRANTS FOR 'app'@'%' USING 'reader'"); + grants.getKind(); // GRANTS + grants.getUsingRoles().size(); // 1 -Table visitors report the table of ``SHOW COLUMNS`` (qualified by the ``FROM db`` clause) -and the table or view of ``SHOW CREATE TABLE`` and ``SHOW CREATE VIEW``; until now -``SHOW COLUMNS`` raised ``UnsupportedOperationException``. ``SHOW TABLE STATUS`` names no -table. Validators report the ``showColumns``, ``showTableStatus`` and ``showCreate`` -features, which the MySQL and MariaDB capabilities enable. Feature analysis reports -``RETURNS_RESULT_SET``. + MySqlShowStatement events = (MySqlShowStatement) CCJSqlParserUtil.parse( + "SHOW BINLOG EVENTS IN 'binlog.000001' FROM 4 LIMIT 10", + parser -> parser.withDialect(Dialect.MYSQL)); + events.getLogName().getValue(); // binlog.000001 + events.getLimit().getRowCount(); // 10 + +``MySqlShowStatement`` is read with ``Dialect.MYSQL`` and ``Dialect.MARIADB`` and without +a dialect; other dialects keep ``UnsupportedStatement``. Without a dialect, a single word +such as ``SHOW VARIABLES`` or ``SHOW GRANTS`` remains a ``ShowStatement``, as in every +dialect but MySQL and MariaDB. The spellings MySQL 8.4 removed, ``SHOW MASTER STATUS``, +``SHOW MASTER LOGS``, ``SHOW SLAVE STATUS`` and ``SHOW SLAVE HOSTS``, are read with +``Dialect.MARIADB`` and without a dialect. + +With ``Dialect.MYSQL`` or ``Dialect.MARIADB`` the ``LIKE`` pattern of every ``SHOW`` +statement is a string literal, as in the server, and parses to a ``StringValue``; +without a dialect it is any simple expression, as before. Limit values are unsigned +integers, names or ``?`` parameters, log names and channels are strings, and a log +position is an unsigned number. + +A typed statement is used only when it covers the whole statement. Other forms, such as +Snowflake's ``SHOW COLUMNS IN SCHEMA s``, ClickHouse's ``SHOW CREATE TABLE t FORMAT f`` or +``SHOW WARNINGS LIKE 'a'``, remain ``UnsupportedStatement``; ``SHOW INDEX`` forms with +such trailing clauses used to fail. A ``WHERE`` filter is read to its end, so +``SHOW COLUMNS FROM t WHERE a = 1 AND b = 2`` no longer becomes an +``UnsupportedStatement``, and text the expression does not take is a parse error. + +``StatementDeParser`` prints the expressions of the filters, limits and log positions with +its ``ExpressionDeParser``. Table visitors report the table of ``SHOW COLUMNS`` and +``SHOW INDEX`` (qualified by the ``FROM db`` clause) and the table or view of +``SHOW CREATE TABLE`` and ``SHOW CREATE VIEW``; until now ``SHOW INDEX`` raised +``UnsupportedOperationException``, as ``SHOW TABLES`` still does. ``SHOW TABLE STATUS`` +and ``MySqlShowStatement`` name no table. Validators report the ``showColumns``, +``showIndex``, ``showTableStatus``, ``showCreate`` and ``mySqlShow`` features, which the +MySQL and MariaDB capabilities enable, and look up the database of ``MySqlShowStatement`` +and the procedure of ``SHOW PROCEDURE CODE``. Feature analysis +reports ``RETURNS_RESULT_SET``. diff --git a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java index f65d16d82e..0407496672 100644 --- a/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java +++ b/src/test/java/net/sf/jsqlparser/parser/ASTNodeCoverageTest.java @@ -143,6 +143,14 @@ public void testStatementsTheCorpusLacks() throws JSQLParserException { "BEGIN SELECT 1; END;", "DISCARD TEMPORARY", "LISTEN \"Ch 1\"", "UNLISTEN *", "SHOW TRANSACTION ISOLATION LEVEL", "RESET SESSION AUTHORIZATION", + "SHOW GLOBAL VARIABLES LIKE 'a%'", + "SHOW STATUS WHERE Variable_name LIKE 'a%' AND Value > 0", + "SHOW BINLOG EVENTS IN 'f' FROM 4 LIMIT 2, 3", + "SHOW RELAYLOG EVENTS FROM 1.5 LIMIT 5 OFFSET a FOR CHANNEL 'c'", + "SHOW GRANTS FOR 'u'@'h' USING r1, `r2`@`h`", + "SHOW PROCEDURE CODE d.p", "SHOW PROFILE CPU, BLOCK IO FOR QUERY 2 LIMIT ?", + "SHOW EXTENDED INDEXES IN t IN d WHERE Key_name = 'x'", + "SHOW ENGINE 'innodb' STATUS", "SHOW TRIGGERS FROM d LIKE 't%'", "COPY s.t (a) TO STDOUT WITH NULL AS 'x' CSV FORCE QUOTE a, \"B\"", "COPY BINARY t FROM STDIN FREEZE ENCODING 'UTF8'", "COPY t FROM STDIN DELIMITER AS ';' CSV FORCE NOT NULL *", diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java index eb07f57d62..71c3b81f4c 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java @@ -130,7 +130,7 @@ void keywordsAreCaseInsensitive() throws JSQLParserException { "SHOW COLUMNS LIKE 'x' IN TABLE t", "SHOW COLUMNS FROM t NOT LIKE 'x'", "SHOW COLUMNS FROM t FROM db LIMIT 3", - "SHOW FULL PROCESSLIST" + "SHOW FULL FUNCTIONS" }) void otherDialectFormsRemainUnsupported(String sql) throws JSQLParserException { Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowIndexStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowIndexStatementTest.java index 0350fd585d..0d432d9081 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowIndexStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowIndexStatementTest.java @@ -9,9 +9,31 @@ */ package net.sf.jsqlparser.statement; -import net.sf.jsqlparser.JSQLParserException; +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; 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.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.show.ShowDbSelection; +import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowIndexStatement.Keyword; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; +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; /** * @@ -20,8 +42,137 @@ public class ShowIndexStatementTest { + private static final List> DIALECTS = List.of(parser -> { + }, parser -> parser.withDialect(Dialect.MYSQL), + parser -> parser.withDialect(Dialect.MARIADB)); + + /** Parses without a dialect and with MySQL and MariaDB, then deparses and reparses. */ + private static ShowIndexStatement roundTrip(String sql) throws JSQLParserException { + ShowIndexStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowIndexStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowIndexStatement) statement; + } + return result; + } + @Test public void testSimpleUse() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SHOW INDEX FROM mydatabase"); } + + @Test + void legacyForm() throws JSQLParserException { + ShowIndexStatement show = roundTrip("SHOW INDEX FROM mytable"); + + assertEquals("mytable", show.getTableName()); + assertEquals(Keyword.INDEX, show.getKeyword()); + assertEquals(SelectionMode.FROM, show.getTableSelectionMode()); + assertFalse(show.isExtended()); + assertNull(show.getDbSelection()); + assertNull(show.getWhereCondition()); + } + + @Test + void trailingDatabase() throws JSQLParserException { + ShowIndexStatement show = roundTrip("SHOW INDEX FROM t FROM v"); + + assertEquals("t", show.getTableName()); + assertEquals("v", show.getDbName()); + assertEquals(SelectionMode.FROM, show.getDbSelection().getSelectionMode()); + assertEquals("v.t", show.getQualifiedTable().getFullyQualifiedName()); + } + + @Test + void extendedIndexesInTable() throws JSQLParserException { + ShowIndexStatement show = roundTrip("SHOW EXTENDED INDEXES IN t"); + + assertTrue(show.isExtended()); + assertEquals(Keyword.INDEXES, show.getKeyword()); + assertEquals(SelectionMode.IN, show.getTableSelectionMode()); + } + + @Test + void keysOfQualifiedTableWithWhere() throws JSQLParserException { + ShowIndexStatement show = + roundTrip("SHOW KEYS FROM db.t IN `other` WHERE Key_name = 'PRIMARY'"); + + assertEquals(Keyword.KEYS, show.getKeyword()); + assertEquals("db", show.getTable().getSchemaName()); + assertEquals(SelectionMode.IN, show.getDbSelection().getSelectionMode()); + assertEquals("`other`.t", show.getQualifiedTable().getFullyQualifiedName()); + assertEquals("Key_name = 'PRIMARY'", show.getWhereCondition().toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW INDEXES FROM t", "SHOW KEYS IN t", "SHOW EXTENDED INDEX FROM t", + "SHOW INDEX IN t IN db", + "SHOW EXTENDED KEYS FROM `t` FROM `db` WHERE Seq_in_index > 1"}) + void variants(String sql) throws JSQLParserException { + roundTrip(sql); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW INDEXES FROM my_table", + CCJSqlParserUtil.parse("show indexes from my_table").toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW INDEXES IN TABLE t", "SHOW INDEX FROM t LIKE 'x'", + "SHOW FULL INDEX FROM t"}) + void otherFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + assertInstanceOf(ShowStatement.class, CCJSqlParserUtil.parse("SHOW INDEXES")); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = + CCJSqlParserUtil.parseStatements("SHOW INDEX FROM t FROM db;\nSELECT 1"); + + assertEquals(2, statements.size()); + assertInstanceOf(ShowIndexStatement.class, statements.get(0)); + } + + @Test + void findsTheTable() throws JSQLParserException { + // used to throw UnsupportedOperationException + assertThat(TablesNamesFinder.findTables("SHOW INDEX FROM s.t")).containsExactly("s.t"); + assertThat(TablesNamesFinder.findTables("SHOW KEYS FROM t IN db")).containsExactly("db.t"); + } + + @Test + void legacyAccessorsAndBuilders() { + ShowIndexStatement show = new ShowIndexStatement("db.t"); + + assertEquals("db", show.getTable().getSchemaName()); + assertEquals("SHOW INDEX FROM db.t", show.toString()); + assertEquals("SHOW INDEX FROM t2", show.withTableName("t2").toString()); + + show = new ShowIndexStatement(new Table("t")).withExtended(true) + .withKeyword(Keyword.KEYS).withTableSelectionMode(SelectionMode.IN) + .withDbSelection(new ShowDbSelection("db")) + .withWhereCondition(new Column("Non_unique")); + assertDeparse(show, "SHOW EXTENDED KEYS IN t FROM db WHERE Non_unique"); + + show.setKeyword(null); + show.setTableSelectionMode(null); + assertEquals("SHOW EXTENDED INDEX FROM t FROM db WHERE Non_unique", show.toString()); + show.setTableName(null); + assertNull(show.getTable()); + assertNull(show.getQualifiedTable()); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = + CCJSqlParserUtil.parse("SHOW INDEX FROM t FROM db").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/show/MySqlShowStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/MySqlShowStatementTest.java new file mode 100644 index 0000000000..688884e398 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/MySqlShowStatementTest.java @@ -0,0 +1,826 @@ +/*- + * #%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.show; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Arrays; +import java.util.EnumSet; +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.DoubleValue; +import net.sf.jsqlparser.expression.JdbcParameter; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.expression.operators.conditional.OrExpression; +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.schema.Column; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.IfElseStatement; +import net.sf.jsqlparser.statement.ShowStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.StatementVisitorAdapter; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.create.user.UserAccount; +import net.sf.jsqlparser.statement.select.Limit; +import net.sf.jsqlparser.statement.select.Offset; +import net.sf.jsqlparser.statement.show.MySqlShowStatement.EngineInfo; +import net.sf.jsqlparser.statement.show.MySqlShowStatement.Kind; +import net.sf.jsqlparser.statement.show.MySqlShowStatement.Part; +import net.sf.jsqlparser.statement.show.MySqlShowStatement.ProfileType; +import net.sf.jsqlparser.statement.show.MySqlShowStatement.Scope; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; +import net.sf.jsqlparser.util.TablesNamesFinder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.ValueSource; + +class MySqlShowStatementTest { + + private static final Consumer MYSQL = parser -> parser.withDialect(Dialect.MYSQL); + + private static final List> DIALECTS = List.of(parser -> { + }, MYSQL, parser -> parser.withDialect(Dialect.MARIADB)); + + /** Parses without a dialect and with MySQL and MariaDB, then deparses and reparses. */ + private static MySqlShowStatement roundTrip(String sql) throws JSQLParserException { + return roundTrip(sql, DIALECTS); + } + + private static MySqlShowStatement roundTrip(String sql, List> dialects) + throws JSQLParserException { + MySqlShowStatement result = null; + for (Consumer dialect : dialects) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(MySqlShowStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (MySqlShowStatement) statement; + } + return result; + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "SHOW GLOBAL VARIABLES LIKE 'a%' | VARIABLES | VARIABLES", + "SHOW SESSION VARIABLES WHERE Variable_name = 'a' | VARIABLES | VARIABLES", + "SHOW LOCAL VARIABLES | VARIABLES | VARIABLES", + "SHOW SESSION STATUS | STATUS | STATUS", + "SHOW GLOBAL STATUS LIKE 'Com%' | STATUS | STATUS", + "SHOW LOCAL STATUS WHERE Value > 1 | STATUS | STATUS", + "SHOW STATUS WHERE Variable_name LIKE 'a%' AND Value > 0 | STATUS | STATUS", + "SHOW FULL PROCESSLIST | PROCESSLIST | PROCESSLIST", + "SHOW BINARY LOG STATUS | BINARY_LOG_STATUS | BINARY LOG STATUS", + "SHOW BINARY LOGS | BINARY_LOGS | BINARY LOGS", + "SHOW BINLOG EVENTS IN 'f' | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS FROM 4 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS LIMIT 2 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS IN 'f' FROM 4 LIMIT 2, 3 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS IN 'f' FROM 4 LIMIT 3 OFFSET 2 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS IN \"f\" | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS FROM 1.5 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS FROM 18446744073709551615 | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW BINLOG EVENTS LIMIT a | BINLOG_EVENTS | BINLOG EVENTS", + "SHOW RELAYLOG EVENTS IN 'f' FROM 4 LIMIT 2, 3 FOR CHANNEL 'c' | RELAYLOG_EVENTS | RELAYLOG EVENTS", + "SHOW RELAYLOG EVENTS FOR CHANNEL 'c' | RELAYLOG_EVENTS | RELAYLOG EVENTS", + "SHOW RELAYLOG EVENTS FOR CHANNEL \"c\" | RELAYLOG_EVENTS | RELAYLOG EVENTS", + "SHOW RELAYLOG EVENTS LIMIT 1 FOR CHANNEL 'c' | RELAYLOG_EVENTS | RELAYLOG EVENTS", + "SHOW GRANTS FOR 'u'@'h' | GRANTS | GRANTS", + "SHOW GRANTS FOR u@h | GRANTS | GRANTS", + "SHOW GRANTS FOR u | GRANTS | GRANTS", + "SHOW GRANTS FOR `u`@`h` | GRANTS | GRANTS", + "SHOW GRANTS FOR CURRENT_USER | GRANTS | GRANTS", + "SHOW GRANTS FOR CURRENT_USER() | GRANTS | GRANTS", + "SHOW GRANTS FOR 'u'@'h' USING 'r1' | GRANTS | GRANTS", + "SHOW GRANTS FOR 'u'@'h' USING 'r1', 'r2'@'h' | GRANTS | GRANTS", + "SHOW GRANTS FOR CURRENT_USER() USING r1, r2@h | GRANTS | GRANTS", + "SHOW WARNINGS LIMIT 5 | WARNINGS | WARNINGS", + "SHOW WARNINGS LIMIT 2, 5 | WARNINGS | WARNINGS", + "SHOW WARNINGS LIMIT 5 OFFSET 2 | WARNINGS | WARNINGS", + "SHOW ERRORS LIMIT 5 | ERRORS | ERRORS", + "SHOW ERRORS LIMIT 2, 5 | ERRORS | ERRORS", + "SHOW ERRORS LIMIT 5 OFFSET 2 | ERRORS | ERRORS", + "SHOW COUNT(*) WARNINGS | WARNINGS | WARNINGS", + "SHOW COUNT(*) ERRORS | ERRORS | ERRORS", + "SHOW ENGINE INNODB STATUS | ENGINE | ENGINE", + "SHOW ENGINE INNODB MUTEX | ENGINE | ENGINE", + "SHOW ENGINE INNODB LOGS | ENGINE | ENGINE", + "SHOW ENGINE PERFORMANCE_SCHEMA STATUS | ENGINE | ENGINE", + "SHOW ENGINE ALL STATUS | ENGINE | ENGINE", + "SHOW ENGINE 'innodb' STATUS | ENGINE | ENGINE", + "SHOW ENGINE `innodb` MUTEX | ENGINE | ENGINE", + "SHOW STORAGE ENGINES | ENGINES | STORAGE ENGINES", + "SHOW TRIGGERS FROM d | TRIGGERS | TRIGGERS", + "SHOW TRIGGERS IN d | TRIGGERS | TRIGGERS", + "SHOW TRIGGERS LIKE 't%' | TRIGGERS | TRIGGERS", + "SHOW TRIGGERS FROM d LIKE 't%' | TRIGGERS | TRIGGERS", + "SHOW TRIGGERS IN d WHERE `Trigger` = 'x' | TRIGGERS | TRIGGERS", + "SHOW FULL TRIGGERS | TRIGGERS | TRIGGERS", + "SHOW FULL TRIGGERS FROM d | TRIGGERS | TRIGGERS", + "SHOW EVENTS FROM d | EVENTS | EVENTS", + "SHOW EVENTS IN d | EVENTS | EVENTS", + "SHOW EVENTS LIKE 'e%' | EVENTS | EVENTS", + "SHOW EVENTS FROM d WHERE Name = 'x' | EVENTS | EVENTS", + "SHOW OPEN TABLES | OPEN_TABLES | OPEN TABLES", + "SHOW OPEN TABLES FROM d | OPEN_TABLES | OPEN TABLES", + "SHOW OPEN TABLES IN d | OPEN_TABLES | OPEN TABLES", + "SHOW OPEN TABLES LIKE 't%' | OPEN_TABLES | OPEN TABLES", + "SHOW OPEN TABLES FROM d WHERE In_use > 0 | OPEN_TABLES | OPEN TABLES", + "SHOW PROCEDURE STATUS | PROCEDURE_STATUS | PROCEDURE STATUS", + "SHOW PROCEDURE STATUS LIKE 'p%' | PROCEDURE_STATUS | PROCEDURE STATUS", + "SHOW PROCEDURE STATUS WHERE Db = 'x' | PROCEDURE_STATUS | PROCEDURE STATUS", + "SHOW FUNCTION STATUS | FUNCTION_STATUS | FUNCTION STATUS", + "SHOW FUNCTION STATUS LIKE 'f%' | FUNCTION_STATUS | FUNCTION STATUS", + "SHOW FUNCTION STATUS WHERE Db = 'x' | FUNCTION_STATUS | FUNCTION STATUS", + "SHOW PROCEDURE CODE p | PROCEDURE_CODE | PROCEDURE CODE", + "SHOW PROCEDURE CODE d.p | PROCEDURE_CODE | PROCEDURE CODE", + "SHOW FUNCTION CODE f | FUNCTION_CODE | FUNCTION CODE", + "SHOW FUNCTION CODE d.`f` | FUNCTION_CODE | FUNCTION CODE", + "SHOW REPLICA STATUS | REPLICA_STATUS | REPLICA STATUS", + "SHOW REPLICA STATUS FOR CHANNEL 'c' | REPLICA_STATUS | REPLICA STATUS", + "SHOW REPLICA STATUS FOR CHANNEL '' | REPLICA_STATUS | REPLICA STATUS", + "SHOW CHARACTER SET | CHARACTER_SET | CHARACTER SET", + "SHOW CHARACTER SET LIKE 'utf8%' | CHARACTER_SET | CHARACTER SET", + "SHOW CHAR SET | CHARACTER_SET | CHAR SET", + "SHOW CHAR SET LIKE 'a' | CHARACTER_SET | CHAR SET", + "SHOW CHARSET WHERE Maxlen > 1 | CHARACTER_SET | CHARSET", + "SHOW COLLATION LIKE 'utf8%' | COLLATION | COLLATION", + "SHOW COLLATION WHERE Charset = 'utf8mb4' | COLLATION | COLLATION", + "SHOW DATABASES LIKE 'd%' | DATABASES | DATABASES", + "SHOW SCHEMAS WHERE `Database` = 'd' | DATABASES | SCHEMAS", + "SHOW PROFILE ALL | PROFILE | PROFILE", + "SHOW PROFILE BLOCK IO | PROFILE | PROFILE", + "SHOW PROFILE CONTEXT SWITCHES | PROFILE | PROFILE", + "SHOW PROFILE CPU | PROFILE | PROFILE", + "SHOW PROFILE IPC | PROFILE | PROFILE", + "SHOW PROFILE MEMORY | PROFILE | PROFILE", + "SHOW PROFILE PAGE FAULTS | PROFILE | PROFILE", + "SHOW PROFILE SOURCE | PROFILE | PROFILE", + "SHOW PROFILE SWAPS | PROFILE | PROFILE", + "SHOW PROFILE CPU, BLOCK IO | PROFILE | PROFILE", + "SHOW PROFILE CPU, CPU | PROFILE | PROFILE", + "SHOW PROFILE CPU, BLOCK IO FOR QUERY 2 | PROFILE | PROFILE", + "SHOW PROFILE FOR QUERY 2 | PROFILE | PROFILE", + "SHOW PROFILE FOR QUERY 2 LIMIT 1 | PROFILE | PROFILE", + "SHOW PROFILE LIMIT 1, 2 | PROFILE | PROFILE", + "SHOW PROFILE ALL, CPU FOR QUERY 2 LIMIT 3 OFFSET 1 | PROFILE | PROFILE" + }) + void mySqlForms(String sql, Kind kind, String keyword) throws JSQLParserException { + MySqlShowStatement show = roundTrip(sql); + + assertEquals(kind, show.getKind()); + assertEquals(keyword, show.getKeyword()); + } + + @ParameterizedTest + @CsvSource(delimiter = '|', value = { + "SHOW MASTER STATUS | BINARY_LOG_STATUS | MASTER STATUS", + "SHOW MASTER LOGS | BINARY_LOGS | MASTER LOGS", + "SHOW SLAVE HOSTS | REPLICAS | SLAVE HOSTS", + "SHOW SLAVE STATUS | REPLICA_STATUS | SLAVE STATUS", + "SHOW SLAVE STATUS FOR CHANNEL 'c1' | REPLICA_STATUS | SLAVE STATUS" + }) + void spellingsRemovedInMySql84(String sql, Kind kind, String keyword) + throws JSQLParserException { + MySqlShowStatement show = roundTrip(sql, List.of(DIALECTS.get(0), DIALECTS.get(2))); + + assertEquals(kind, show.getKind()); + assertEquals(keyword, show.getKeyword()); + assertInstanceOf(UnsupportedStatement.class, + assertSqlCanBeParsedAndDeparsed(sql, true, MYSQL)); + } + + @Test + void scopeAndFilter() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW GLOBAL STATUS WHERE Variable_name = 'Uptime'"); + + assertEquals(Scope.GLOBAL, show.getScope()); + assertEquals("Variable_name = 'Uptime'", show.getWhereCondition().toString()); + assertNull(show.getLikeExpression()); + assertEquals(ShowFilter.Kind.WHERE, show.getFilter().getKind()); + + show = roundTrip("SHOW VARIABLES LIKE 'sql_mode'"); + assertNull(show.getScope()); + assertEquals("sql_mode", ((StringValue) show.getLikeExpression()).getValue()); + + show = roundTrip("SHOW LOCAL VARIABLES LIKE \"a%\"", List.of(MYSQL)); + assertEquals(Scope.LOCAL, show.getScope()); + assertEquals("a%", ((StringValue) show.getLikeExpression()).getValue()); + } + + @Test + void whereTakesAWholeExpression() throws JSQLParserException { + String sql = "SHOW VARIABLES WHERE Variable_name LIKE 'innodb%' AND Value > 0 " + + "OR Variable_name IN ('a', 'b')"; + MySqlShowStatement show = roundTrip(sql); + + assertInstanceOf(OrExpression.class, show.getWhereCondition()); + // the statements of #2824 read the same filter + for (String other : Arrays.asList("SHOW COLUMNS FROM t WHERE a = 1 AND b = 2", + "SHOW TABLE STATUS WHERE a = 1 OR b = 2", + "SHOW INDEX FROM t WHERE Key_name = 'a' AND Seq_in_index > 1")) { + for (Consumer dialect : DIALECTS) { + assertFalse(assertSqlCanBeParsedAndDeparsed(other, false, + dialect) instanceof UnsupportedStatement, other); + } + } + } + + @Test + void fullProcessListAndTriggers() throws JSQLParserException { + assertTrue(roundTrip("SHOW FULL PROCESSLIST").isFull()); + assertFalse(roundTrip("SHOW PROCESSLIST", List.of(MYSQL)).isFull()); + assertTrue(roundTrip("SHOW FULL TRIGGERS IN d").isFull()); + assertFalse(roundTrip("SHOW TRIGGERS IN d").isFull()); + } + + @Test + void databaseSelection() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW TRIGGERS FROM v LIKE 't%'"); + + assertEquals(SelectionMode.FROM, show.getDbSelection().getSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals("t%", ((StringValue) show.getLikeExpression()).getValue()); + + show = roundTrip("SHOW OPEN TABLES IN v LIKE 't%'"); + assertEquals(SelectionMode.IN, show.getDbSelection().getSelectionMode()); + } + + @Test + void binaryLogEvents() throws JSQLParserException { + MySqlShowStatement show = + roundTrip("SHOW BINLOG EVENTS IN 'binlog.000001' FROM 4 LIMIT 10"); + + assertEquals("binlog.000001", show.getLogName().getValue()); + assertEquals(4L, ((LongValue) show.getPosition()).getValue()); + assertNull(show.getLimit().getOffset()); + assertEquals("10", show.getLimit().getRowCount().toString()); + assertNull(show.getOffset()); + assertNull(show.getChannel()); + + show = roundTrip("SHOW BINLOG EVENTS IN \"f\" FROM 1.5 LIMIT 3 OFFSET 2"); + assertEquals("f", show.getLogName().getValue()); + assertEquals("\"", show.getLogName().getQuoteStr()); + assertInstanceOf(DoubleValue.class, show.getPosition()); + assertEquals("3", show.getLimit().getRowCount().toString()); + assertEquals("2", show.getOffset().getOffset().toString()); + } + + @Test + void relayLogEvents() throws JSQLParserException { + MySqlShowStatement show = roundTrip( + "SHOW RELAYLOG EVENTS IN 'relay.000002' FROM 4 LIMIT 2, 10 FOR CHANNEL 'c1'"); + + assertEquals("2", show.getLimit().getOffset().toString()); + assertEquals("10", show.getLimit().getRowCount().toString()); + assertEquals("c1", show.getChannel().getValue()); + } + + @Test + void limitValues() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW WARNINGS LIMIT a, 18446744073709551615"); + + assertInstanceOf(Column.class, show.getLimit().getOffset()); + assertEquals("18446744073709551615", + ((LongValue) show.getLimit().getRowCount()).getStringValue()); + + // a ? is a syntax error in the text protocol, but PREPARE reads it + show = roundTrip("SHOW ERRORS LIMIT ? OFFSET ?"); + assertInstanceOf(JdbcParameter.class, show.getLimit().getRowCount()); + assertInstanceOf(JdbcParameter.class, show.getOffset().getOffset()); + } + + @Test + void grantsForAccount() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW GRANTS FOR 'app'@'%'"); + + assertEquals("app", show.getAccount().getUser().getValue()); + assertEquals("%", show.getAccount().getHost().getValue()); + assertTrue(show.getUsingRoles().isEmpty()); + + show = roundTrip("SHOW GRANTS FOR app@localhost USING r1, 'r2'@'%'"); + assertEquals("app", show.getAccount().getUser().getValue()); + assertEquals("localhost", show.getAccount().getHost().getValue()); + assertEquals(2, show.getUsingRoles().size()); + assertEquals("'r2'@'%'", show.getUsingRoles().get(1).toString()); + } + + @Test + void grantsForCurrentUser() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW GRANTS FOR CURRENT_USER"); + + assertEquals("CURRENT_USER", show.getAccount().getUser().getValue()); + assertFalse(show.getAccount().isCurrentUserParentheses()); + + show = roundTrip("SHOW GRANTS FOR CURRENT_USER()"); + assertTrue(show.getAccount().isCurrentUserParentheses()); + + assertNull(roundTrip("SHOW GRANTS", List.of(MYSQL)).getAccount()); + } + + @Test + void diagnostics() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW WARNINGS LIMIT 5"); + + assertFalse(show.isCount()); + assertEquals("5", show.getLimit().getRowCount().toString()); + + show = roundTrip("SHOW COUNT(*) ERRORS"); + assertEquals(Kind.ERRORS, show.getKind()); + assertTrue(show.isCount()); + assertNull(show.getLimit()); + + assertEquals("SHOW COUNT(*) WARNINGS", + CCJSqlParserUtil.parse("SHOW COUNT( * )WARNINGS", MYSQL).toString()); + } + + @Test + void engineAndRoutineCode() throws JSQLParserException { + MySqlShowStatement show = roundTrip("SHOW ENGINE INNODB STATUS"); + + assertEquals("INNODB", show.getEngineName()); + assertEquals(EngineInfo.STATUS, show.getEngineInfo()); + + show = roundTrip("SHOW ENGINE ALL MUTEX"); + assertEquals("ALL", show.getEngineName()); + assertEquals(EngineInfo.MUTEX, show.getEngineInfo()); + + show = roundTrip("SHOW ENGINE 'innodb' LOGS"); + assertEquals("'innodb'", show.getEngineName()); + assertEquals(EngineInfo.LOGS, show.getEngineInfo()); + + show = roundTrip("SHOW PROCEDURE CODE db.p"); + assertEquals("db", show.getRoutineName().getSchemaName()); + assertEquals("p", show.getRoutineName().getName()); + assertNull(roundTrip("SHOW FUNCTION CODE f").getRoutineName().getSchemaName()); + } + + @Test + void profile() throws JSQLParserException { + MySqlShowStatement show = + roundTrip("SHOW PROFILE BLOCK IO, PAGE FAULTS, CPU FOR QUERY 2 LIMIT 3 OFFSET 1"); + + assertEquals(List.of(ProfileType.BLOCK_IO, ProfileType.PAGE_FAULTS, ProfileType.CPU), + show.getProfileTypes()); + assertEquals("2", show.getProfileQuery().toString()); + assertEquals("3", show.getLimit().getRowCount().toString()); + assertEquals("1", show.getOffset().getOffset().toString()); + + show = roundTrip("SHOW PROFILE", List.of(MYSQL)); + assertTrue(show.getProfileTypes().isEmpty()); + assertNull(show.getProfileQuery()); + assertEquals(ProfileType.CONTEXT_SWITCHES, ProfileType.fromKeyword("context switches")); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW GLOBAL VARIABLES LIKE 'x%'", + CCJSqlParserUtil.parse("show global variables like 'x%'", MYSQL).toString()); + assertEquals("SHOW SLAVE STATUS FOR CHANNEL 'c'", + CCJSqlParserUtil.parse("show slave status for channel 'c'").toString()); + assertEquals("SHOW CHARSET", CCJSqlParserUtil.parse("show charset", MYSQL).toString()); + assertEquals("SHOW PROFILE BLOCK IO, CONTEXT SWITCHES", CCJSqlParserUtil + .parse("show profile block io, Context Switches", MYSQL).toString()); + assertEquals("SHOW ENGINE innodb STATUS", + CCJSqlParserUtil.parse("show engine innodb status", MYSQL).toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW VARIABLES", "SHOW STATUS", "SHOW PROCESSLIST", + "SHOW DATABASES", "SHOW SCHEMAS", "SHOW TRIGGERS", "SHOW EVENTS", "SHOW CHARSET", + "SHOW COLLATION", "SHOW ENGINES", "SHOW PLUGINS", "SHOW PRIVILEGES", "SHOW PROFILES", + "SHOW PROFILE", "SHOW REPLICAS", "SHOW GRANTS", "SHOW WARNINGS", "SHOW ERRORS"}) + void singleWordsAreTypedForMySqlOnly(String sql) throws JSQLParserException { + roundTrip(sql, List.of(MYSQL, parser -> parser.withDialect(Dialect.MARIADB))); + + // without a dialect and in other dialects, a single word stays a ShowStatement + for (Consumer dialect : List.>of(parser -> { + }, parser -> parser.withDialect(Dialect.SNOWFLAKE), + parser -> parser.withDialect(Dialect.POSTGRESQL))) { + Statement statement = CCJSqlParserUtil.parse(sql, dialect); + assertInstanceOf(ShowStatement.class, statement); + assertEquals(sql, statement.toString()); + } + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW VARIABLES LIKE 'sql_mode'", "SHOW DATABASES LIKE 'v%'", + "SHOW GRANTS FOR 'app'@'%'", "SHOW FULL PROCESSLIST", "SHOW WARNINGS LIMIT 5", + "SHOW BINARY LOGS", "SHOW ENGINE INNODB STATUS", "SHOW PROFILE CPU", + "SHOW CHARACTER SET", "SHOW REPLICA STATUS"}) + void otherDialectsKeepUnsupportedStatement(String sql) throws JSQLParserException { + for (Dialect dialect : Arrays.asList(Dialect.SNOWFLAKE, Dialect.POSTGRESQL, + Dialect.ORACLE, Dialect.DATABRICKS)) { + Statement statement = + CCJSqlParserUtil.parse(sql, parser -> parser.withDialect(dialect)); + assertInstanceOf(UnsupportedStatement.class, statement, dialect.name()); + } + } + + @Test + void postgreSqlShowIsUnchanged() throws JSQLParserException { + Consumer postgreSql = parser -> parser.withDialect(Dialect.POSTGRESQL); + for (String sql : Arrays.asList("SHOW TIME ZONE", "SHOW TRANSACTION ISOLATION LEVEL", + "SHOW SESSION AUTHORIZATION", "SHOW search_path", "SHOW ALL", "SHOW status")) { + assertInstanceOf(ShowStatement.class, assertSqlCanBeParsedAndDeparsed(sql, true, + postgreSql), sql); + } + for (String sql : Arrays.asList("SHOW TIME ZONE", "SHOW TRANSACTION ISOLATION LEVEL", + "SHOW SESSION AUTHORIZATION")) { + assertInstanceOf(ShowStatement.class, assertSqlCanBeParsedAndDeparsed(sql, true)); + } + } + + /** MySQL 8.4 reports a syntax error for each; the parser keeps them unsupported statements. */ + @ParameterizedTest + @ValueSource(strings = { + "SHOW PERSIST VARIABLES", + "SHOW GLOBAL SESSION VARIABLES", + "SHOW VARIABLES LIKE a", + "SHOW VARIABLES LIKE 1", + "SHOW VARIABLES LIKE N'a%'", + "SHOW VARIABLES LIKE _utf8mb4'a%'", + "SHOW VARIABLES LIKE CONCAT('a', '%')", + "SHOW VARIABLES LIKE ?", + "SHOW VARIABLES LIKE @v", + "SHOW VARIABLES LIKE 'a%' WHERE Value = 1", + "SHOW VARIABLES FROM d", + "SHOW VARIABLES LIMIT 1", + "SHOW FULL VARIABLES", + "SHOW FULL STATUS", + "SHOW PROCESSLIST LIKE 'a'", + "SHOW EXTENDED PROCESSLIST", + "SHOW SESSION PROCESSLIST", + "SHOW BINARY LOG", + "SHOW BINARY LOGS LIKE 'a'", + "SHOW BINLOG EVENTS IN f", + "SHOW BINLOG EVENTS FROM 'x'", + "SHOW BINLOG EVENTS FROM -1", + "SHOW BINLOG EVENTS FROM 0x10", + "SHOW BINLOG EVENTS FROM 4 IN 'f'", + "SHOW BINLOG EVENTS LIMIT 2 FROM 4", + "SHOW BINLOG EVENTS FOR CHANNEL 'c'", + "SHOW BINLOG EVENTS LIMIT @v", + "SHOW BINLOG EVENTS LIMIT 1 + 1", + "SHOW BINLOG EVENTS LIMIT -1", + "SHOW BINLOG EVENTS LIMIT 1, 2, 3", + "SHOW BINLOG EVENTS LIMIT 'a'", + "SHOW RELAYLOG EVENTS FOR CHANNEL c", + "SHOW RELAYLOG EVENTS FOR CHANNEL 'c' LIMIT 1", + "SHOW RELAYLOG EVENTS FOR CHANNEL", + "SHOW GRANTS USING 'r1'", + "SHOW GRANTS FOR", + "SHOW GRANTS FOR 'u'@'h' USING", + "SHOW GRANTS FOR 'u'@'h', 'u2'@'h'", + "SHOW GRANTS ON d.t", + "SHOW COUNT (*) ERRORS", + "SHOW COUNT(*) WARNINGS LIMIT 1", + "SHOW COUNT(1) WARNINGS", + "SHOW COUNT(*) VARIABLES", + "SHOW COUNT(*)", + "SHOW WARNINGS LIKE 'a'", + "SHOW WARNINGS WHERE Code = 1", + "SHOW WARNINGS LIMIT", + "SHOW WARNINGS OFFSET 2", + "SHOW WARNINGS LIMIT \"a\"", + "SHOW WARNINGS LIMIT 1.5", + "SHOW WARNINGS LIMIT 0x10", + "SHOW WARNINGS LIMIT a.b", + "SHOW ENGINE INNODB", + "SHOW ENGINE STATUS", + "SHOW ENGINE INNODB STATUS LIKE 'a'", + "SHOW ENGINE d.innodb STATUS", + "SHOW ENGINE N'innodb' STATUS", + "SHOW ENGINES LIKE 'a'", + "SHOW STORAGE ENGINE INNODB STATUS", + "SHOW EXTENDED TRIGGERS", + "SHOW TRIGGERS FROM d.t", + "SHOW TRIGGERS LIKE 't%' FROM d", + "SHOW TRIGGERS FROM 'd'", + "SHOW FULL EVENTS", + "SHOW EVENTS FROM d.e", + "SHOW FULL OPEN TABLES", + "SHOW OPEN FULL TABLES", + "SHOW OPEN TABLE", + "SHOW PROCEDURE STATUS FROM d", + "SHOW PROCEDURE CODE", + "SHOW PROCEDURE CODE p LIKE 'a'", + "SHOW PROCEDURE CODE a.b.c", + "SHOW PROCEDURE CODE 'p'", + "SHOW PROCEDURE p", + "SHOW MASTER STATUS", + "SHOW MASTER LOGS", + "SHOW MASTER HOSTS", + "SHOW SLAVE STATUS", + "SHOW SLAVE HOSTS", + "SHOW REPLICAS LIKE 'a'", + "SHOW REPLICA STATUS LIKE 'a'", + "SHOW REPLICA STATUS FOR 'c'", + "SHOW REPLICA STATUS FOR CHANNEL c", + "SHOW REPLICA STATUS FOR CHANNEL N'c'", + "SHOW REPLICA STATUS FOR CHANNEL 'c' LIMIT 1", + "SHOW REPLICA HOSTS", + "SHOW CHARACTER SET FROM d", + "SHOW CHARACTER SETS", + "SHOW CHARSET LIKE 'a' LIKE 'b'", + "SHOW COLLATION FROM d", + "SHOW DATABASES FROM d", + "SHOW GLOBAL DATABASES", + "SHOW FULL DATABASES", + "SHOW PLUGINS LIKE 'a'", + "SHOW PLUGINS WHERE Name = 'a'", + "SHOW PRIVILEGES LIKE 'a'", + "SHOW PROFILES LIMIT 1", + "SHOW PROFILE CPU,", + "SHOW PROFILE BLOCK", + "SHOW PROFILE FOR QUERY", + "SHOW PROFILE FOR QUERY 'a'", + "SHOW PROFILE FOR QUERY ?", + "SHOW PROFILE LIMIT 1 FOR QUERY 2", + "SHOW PROFILE FOO", + "SHOW PROFILE CPU CPU", + "SHOW INNODB STATUS", + "SHOW MUTEX STATUS", + "SHOW TABLE TYPES" + }) + void formsMySqlRejectsRemainUnsupported(String sql) throws JSQLParserException { + assertInstanceOf(UnsupportedStatement.class, CCJSqlParserUtil.parse(sql, MYSQL)); + } + + @Test + void textAfterAWhereConditionIsAnError() { + for (String sql : Arrays.asList("SHOW VARIABLES WHERE a = 1 LIMIT 1", + "SHOW EVENTS FROM d WHERE a = 1 LIKE", "SHOW STATUS WHERE a = 1 FROM d")) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL), sql); + } + } + + @Test + void withoutADialectLikeTakesAnExpressionAsBefore() throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed("SHOW VARIABLES LIKE a", true); + + assertInstanceOf(Column.class, ((MySqlShowStatement) statement).getLikeExpression()); + assertInstanceOf(ShowTablesStatement.class, + assertSqlCanBeParsedAndDeparsed("SHOW TABLES LIKE 'a' || 'b'", true)); + assertInstanceOf(ShowTablesStatement.class, + assertSqlCanBeParsedAndDeparsed("SHOW TABLES LIKE 'a%'", true, MYSQL)); + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW VARIABLES LIKE 'a%'", "SHOW GLOBAL STATUS", + "SHOW FULL PROCESSLIST", "SHOW SCHEMAS", "SHOW TRIGGERS FROM v", + "SHOW OPEN TABLES", "SHOW PROCEDURE STATUS", "SHOW FUNCTION CODE f", + "SHOW CHARACTER SET", "SHOW STORAGE ENGINES", "SHOW PLUGINS", "SHOW BINARY LOGS", + "SHOW BINARY LOG STATUS", "SHOW REPLICAS", + "SHOW REPLICA STATUS FOR CHANNEL 'c'", "SHOW GRANTS FOR CURRENT_USER()", + "SHOW WARNINGS LIMIT 2, 5", "SHOW ERRORS LIMIT 5 OFFSET a", "SHOW COUNT(*) ERRORS", + "SHOW ENGINE INNODB MUTEX", "SHOW BINLOG EVENTS FROM 4", + "SHOW RELAYLOG EVENTS FOR CHANNEL 'c'", "SHOW PROFILE CPU FOR QUERY 2", + "SHOW STATUS WHERE a = 1 AND b = 2", "SHOW INDEX FROM t FROM v", "SHOW TABLES IN v"}) + void endsAtTheStatementBoundary(String sql) throws JSQLParserException { + for (String script : Arrays.asList(sql + ";\nSELECT 1", sql + ";\nSHOW TABLES", + "SHOW TABLES;\n" + sql)) { + Statements statements = CCJSqlParserUtil.parseStatements(script, MYSQL); + + assertEquals(2, statements.size(), script); + Statement show = statements.get(script.startsWith(sql) ? 0 : 1); + assertEquals(sql, show.toString()); + assertFalse(show instanceof UnsupportedStatement, script); + } + IfElseStatement ifElse = (IfElseStatement) CCJSqlParserUtil + .parse("IF a = 1 " + sql + " ELSE " + sql, MYSQL); + assertEquals(sql, ifElse.getIfStatement().toString()); + assertEquals(sql, ifElse.getElseStatement().toString()); + } + + @Test + void namesNoTable() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW OPEN TABLES FROM db LIKE 't%'")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW PROCEDURE CODE db.p")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW GRANTS FOR u")).isEmpty(); + assertThat(new TablesNamesFinder().getTables( + CCJSqlParserUtil.parse("SHOW EVENTS FROM db WHERE Name = 'x'", MYSQL))).isEmpty(); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = + CCJSqlParserUtil.parse("SHOW GLOBAL VARIABLES LIKE 'a%'").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } + + @Test + void visitorsReceiveTheStatement() throws JSQLParserException { + Statement statement = CCJSqlParserUtil.parse("SHOW BINARY LOGS"); + MySqlShowStatement[] visited = new MySqlShowStatement[1]; + statement.accept(new StatementVisitorAdapter() { + @Override + public Void visit(MySqlShowStatement mySqlShow, S context) { + visited[0] = mySqlShow; + return null; + } + }); + + assertSame(statement, visited[0]); + assertNull(statement.accept(new StatementVisitorAdapter(), null)); + } + + @Test + void buildsAndDeparses() { + MySqlShowStatement show = new MySqlShowStatement(Kind.VARIABLES).withScope(Scope.SESSION) + .withFilter(ShowFilter.like(new StringValue("max%"))); + assertDeparse(show, "SHOW SESSION VARIABLES LIKE 'max%'"); + + show = new MySqlShowStatement().withKeyword("slave status") + .withChannel(new StringValue("c1")); + assertEquals(Kind.REPLICA_STATUS, show.getKind()); + assertDeparse(show, "SHOW SLAVE STATUS FOR CHANNEL 'c1'"); + + UserAccount account = new UserAccount().withUser(new StringValue("app")) + .withHost(new StringValue("%")); + show = new MySqlShowStatement(Kind.GRANTS).withAccount(account) + .withUsingRoles(List.of(new UserAccount().withUser(new StringValue("r")))); + assertDeparse(show, "SHOW GRANTS FOR 'app'@'%' USING 'r'"); + + show = new MySqlShowStatement(Kind.BINLOG_EVENTS) + .withLogName(new StringValue("binlog.000002")).withPosition(new LongValue(4)) + .withLimit(new Limit().withOffset(new LongValue(1)).withRowCount(new LongValue(2))); + assertDeparse(show, "SHOW BINLOG EVENTS IN 'binlog.000002' FROM 4 LIMIT 1, 2"); + + show = new MySqlShowStatement(Kind.ENGINE).withEngineName("INNODB") + .withEngineInfo(EngineInfo.MUTEX); + assertDeparse(show, "SHOW ENGINE INNODB MUTEX"); + + show = new MySqlShowStatement(Kind.PROCEDURE_CODE).withRoutineName(new Table("p")); + assertDeparse(show, "SHOW PROCEDURE CODE p"); + + show = new MySqlShowStatement(Kind.TRIGGERS).withFull(true) + .withDbSelection(new ShowDbSelection(SelectionMode.IN, "db")); + assertDeparse(show, "SHOW FULL TRIGGERS IN db"); + + show = new MySqlShowStatement(Kind.PROFILE) + .withProfileTypes(List.of(ProfileType.CPU, ProfileType.BLOCK_IO)) + .withProfileQuery(new LongValue(2)) + .withLimit(new Limit().withRowCount(new LongValue(3))) + .withOffset(new Offset().withOffset(new LongValue(1))); + assertDeparse(show, "SHOW PROFILE CPU, BLOCK IO FOR QUERY 2 LIMIT 3 OFFSET 1"); + } + + @Test + void kindsAllowTheirParts() { + assertTrue(Kind.VARIABLES.allows(Part.SCOPE)); + assertFalse(Kind.DATABASES.allows(Part.SCOPE)); + assertEquals(List.of("BINARY LOG STATUS", "MASTER STATUS"), + Kind.BINARY_LOG_STATUS.getKeywords()); + assertEquals(List.of("CHARACTER SET", "CHARSET", "CHAR SET"), + Kind.CHARACTER_SET.getKeywords()); + assertEquals(EnumSet.of(Part.PROFILE, Part.LIMIT), Kind.PROFILE.getParts()); + assertEquals(Kind.CHARACTER_SET, Kind.fromKeyword(" charset ")); + assertThrows(IllegalArgumentException.class, () -> Kind.fromKeyword("FUNCTIONS")); + + MySqlShowStatement show = new MySqlShowStatement(Kind.DATABASES); + assertThrows(IllegalArgumentException.class, () -> show.setScope(Scope.GLOBAL)); + assertThrows(IllegalArgumentException.class, () -> show.setFull(true)); + assertThrows(IllegalArgumentException.class, + () -> show.setDbSelection(new ShowDbSelection("db"))); + assertThrows(IllegalArgumentException.class, + () -> show.setChannel(new StringValue("c"))); + assertThrows(IllegalArgumentException.class, () -> show.setLimit(new Limit())); + assertThrows(IllegalArgumentException.class, + () -> show.setProfileTypes(List.of(ProfileType.CPU))); + assertThrows(IllegalArgumentException.class, + () -> show.setProfileQuery(new LongValue(1))); + + MySqlShowStatement binlog = new MySqlShowStatement(Kind.BINLOG_EVENTS); + assertThrows(IllegalArgumentException.class, + () -> binlog.setChannel(new StringValue("c"))); + // OFFSET needs LIMIT row_count before it + assertThrows(IllegalArgumentException.class, + () -> binlog.setOffset(new Offset().withOffset(new LongValue(1)))); + binlog.setLimit(new Limit().withOffset(new LongValue(1)).withRowCount(new LongValue(2))); + assertThrows(IllegalArgumentException.class, + () -> binlog.setOffset(new Offset().withOffset(new LongValue(1)))); + } + + @Test + void changingTheKindDropsPartsItDoesNotAllow() { + MySqlShowStatement show = new MySqlShowStatement(Kind.TRIGGERS) + .withDbSelection(new ShowDbSelection("db")) + .withFilter(ShowFilter.like(new StringValue("t%"))); + + show.setKind(Kind.DATABASES); + assertNull(show.getDbSelection()); + assertEquals("SHOW DATABASES LIKE 't%'", show.toString()); + + show.setKeyword("SCHEMAS"); + assertEquals(Kind.DATABASES, show.getKind()); + assertEquals("SHOW SCHEMAS LIKE 't%'", show.toString()); + + show.setKeyword("PLUGINS"); + assertEquals(Kind.PLUGINS, show.getKind()); + assertNull(show.getFilter()); + assertEquals("SHOW PLUGINS", show.toString()); + + show.setKind(Kind.REPLICAS); + assertEquals("REPLICAS", show.getKeyword()); + show.setKeyword(null); + assertEquals("REPLICAS", show.getKeyword()); + } + + @Test + void countAndLimitReplaceEachOther() { + MySqlShowStatement show = new MySqlShowStatement(Kind.WARNINGS) + .withLimit(new Limit().withRowCount(new LongValue(3))); + + show.setCount(true); + assertNull(show.getLimit()); + assertEquals("SHOW COUNT(*) WARNINGS", show.toString()); + + show.setLimit(new Limit().withRowCount(new LongValue(3))); + show.setOffset(new Offset().withOffset(new LongValue(1))); + assertFalse(show.isCount()); + assertEquals("SHOW WARNINGS LIMIT 3 OFFSET 1", show.toString()); + + show.setLimit(null); + assertNull(show.getOffset()); + assertEquals("SHOW WARNINGS", show.toString()); + } + + @Test + void removingTheAccountRemovesTheRoles() { + MySqlShowStatement show = new MySqlShowStatement(Kind.GRANTS) + .withAccount(new UserAccount().withUser(new StringValue("u"))) + .withUsingRoles(List.of(new UserAccount().withUser(new StringValue("r")))); + + show.setAccount(null); + assertTrue(show.getUsingRoles().isEmpty()); + assertEquals("SHOW GRANTS", show.toString()); + assertThrows(UnsupportedOperationException.class, + () -> show.getUsingRoles().add(new UserAccount())); + } + + @Test + void statementDeParserPrintsExpressionsWithTheExpressionDeParser() + throws JSQLParserException { + assertEquals("SHOW VARIABLES LIKE ", + ShowExpressionDeParsing.deparse("SHOW VARIABLES LIKE 'sql_mode'")); + assertEquals("SHOW BINLOG EVENTS IN FROM #4 LIMIT #2, #10", + ShowExpressionDeParsing.deparse( + "SHOW BINLOG EVENTS IN 'b.000001' FROM 4 LIMIT 2, 10")); + assertEquals("SHOW RELAYLOG EVENTS LIMIT #10 OFFSET #2 FOR CHANNEL ", + ShowExpressionDeParsing.deparse( + "SHOW RELAYLOG EVENTS LIMIT 10 OFFSET 2 FOR CHANNEL 'c'")); + assertEquals("SHOW PROFILE CPU FOR QUERY #2", + ShowExpressionDeParsing.deparse("SHOW PROFILE CPU FOR QUERY 2")); + } + + @Test + void parsedObjectsHaveNodes() throws JSQLParserException { + String sql = "SHOW BINLOG EVENTS IN 'f' FROM 4 LIMIT 2, 3"; + MySqlShowStatement show = (MySqlShowStatement) CCJSqlParserUtil.parse(sql, MYSQL); + + assertEquals(sql, span(sql, show)); + assertEquals("'f'", span(sql, show.getLogName())); + assertEquals("4", span(sql, (ASTNodeAccess) show.getPosition())); + assertEquals("LIMIT 2, 3", span(sql, show.getLimit())); + + sql = "SHOW PROCEDURE CODE d.p"; + show = (MySqlShowStatement) CCJSqlParserUtil.parse(sql, MYSQL); + assertEquals("d.p", span(sql, show.getRoutineName())); + + sql = "SHOW WARNINGS LIMIT 5 OFFSET a"; + show = (MySqlShowStatement) CCJSqlParserUtil.parse(sql, MYSQL); + assertEquals("LIMIT 5", span(sql, show.getLimit())); + assertEquals("a", span(sql, (ASTNodeAccess) show.getOffset().getOffset())); + } + + private static String span(String sql, ASTNodeAccess access) { + return sql.substring(access.getASTNode().jjtGetFirstToken().absoluteBegin - 1, + access.getASTNode().jjtGetLastToken().absoluteEnd - 1); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowClausesTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowClausesTest.java new file mode 100644 index 0000000000..936bb716e1 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowClausesTest.java @@ -0,0 +1,185 @@ +/*- + * #%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.show; + +import static net.sf.jsqlparser.test.TestUtils.assertDeparse; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +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.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.EnumSet; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.statement.ShowColumnsStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.UnsupportedStatement; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.Modifiers; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +/** The shared {FROM | IN} db and LIKE | WHERE clauses of the SHOW statements. */ +class ShowClausesTest { + + @Test + void showTablesHoldsTheSharedClauses() throws JSQLParserException { + ShowTablesStatement show = (ShowTablesStatement) assertSqlCanBeParsedAndDeparsed( + "SHOW FULL TABLES IN v WHERE Table_type = 'VIEW'"); + + assertEquals(SelectionMode.IN, show.getDbSelection().getSelectionMode()); + assertEquals("v", show.getDbSelection().getName()); + assertEquals(SelectionMode.IN, show.getSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals(ShowFilter.Kind.WHERE, show.getFilter().getKind()); + assertSame(show.getFilter().getExpression(), show.getWhereCondition()); + assertNull(show.getLikeExpression()); + + show = (ShowTablesStatement) assertSqlCanBeParsedAndDeparsed("SHOW TABLES"); + assertNull(show.getSelectionMode()); + assertNull(show.getDbSelection()); + assertNull(show.getFilter()); + } + + @ParameterizedTest + @ValueSource(strings = {"SHOW INDEXES IN TABLE t", "SHOW INDEX FROM t LIMIT 3", + "SHOW INDEX FROM t FROM d FROM e", "SHOW COLUMNS FROM t LIMIT 3"}) + void otherFormsFallBackInsteadOfFailing(String sql) throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.SNOWFLAKE, Dialect.MYSQL}) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true, + parser -> parser.withDialect(dialect)); + assertInstanceOf(UnsupportedStatement.class, statement); + } + } + + @Test + void mySqlTakesAStringAsTheLikePattern() throws JSQLParserException { + for (Dialect dialect : new Dialect[] {Dialect.MYSQL, Dialect.MARIADB}) { + ShowColumnsStatement columns = (ShowColumnsStatement) assertSqlCanBeParsedAndDeparsed( + "SHOW COLUMNS FROM t LIKE \"a%\"", true, + parser -> parser.withDialect(dialect)); + assertEquals("a%", ((StringValue) columns.getLikeExpression()).getValue()); + + // MySQL reports a syntax error for these + for (String sql : new String[] {"SHOW COLUMNS FROM t LIKE a", + "SHOW TABLE STATUS LIKE 1", "SHOW TABLE STATUS LIKE CONCAT('a', '%')"}) { + assertInstanceOf(UnsupportedStatement.class, assertSqlCanBeParsedAndDeparsed(sql, + true, parser -> parser.withDialect(dialect)), sql); + } + } + // without a dialect the pattern is any simple expression, as before + ShowTableStatusStatement status = + (ShowTableStatusStatement) assertSqlCanBeParsedAndDeparsed( + "SHOW TABLE STATUS LIKE CONCAT('a', '%')", true); + assertInstanceOf(Expression.class, status.getLikeExpression()); + } + + @Test + void showTablesRendersWithoutModifiersOrSelectionMode() { + ShowTablesStatement show = new ShowTablesStatement(); + assertEquals("SHOW TABLES", show.toString()); + + show.setDbName("db"); + assertEquals("SHOW TABLES FROM db", show.toString()); + + show.setModifiers(EnumSet.of(Modifiers.FULL)); + show.setSelectionMode(SelectionMode.IN); + assertDeparse(show, "SHOW FULL TABLES IN db"); + } + + @Test + void legacySettersWorkInEitherOrder() { + ShowTablesStatement tables = new ShowTablesStatement(); + tables.setSelectionMode(SelectionMode.IN); + assertEquals(SelectionMode.IN, tables.getSelectionMode()); + assertNull(tables.getDbName()); + assertEquals("SHOW TABLES", tables.toString()); + tables.setDbName("db"); + assertEquals("SHOW TABLES IN db", tables.toString()); + tables.setDbName(null); + tables.setSelectionMode(null); + assertNull(tables.getSelectionMode()); + assertNull(tables.getDbSelection()); + + ShowTableStatusStatement status = new ShowTableStatusStatement().withDbName("db") + .withSelectionMode(SelectionMode.IN); + assertEquals("SHOW TABLE STATUS IN db", status.toString()); + assertEquals(SelectionMode.FROM, new ShowTableStatusStatement().getSelectionMode()); + + ShowColumnsStatement columns = new ShowColumnsStatement("t"); + columns.setDbSelectionMode(SelectionMode.IN); + columns.setDbName("db"); + assertEquals("SHOW COLUMNS FROM t IN db", columns.toString()); + assertEquals(new ShowDbSelection(SelectionMode.IN, "db").toString(), + columns.getDbSelection().toString()); + } + + @Test + void legacyFilterSettersReplaceTheFilter() { + ShowTablesStatement show = new ShowTablesStatement(); + show.setLikeExpression(new StringValue("a%")); + assertEquals("SHOW TABLES LIKE 'a%'", show.toString()); + + Expression condition = new Column("Tables_in_db"); + show.setWhereCondition(condition); + assertNull(show.getLikeExpression()); + assertSame(condition, show.getWhereCondition()); + + // clearing the other kind keeps the filter + show.setLikeExpression(null); + assertSame(condition, show.getWhereCondition()); + show.setWhereCondition(null); + assertNull(show.getFilter()); + + ShowTableStatusStatement status = + new ShowTableStatusStatement().withLikeExpression(new StringValue("t%")); + status.setWhereCondition(new Column("Rows")); + assertEquals("SHOW TABLE STATUS WHERE Rows", status.toString()); + } + + @Test + void sharedClauseValues() { + ShowFilter filter = ShowFilter.where(new Column("a")); + assertEquals(ShowFilter.Kind.WHERE, filter.getKind()); + assertEquals("WHERE a", filter.toString()); + assertNull(ShowFilter.likeExpression(filter)); + assertEquals("a", ShowFilter.whereCondition(filter).toString()); + assertEquals(ShowFilter.Kind.LIKE, new ShowFilter().withKind(null).getKind()); + assertEquals("LIKE 'x'", + new ShowFilter().withExpression(new StringValue("x")).toString()); + + ShowDbSelection selection = new ShowDbSelection(null, "db"); + assertEquals(SelectionMode.FROM, selection.getSelectionMode()); + assertTrue(ShowDbSelection.isPresent(selection)); + assertFalse(ShowDbSelection.isPresent(new ShowDbSelection())); + assertEquals("IN db", selection.withSelectionMode(SelectionMode.IN).toString()); + assertEquals("FROM x", new ShowDbSelection().withName("x").toString()); + } + + @Test + void statementDeParserPrintsFiltersWithTheExpressionDeParser() + throws JSQLParserException { + assertEquals("SHOW TABLES LIKE ", ShowExpressionDeParsing.deparse( + "SHOW TABLES LIKE 'a%'")); + assertEquals("SHOW FULL COLUMNS FROM t LIKE ", ShowExpressionDeParsing.deparse( + "SHOW FULL COLUMNS FROM t LIKE 'c%'")); + assertEquals("SHOW TABLE STATUS FROM db WHERE Comment = ", + ShowExpressionDeParsing.deparse("SHOW TABLE STATUS FROM db WHERE Comment = 'x'")); + assertEquals("SHOW INDEX FROM t WHERE Key_name = ", + ShowExpressionDeParsing.deparse("SHOW INDEX FROM t WHERE Key_name = 'PRIMARY'")); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowExpressionDeParsing.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowExpressionDeParsing.java new file mode 100644 index 0000000000..f1e0fcf504 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowExpressionDeParsing.java @@ -0,0 +1,45 @@ +/*- + * #%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.show; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.LongValue; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.util.deparser.ExpressionDeParser; +import net.sf.jsqlparser.util.deparser.SelectDeParser; +import net.sf.jsqlparser.util.deparser.StatementDeParser; + +/** Deparses a SHOW statement with an expression deparser that marks the values it prints. */ +final class ShowExpressionDeParsing { + + private ShowExpressionDeParsing() { + // utility class + } + + /** Strings print as {@code } and numbers as {@code #value}. */ + static String deparse(String sql) throws JSQLParserException { + ExpressionDeParser expressions = new ExpressionDeParser() { + @Override + public StringBuilder visit(StringValue value, S context) { + return getBuilder().append('<').append(value.getValue()).append('>'); + } + + @Override + public StringBuilder visit(LongValue value, S context) { + return getBuilder().append('#').append(value.getValue()); + } + }; + StringBuilder builder = new StringBuilder(); + CCJSqlParserUtil.parse(sql) + .accept(new StatementDeParser(expressions, new SelectDeParser(), builder)); + return builder.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidatorTest.java new file mode 100644 index 0000000000..20771653de --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/MySqlShowStatementValidatorTest.java @@ -0,0 +1,79 @@ +/*- + * #%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 static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +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.MariaDbVersion; +import net.sf.jsqlparser.util.validation.feature.MySqlVersion; +import net.sf.jsqlparser.util.validation.feature.PostgresqlVersion; +import net.sf.jsqlparser.util.validation.metadata.AbstractDatabaseMetaDataCapability; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamesLookup; +import org.junit.jupiter.api.Test; + +public class MySqlShowStatementValidatorTest extends ValidationTestAsserts { + + @Test + public void testAllowedForMySqlAndMariaDb() throws JSQLParserException { + for (String sql : new String[] {"SHOW GLOBAL VARIABLES LIKE 'a%'", "SHOW FULL PROCESSLIST", + "SHOW TRIGGERS FROM db", "SHOW GRANTS FOR 'u'@'%'", "SHOW WARNINGS LIMIT 5", + "SHOW BINLOG EVENTS IN 'b.000001'", "SHOW ENGINE INNODB STATUS", + "SHOW PROCEDURE CODE p", "SHOW BINARY LOGS"}) { + validateNoErrors(sql, 1, MySqlVersion.V8_0, MariaDbVersion.V10_5_4, + DatabaseType.MYSQL, DatabaseType.MARIADB); + } + } + + @Test + public void testNotAllowedForPostgresql() throws JSQLParserException { + validate("SHOW GLOBAL STATUS", 1, 1, PostgresqlVersion.V14); + } + + @Test + public void testFeatureNotAllowed() throws JSQLParserException { + validateNotAllowed("SHOW DATABASES LIKE 'a%'", 1, 1, FeaturesAllowed.DML, + Feature.mySqlShow); + } + + @Test + public void testMetadataLooksUpDatabasesAndProcedures() throws JSQLParserException { + List names = new ArrayList<>(); + AbstractDatabaseMetaDataCapability metadata = + new AbstractDatabaseMetaDataCapability(null, NamesLookup.NO_TRANSFORMATION) { + @Override + protected boolean databaseExists(Map results, Named named) { + names.add("database " + named.getFqn()); + return true; + } + + @Override + protected boolean procedureExists(Map results, Named named) { + names.add("procedure " + named.getFqn()); + return true; + } + }; + + validateNoErrors("SHOW EVENTS IN db", 1, metadata); + validateNoErrors("SHOW PROCEDURE CODE s.p", 1, metadata); + validateNoErrors("SHOW FUNCTION CODE f", 1, metadata); + validateNoErrors("SHOW GLOBAL STATUS", 1, metadata); + assertEquals(Arrays.asList("database db", "procedure s.p"), names); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowIndexStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowIndexStatementValidatorTest.java index 26b62d3820..d06fd6f1a8 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowIndexStatementValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowIndexStatementValidatorTest.java @@ -9,7 +9,12 @@ */ package net.sf.jsqlparser.util.validation.validator; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; +import java.util.Map; import org.junit.jupiter.api.Test; @@ -17,6 +22,9 @@ 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.metadata.AbstractDatabaseMetaDataCapability; +import net.sf.jsqlparser.util.validation.metadata.Named; +import net.sf.jsqlparser.util.validation.metadata.NamesLookup; /** * @@ -27,7 +35,8 @@ public class ShowIndexStatementValidatorTest extends ValidationTestAsserts { @Test public void testValidationShowIndex() throws Exception { - for (String sql : Arrays.asList("SHOW INDEX FROM mydatabase")) { + for (String sql : Arrays.asList("SHOW INDEX FROM mydatabase", + "SHOW EXTENDED KEYS IN t FROM db WHERE Non_unique = 0")) { validateNoErrors(sql, 1, DatabaseType.MARIADB, DatabaseType.MYSQL); } } @@ -39,4 +48,21 @@ public void testValidationShowIndexNotAllowed() throws Exception { validateNotAllowed(sql, 1, 1, FeaturesAllowed.DML, Feature.showIndex); } } + + @Test + public void testMetadataLooksUpTheTableInTheTrailingDatabase() throws Exception { + List names = new ArrayList<>(); + AbstractDatabaseMetaDataCapability metadata = + new AbstractDatabaseMetaDataCapability(null, NamesLookup.NO_TRANSFORMATION) { + @Override + protected boolean tableExists(Map results, Named named) { + names.add(named.getFqn()); + return true; + } + }; + + validateNoErrors("SHOW INDEX FROM t FROM db", 1, metadata); + validateNoErrors("SHOW INDEXES IN s.t2", 1, metadata); + assertEquals(Arrays.asList("db.t", "s.t2"), names); + } }