diff --git a/core/src/main/java/com/alibaba/druid/sql/ast/SQLPartitionOf.java b/core/src/main/java/com/alibaba/druid/sql/ast/SQLPartitionOf.java new file mode 100644 index 0000000000..40765c7790 --- /dev/null +++ b/core/src/main/java/com/alibaba/druid/sql/ast/SQLPartitionOf.java @@ -0,0 +1,165 @@ +/* + * Copyright 1999-2017 Alibaba Group Holding Ltd. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.alibaba.druid.sql.ast; + + +import com.alibaba.druid.sql.ast.statement.SQLExprTableSource; +import com.alibaba.druid.sql.visitor.SQLASTVisitor; + +import java.util.List; + +/** + * @author lizongbo + */ +public class SQLPartitionOf extends SQLObjectImpl { + + protected SQLExprTableSource parentTable; + private boolean useDefault; + private SQLName columnName; + private SQLName constraintName; + private SQLExpr checkExpr; + private SQLExpr defaultExpr; + private List forValuesFrom; + private List forValuesTo; + private List forValuesIn; + + private SQLExpr forValuesModulus; + + private SQLExpr forValuesRemainder; + + public SQLExprTableSource getParentTable() { + return parentTable; + } + + public void setParentTable(SQLExprTableSource parentTable) { + this.parentTable = parentTable; + } + + public boolean isUseDefault() { + return useDefault; + } + + public void setUseDefault(boolean useDefault) { + this.useDefault = useDefault; + } + + public List getForValuesFrom() { + return forValuesFrom; + } + + public void setForValuesFrom(List forValuesFrom) { + this.forValuesFrom = forValuesFrom; + } + + public List getForValuesTo() { + return forValuesTo; + } + + public void setForValuesTo(List forValuesTo) { + this.forValuesTo = forValuesTo; + } + + public List getForValuesIn() { + return forValuesIn; + } + + public void setForValuesIn(List forValuesIn) { + this.forValuesIn = forValuesIn; + } + + public SQLName getColumnName() { + return columnName; + } + + public void setColumnName(SQLName columnName) { + this.columnName = columnName; + } + + public SQLName getConstraintName() { + return constraintName; + } + + public void setConstraintName(SQLName constraintName) { + this.constraintName = constraintName; + } + + public SQLExpr getCheckExpr() { + return checkExpr; + } + + public void setCheckExpr(SQLExpr checkExpr) { + this.checkExpr = checkExpr; + } + + public SQLExpr getDefaultExpr() { + return defaultExpr; + } + + public void setDefaultExpr(SQLExpr defaultExpr) { + this.defaultExpr = defaultExpr; + } + + public SQLExpr getForValuesModulus() { + return forValuesModulus; + } + + public void setForValuesModulus(SQLExpr forValuesModulus) { + this.forValuesModulus = forValuesModulus; + } + + public SQLExpr getForValuesRemainder() { + return forValuesRemainder; + } + + public void setForValuesRemainder(SQLExpr forValuesRemainder) { + this.forValuesRemainder = forValuesRemainder; + } + + @Override + protected void accept0(SQLASTVisitor v) { + if (v.visit(this)) { + acceptChild(v, parentTable); + if (columnName != null) { + acceptChild(v, columnName); + } + if (constraintName != null) { + acceptChild(v, constraintName); + } + if (checkExpr != null) { + acceptChild(v, checkExpr); + } + if (defaultExpr != null) { + acceptChild(v, defaultExpr); + } + if (forValuesFrom != null) { + acceptChild(v, forValuesFrom); + } + if (forValuesTo != null) { + acceptChild(v, forValuesTo); + } + if (forValuesIn != null) { + acceptChild(v, forValuesIn); + } + if (forValuesModulus != null) { + acceptChild(v, forValuesModulus); + } + if (forValuesRemainder != null) { + acceptChild(v, forValuesRemainder); + } + } + v.endVisit(this); + } +} diff --git a/core/src/main/java/com/alibaba/druid/sql/ast/statement/SQLCreateTableStatement.java b/core/src/main/java/com/alibaba/druid/sql/ast/statement/SQLCreateTableStatement.java index eb0fa981c7..ce0df4d166 100644 --- a/core/src/main/java/com/alibaba/druid/sql/ast/statement/SQLCreateTableStatement.java +++ b/core/src/main/java/com/alibaba/druid/sql/ast/statement/SQLCreateTableStatement.java @@ -50,6 +50,7 @@ public class SQLCreateTableStatement extends SQLStatementImpl implements SQLDDLS protected SQLName tablespace; protected SQLPartitionBy partitioning; + protected SQLPartitionOf partitionOf; protected SQLPartitionBy localPartitioning; protected SQLExpr storedAs; protected SQLExpr location; @@ -362,6 +363,16 @@ public void setPartitioning(SQLPartitionBy partitioning) { this.partitioning = partitioning; } + public SQLPartitionOf getPartitionOf() { + return partitionOf; + } + + public void setPartitionOf(SQLPartitionOf partitionOf) { + if (partitionOf != null) { + partitionOf.setParent(this); + } + this.partitionOf = partitionOf; + } public void setLocalPartitioning(SQLPartitionBy localPartitioning) { if (localPartitioning != null) { localPartitioning.setParent(this); diff --git a/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGCreateTableParser.java b/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGCreateTableParser.java index 80ce0f5580..df3872b1da 100644 --- a/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGCreateTableParser.java +++ b/core/src/main/java/com/alibaba/druid/sql/dialect/postgresql/parser/PGCreateTableParser.java @@ -1,9 +1,22 @@ package com.alibaba.druid.sql.dialect.postgresql.parser; + +import com.alibaba.druid.sql.ast.SQLExpr; +import com.alibaba.druid.sql.ast.SQLName; import com.alibaba.druid.sql.ast.SQLPartitionBy; import com.alibaba.druid.sql.ast.SQLPartitionByHash; import com.alibaba.druid.sql.ast.SQLPartitionByList; +import com.alibaba.druid.sql.ast.SQLPartitionByRange; +import com.alibaba.druid.sql.ast.SQLPartitionOf; +import com.alibaba.druid.sql.ast.expr.SQLBetweenExpr; +import com.alibaba.druid.sql.ast.expr.SQLIntegerExpr; +import com.alibaba.druid.sql.ast.statement.SQLExprTableSource; import com.alibaba.druid.sql.parser.*; +import com.alibaba.druid.util.FnvHash; + +import java.util.ArrayList; +import java.util.List; + public class PGCreateTableParser extends SQLCreateTableParser { public PGCreateTableParser(Lexer lexer) { @@ -63,8 +76,121 @@ public SQLPartitionBy parsePartitionBy() { this.exprParser.exprList(hash.getColumns(), hash); accept(Token.RPAREN); return hash; + } else if (lexer.token() == Token.IDENTIFIER) { + SQLPartitionByRange clause = partitionByRange(); + return clause; } throw new ParserException("TODO " + lexer.info()); } + protected SQLPartitionByRange partitionByRange() { + SQLPartitionByRange clause = new SQLPartitionByRange(); + if (lexer.identifierEquals(FnvHash.Constants.RANGE)) { + lexer.nextToken(); + + if (lexer.token() == Token.LPAREN) { + lexer.nextToken(); + clause.addColumn(this.exprParser.expr()); + accept(Token.RPAREN); + } else { + acceptIdentifier("COLUMNS"); + accept(Token.LPAREN); + for (; ; ) { + clause.addColumn(this.exprParser.name()); + if (lexer.token() == Token.COMMA) { + lexer.nextToken(); + continue; + } + break; + } + accept(Token.RPAREN); + } + } else { + SQLExpr expr = this.exprParser.expr(); + if (lexer.identifierEquals(FnvHash.Constants.STARTWITH)) { + lexer.nextToken(); + SQLExpr start = this.exprParser.primary(); + acceptIdentifier("ENDWITH"); + SQLExpr end = this.exprParser.primary(); + expr = new SQLBetweenExpr(expr, start, end); + } + clause.setInterval(expr); + } + + return clause; + } + + public SQLPartitionOf parsePartitionOf() { + lexer.nextToken(); + accept(Token.OF); + SQLPartitionOf partitionOf = new SQLPartitionOf(); + SQLName tableNameTmp = this.exprParser.name(); + SQLExprTableSource sqlExprTableSource = new SQLExprTableSource(tableNameTmp); + partitionOf.setParentTable(sqlExprTableSource); + if (lexer.token() == Token.LPAREN) { + lexer.nextToken(); + if (lexer.token() == Token.CONSTRAINT) { + lexer.nextToken(); + SQLName constraintName = this.exprParser.name(); + partitionOf.setConstraintName(constraintName); + accept(Token.CHECK); + SQLExpr checkExpr = this.exprParser.expr(); + partitionOf.setCheckExpr(checkExpr); + } else { + SQLName columnName = this.exprParser.name(); + partitionOf.setColumnName(columnName); + if (lexer.token() == Token.DEFAULT) { + accept(Token.DEFAULT); + SQLExpr defaultExpr = this.exprParser.primary(); + partitionOf.setDefaultExpr(defaultExpr); + } + } + accept(Token.RPAREN); + } + if (lexer.token() == Token.DEFAULT) { + accept(Token.DEFAULT); + partitionOf.setUseDefault(true); + return partitionOf; + } + accept(Token.FOR); + accept(Token.VALUES); + if (lexer.token() == Token.FROM) { + accept(Token.FROM); + accept(Token.LPAREN); + List sqlExprBetweens = new ArrayList<>(); + this.exprParser.exprList(sqlExprBetweens, partitionOf); + partitionOf.setForValuesFrom(sqlExprBetweens); + accept(Token.RPAREN); + accept(Token.TO); + accept(Token.LPAREN); + List sqlExprAnds = new ArrayList<>(); + this.exprParser.exprList(sqlExprAnds, partitionOf); + partitionOf.setForValuesTo(sqlExprAnds); + accept(Token.RPAREN); + return partitionOf; + } else if (lexer.token() == Token.IN) { + accept(Token.IN); + accept(Token.LPAREN); + List sqlExprBetweens = new ArrayList<>(); + this.exprParser.exprList(sqlExprBetweens, partitionOf); + partitionOf.setForValuesIn(sqlExprBetweens); + accept(Token.RPAREN); + return partitionOf; + } + if (lexer.token() == Token.WITH) { + accept(Token.WITH); + accept(Token.LPAREN); + acceptIdentifier("MODULUS"); + SQLExpr modulus = this.exprParser.primary(); + partitionOf.setForValuesModulus(modulus); + accept(Token.COMMA); + acceptIdentifier("REMAINDER"); + SQLExpr remainder = (SQLIntegerExpr) this.exprParser.primary(); + partitionOf.setForValuesRemainder(remainder); + accept(Token.RPAREN); + return partitionOf; + } else { + throw new ParserException("TODO " + lexer.info()); + } + } } diff --git a/core/src/main/java/com/alibaba/druid/sql/parser/SQLCreateTableParser.java b/core/src/main/java/com/alibaba/druid/sql/parser/SQLCreateTableParser.java index 858399e168..c79d786949 100644 --- a/core/src/main/java/com/alibaba/druid/sql/parser/SQLCreateTableParser.java +++ b/core/src/main/java/com/alibaba/druid/sql/parser/SQLCreateTableParser.java @@ -18,6 +18,7 @@ import com.alibaba.druid.DbType; import com.alibaba.druid.sql.ast.SQLName; import com.alibaba.druid.sql.ast.SQLPartitionBy; +import com.alibaba.druid.sql.ast.SQLPartitionOf; import com.alibaba.druid.sql.ast.statement.*; import com.alibaba.druid.sql.dialect.oracle.parser.OracleSelectParser; import com.alibaba.druid.util.FnvHash; @@ -176,8 +177,31 @@ public SQLCreateTableStatement parseCreateTable(boolean acceptCreate) { } if (lexer.token() == Token.PARTITION) { - SQLPartitionBy partitionClause = parsePartitionBy(); - createTable.setPartitioning(partitionClause); + Lexer.SavePoint mark = lexer.mark(); + lexer.nextToken(); + if (Token.OF.equals(lexer.token())) { + lexer.reset(mark); + SQLPartitionOf partitionOf = parsePartitionOf(); + createTable.setPartitionOf(partitionOf); + } else if (Token.BY.equals(lexer.token())) { + lexer.reset(mark); + SQLPartitionBy partitionClause = parsePartitionBy(); + createTable.setPartitioning(partitionClause); + } + } + + if (lexer.token() == Token.PARTITION) { + Lexer.SavePoint mark = lexer.mark(); + lexer.nextToken(); + if (Token.OF.equals(lexer.token())) { + lexer.reset(mark); + SQLPartitionOf partitionOf = parsePartitionOf(); + createTable.setPartitionOf(partitionOf); + } else if (Token.BY.equals(lexer.token())) { + lexer.reset(mark); + SQLPartitionBy partitionClause = parsePartitionBy(); + createTable.setPartitioning(partitionClause); + } } parseCreateTableRest(createTable); @@ -192,6 +216,10 @@ public SQLPartitionBy parsePartitionBy() { return null; } + public SQLPartitionOf parsePartitionOf() { + return null; + } + protected SQLTableElement parseCreateTableSupplementalLogingProps() { throw new ParserException("TODO " + lexer.info()); } diff --git a/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTOutputVisitor.java b/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTOutputVisitor.java index 78bdbb0b11..cf98823f2f 100644 --- a/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTOutputVisitor.java +++ b/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTOutputVisitor.java @@ -3651,13 +3651,18 @@ protected void printTableElements(List tableElementList) { public boolean visit(SQLCreateTableStatement x) { printCreateTable(x, true); + SQLPartitionOf partitionOf = x.getPartitionOf(); + if (partitionOf != null) { + println(); + print0(ucase ? "PARTITION OF " : "partition of "); + partitionOf.accept(this); + } SQLPartitionBy partitionBy = x.getPartitioning(); if (partitionBy != null) { println(); print0(ucase ? "PARTITION BY " : "partition by "); partitionBy.accept(this); } - List options = x.getTableOptions(); if (options.size() > 0) { println(); @@ -11575,4 +11580,55 @@ public boolean visit(SQLCostStatement x) { x.getStatement().accept(this); return false; } + + public boolean visit(SQLPartitionOf x) { + x.getParentTable().accept(this); + if (x.isUseDefault()) { + print0(ucase ? " DEFAULT" : " pdefault"); + } + if (x.getColumnName() != null || x.getConstraintName() != null || x.getCheckExpr() != null) { + print0(" ("); + } + if (x.getColumnName() != null) { + x.getColumnName().accept(this); + } + if (x.getConstraintName() != null) { + print0(ucase ? " CONSTRAINT " : " constraint "); + x.getConstraintName().accept(this); + } + if (x.getCheckExpr() != null) { + print0(ucase ? " CHECK (" : " check ("); + x.getCheckExpr().accept(this); + print0(")"); + } + if (x.getDefaultExpr() != null) { + print0(ucase ? " DEFAULT " : " default "); + x.getDefaultExpr().accept(this); + } + if (x.getColumnName() != null || x.getConstraintName() != null || x.getCheckExpr() != null) { + print0(" )"); + } + if (x.getForValuesFrom() != null && x.getForValuesTo() != null) { + print0(ucase ? " FOR VALUES FROM (" : " for values from ("); + printAndAccept(x.getForValuesFrom(), ", "); + print0(ucase ? ") TO (" : ") to ("); + printAndAccept(x.getForValuesTo(), ", "); + print0(")"); + } + if (x.getForValuesIn() != null) { + print0(ucase ? " FOR VALUES IN (" : " for values in ("); + printAndAccept(x.getForValuesIn(), ", "); + print0(")"); + } + if (x.getForValuesModulus() != null && x.getForValuesRemainder() != null) { + print0(ucase ? " FOR VALUES WITH (" : " for values with ("); + print0(ucase ? "MODULUS " : "modulus "); + x.getForValuesModulus().accept(this); + print0(" , "); + print0(ucase ? "REMAINDER " : "remainder "); + x.getForValuesRemainder().accept(this); + print0(")"); + } + return false; + } } diff --git a/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTVisitor.java b/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTVisitor.java index e79667c408..6c2a571b15 100644 --- a/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTVisitor.java +++ b/core/src/main/java/com/alibaba/druid/sql/visitor/SQLASTVisitor.java @@ -1230,6 +1230,12 @@ default boolean visit(SQLPartitionByList x) { default void endVisit(SQLPartitionByList x) { } + default boolean visit(SQLPartitionOf x) { + return true; + } + + default void endVisit(SQLPartitionOf x) { + } default boolean visit(SQLSubPartition x) { return true; diff --git a/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue5366.java b/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue5366.java new file mode 100644 index 0000000000..b0afdf865c --- /dev/null +++ b/core/src/test/java/com/alibaba/druid/bvt/sql/postgresql/issues/Issue5366.java @@ -0,0 +1,114 @@ +package com.alibaba.druid.bvt.sql.postgresql.issues; + +import java.util.Map; + +import com.alibaba.druid.DbType; +import com.alibaba.druid.sql.SQLUtils; +import com.alibaba.druid.sql.ast.SQLStatement; +import com.alibaba.druid.sql.parser.SQLParserUtils; +import com.alibaba.druid.sql.parser.SQLStatementParser; +import com.alibaba.druid.sql.visitor.SchemaStatVisitor; +import com.alibaba.druid.stat.TableStat; + +import org.apache.commons.lang3.StringUtils; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * 验证 Postgresql 无法解析 create table PARTITION OF 语句 #5366 + * + * @author lizongbo + * @see 增强 #5366 + * @see CREATE TABLE + */ +public class Issue5366 { + + @Test + public void test_create_table() throws Exception { + for (DbType dbType : new DbType[]{DbType.postgresql}) { + for (String sql : new String[]{ + "CREATE TABLE orders_p4 PARTITION OF orders\n" + + " FOR VALUES WITH (MODULUS 4, REMAINDER 3);", + "CREATE TABLE measurement_y2016m07\n" + + " PARTITION OF measurement (\n" + + " unitsales DEFAULT 0\n" + + ") FOR VALUES FROM ('2016-07-01') TO ('2016-08-01');", + "CREATE TABLE cities_ab\n" + + " PARTITION OF cities (\n" + + " CONSTRAINT city_id_nonzero CHECK (city_id != 0)\n" + + ") FOR VALUES IN ('a', 'b');", + "CREATE TABLE measurement_ym_y2017m01\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES IN (2017, 1) PARTITION BY RANGE (population);", + "CREATE TABLE IF NOT EXISTS lc_event_1689811200000 PARTITION OF lc_event FOR VALUES FROM (1689811200000) TO (1690416000000);", + "CREATE TABLE measurement_ym_older\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES FROM (MINVALUE, MINVALUE) TO (2016, 11);", + "CREATE TABLE measurement_ym_y2016m11\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES FROM (2016, 11) TO (2016, 12);", + "CREATE TABLE measurement_ym_y2016m12\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES FROM (2016, 12) TO (2017, 1);", + "CREATE TABLE measurement_ym_y2017m01\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES FROM (2017, 1) TO (2017, 2);", + "CREATE TABLE measurement_ym_y2017m02\n" + + " PARTITION OF measurement_year_month\n" + + " FOR VALUES IN (2017, 1) PARTITION BY RANGE (population);", + "CREATE TABLE cities_ab\n" + + " PARTITION OF cities (\n" + + " CONSTRAINT city_id_nonzero CHECK (city_id != 0)\n" + + ") FOR VALUES IN ('a', 'b') PARTITION BY RANGE (population);", + "CREATE TABLE cities_ab_10000_to_100000\n" + + " PARTITION OF cities_ab FOR VALUES FROM (10000) TO (100000);", + "CREATE TABLE orders_p1 PARTITION OF orders\n" + + " FOR VALUES WITH (MODULUS 4, REMAINDER 0);", + "CREATE TABLE orders_p2 PARTITION OF orders\n" + + " FOR VALUES WITH (MODULUS 4, REMAINDER 1);", + "CREATE TABLE orders_p3 PARTITION OF orders\n" + + " FOR VALUES WITH (MODULUS 4, REMAINDER 2);", + "CREATE TABLE cities_partdef\n" + + " PARTITION OF cities DEFAULT;", + }) { + System.out.println(dbType + "原始的sql===" + sql); + String normalizeSql = normalizeSql(sql); + SQLStatementParser parser = SQLParserUtils.createSQLStatementParser(sql, dbType); + SQLStatement statement = parser.parseStatement(); + System.out.println(dbType + "生成的sql===" + statement); + String newSql = statement.toString()+";"; + String normalizeNewSql = normalizeSql(newSql); + assertEquals(normalizeSql.toLowerCase(),normalizeNewSql.toLowerCase()); + SchemaStatVisitor visitor = SQLUtils.createSchemaStatVisitor(dbType); + statement.accept(visitor); + System.out.println(dbType + "getTables==" + visitor.getTables()); + Map tableMap = visitor.getTables(); + assertTrue(!tableMap.isEmpty()); + } + + } + } + static String normalizeSql(String sql) { + sql = StringUtils.replace(sql, " ( ", "("); + sql = StringUtils.replace(sql, "( ", "("); + sql = StringUtils.replace(sql, " )", ")"); + sql = StringUtils.replace(sql, "\t", " "); + sql = StringUtils.replace(sql, "\n", " "); + sql = StringUtils.replace(sql, "\'", "\""); + sql = StringUtils.replace(sql, " ( ", "("); + sql = StringUtils.replace(sql, " (", "("); + sql = StringUtils.replace(sql, "( ", "("); + sql = StringUtils.replace(sql, " )", ")"); + sql = StringUtils.replace(sql, "( ", "("); + sql = StringUtils.replace(sql, " ", " "); + sql = StringUtils.replace(sql, " ", " "); + sql = StringUtils.replace(sql, " ", " "); + sql = StringUtils.replace(sql, " ", " "); + sql = StringUtils.replace(sql, "( ", "("); + sql = StringUtils.replace(sql, ", ", ","); + sql = StringUtils.replace(sql, " ,", ","); + return sql; + } +}