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); + } }