diff --git a/README.md b/README.md index d39e81ea68..32e6f19f8b 100644 --- a/README.md +++ b/README.md @@ -145,6 +145,7 @@ and missing syntax gets added on demand — [open an issue](https://github.com/J | **DDL** | `CREATE …` · `ALTER …` · `DROP …` | | **MySQL table maintenance** | `ANALYZE` · `CHECK` · `OPTIMIZE` · `REPAIR TABLE` | | **PostgreSQL RLS** | `CREATE POLICY` · `ALTER TABLE … ENABLE`/`DISABLE`/`FORCE`/`NO FORCE ROW LEVEL SECURITY` | +| **MySQL metadata statements** | `SHOW [FULL] COLUMNS`/`FIELDS` · `SHOW TABLE STATUS` · `SHOW CREATE TABLE`/`VIEW`/`PROCEDURE`/`FUNCTION`/`TRIGGER`/`EVENT`/`DATABASE`/`USER` | | **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 0d3d080fbb..5fb07df29b 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -683,6 +683,18 @@ public enum Feature { * @see ShowIndexStatement */ showIndex, + /** + * MySQL/MariaDB "SHOW TABLE STATUS" statement is allowed + * + * @see ShowTableStatusStatement + */ + showTableStatus, + /** + * MySQL/MariaDB "SHOW CREATE" statement is allowed + * + * @see ShowCreateStatement + */ + showCreate, /** * @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 51f1ed26d9..b5ec8324d4 100644 --- a/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java +++ b/src/main/java/net/sf/jsqlparser/statement/ShowColumnsStatement.java @@ -9,29 +9,148 @@ */ package net.sf.jsqlparser.statement; +import net.sf.jsqlparser.expression.Expression; +import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; + +/** + * A MySQL {@code SHOW [EXTENDED] [FULL] {COLUMNS | FIELDS} {FROM | IN} table [{FROM | IN} db] + * [LIKE 'pattern' | WHERE expr]} statement. + * + * @see MySQL SHOW COLUMNS + */ public class ShowColumnsStatement implements Statement { - private String tableName; + private boolean extended; + private boolean full; + 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; public ShowColumnsStatement() { // empty constructor } public ShowColumnsStatement(String tableName) { - this.tableName = tableName; + setTableName(tableName); + } + + public ShowColumnsStatement(Table table) { + this.table = table; + } + + public boolean isExtended() { + return extended; + } + + public void setExtended(boolean extended) { + this.extended = extended; + } + + public boolean isFull() { + return full; + } + + public void setFull(boolean full) { + this.full = full; + } + + /** Whether the statement is spelled with {@code FIELDS} instead of {@code COLUMNS}. */ + public boolean isFieldsKeyword() { + return fieldsKeyword; + } + + public void setFieldsKeyword(boolean fieldsKeyword) { + this.fieldsKeyword = fieldsKeyword; + } + + /** The keyword before the table name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getTableSelectionMode() { + return tableSelectionMode; + } + + public void setTableSelectionMode(SelectionMode tableSelectionMode) { + this.tableSelectionMode = 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 keyword before the database name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getDbSelectionMode() { + return dbSelectionMode; + } + + public void setDbSelectionMode(SelectionMode dbSelectionMode) { + this.dbSelectionMode = dbSelectionMode; + } + + /** The database of the trailing {@code FROM db} or {@code IN db} clause, or null. */ + public String getDbName() { + return dbName; + } + + public void setDbName(String dbName) { + this.dbName = dbName; + } + + public Expression getLikeExpression() { + return likeExpression; + } + + public void setLikeExpression(Expression likeExpression) { + this.likeExpression = likeExpression; + } + + public Expression getWhereCondition() { + return whereCondition; + } + + public void setWhereCondition(Expression whereCondition) { + this.whereCondition = whereCondition; } @Override public String toString() { - return "SHOW COLUMNS FROM " + tableName; + StringBuilder builder = new StringBuilder("SHOW"); + if (extended) { + builder.append(" EXTENDED"); + } + if (full) { + builder.append(" FULL"); + } + 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 (likeExpression != null) { + builder.append(" LIKE ").append(likeExpression); + } else if (whereCondition != null) { + builder.append(" WHERE ").append(whereCondition); + } + return builder.toString(); } @Override @@ -43,4 +162,49 @@ public ShowColumnsStatement withTableName(String tableName) { this.setTableName(tableName); return this; } + + public ShowColumnsStatement withTable(Table table) { + this.setTable(table); + return this; + } + + public ShowColumnsStatement withExtended(boolean extended) { + this.setExtended(extended); + return this; + } + + public ShowColumnsStatement withFull(boolean full) { + this.setFull(full); + return this; + } + + public ShowColumnsStatement withFieldsKeyword(boolean fieldsKeyword) { + this.setFieldsKeyword(fieldsKeyword); + return this; + } + + public ShowColumnsStatement withTableSelectionMode(SelectionMode tableSelectionMode) { + this.setTableSelectionMode(tableSelectionMode); + return this; + } + + public ShowColumnsStatement withDbSelectionMode(SelectionMode dbSelectionMode) { + this.setDbSelectionMode(dbSelectionMode); + return this; + } + + public ShowColumnsStatement withDbName(String dbName) { + this.setDbName(dbName); + return this; + } + + public ShowColumnsStatement withLikeExpression(Expression likeExpression) { + this.setLikeExpression(likeExpression); + return this; + } + + public ShowColumnsStatement withWhereCondition(Expression whereCondition) { + this.setWhereCondition(whereCondition); + 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 91b2721249..5298d37080 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementFeatureVisitor.java @@ -109,7 +109,9 @@ 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.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.Update; @@ -923,6 +925,16 @@ public Void visit(ShowTablesStatement showTables, S context) { return metadataOnly(); } + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + return metadataOnly(); + } + + @Override + public Void visit(ShowCreateStatement showCreate, 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 bf0bf3e8b5..9910767a06 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitor.java @@ -84,7 +84,9 @@ import net.sf.jsqlparser.statement.merge.Merge; import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.select.Select; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -311,6 +313,22 @@ default void visit(ShowTablesStatement showTables) { this.visit(showTables, null); } + default T visit(ShowTableStatusStatement showTableStatus, S context) { + return null; + } + + default void visit(ShowTableStatusStatement showTableStatus) { + this.visit(showTableStatus, null); + } + + default T visit(ShowCreateStatement showCreate, S context) { + return null; + } + + default void visit(ShowCreateStatement showCreate) { + this.visit(showCreate, 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 8a80763ca3..2c22fc97df 100644 --- a/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/statement/StatementVisitorAdapter.java @@ -98,7 +98,9 @@ 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.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -567,6 +569,16 @@ public T visit(ShowTablesStatement showTables, S context) { return null; } + @Override + public T visit(ShowTableStatusStatement showTableStatus, S context) { + return null; + } + + @Override + public T visit(ShowCreateStatement showCreate, 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/ShowCreateStatement.java b/src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java new file mode 100644 index 0000000000..fa36ea1717 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java @@ -0,0 +1,191 @@ +/*- + * #%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.expression.StringValue; +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; + +/** + * A MySQL {@code SHOW CREATE} statement, which returns the definition of one object. + * + *
    + *
  • {@code TABLE}, {@code VIEW}, {@code PROCEDURE}, {@code FUNCTION}, {@code TRIGGER} and + * {@code EVENT} name the object with {@link #getName()}.
  • + *
  • {@code DATABASE} (also spelled {@code SCHEMA}) uses {@link #getDatabaseName()} and may carry + * {@code IF NOT EXISTS}.
  • + *
  • {@code USER} uses {@link #getAccount()}, which is {@code CURRENT_USER} for the session + * account.
  • + *
+ * + * @see MySQL SHOW CREATE + * TABLE + */ +public class ShowCreateStatement implements Statement { + + public enum ObjectType { + TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, EVENT, DATABASE, USER; + + /** Whether objects of this type are named by {@link ShowCreateStatement#getName()}. */ + public boolean isNamedByTable() { + return this != DATABASE && this != USER; + } + } + + private ObjectType objectType; + private boolean schemaKeyword; + private boolean ifNotExists; + private Table name; + private String databaseName; + private UserAccount account; + private boolean currentUserParentheses; + + public ShowCreateStatement() { + // empty constructor + } + + public ShowCreateStatement(ObjectType objectType, Table name) { + this.objectType = objectType; + this.name = name; + } + + public ObjectType getObjectType() { + return objectType; + } + + public void setObjectType(ObjectType objectType) { + this.objectType = objectType; + } + + /** Whether {@code DATABASE} was spelled {@code SCHEMA}. */ + public boolean isSchemaKeyword() { + return schemaKeyword; + } + + public void setSchemaKeyword(boolean schemaKeyword) { + this.schemaKeyword = schemaKeyword; + } + + /** Whether {@code SHOW CREATE DATABASE IF NOT EXISTS} was written. */ + public boolean isIfNotExists() { + return ifNotExists; + } + + public void setIfNotExists(boolean ifNotExists) { + this.ifNotExists = ifNotExists; + } + + /** The table, view, routine, trigger or event name; null for DATABASE and USER. */ + public Table getName() { + return name; + } + + public void setName(Table name) { + this.name = name; + } + + /** The database name of {@code SHOW CREATE DATABASE}; null otherwise. */ + public String getDatabaseName() { + return databaseName; + } + + public void setDatabaseName(String databaseName) { + this.databaseName = databaseName; + } + + /** The account of {@code SHOW CREATE USER}; null otherwise. */ + public UserAccount getAccount() { + return account; + } + + public void setAccount(UserAccount account) { + this.account = account; + } + + /** Whether the account was written as {@code CURRENT_USER()}. */ + public boolean isCurrentUserParentheses() { + return currentUserParentheses; + } + + public void setCurrentUserParentheses(boolean currentUserParentheses) { + this.currentUserParentheses = currentUserParentheses; + } + + /** Whether the account is {@code CURRENT_USER} or {@code CURRENT_USER()}. */ + public boolean isCurrentUser() { + if (account == null || account.getHost() != null || account.getUser() == null) { + return false; + } + StringValue user = account.getUser(); + return "".equals(user.getQuoteStr()) && "CURRENT_USER".equalsIgnoreCase(user.getValue()); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("SHOW CREATE "); + if (objectType == ObjectType.DATABASE) { + builder.append(schemaKeyword ? "SCHEMA" : "DATABASE"); + if (ifNotExists) { + builder.append(" IF NOT EXISTS"); + } + builder.append(' ').append(databaseName); + } else if (objectType == ObjectType.USER) { + builder.append("USER ").append(account); + if (currentUserParentheses) { + builder.append("()"); + } + } else { + builder.append(objectType).append(' ').append(name); + } + return builder.toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public ShowCreateStatement withObjectType(ObjectType objectType) { + this.setObjectType(objectType); + return this; + } + + public ShowCreateStatement withSchemaKeyword(boolean schemaKeyword) { + this.setSchemaKeyword(schemaKeyword); + return this; + } + + public ShowCreateStatement withIfNotExists(boolean ifNotExists) { + this.setIfNotExists(ifNotExists); + return this; + } + + public ShowCreateStatement withName(Table name) { + this.setName(name); + return this; + } + + public ShowCreateStatement withDatabaseName(String databaseName) { + this.setDatabaseName(databaseName); + return this; + } + + public ShowCreateStatement withAccount(UserAccount account) { + this.setAccount(account); + return this; + } + + public ShowCreateStatement withCurrentUserParentheses(boolean currentUserParentheses) { + this.setCurrentUserParentheses(currentUserParentheses); + 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 new file mode 100644 index 0000000000..d9a2385e84 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java @@ -0,0 +1,102 @@ +/*- + * #%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.expression.Expression; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementVisitor; +import net.sf.jsqlparser.statement.show.ShowTablesStatement.SelectionMode; + +/** + * A MySQL {@code SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'pattern' | WHERE expr]} statement. + * + * @see MySQL SHOW TABLE + * STATUS + */ +public class ShowTableStatusStatement implements Statement { + + private SelectionMode selectionMode = SelectionMode.FROM; + private String dbName; + private Expression likeExpression; + private Expression whereCondition; + + /** The keyword before the database name, {@code FROM} unless {@code IN} was written. */ + public SelectionMode getSelectionMode() { + return selectionMode; + } + + public void setSelectionMode(SelectionMode selectionMode) { + this.selectionMode = selectionMode; + } + + public String getDbName() { + return dbName; + } + + public void setDbName(String dbName) { + this.dbName = dbName; + } + + public Expression getLikeExpression() { + return likeExpression; + } + + public void setLikeExpression(Expression likeExpression) { + this.likeExpression = likeExpression; + } + + public Expression getWhereCondition() { + return whereCondition; + } + + public void setWhereCondition(Expression whereCondition) { + this.whereCondition = 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); + } + if (likeExpression != null) { + builder.append(" LIKE ").append(likeExpression); + } else if (whereCondition != null) { + builder.append(" WHERE ").append(whereCondition); + } + return builder.toString(); + } + + @Override + public T accept(StatementVisitor statementVisitor, S context) { + return statementVisitor.visit(this, context); + } + + public ShowTableStatusStatement withSelectionMode(SelectionMode selectionMode) { + this.setSelectionMode(selectionMode); + return this; + } + + public ShowTableStatusStatement withDbName(String dbName) { + this.setDbName(dbName); + return this; + } + + public ShowTableStatusStatement withLikeExpression(Expression likeExpression) { + this.setLikeExpression(likeExpression); + return this; + } + + public ShowTableStatusStatement withWhereCondition(Expression whereCondition) { + this.setWhereCondition(whereCondition); + return this; + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java index aeecdb89d8..89c6b31eea 100644 --- a/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java +++ b/src/main/java/net/sf/jsqlparser/util/TablesNamesFinder.java @@ -243,7 +243,9 @@ 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.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -1879,7 +1881,14 @@ public void visit(ResetStatement reset) { @Override public Void visit(ShowColumnsStatement showColumnsStatement, S context) { - throwUnsupported(showColumnsStatement); + 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()); + } + if (table != null) { + visit(table, context); + } return null; } @@ -2336,6 +2345,32 @@ public void visit(ShowTablesStatement showTables) { StatementVisitor.super.visit(showTables); } + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + // reads the status of the tables of a database, but names no table + return null; + } + + @Override + public void visit(ShowTableStatusStatement showTableStatus) { + StatementVisitor.super.visit(showTableStatus); + } + + @Override + public Void visit(ShowCreateStatement showCreate, S context) { + ShowCreateStatement.ObjectType type = showCreate.getObjectType(); + if ((type == ShowCreateStatement.ObjectType.TABLE + || type == ShowCreateStatement.ObjectType.VIEW) && showCreate.getName() != null) { + visit(showCreate.getName(), context); + } + return null; + } + + @Override + public void visit(ShowCreateStatement showCreate) { + StatementVisitor.super.visit(showCreate); + } + @Override public Void visit(TSQLLeftJoin tsqlLeftJoin, S context) { visitBinaryExpression(tsqlLeftJoin); diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java index 9fc744010a..1bc8534a53 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ShowColumnsStatementDeParser.java @@ -19,6 +19,6 @@ public ShowColumnsStatementDeParser(StringBuilder buffer) { @Override public void deParse(ShowColumnsStatement show) { - builder.append("SHOW COLUMNS FROM ").append(show.getTableName()); + builder.append(show); } } 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 2490baa3ec..1bbe47c9c1 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/StatementDeParser.java @@ -114,7 +114,9 @@ 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.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -454,6 +456,18 @@ public StringBuilder visit(ShowTablesStatement showTables, S context) { return builder; } + @Override + public StringBuilder visit(ShowTableStatusStatement showTableStatus, S context) { + builder.append(showTableStatus); + return builder; + } + + @Override + public StringBuilder visit(ShowCreateStatement showCreate, S context) { + builder.append(showCreate); + return builder; + } + @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 de84d3f823..3f49533302 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 @@ -122,7 +122,9 @@ public enum MariaDbVersion implements Version { Feature.show, // https://mariadb.com/kb/en/show-tables/ Feature.showTables, // https://mariadb.com/kb/en/show-columns/ Feature.showColumns, // https://mariadb.com/kb/en/show-index/ - Feature.showIndex, // https://mariadb.com/kb/en/use/ + 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.use, // https://mariadb.com/kb/en/grant/ Feature.grant, // https://mariadb.com/kb/en/commit/ Feature.commit, // https://mariadb.com/kb/en/optimizer-hints/ 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 6aabf1fe0d..1a25274ea9 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 @@ -111,7 +111,9 @@ public enum MySqlVersion implements Version { Feature.show, // https://dev.mysql.com/doc/refman/8.0/en/show-tables.html Feature.showTables, // https://dev.mysql.com/doc/refman/8.0/en/show-columns.html 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/grant.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.grant, // https://dev.mysql.com/doc/refman/8.0/en/use.html Feature.use, // https://dev.mysql.com/doc/refman/8.0/en/commit.html Feature.commit, Feature.startTransaction, Feature.releaseSavepoint, 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 e917d9e8ce..4cb91dbf45 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 @@ -20,6 +20,11 @@ public class ShowColumnsStatementValidator extends AbstractValidator { + + @Override + public void validate(ShowCreateStatement show) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.showCreate); + NamedObject namedObject = namedObject(show.getObjectType()); + if (namedObject == NamedObject.database) { + validateOptionalName(c, namedObject, show.getDatabaseName()); + } else if (namedObject != null && show.getName() != null) { + validateName(c, namedObject, show.getName().getFullyQualifiedName()); + } + } + } + + /** The metadata kind checked for the shown object, or null if there is none. */ + private static NamedObject namedObject(ShowCreateStatement.ObjectType type) { + if (type == null) { + return null; + } + switch (type) { + case TABLE: + return NamedObject.table; + case VIEW: + return NamedObject.view; + case PROCEDURE: + return NamedObject.procedure; + case DATABASE: + return NamedObject.database; + default: + // functions, triggers, events and accounts are not looked up + return null; + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java new file mode 100644 index 0000000000..1903f358b9 --- /dev/null +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java @@ -0,0 +1,27 @@ +/*- + * #%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.ShowTableStatusStatement; +import net.sf.jsqlparser.util.validation.ValidationCapability; +import net.sf.jsqlparser.util.validation.metadata.NamedObject; + +public class ShowTableStatusStatementValidator + extends AbstractValidator { + + @Override + public void validate(ShowTableStatusStatement show) { + for (ValidationCapability c : getCapabilities()) { + validateFeature(c, Feature.showTableStatus); + validateOptionalName(c, NamedObject.database, show.getDbName()); + } + } +} diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java index 1de8380a75..fbe7846c53 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/StatementValidator.java @@ -114,7 +114,9 @@ import net.sf.jsqlparser.statement.refresh.RefreshMaterializedViewStatement; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.SelectVisitor; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import net.sf.jsqlparser.statement.show.ShowIndexStatement; +import net.sf.jsqlparser.statement.show.ShowTableStatusStatement; import net.sf.jsqlparser.statement.show.ShowTablesStatement; import net.sf.jsqlparser.statement.truncate.Truncate; import net.sf.jsqlparser.statement.update.ParenthesedUpdate; @@ -383,6 +385,18 @@ public Void visit(ShowTablesStatement showTables, S context) { return null; } + @Override + public Void visit(ShowTableStatusStatement showTableStatus, S context) { + getValidator(ShowTableStatusStatementValidator.class).validate(showTableStatus); + return null; + } + + @Override + public Void visit(ShowCreateStatement showCreate, S context) { + getValidator(ShowCreateStatementValidator.class).validate(showCreate); + return null; + } + @Override public Void visit(Block block, S context) { validateFeature(Feature.block); @@ -774,6 +788,16 @@ public void visit(ShowTablesStatement showTables) { visit(showTables, null); } + @Override + public void visit(ShowTableStatusStatement showTableStatus) { + visit(showTableStatus, null); + } + + @Override + public void visit(ShowCreateStatement showCreate) { + visit(showCreate, 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 2210dbe9d8..6deb75742e 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -6203,12 +6203,20 @@ Statement Show(): { ( - LOOKAHEAD(2) statement = ShowColumns() + // 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(2) statement = ShowTables() | + LOOKAHEAD(ShowTableStatus() ShowStatementEnd(), + { getToken(1).kind == K_TABLE && "STATUS".equalsIgnoreCase(getToken(2).image) }) + statement = ShowTableStatus() + | + LOOKAHEAD(ShowCreate() ShowStatementEnd()) statement = ShowCreate() + | // any of the RDBMS specific SHOW syntax captureRest = captureRest() { @@ -6224,13 +6232,135 @@ 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(). + */ +void ShowStatementEnd(): +{} +{ + + | + + | + + | + + | + LOOKAHEAD({ getToken(1).kind != K_FROM && getToken(1).kind != K_PIVOT + && getToken(1).kind != K_UNPIVOT && isStatementStartAhead() }) {} +} + +// https://dev.mysql.com/doc/refman/8.4/en/show-columns.html ShowColumnsStatement ShowColumns(): { - String tableName; + ShowColumnsStatement statement = new ShowColumnsStatement(); + Table table; + String dbName; + Expression expression; +} +{ + [ { statement.setExtended(true); } ] + [ { statement.setFull(true); } ] + ( | { statement.setFieldsKeyword(true); } ) + ( + + | + { 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); } + ] + { + return statement; + } +} + +// https://dev.mysql.com/doc/refman/8.4/en/show-table-status.html +ShowTableStatusStatement ShowTableStatus(): { + ShowTableStatusStatement statement = new ShowTableStatusStatement(); + String dbName; + Expression expression; +} +{ + ContextualKeyword("STATUS") + [ + LOOKAHEAD(2) + ( + + | + { statement.setSelectionMode(ShowTablesStatement.SelectionMode.IN); } + ) + dbName = RelObjectName() { statement.setDbName(dbName); } + ] + [ + expression = SimpleExpression() { statement.setLikeExpression(expression); } + | + expression = Expression() { statement.setWhereCondition(expression); } + ] + { + return statement; + } +} + +/** + * MySQL SHOW CREATE {TABLE | VIEW | PROCEDURE | FUNCTION | TRIGGER | EVENT} name, + * SHOW CREATE {DATABASE | SCHEMA} [IF NOT EXISTS] db and SHOW CREATE USER account. + */ +ShowCreateStatement ShowCreate(): { + ShowCreateStatement statement = new ShowCreateStatement(); + Table name; + String databaseName; + String currentUser; + UserAccount account = new UserAccount(); } { - tableName = RelObjectName() + + ( + ( + { statement.setObjectType(ShowCreateStatement.ObjectType.TABLE); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.VIEW); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.PROCEDURE); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.FUNCTION); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.TRIGGER); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.EVENT); } + ) + name = Table() { statement.setName(name); } + | + ( | { statement.setSchemaKeyword(true); } ) + { statement.setObjectType(ShowCreateStatement.ObjectType.DATABASE); } + [ LOOKAHEAD(3) { statement.setIfNotExists(true); } ] + databaseName = RelObjectName() { statement.setDatabaseName(databaseName); } + | + { statement.setObjectType(ShowCreateStatement.ObjectType.USER); } + ( + LOOKAHEAD({ isKeywordAhead("CURRENT_USER") }) currentUser = RelObjectName() + { account.setUser(new StringValue(currentUser).setQuoteStr("")); } + [ LOOKAHEAD(2) "(" ")" { statement.setCurrentUserParentheses(true); } ] + | + account = MySqlAccountName() + ) + { statement.setAccount(account); } + ) { - return new ShowColumnsStatement(tableName); + return statement; } } @@ -15502,11 +15632,66 @@ StringValue MySqlAccountNamePart(): ( token= { value = stringValue(token); } | - name=RelObjectName() { value = new StringValue(name).setQuoteStr(""); } + name=RelObjectName() + { + // a double-quoted or backquoted part keeps its quote character separately from its + // value, like a string literal; new StringValue(name) would drop the double quotes + value = name.length() > 1 && (name.charAt(0) == '"' || name.charAt(0) == '`') + && name.charAt(name.length() - 1) == name.charAt(0) + ? new StringValue().withValue(name.substring(1, name.length() - 1)) + .setQuoteStr(name.substring(0, 1)) + : new StringValue().withValue(name).setQuoteStr(""); + } ) { return value; } } +/** + * A MySQL account name, user[@host], shared by CREATE USER, DEFINER and the account statements. + * Each part is unquoted or quoted with ', " or `. The lexer reads an unquoted user@host, and the + * user@ in front of a quoted host, as one identifier, and @host after a quoted user as one + * at-identifier, so those spellings are split here. + */ +UserAccount MySqlAccountName(): +{ + UserAccount account = new UserAccount(); + StringValue part; + Token token; + int at; +} +{ + ( + // user@ followed by a quoted host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.length() - 1 }) + token= + { account.setUser(new StringValue().withValue(token.image.substring(0, token.image.length() - 1)) + .setQuoteStr("")); } + part=MySqlAccountNamePart() { account.setHost(part); } + | + // unquoted user@host + LOOKAHEAD({ getToken(1).kind == S_IDENTIFIER && getToken(1).image.indexOf('@') > 0 + && getToken(1).image.indexOf('@') == getToken(1).image.lastIndexOf('@') }) + token= + { + at = token.image.indexOf('@'); + account.setUser(new StringValue().withValue(token.image.substring(0, at)).setQuoteStr("")); + account.setHost(new StringValue().withValue(token.image.substring(at + 1)).setQuoteStr("")); + } + | + part=MySqlAccountNamePart() { account.setUser(part); } + [ + part=MySqlAccountNamePart() { account.setHost(part); } + | + // a quoted user followed by an unquoted @host + LOOKAHEAD({ getToken(1).kind == S_AT_IDENTIFIER && !getToken(1).image.startsWith("@@") }) + token= + { account.setHost(new StringValue().withValue(token.image.substring(1)).setQuoteStr("")); } + ] + ) + { return account; } +} + UserAuthentication MySqlUserAuthentication(): { UserAuthentication authentication = new UserAuthentication(); @@ -15557,14 +15742,11 @@ UserAuthentication MySqlUserAuthentication(): UserAccount MySqlUserAccount(): { - UserAccount account = new UserAccount(); - StringValue user = null; - StringValue host = null; + UserAccount account; UserAuthentication authentication = null; } { - user=MySqlAccountNamePart() { account.setUser(user); } - [ host=MySqlAccountNamePart() { account.setHost(host); } ] + account=MySqlAccountName() [ authentication=MySqlUserAuthentication() { account.setAuthentication(authentication); } ] { return account; } } @@ -15710,8 +15892,7 @@ AlterEvent AlterEvent(): /** Account spelling is shared, including CURRENT_USER and CURRENT_USER(). */ void MySqlDefiner(MySqlDefiner definer): { - StringValue user; - StringValue host; + UserAccount account; String name; } { @@ -15721,8 +15902,7 @@ void MySqlDefiner(MySqlDefiner definer): { definer.setUser(new StringValue(name).setQuoteStr("")); } [ "(" ")" { definer.setCurrentUserParentheses(true); } ] | - user=MySqlAccountNamePart() { definer.setUser(user); } - [ host=MySqlAccountNamePart() { definer.setHost(host); } ] + account=MySqlAccountName() { definer.setUser(account.getUser()); definer.setHost(account.getHost()); } ) } diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 5f3b0157b4..55962c1f30 100644 --- a/src/site/sphinx/index.rst +++ b/src/site/sphinx/index.rst @@ -160,6 +160,8 @@ One grammar covers the SQL standard plus all major RDBMS. Missing syntax gets ad - ``ANALYZE`` · ``CHECK`` · ``OPTIMIZE`` · ``REPAIR TABLE`` * - **PostgreSQL RLS** - ``CREATE POLICY`` · ``ALTER TABLE … ENABLE``/``DISABLE``/``FORCE``/``NO FORCE ROW LEVEL SECURITY`` + * - **MySQL metadata statements** + - ``SHOW [FULL] COLUMNS``/``FIELDS`` · ``SHOW TABLE STATUS`` · ``SHOW CREATE TABLE``/``VIEW``/``PROCEDURE``/``FUNCTION``/``TRIGGER``/``EVENT``/``DATABASE``/``USER`` * - **Salesforce SOQL** - ``INCLUDES`` · ``EXCLUDES`` diff --git a/src/site/sphinx/usage.rst b/src/site/sphinx/usage.rst index 901608d62c..3a13967601 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -1718,6 +1718,20 @@ the PostgreSQL ``CreateRole`` AST for this ambiguous form:: "CREATE USER app", parser -> parser.withDialect(AbstractJSqlParser.Dialect.POSTGRESQL)); +MySQL account names (``user[@host]``) of ``CREATE USER`` and of view, trigger and +routine ``DEFINER`` clauses share one grammar rule and are read into separate user +and host ``StringValue`` parts (``UserAccount`` and ``MySqlDefiner``). Every MySQL +spelling is accepted, including ``app@localhost``, ``app@'%'`` and +``'app'@localhost``. A part quoted with ``'``, ``"`` or a backquote keeps its quote +character in ``getQuoteStr()`` and its text in ``getValue()``:: + + CreateUser createUser = (CreateUser) CCJSqlParserUtil.parse( + "CREATE USER `app`@'%' IDENTIFIED BY 'secret'"); + UserAccount account = createUser.getAccounts().get(0); + account.getUser().getValue(); // app + account.getUser().getQuoteStr(); // ` + account.getHost().getValue(); // % + ``Grant`` retains its string-based getters, setters and fluent methods. Its ``PrivilegeClause`` exposes typed ``Privilege`` items, column lists, ``PrivilegeTarget`` kinds, multiple role memberships and grantor information. @@ -2039,3 +2053,47 @@ The validator checks the new statement and mode capabilities. PostgreSQL ``AND [NO] CHAIN`` is enabled beginning with its version 12 capability set. This support does not include MySQL transaction completion ``[NO] RELEASE``, PostgreSQL ``COMMIT PREPARED`` / ``ROLLBACK PREPARED``, or XA commands. + +MySQL SHOW statements +===================== + +The MySQL statements that read table, column and object definitions parse to typed +statements in every dialect: + +* ``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. +* ``SHOW TABLE STATUS [{FROM | IN} db] [LIKE 'p' | WHERE expr]`` parses to + ``ShowTableStatusStatement``, with the same accessors as ``ShowTablesStatement``. +* ``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 + ``getDatabaseName()``, ``isSchemaKeyword()`` and ``isIfNotExists()``. ``USER`` uses + ``getAccount()``, a ``UserAccount`` without authentication; ``isCurrentUser()`` and + ``isCurrentUserParentheses()`` describe ``CURRENT_USER`` and ``CURRENT_USER()``. + +.. code-block:: java + + ShowColumnsStatement columns = (ShowColumnsStatement) CCJSqlParserUtil.parse( + "SHOW FULL COLUMNS FROM orders FROM shop LIKE 'c%'"); + columns.getTableName(); // orders + columns.getDbName(); // shop + + ShowCreateStatement create = (ShowCreateStatement) CCJSqlParserUtil.parse( + "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``. + +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``. diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java index 825a18def0..eb07f57d62 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowColumnsStatementTest.java @@ -9,9 +9,28 @@ */ 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.expression.StringValue; +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.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; /** * @@ -19,8 +38,154 @@ */ public class ShowColumnsStatementTest { + 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 ShowColumnsStatement roundTrip(String sql) throws JSQLParserException { + ShowColumnsStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowColumnsStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowColumnsStatement) statement; + } + return result; + } + @Test public void testSimpleUse() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed("SHOW COLUMNS FROM mydatabase"); } + + @Test + void plainFormKeepsTheTableNameApi() throws JSQLParserException { + ShowColumnsStatement show = roundTrip("SHOW COLUMNS FROM mytable"); + + assertEquals("mytable", show.getTableName()); + assertEquals("mytable", show.getTable().getName()); + assertFalse(show.isExtended()); + assertFalse(show.isFull()); + assertFalse(show.isFieldsKeyword()); + assertEquals(SelectionMode.FROM, show.getTableSelectionMode()); + assertNull(show.getDbName()); + assertNull(show.getLikeExpression()); + assertNull(show.getWhereCondition()); + } + + @Test + void fullColumnsWithDatabaseAndLike() throws JSQLParserException { + ShowColumnsStatement show = roundTrip("SHOW FULL COLUMNS FROM gc20 FROM v LIKE 'n%'"); + + assertTrue(show.isFull()); + assertEquals("gc20", show.getTableName()); + assertEquals(SelectionMode.FROM, show.getDbSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals("n%", ((StringValue) show.getLikeExpression()).getValue()); + } + + @Test + void extendedFullFieldsInWithWhere() throws JSQLParserException { + ShowColumnsStatement show = + roundTrip("SHOW EXTENDED FULL FIELDS IN db.t IN `other` WHERE `Key` = 'PRI'"); + + assertTrue(show.isExtended()); + assertTrue(show.isFull()); + assertTrue(show.isFieldsKeyword()); + assertEquals(SelectionMode.IN, show.getTableSelectionMode()); + assertEquals("db", show.getTable().getSchemaName()); + assertEquals("db.t", show.getTableName()); + assertEquals(SelectionMode.IN, show.getDbSelectionMode()); + assertEquals("`other`", show.getDbName()); + assertEquals("`Key` = 'PRI'", show.getWhereCondition().toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW FIELDS FROM t", + "SHOW FULL FIELDS FROM gc20", + "SHOW COLUMNS IN t", + "SHOW COLUMNS FROM `db`.`t`", + "SHOW COLUMNS FROM t IN db", + "SHOW EXTENDED COLUMNS FROM t", + "SHOW COLUMNS FROM t LIKE 'a%'", + "SHOW FULL COLUMNS FROM t WHERE Comment <> ''" + }) + void parsesEveryMySqlForm(String sql) throws JSQLParserException { + roundTrip(sql); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW FULL FIELDS IN t FROM db", + CCJSqlParserUtil.parse("show full fields in t from db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW COLUMNS IN SCHEMA s", + "SHOW COLUMNS IN TABLE t", + "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" + }) + void otherDialectFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = + CCJSqlParserUtil.parseStatements("SHOW FULL COLUMNS FROM t;\nSELECT 1"); + + assertEquals(2, statements.size()); + assertInstanceOf(ShowColumnsStatement.class, statements.get(0)); + + IfElseStatement ifElse = (IfElseStatement) CCJSqlParserUtil + .parse("IF a = 1 SHOW COLUMNS FROM t ELSE SHOW TABLE STATUS"); + assertInstanceOf(ShowColumnsStatement.class, ifElse.getIfStatement()); + assertEquals("SHOW TABLE STATUS", ifElse.getElseStatement().toString()); + } + + @Test + void findsTheTable() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW FULL COLUMNS FROM s.t")) + .containsExactly("s.t"); + // the trailing FROM db names the database of the table + assertThat(TablesNamesFinder.findTables("SHOW COLUMNS FROM t FROM db")) + .containsExactly("db.t"); + } + + @Test + void legacyTableNameAccessors() { + ShowColumnsStatement show = new ShowColumnsStatement("db.t"); + + assertEquals("db", show.getTable().getSchemaName()); + assertEquals("db.t", show.getTableName()); + assertEquals("SHOW COLUMNS FROM db.t", show.toString()); + assertEquals("SHOW COLUMNS FROM t2", show.withTableName("t2").toString()); + show.setTableName(null); + assertNull(show.getTable()); + } + + @Test + void buildsAndDeparses() { + ShowColumnsStatement show = new ShowColumnsStatement(new Table("t")).withFull(true) + .withFieldsKeyword(true).withTableSelectionMode(SelectionMode.IN) + .withDbName("db").withLikeExpression(new StringValue("a%")); + + assertDeparse(show, "SHOW FULL FIELDS IN t FROM db LIKE 'a%'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = + CCJSqlParserUtil.parse("SHOW FULL COLUMNS FROM t").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java index 7f10338964..23830cbc9e 100644 --- a/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/ShowStatementTest.java @@ -10,6 +10,7 @@ package net.sf.jsqlparser.statement; import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.statement.show.ShowCreateStatement; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -43,6 +44,6 @@ void testShowCreateTable() throws JSQLParserException { String sqlStr = "show create table my_table"; Statement statement = assertSqlCanBeParsedAndDeparsed(sqlStr, true); - Assertions.assertTrue(statement instanceof UnsupportedStatement); + Assertions.assertTrue(statement instanceof ShowCreateStatement); } } diff --git a/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java new file mode 100644 index 0000000000..455f18b6a5 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java @@ -0,0 +1,123 @@ +/*- + * #%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.create.user; + +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.MySqlDefiner; +import net.sf.jsqlparser.statement.create.trigger.CreateTrigger; +import net.sf.jsqlparser.statement.create.view.CreateView; +import net.sf.jsqlparser.util.deparser.StatementDeParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.util.stream.Stream; + +class MySqlAccountNameTest { + + private static final Consumer MYSQL = p -> p.withDialect(Dialect.MYSQL); + private static final Consumer NO_DIALECT = p -> { + }; + + /** SQL spelling, user value, user quote, host value, host quote. */ + static Stream accounts() { + return Stream.of( + Arguments.of("app", "app", "", null, null), + Arguments.of("app@localhost", "app", "", "localhost", ""), + Arguments.of("app@'%'", "app", "", "%", "'"), + Arguments.of("app@'10.0.0.%'", "app", "", "10.0.0.%", "'"), + Arguments.of("'app'@localhost", "app", "'", "localhost", ""), + Arguments.of("'app'@'%'", "app", "'", "%", "'"), + Arguments.of("`app`@`%`", "app", "`", "%", "`"), + Arguments.of("\"app\"@\"%\"", "app", "\"", "%", "\""), + Arguments.of("'app'", "app", "'", null, null), + Arguments.of("`my app`@'%'", "my app", "`", "%", "'")); + } + + @ParameterizedTest + @MethodSource("accounts") + void createUserSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + for (Consumer mode : java.util.Arrays.asList(MYSQL, NO_DIALECT)) { + String sql = "CREATE USER " + account + " IDENTIFIED BY 'x'"; + CreateUser createUser = assertInstanceOf(CreateUser.class, + assertSqlCanBeParsedAndDeparsed(sql, true, mode)); + assertEquals(sql, createUser.toString()); + assertEquals(sql, deparse(createUser)); + assertPart(createUser.getAccounts().get(0).getUser(), user, userQuote); + assertPart(createUser.getAccounts().get(0).getHost(), host, hostQuote); + } + } + + @ParameterizedTest + @MethodSource("accounts") + void definerSplitsEveryAccountSpelling(String account, String user, String userQuote, + String host, String hostQuote) throws JSQLParserException { + String view = "CREATE DEFINER = " + account + " VIEW v AS SELECT 1"; + CreateView createView = assertInstanceOf(CreateView.class, + assertSqlCanBeParsedAndDeparsed(view, true, MYSQL)); + MySqlDefiner definer = createView.getMySqlOptions().getDefiner(); + assertPart(definer.getUser(), user, userQuote); + assertPart(definer.getHost(), host, hostQuote); + + String trigger = "CREATE DEFINER = " + account + + " TRIGGER trg BEFORE INSERT ON t FOR EACH ROW SET NEW.a = 1"; + CreateTrigger createTrigger = assertInstanceOf(CreateTrigger.class, + assertSqlCanBeParsedAndDeparsed(trigger, true, MYSQL)); + assertEquals(trigger, createTrigger.toString()); + } + + @ParameterizedTest + @ValueSource(strings = {"CREATE USER app@", "CREATE USER @host", "CREATE USER 'app'@"}) + void rejectsMalformedAccounts(String sql) { + assertThrows(JSQLParserException.class, () -> CCJSqlParserUtil.parse(sql, MYSQL)); + } + + @org.junit.jupiter.api.Test + void keepsDoubleQuotesWithoutDialect() throws JSQLParserException { + // before, the double quotes were dropped and the account rendered as app@% + String sql = "CREATE USER \"app\"@\"%\" IDENTIFIED BY 'x'"; + assertEquals(sql, CCJSqlParserUtil.parse(sql).toString()); + assertEquals(sql, CCJSqlParserUtil.parse(sql, MYSQL).toString()); + } + + @org.junit.jupiter.api.Test + void definerCurrentUserIsUnchanged() throws JSQLParserException { + CreateView view = assertInstanceOf(CreateView.class, assertSqlCanBeParsedAndDeparsed( + "CREATE DEFINER = CURRENT_USER() VIEW v AS SELECT 1", true, MYSQL)); + assertNull(view.getMySqlOptions().getDefiner().getHost()); + } + + private static void assertPart(StringValue part, String value, String quote) { + if (value == null) { + assertNull(part); + return; + } + assertEquals(value, part.getValue()); + assertEquals(quote, part.getQuoteStr()); + } + + private static String deparse(CreateUser createUser) { + StringBuilder builder = new StringBuilder(); + createUser.accept(new StatementDeParser(builder), null); + return builder.toString(); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java new file mode 100644 index 0000000000..2d82488e10 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java @@ -0,0 +1,212 @@ +/*- + * #%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.assertTrue; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +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.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +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.show.ShowCreateStatement.ObjectType; +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 ShowCreateStatementTest { + + 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 ShowCreateStatement roundTrip(String sql) throws JSQLParserException { + ShowCreateStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowCreateStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowCreateStatement) statement; + } + return result; + } + + @Test + void createTable() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE TABLE c20"); + + assertEquals(ObjectType.TABLE, show.getObjectType()); + assertEquals("c20", show.getName().getFullyQualifiedName()); + assertNull(show.getDatabaseName()); + assertNull(show.getAccount()); + } + + @ParameterizedTest + @CsvSource({ + "SHOW CREATE TABLE `db`.`t`, TABLE, `db`.`t`", + "SHOW CREATE VIEW db.v, VIEW, db.v", + "SHOW CREATE PROCEDURE gp13, PROCEDURE, gp13", + "SHOW CREATE FUNCTION db.f, FUNCTION, db.f", + "SHOW CREATE TRIGGER tr, TRIGGER, tr", + "SHOW CREATE EVENT db.e, EVENT, db.e" + }) + void namedObjects(String sql, ObjectType type, String name) throws JSQLParserException { + ShowCreateStatement show = roundTrip(sql); + + assertEquals(type, show.getObjectType()); + assertTrue(type.isNamedByTable()); + assertEquals(name, show.getName().getFullyQualifiedName()); + } + + @Test + void createDatabase() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE DATABASE `shop`"); + + assertEquals(ObjectType.DATABASE, show.getObjectType()); + assertFalse(show.isSchemaKeyword()); + assertFalse(show.isIfNotExists()); + assertEquals("`shop`", show.getDatabaseName()); + assertNull(show.getName()); + } + + @Test + void createSchemaIfNotExists() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE SCHEMA IF NOT EXISTS shop"); + + assertEquals(ObjectType.DATABASE, show.getObjectType()); + assertTrue(show.isSchemaKeyword()); + assertTrue(show.isIfNotExists()); + assertEquals("shop", show.getDatabaseName()); + } + + @Test + void createUserWithHost() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE USER 'app'@'%'"); + + assertEquals(ObjectType.USER, show.getObjectType()); + assertEquals("app", show.getAccount().getUser().getNotExcapedValue()); + assertEquals("%", show.getAccount().getHost().getNotExcapedValue()); + assertNull(show.getAccount().getAuthentication()); + assertFalse(show.isCurrentUser()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW CREATE USER app", + "SHOW CREATE USER 'app'", + "SHOW CREATE USER `app`@`localhost`", + "SHOW CREATE USER app@localhost", + "SHOW CREATE USER app@'%'", + "SHOW CREATE USER 'app'@localhost", + "SHOW CREATE USER \"app\"@\"%\"" + }) + void accountSpellings(String sql) throws JSQLParserException { + ShowCreateStatement show = roundTrip(sql); + + assertEquals(ObjectType.USER, show.getObjectType()); + assertFalse(show.isCurrentUser()); + assertEquals("app", show.getAccount().getUser().getValue()); + } + + @Test + void currentUser() throws JSQLParserException { + ShowCreateStatement show = roundTrip("SHOW CREATE USER CURRENT_USER"); + assertTrue(show.isCurrentUser()); + assertFalse(show.isCurrentUserParentheses()); + + show = roundTrip("SHOW CREATE USER CURRENT_USER()"); + assertTrue(show.isCurrentUser()); + assertTrue(show.isCurrentUserParentheses()); + assertNull(show.getAccount().getHost()); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW CREATE SCHEMA IF NOT EXISTS db", + CCJSqlParserUtil.parse("show create schema if not exists db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW CREATE TEMPORARY TABLE t", + "SHOW CREATE TABLE t FORMAT TabSeparated", + "SHOW CREATE TABLE t AS SERDE", + "SHOW CREATE SEQUENCE s", + "SHOW CREATE PACKAGE BODY p", + "SHOW CREATE VIEW IF NOT EXISTS v", + "SHOW CREATE DATABASE IF NOT EXISTS", + "SHOW CREATE USER u IDENTIFIED BY 'x'", + "SHOW CREATE TABLE" + }) + void otherFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = CCJSqlParserUtil.parseStatements( + "SHOW CREATE TABLE a;\nSHOW CREATE USER CURRENT_USER();\nSELECT 1", + parser -> parser.withDialect(Dialect.MYSQL)); + + assertEquals(3, statements.size()); + assertInstanceOf(ShowCreateStatement.class, statements.get(0)); + assertInstanceOf(ShowCreateStatement.class, statements.get(1)); + } + + @Test + void findsTablesAndViewsOnly() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW CREATE TABLE s.t")).containsExactly("s.t"); + assertThat(TablesNamesFinder.findTables("SHOW CREATE VIEW v")).containsExactly("v"); + assertThat(TablesNamesFinder.findTables("SHOW CREATE TRIGGER tr")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW CREATE DATABASE db")).isEmpty(); + assertThat(TablesNamesFinder.findTables("SHOW CREATE USER u")).isEmpty(); + } + + @Test + void buildsAndDeparses() { + assertDeparse(new ShowCreateStatement(ObjectType.VIEW, new Table("s", "v")), + "SHOW CREATE VIEW s.v"); + assertDeparse(new ShowCreateStatement().withObjectType(ObjectType.DATABASE) + .withSchemaKeyword(true).withIfNotExists(true).withDatabaseName("db"), + "SHOW CREATE SCHEMA IF NOT EXISTS db"); + UserAccount account = new UserAccount().withUser(new StringValue("'app'")) + .withHost(new StringValue("'localhost'")); + assertDeparse(new ShowCreateStatement().withObjectType(ObjectType.USER) + .withAccount(account), "SHOW CREATE USER 'app'@'localhost'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse("SHOW CREATE TABLE t").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } +} diff --git a/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java new file mode 100644 index 0000000000..0905f32853 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java @@ -0,0 +1,143 @@ +/*- + * #%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.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.List; +import java.util.function.Consumer; +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.expression.StringValue; +import net.sf.jsqlparser.parser.AbstractJSqlParser.Dialect; +import net.sf.jsqlparser.parser.CCJSqlParser; +import net.sf.jsqlparser.parser.CCJSqlParserUtil; +import net.sf.jsqlparser.statement.ShowStatement; +import net.sf.jsqlparser.statement.Statement; +import net.sf.jsqlparser.statement.StatementFeatures; +import net.sf.jsqlparser.statement.Statements; +import net.sf.jsqlparser.statement.StmtFeature; +import net.sf.jsqlparser.statement.UnsupportedStatement; +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; + +class ShowTableStatusStatementTest { + + 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 ShowTableStatusStatement roundTrip(String sql) throws JSQLParserException { + ShowTableStatusStatement result = null; + for (Consumer dialect : DIALECTS) { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, false, dialect); + assertInstanceOf(ShowTableStatusStatement.class, statement); + assertDeparse(statement, sql); + assertEquals(sql, CCJSqlParserUtil.parse(statement.toString(), dialect).toString()); + result = (ShowTableStatusStatement) statement; + } + return result; + } + + @Test + void currentDatabase() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS"); + + assertNull(show.getDbName()); + assertNull(show.getLikeExpression()); + assertNull(show.getWhereCondition()); + } + + @Test + void likePattern() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS LIKE 'c0%'"); + + assertEquals("c0%", ((StringValue) show.getLikeExpression()).getValue()); + } + + @Test + void databaseAndWhere() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS FROM v WHERE Comment <> ''"); + + assertEquals(SelectionMode.FROM, show.getSelectionMode()); + assertEquals("v", show.getDbName()); + assertEquals("Comment <> ''", show.getWhereCondition().toString()); + } + + @Test + void inDatabaseAndLike() throws JSQLParserException { + ShowTableStatusStatement show = roundTrip("SHOW TABLE STATUS IN `db` LIKE 'a%'"); + + assertEquals(SelectionMode.IN, show.getSelectionMode()); + assertEquals("`db`", show.getDbName()); + } + + @Test + void keywordsAreCaseInsensitive() throws JSQLParserException { + assertEquals("SHOW TABLE STATUS IN db", + CCJSqlParserUtil.parse("show table status in db").toString()); + } + + @ParameterizedTest + @ValueSource(strings = { + "SHOW TABLE foo", + "SHOW TABLE EXTENDED IN db LIKE 'x'", + "SHOW TABLE STATUS FROM db LIMIT 3" + }) + void otherTableFormsRemainUnsupported(String sql) throws JSQLParserException { + Statement statement = assertSqlCanBeParsedAndDeparsed(sql, true); + assertInstanceOf(UnsupportedStatement.class, statement); + } + + @Test + void neighbouringShowFormsAreUnchanged() throws JSQLParserException { + assertInstanceOf(ShowTablesStatement.class, CCJSqlParserUtil.parse("SHOW TABLES")); + assertInstanceOf(ShowStatement.class, CCJSqlParserUtil.parse("SHOW STATUS")); + assertSqlCanBeParsedAndDeparsed("SELECT status FROM t WHERE status = 1"); + } + + @Test + void endsAtTheStatementBoundary() throws JSQLParserException { + Statements statements = + CCJSqlParserUtil.parseStatements("SHOW TABLE STATUS;\nSHOW TABLES"); + + assertEquals(2, statements.size()); + assertInstanceOf(ShowTableStatusStatement.class, statements.get(0)); + } + + @Test + void namesNoTable() throws JSQLParserException { + assertThat(TablesNamesFinder.findTables("SHOW TABLE STATUS FROM db LIKE 't%'")).isEmpty(); + } + + @Test + void buildsAndDeparses() { + ShowTableStatusStatement show = new ShowTableStatusStatement() + .withSelectionMode(SelectionMode.IN).withDbName("db") + .withLikeExpression(new StringValue("t%")); + + assertDeparse(show, "SHOW TABLE STATUS IN db LIKE 't%'"); + } + + @Test + void reportsMetadataFeatures() throws JSQLParserException { + StatementFeatures features = CCJSqlParserUtil.parse("SHOW TABLE STATUS").getFeatures(); + + assertThat(features.getCertain()).containsExactly(StmtFeature.RETURNS_RESULT_SET); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidatorTest.java index d0d2f3ac94..24e0ffac59 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowColumnsStatementValidatorTest.java @@ -9,27 +9,55 @@ */ 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.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.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 ShowColumnsStatementValidatorTest extends ValidationTestAsserts { @Test public void testValidationShowTables() throws Exception { - for (String sql : Arrays.asList("SHOW COLUMNS FROM mytable")) { + for (String sql : Arrays.asList("SHOW COLUMNS FROM mytable", + "SHOW FULL COLUMNS FROM mytable FROM db LIKE 'a%'", + "SHOW EXTENDED FIELDS IN db.mytable WHERE `Key` = 'PRI'")) { validateNoErrors(sql, 1, DatabaseType.MARIADB, DatabaseType.MYSQL); } } @Test public void testValidationShowTablesNotAllowed() throws Exception { - for (String sql : Arrays.asList("SHOW COLUMNS FROM mytable")) { + for (String sql : Arrays.asList("SHOW COLUMNS FROM mytable", + "SHOW FULL FIELDS FROM mytable")) { validateNotAllowed(sql, 1, 1, FeaturesAllowed.DML, Feature.showColumns); } } + @Test + public void testMetadataUsesTheDatabaseClause() throws Exception { + List tables = new ArrayList<>(); + AbstractDatabaseMetaDataCapability metadata = + new AbstractDatabaseMetaDataCapability(null, NamesLookup.NO_TRANSFORMATION) { + @Override + protected boolean tableExists(Map results, Named named) { + tables.add(named.getFqn()); + return true; + } + }; + + validateNoErrors("SHOW COLUMNS FROM t FROM db", 1, metadata); + validateNoErrors("SHOW FULL COLUMNS FROM s.t2", 1, metadata); + assertEquals(Arrays.asList("db.t", "s.t2"), tables); + } + } diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowCreateStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowCreateStatementValidatorTest.java new file mode 100644 index 0000000000..107b78cdcf --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowCreateStatementValidatorTest.java @@ -0,0 +1,77 @@ +/*- + * #%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 ShowCreateStatementValidatorTest extends ValidationTestAsserts { + + @Test + public void testAllowedForMySqlAndMariaDb() throws JSQLParserException { + for (String sql : new String[] {"SHOW CREATE TABLE t", "SHOW CREATE VIEW db.v", + "SHOW CREATE PROCEDURE p", "SHOW CREATE FUNCTION f", "SHOW CREATE TRIGGER tr", + "SHOW CREATE EVENT e", "SHOW CREATE DATABASE IF NOT EXISTS db", + "SHOW CREATE USER 'u'@'h'"}) { + validateNoErrors(sql, 1, MySqlVersion.V8_0, MariaDbVersion.V10_5_4, + DatabaseType.MYSQL, DatabaseType.MARIADB); + } + } + + @Test + public void testNotAllowedForPostgresql() throws JSQLParserException { + validate("SHOW CREATE TABLE t", 1, 1, PostgresqlVersion.V14); + } + + @Test + public void testFeatureNotAllowed() throws JSQLParserException { + validateNotAllowed("SHOW CREATE TABLE t", 1, 1, FeaturesAllowed.DML, Feature.showCreate); + } + + @Test + public void testMetadataLooksUpTablesAndViews() throws JSQLParserException { + List names = new ArrayList<>(); + AbstractDatabaseMetaDataCapability metadata = + new AbstractDatabaseMetaDataCapability(null, NamesLookup.NO_TRANSFORMATION) { + @Override + protected boolean tableExists(Map results, Named named) { + names.add("table " + named.getFqn()); + return true; + } + + @Override + protected boolean viewExists(Map results, Named named) { + names.add("view " + named.getFqn()); + return true; + } + }; + + validateNoErrors("SHOW CREATE TABLE s.t", 1, metadata); + validateNoErrors("SHOW CREATE VIEW v", 1, metadata); + validateNoErrors("SHOW CREATE TRIGGER tr", 1, metadata); + assertEquals(Arrays.asList("table s.t", "view v"), names); + } +} diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidatorTest.java new file mode 100644 index 0000000000..c5412d8c0e --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidatorTest.java @@ -0,0 +1,43 @@ +/*- + * #%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.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 org.junit.jupiter.api.Test; + +public class ShowTableStatusStatementValidatorTest extends ValidationTestAsserts { + + @Test + public void testAllowedForMySqlAndMariaDb() throws JSQLParserException { + for (String sql : new String[] {"SHOW TABLE STATUS", "SHOW TABLE STATUS LIKE 'a%'", + "SHOW TABLE STATUS IN db WHERE Comment <> ''"}) { + validateNoErrors(sql, 1, MySqlVersion.V8_0, MariaDbVersion.V10_5_4, + DatabaseType.MYSQL, DatabaseType.MARIADB); + } + } + + @Test + public void testNotAllowedForPostgresql() throws JSQLParserException { + validate("SHOW TABLE STATUS", 1, 1, PostgresqlVersion.V14); + } + + @Test + public void testFeatureNotAllowed() throws JSQLParserException { + validateNotAllowed("SHOW TABLE STATUS FROM db", 1, 1, FeaturesAllowed.DML, + Feature.showTableStatus); + } +}