From 1af2ff3e8656992fca98f24fcfb06aec5a001278 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 14 Oct 2026 13:17:49 +0900 Subject: [PATCH 1/2] fix: share MySQL account names across CREATE USER and DEFINER CREATE USER and DEFINER read user@host with their own copies of the same rule. Neither accepted app@'%' or 'app'@localhost, because the lexer reads app@ and @localhost as single tokens. Without a dialect a double-quoted part also lost its quotes, so CREATE USER "app"@"%" rendered as app@%. Add one MySqlAccountName() production that splits those tokens, keep the quote character of double-quoted and backquoted parts apart from their value, and use it for CREATE USER and DEFINER. Signed-off-by: minleejae --- .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 70 ++++++++-- src/site/sphinx/usage.rst | 14 ++ .../create/user/MySqlAccountNameTest.java | 123 ++++++++++++++++++ 3 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 src/test/java/net/sf/jsqlparser/statement/create/user/MySqlAccountNameTest.java diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2210dbe9d..9fcc539ce 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -15502,11 +15502,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 +15612,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 +15762,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 +15772,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/usage.rst b/src/site/sphinx/usage.rst index 901608d62..26f7ff5d8 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. 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 000000000..455f18b6a --- /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(); + } +} From 37573c5390e20c82e2f4c20f5572d509bf274793 Mon Sep 17 00:00:00 2001 From: minleejae Date: Wed, 14 Oct 2026 18:30:21 +0900 Subject: [PATCH 2/2] feat: parse MySQL SHOW FULL COLUMNS, SHOW TABLE STATUS and SHOW CREATE statements MySQL tools read table and column definitions and comments with SHOW FULL COLUMNS, SHOW TABLE STATUS and SHOW CREATE. Only plain SHOW COLUMNS FROM t was typed; the other forms became UnsupportedStatement, and LIKE, WHERE or a qualified table after SHOW COLUMNS failed to parse. Extend ShowColumnsStatement with EXTENDED, FULL, the FIELDS spelling, FROM/IN, a Table name, the trailing database and LIKE/WHERE, keeping getTableName() and setTableName(String). Add ShowTableStatusStatement and ShowCreateStatement for TABLE, VIEW, PROCEDURE, FUNCTION, TRIGGER, EVENT, DATABASE/SCHEMA [IF NOT EXISTS] and USER, whose account reuses MySqlAccountNamePart and accepts CURRENT_USER[()]. A typed SHOW form is chosen only when it covers the whole statement, so other dialects' SHOW variants still fall back to UnsupportedStatement. Wire the statements into the deparser, table finder, validation (showTableStatus, showCreate) and feature analysis. Signed-off-by: minleejae --- README.md | 1 + .../sf/jsqlparser/parser/feature/Feature.java | 12 + .../statement/ShowColumnsStatement.java | 174 +++++++++++++- .../statement/StatementFeatureVisitor.java | 12 + .../statement/StatementVisitor.java | 18 ++ .../statement/StatementVisitorAdapter.java | 12 + .../statement/show/ShowCreateStatement.java | 191 ++++++++++++++++ .../show/ShowTableStatusStatement.java | 102 +++++++++ .../sf/jsqlparser/util/TablesNamesFinder.java | 37 ++- .../ShowColumnsStatementDeParser.java | 2 +- .../util/deparser/StatementDeParser.java | 14 ++ .../validation/feature/MariaDbVersion.java | 4 +- .../util/validation/feature/MySqlVersion.java | 4 +- .../ShowColumnsStatementValidator.java | 7 +- .../ShowCreateStatementValidator.java | 51 +++++ .../ShowTableStatusStatementValidator.java | 27 +++ .../validator/StatementValidator.java | 24 ++ .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 138 +++++++++++- src/site/sphinx/index.rst | 2 + src/site/sphinx/usage.rst | 44 ++++ .../statement/ShowColumnsStatementTest.java | 167 +++++++++++++- .../statement/ShowStatementTest.java | 3 +- .../show/ShowCreateStatementTest.java | 212 ++++++++++++++++++ .../show/ShowTableStatusStatementTest.java | 143 ++++++++++++ .../ShowColumnsStatementValidatorTest.java | 32 ++- .../ShowCreateStatementValidatorTest.java | 77 +++++++ ...ShowTableStatusStatementValidatorTest.java | 43 ++++ 27 files changed, 1535 insertions(+), 18 deletions(-) create mode 100644 src/main/java/net/sf/jsqlparser/statement/show/ShowCreateStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatement.java create mode 100644 src/main/java/net/sf/jsqlparser/util/validation/validator/ShowCreateStatementValidator.java create mode 100644 src/main/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidator.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java create mode 100644 src/test/java/net/sf/jsqlparser/statement/show/ShowTableStatusStatementTest.java create mode 100644 src/test/java/net/sf/jsqlparser/util/validation/validator/ShowCreateStatementValidatorTest.java create mode 100644 src/test/java/net/sf/jsqlparser/util/validation/validator/ShowTableStatusStatementValidatorTest.java diff --git a/README.md b/README.md index d39e81ea6..32e6f19f8 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 0d3d080fb..5fb07df29 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 51f1ed26d..b5ec8324d 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 91b272124..5298d3708 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 bf0bf3e8b..9910767a0 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 8a80763ca..2c22fc97d 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 000000000..fa36ea171 --- /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 000000000..d9a2385e8 --- /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 aeecdb89d..89c6b31ee 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 9fc744010..1bc8534a5 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 2490baa3e..1bbe47c9c 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 de84d3f82..3f4953330 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 6aabf1fe0..1a25274ea 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 e917d9e8c..4cb91dbf4 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 000000000..1903f358b --- /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 1de8380a7..fbe7846c5 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 9fcc539ce..6deb75742 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; } { - tableName = RelObjectName() + [ { 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 new ShowColumnsStatement(tableName); + 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(); +} +{ + + ( + ( + { 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 statement; } } diff --git a/src/site/sphinx/index.rst b/src/site/sphinx/index.rst index 5f3b0157b..55962c1f3 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 26f7ff5d8..3a1396760 100644 --- a/src/site/sphinx/usage.rst +++ b/src/site/sphinx/usage.rst @@ -2053,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 825a18def..eb07f57d6 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 7f1033896..23830cbc9 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/show/ShowCreateStatementTest.java b/src/test/java/net/sf/jsqlparser/statement/show/ShowCreateStatementTest.java new file mode 100644 index 000000000..2d82488e1 --- /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 000000000..0905f3285 --- /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 d0d2f3ac9..24e0ffac5 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 000000000..107b78cdc --- /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 000000000..c5412d8c0 --- /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); + } +}