From 4d47e0ab7bc28725a875a75fba29edb5e390343e Mon Sep 17 00:00:00 2001 From: jxnu-liguobin Date: Thu, 14 Dec 2023 14:30:33 +0800 Subject: [PATCH] feat: Add support comment in `create view` for MySQL and MariaDb (#1913) * CreateView with comment for mysql * Add test * Fix * Fix * Fix * Fix --- .../sf/jsqlparser/parser/feature/Feature.java | 6 +++ .../java/net/sf/jsqlparser/schema/Column.java | 30 +++++++++--- .../statement/create/view/CreateView.java | 34 ++++++------- .../util/deparser/CreateViewDeParser.java | 8 ++- .../validation/feature/MariaDbVersion.java | 21 +++++--- .../util/validation/feature/MySqlVersion.java | 13 +++-- .../validator/CreateViewValidator.java | 2 + .../net/sf/jsqlparser/parser/JSqlParserCC.jjt | 49 +++++++++++++++++-- .../statement/create/CreateViewTest.java | 43 +++++++++++++--- .../validator/CreateViewValidatorTest.java | 17 +++++-- 10 files changed, 171 insertions(+), 52 deletions(-) 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 3d5825f5d..b59191844 100644 --- a/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java +++ b/src/main/java/net/sf/jsqlparser/parser/feature/Feature.java @@ -483,6 +483,12 @@ public enum Feature { * SQL "CREATE MATERIALIZED VIEW" statement is allowed */ createViewMaterialized, + + /** + * SQL "CREATE VIEW(x comment 'x', y comment 'y') comment 'view'" statement is allowed + */ + createViewWithComment, + /** * SQL "CREATE TABLE" statement is allowed * diff --git a/src/main/java/net/sf/jsqlparser/schema/Column.java b/src/main/java/net/sf/jsqlparser/schema/Column.java index 684e0faf5..3f7721ca3 100644 --- a/src/main/java/net/sf/jsqlparser/schema/Column.java +++ b/src/main/java/net/sf/jsqlparser/schema/Column.java @@ -9,13 +9,12 @@ */ package net.sf.jsqlparser.schema; +import java.util.List; import net.sf.jsqlparser.expression.ArrayConstructor; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.ExpressionVisitor; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; -import java.util.List; - /** * A column. It can have the table name it belongs to. */ @@ -23,6 +22,7 @@ public class Column extends ASTNodeAccessImpl implements Expression, MultiPartNa private Table table; private String columnName; + private String commentText; private ArrayConstructor arrayConstructor; public Column() {} @@ -56,19 +56,19 @@ public Column setArrayConstructor(ArrayConstructor arrayConstructor) { *

* The inference is based only on local information, and not on the whole SQL command. For * example, consider the following query:

- * + * *
      *  SELECT x FROM Foo
      * 
- * + * *
Given the {@code Column} called {@code x}, this method would return * {@code null}, and not the info about the table {@code Foo}. On the other hand, consider: *
- * + * *
      *  SELECT t.x FROM Foo t
      * 
- * + * *
Here, we will get a {@code Table} object for a table called {@code t}. But * because the inference is local, such object will not know that {@code t} is just an alias for * {@code Foo}. @@ -114,6 +114,11 @@ public String getFullyQualifiedName(boolean aliases) { fqn.append(columnName); } + if (commentText != null) { + fqn.append(" COMMENT "); + fqn.append(commentText); + } + if (arrayConstructor != null) { fqn.append(arrayConstructor); } @@ -146,4 +151,17 @@ public Column withColumnName(String columnName) { this.setColumnName(columnName); return this; } + + public Column withCommentText(String commentText) { + this.setCommentText(commentText); + return this; + } + + public void setCommentText(String commentText) { + this.commentText = commentText; + } + + public String getCommentText() { + return commentText; + } } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java b/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java index a623beb41..8381e50af 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/view/CreateView.java @@ -9,11 +9,9 @@ */ package net.sf.jsqlparser.statement.create.view; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; -import java.util.Optional; +import net.sf.jsqlparser.expression.operators.relational.ExpressionList; +import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.Statement; import net.sf.jsqlparser.statement.StatementVisitor; @@ -25,13 +23,14 @@ public class CreateView implements Statement { private Table view; private Select select; private boolean orReplace = false; - private List columnNames = null; + private ExpressionList columnNames = null; private boolean materialized = false; private ForceOption force = ForceOption.NONE; private TemporaryOption temp = TemporaryOption.NONE; private AutoRefreshOption autoRefresh = AutoRefreshOption.NONE; private boolean withReadOnly = false; private boolean ifNotExists = false; + private List viewCommentOptions = null; @Override public void accept(StatementVisitor statementVisitor) { @@ -65,11 +64,11 @@ public void setSelect(Select select) { this.select = select; } - public List getColumnNames() { + public ExpressionList getColumnNames() { return columnNames; } - public void setColumnNames(List columnNames) { + public void setColumnNames(ExpressionList columnNames) { this.columnNames = columnNames; } @@ -145,7 +144,12 @@ public String toString() { sql.append(" AUTO REFRESH ").append(autoRefresh.name()); } if (columnNames != null) { - sql.append(PlainSelect.getStringList(columnNames, true, true)); + sql.append("("); + sql.append(columnNames); + sql.append(")"); + } + if (viewCommentOptions != null) { + sql.append(PlainSelect.getStringList(viewCommentOptions, false, false)); } sql.append(" AS ").append(select); if (isWithReadOnly()) { @@ -182,7 +186,7 @@ public CreateView withOrReplace(boolean orReplace) { return this; } - public CreateView withColumnNames(List columnNames) { + public CreateView withColumnNames(ExpressionList columnNames) { this.setColumnNames(columnNames); return this; } @@ -202,15 +206,11 @@ public CreateView withWithReadOnly(boolean withReadOnly) { return this; } - public CreateView addColumnNames(String... columnNames) { - List collection = Optional.ofNullable(getColumnNames()).orElseGet(ArrayList::new); - Collections.addAll(collection, columnNames); - return this.withColumnNames(collection); + public List getViewCommentOptions() { + return viewCommentOptions; } - public CreateView addColumnNames(Collection columnNames) { - List collection = Optional.ofNullable(getColumnNames()).orElseGet(ArrayList::new); - collection.addAll(columnNames); - return this.withColumnNames(collection); + public void setViewCommentOptions(List viewCommentOptions) { + this.viewCommentOptions = viewCommentOptions; } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java index 13973b06e..1f5662e68 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/CreateViewDeParser.java @@ -67,7 +67,13 @@ public void deParse(CreateView createView) { buffer.append(" AUTO REFRESH ").append(createView.getAutoRefresh().name()); } if (createView.getColumnNames() != null) { - buffer.append(PlainSelect.getStringList(createView.getColumnNames(), true, true)); + buffer.append("("); + buffer.append(createView.getColumnNames()); + buffer.append(")"); + } + if (createView.getViewCommentOptions() != null) { + buffer.append( + PlainSelect.getStringList(createView.getViewCommentOptions(), false, false)); } buffer.append(" AS "); 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 185da5664..983adb4fd 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 @@ -9,11 +9,10 @@ */ package net.sf.jsqlparser.util.validation.feature; -import net.sf.jsqlparser.parser.feature.Feature; - import java.util.Collections; import java.util.EnumSet; import java.util.Set; +import net.sf.jsqlparser.parser.feature.Feature; /** * Please add Features supported and place a link to public documentation @@ -41,7 +40,8 @@ public enum MariaDbVersion implements Version { Feature.selectForUpdateSkipLocked, // https://mariadb.com/kb/en/join-syntax/ - Feature.join, Feature.joinSimple, Feature.joinRight, Feature.joinNatural, Feature.joinLeft, + Feature.join, Feature.joinSimple, Feature.joinRight, Feature.joinNatural, + Feature.joinLeft, Feature.joinCross, Feature.joinOuter, Feature.joinInner, Feature.joinStraight, Feature.joinUsingColumns, @@ -64,8 +64,10 @@ public enum MariaDbVersion implements Version { // https://mariadb.com/kb/en/insert/ Feature.insert, Feature.insertValues, Feature.values, - Feature.insertFromSelect, Feature.insertModifierPriority, Feature.insertModifierIgnore, - Feature.insertUseSet, Feature.insertUseDuplicateKeyUpdate, Feature.insertReturningExpressionList, + Feature.insertFromSelect, Feature.insertModifierPriority, + Feature.insertModifierIgnore, + Feature.insertUseSet, Feature.insertUseDuplicateKeyUpdate, + Feature.insertReturningExpressionList, // https://mariadb.com/kb/en/update/ Feature.update, @@ -96,7 +98,8 @@ public enum MariaDbVersion implements Version { Feature.dropView, // https://mariadb.com/kb/en/drop-sequence/ Feature.dropSequence, Feature.dropTableIfExists, Feature.dropIndexIfExists, - Feature.dropViewIfExists, Feature.dropSchemaIfExists, Feature.dropSequenceIfExists, + Feature.dropViewIfExists, Feature.dropSchemaIfExists, + Feature.dropSequenceIfExists, // https://mariadb.com/kb/en/replace/ Feature.upsert, @@ -110,9 +113,11 @@ public enum MariaDbVersion implements Version { // https://mariadb.com/kb/en/create-view/ Feature.createView, Feature.createOrReplaceView, + Feature.createViewWithComment, // https://mariadb.com/kb/en/create-table/ - Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, + Feature.createTable, Feature.createTableCreateOptionStrings, + Feature.createTableTableOptionStrings, Feature.createTableFromSelect, Feature.createTableIfNotExists, // https://mariadb.com/kb/en/create-index/ Feature.createIndex, @@ -143,7 +148,7 @@ public enum MariaDbVersion implements Version { Feature.commit, // https://mariadb.com/kb/en/optimizer-hints/ Feature.mySqlHintStraightJoin, - Feature.mysqlCalcFoundRows, + Feature.mysqlCalcFoundRows, Feature.mysqlSqlCacheFlag)), ORACLE_MODE("oracle_mode", V10_5_4.copy().add(Feature.selectUnique).getFeatures()); 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 01bbce4c1..d631344d8 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 @@ -9,11 +9,10 @@ */ package net.sf.jsqlparser.util.validation.feature; -import net.sf.jsqlparser.parser.feature.Feature; - import java.util.Collections; import java.util.EnumSet; import java.util.Set; +import net.sf.jsqlparser.parser.feature.Feature; /** * Please add Features supported and place a link to public documentation @@ -33,7 +32,8 @@ public enum MySqlVersion implements Version { // https://dev.mysql.com/doc/refman/8.0/en/select.html Feature.select, Feature.selectGroupBy, Feature.selectHaving, - Feature.limit, Feature.limitOffset, Feature.offset, Feature.offsetParam, Feature.orderBy, + Feature.limit, Feature.limitOffset, Feature.offset, Feature.offsetParam, + Feature.orderBy, Feature.selectForUpdate, Feature.selectForUpdateOfTable, Feature.selectForUpdateNoWait, @@ -51,7 +51,8 @@ public enum MySqlVersion implements Version { Feature.function, // https://dev.mysql.com/doc/refman/8.0/en/join.html - Feature.join, Feature.joinSimple, Feature.joinLeft, Feature.joinRight, Feature.joinOuter, + Feature.join, Feature.joinSimple, Feature.joinLeft, Feature.joinRight, + Feature.joinOuter, Feature.joinNatural, Feature.joinInner, Feature.joinCross, Feature.joinStraight, Feature.joinUsingColumns, @@ -99,9 +100,11 @@ public enum MySqlVersion implements Version { Feature.createSchema, // https://dev.mysql.com/doc/refman/8.0/en/create-view.html Feature.createView, + Feature.createViewWithComment, Feature.createOrReplaceView, // https://dev.mysql.com/doc/refman/8.0/en/create-table.html - Feature.createTable, Feature.createTableCreateOptionStrings, Feature.createTableTableOptionStrings, + Feature.createTable, Feature.createTableCreateOptionStrings, + Feature.createTableTableOptionStrings, Feature.createTableFromSelect, Feature.createTableIfNotExists, // https://dev.mysql.com/doc/refman/8.0/en/create-index.html Feature.createIndex, diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java index 2723cfc25..e2910ada3 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidator.java @@ -33,6 +33,8 @@ public void validate(CreateView createView) { Feature.createViewTemporary); validateFeature(c, createView.isMaterialized(), Feature.createViewMaterialized); validateName(c, NamedObject.view, createView.getView().getFullyQualifiedName(), false); + validateFeature(c, createView.getViewCommentOptions() != null, + Feature.createViewWithComment); } SelectValidator v = getValidator(SelectValidator.class); Select select = createView.getSelect(); diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index 2045dfdda..91f4df886 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -1767,16 +1767,18 @@ Column Column() #Column : { List data = new ArrayList(); ArrayConstructor arrayConstructor = null; + Token tk = null; } { data = RelObjectNameList() - + [ tk= ] // @todo: we better should return a SEQUENCE instead of a COLUMN [ "." { data.add("nextval"); } ] [ LOOKAHEAD(2) arrayConstructor = ArrayConstructor(false) ] { Column col = new Column(data); + if (tk != null) { col.withCommentText(tk.image); } if (arrayConstructor!=null) { col.setArrayConstructor(arrayConstructor); } @@ -5705,13 +5707,30 @@ Analyze Analyze(): } } +ExpressionList ColumnWithCommentList(): +{ + ExpressionList expressions = new ExpressionList(); + Column img = null; +} +{ + "(" + img=Column() { expressions.add(img); } + ( "," img=Column() { expressions.add(img); } )* + ")" + { + return expressions; + } +} + + CreateView CreateView(boolean isUsingOrReplace): { CreateView createView = new CreateView(); Table view = null; Select select = null; - List columnNames = null; + ExpressionList columnNames = null; Token tk = null; + List commentTokens = null; } { { createView.setOrReplace(isUsingOrReplace);} @@ -5727,13 +5746,36 @@ CreateView CreateView(boolean isUsingOrReplace): view=Table() { createView.setView(view); } [LOOKAHEAD(3) (tk= | tk=) { createView.setAutoRefresh(AutoRefreshOption.from(tk.image)); } ] [LOOKAHEAD(3) {createView.setIfNotExists(true);}] - [ columnNames = ColumnsNamesList() { createView.setColumnNames(columnNames); } ] + [ columnNames=ColumnWithCommentList( ) { createView.setColumnNames(columnNames); } ] + [ commentTokens=CreateViewTailComment( ) { createView.setViewCommentOptions(commentTokens); } ] select=Select( ) { createView.setSelect(select); } [ { createView.setWithReadOnly(true); } ] { return createView; } } +List CreateViewTailComment(): +{ + Token tk = null; + Token tk2 = null; + String op = null; + List result = new ArrayList(); +} +{ + tk= + [ "=" { op = "="; } ] + tk2 = { + result.add(""); + result.add(tk.image); + if (op != null) { + result.add(op); + } + result.add(tk2.image); + } + { return result;} +} + + ReferentialAction.Action Action(): { ReferentialAction.Action action = null; @@ -5778,7 +5820,6 @@ List CreateParameter(): Token tk = null, tk2 = null; Expression exp = null; ColDataType colDataType; - List param = new ArrayList(); } { diff --git a/src/test/java/net/sf/jsqlparser/statement/create/CreateViewTest.java b/src/test/java/net/sf/jsqlparser/statement/create/CreateViewTest.java index 3499a83be..240a22fc3 100644 --- a/src/test/java/net/sf/jsqlparser/statement/create/CreateViewTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/create/CreateViewTest.java @@ -9,8 +9,13 @@ */ package net.sf.jsqlparser.statement.create; -import java.io.StringReader; +import static net.sf.jsqlparser.test.TestUtils.assertSqlCanBeParsedAndDeparsed; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.StringReader; import net.sf.jsqlparser.JSQLParserException; import net.sf.jsqlparser.parser.CCJSqlParserManager; import net.sf.jsqlparser.parser.CCJSqlParserUtil; @@ -20,14 +25,7 @@ import net.sf.jsqlparser.statement.create.view.CreateView; import net.sf.jsqlparser.statement.select.ParenthesedSelect; import net.sf.jsqlparser.statement.select.PlainSelect; -import static net.sf.jsqlparser.test.TestUtils.*; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; - import org.assertj.core.api.ThrowableAssert.ThrowingCallable; -import static org.junit.jupiter.api.Assertions.assertTrue; - import org.junit.jupiter.api.Test; public class CreateViewTest { @@ -212,4 +210,33 @@ public void testCreateMaterializedViewIfNotExists() throws JSQLParserException { assertTrue(createView.isIfNotExists()); } + @Test + public void testCreateViewWithColumnComment() throws JSQLParserException { + String stmt = + "CREATE VIEW v14(c1 COMMENT 'comment1', c2 COMMENT 'comment2') AS SELECT c1, C2 FROM t1 WITH READ ONLY"; + assertSqlCanBeParsedAndDeparsed(stmt); + + String stmt2 = + "CREATE VIEW v14(c1 COMMENT 'comment1', c2) AS SELECT c1, C2 FROM t1 WITH READ ONLY"; + assertSqlCanBeParsedAndDeparsed(stmt2); + + String stmt3 = + "CREATE VIEW v14(c1, c2) COMMENT = 'view' AS SELECT c1, C2 FROM t1 WITH READ ONLY"; + assertSqlCanBeParsedAndDeparsed(stmt3); + } + + @Test + public void testCreateViewWithTableComment1() throws JSQLParserException { + String stmt = + "CREATE VIEW v14(c1 COMMENT 'comment1', c2 COMMENT 'comment2') COMMENT 'view' AS SELECT c1, C2 FROM t1 WITH READ ONLY"; + assertSqlCanBeParsedAndDeparsed(stmt); + } + + @Test + public void testCreateViewWithTableComment2() throws JSQLParserException { + String stmt = + "CREATE VIEW v14(c1 COMMENT 'comment1', c2 COMMENT 'comment2') COMMENT = 'view' AS SELECT c1, C2 FROM t1 WITH READ ONLY"; + assertSqlCanBeParsedAndDeparsed(stmt); + } + } diff --git a/src/test/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidatorTest.java b/src/test/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidatorTest.java index 92d4cd294..4c94bd041 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidatorTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/validator/CreateViewValidatorTest.java @@ -37,18 +37,21 @@ public void testValidateCreateViewNotAllowed() throws JSQLParserException { @Test public void testValidateCreateViewMaterialized() throws JSQLParserException { - validateNoErrors("CREATE MATERIALIZED VIEW myview AS SELECT * FROM mytab", 1, DatabaseType.ORACLE); + validateNoErrors("CREATE MATERIALIZED VIEW myview AS SELECT * FROM mytab", 1, + DatabaseType.ORACLE); } @Test public void testValidateCreateOrReplaceView() throws JSQLParserException { - validateNoErrors("CREATE OR REPLACE VIEW myview AS SELECT * FROM mytab", 1, DatabaseType.ORACLE, + validateNoErrors("CREATE OR REPLACE VIEW myview AS SELECT * FROM mytab", 1, + DatabaseType.ORACLE, DatabaseType.POSTGRESQL, DatabaseType.MYSQL, DatabaseType.MARIADB, DatabaseType.H2); } @Test public void testValidateCreateForceView() throws JSQLParserException { - validateNoErrors("CREATE FORCE VIEW myview AS SELECT * FROM mytab", 1, DatabaseType.ORACLE, DatabaseType.H2); + validateNoErrors("CREATE FORCE VIEW myview AS SELECT * FROM mytab", 1, DatabaseType.ORACLE, + DatabaseType.H2); } @Test @@ -66,4 +69,12 @@ public void testValidateCreateViewWith() throws JSQLParserException { validateNoErrors(sql, 1, DatabaseType.DATABASES); } } + + @Test + public void testValidateCreateViewWithComment() throws JSQLParserException { + validateNoErrors( + "CREATE VIEW v14(c1 COMMENT 'comment1', c2 COMMENT 'comment2') COMMENT = 'view' AS SELECT c1, C2 FROM t1 WITH READ ONLY", + 1, + DatabaseType.MYSQL, DatabaseType.MARIADB); + } }