diff --git a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java index e5f12f7d8..519eb4ef6 100644 --- a/src/main/java/net/sf/jsqlparser/expression/CastExpression.java +++ b/src/main/java/net/sf/jsqlparser/expression/CastExpression.java @@ -16,7 +16,22 @@ public class CastExpression extends ASTNodeAccessImpl implements Expression { private Expression leftExpression; private ColDataType type; + private RowConstructor rowConstructor; private boolean useCastKeyword = true; + + public RowConstructor getRowConstructor() { + return rowConstructor; + } + + public void setRowConstructor(RowConstructor rowConstructor) { + this.rowConstructor = rowConstructor; + this.type = null; + } + + public CastExpression withRowConstructor(RowConstructor rowConstructor) { + setRowConstructor(rowConstructor); + return this; + } public ColDataType getType() { return type; @@ -24,6 +39,7 @@ public ColDataType getType() { public void setType(ColDataType type) { this.type = type; + this.rowConstructor = null; } public Expression getLeftExpression() { @@ -50,7 +66,9 @@ public void setUseCastKeyword(boolean useCastKeyword) { @Override public String toString() { if (useCastKeyword) { - return "CAST(" + leftExpression + " AS " + type.toString() + ")"; + return rowConstructor!=null + ? "CAST(" + leftExpression + " AS " + rowConstructor.toString() + ")" + : "CAST(" + leftExpression + " AS " + type.toString() + ")"; } else { return leftExpression + "::" + type.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java index 443f14500..7b28cd191 100644 --- a/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java +++ b/src/main/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapter.java @@ -15,6 +15,7 @@ import net.sf.jsqlparser.expression.operators.conditional.XorExpression; import net.sf.jsqlparser.expression.operators.relational.*; import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.select.AllColumns; import net.sf.jsqlparser.statement.select.AllTableColumns; import net.sf.jsqlparser.statement.select.ExpressionListItem; @@ -503,8 +504,14 @@ public void visit(SelectExpressionItem selectExpressionItem) { @Override public void visit(RowConstructor rowConstructor) { - for (Expression expr : rowConstructor.getExprList().getExpressions()) { - expr.accept(this); + if (rowConstructor.getColumnDefinitions().isEmpty()) { + for (Expression expression: rowConstructor.getExprList().getExpressions()) { + expression.accept(this); + } + } else { + for (ColumnDefinition columnDefinition : rowConstructor.getColumnDefinitions()) { + columnDefinition.accept(this); + } } } @@ -605,4 +612,8 @@ public void visit(JsonFunction expression) { expr.getExpression().accept(this); } } + + public void visit(ColumnDefinition columnDefinition) { + columnDefinition.accept(this); + } } diff --git a/src/main/java/net/sf/jsqlparser/expression/RowConstructor.java b/src/main/java/net/sf/jsqlparser/expression/RowConstructor.java index fb499dee1..12891b915 100644 --- a/src/main/java/net/sf/jsqlparser/expression/RowConstructor.java +++ b/src/main/java/net/sf/jsqlparser/expression/RowConstructor.java @@ -9,16 +9,26 @@ */ package net.sf.jsqlparser.expression; +import java.util.ArrayList; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.parser.ASTNodeAccessImpl; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; public class RowConstructor extends ASTNodeAccessImpl implements Expression { - private ExpressionList exprList; + private ArrayList columnDefinitions = new ArrayList<>(); private String name = null; public RowConstructor() { } + + public ArrayList getColumnDefinitions() { + return columnDefinitions; + } + + public boolean addColumnDefinition(ColumnDefinition columnDefinition) { + return columnDefinitions.add(columnDefinition); + } public ExpressionList getExprList() { return exprList; @@ -43,6 +53,17 @@ public void accept(ExpressionVisitor expressionVisitor) { @Override public String toString() { + if (columnDefinitions.size()>0) { + StringBuilder builder = new StringBuilder(name != null ? name : ""); + builder.append("("); + int i = 0; + for (ColumnDefinition columnDefinition:columnDefinitions) { + builder.append(i>0 ? ", " : "").append(columnDefinition.toString()); + i++; + } + builder.append(")"); + return builder.toString(); + } return (name != null ? name : "") + exprList.toString(); } diff --git a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java index 76a1be877..793e5adc2 100644 --- a/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java +++ b/src/main/java/net/sf/jsqlparser/statement/create/table/ColumnDefinition.java @@ -14,6 +14,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import net.sf.jsqlparser.expression.ExpressionVisitorAdapter; import net.sf.jsqlparser.statement.select.PlainSelect; /** @@ -99,4 +100,8 @@ public ColumnDefinition addColumnSpecs(Collection columnSpecs) { collection.addAll(columnSpecs); return this.withColumnSpecs(collection); } + + public void accept(ExpressionVisitorAdapter expressionVisitor) { + expressionVisitor.visit(this); + } } diff --git a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java index 3b7eb9f27..8d3417246 100644 --- a/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java +++ b/src/main/java/net/sf/jsqlparser/util/deparser/ExpressionDeParser.java @@ -101,6 +101,7 @@ import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.select.OrderByElement; import net.sf.jsqlparser.statement.select.PlainSelect; import net.sf.jsqlparser.statement.select.SelectVisitor; @@ -668,7 +669,7 @@ public void visit(CastExpression cast) { buffer.append("CAST("); cast.getLeftExpression().accept(this); buffer.append(" AS "); - buffer.append(cast.getType()); + buffer.append( cast.getRowConstructor()!=null ? cast.getRowConstructor() : cast.getType() ); buffer.append(")"); } else { cast.getLeftExpression().accept(this); @@ -872,14 +873,25 @@ public void visit(RowConstructor rowConstructor) { buffer.append(rowConstructor.getName()); } buffer.append("("); - boolean first = true; - for (Expression expr : rowConstructor.getExprList().getExpressions()) { - if (first) { - first = false; - } else { - buffer.append(", "); + + if (rowConstructor.getColumnDefinitions().size()>0) { + buffer.append("("); + int i = 0; + for (ColumnDefinition columnDefinition:rowConstructor.getColumnDefinitions()) { + buffer.append(i>0 ? ", " : "").append(columnDefinition.toString()); + i++; + } + buffer.append(")"); + } else { + boolean first = true; + for (Expression expr : rowConstructor.getExprList().getExpressions()) { + if (first) { + first = false; + } else { + buffer.append(", "); + } + expr.accept(this); } - expr.accept(this); } buffer.append(")"); } diff --git a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java index 5b2ea3606..93514c1ce 100644 --- a/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java +++ b/src/main/java/net/sf/jsqlparser/util/validation/validator/ExpressionValidator.java @@ -95,6 +95,7 @@ import net.sf.jsqlparser.expression.operators.relational.SupportsOldOracleJoinSyntax; import net.sf.jsqlparser.parser.feature.Feature; import net.sf.jsqlparser.schema.Column; +import net.sf.jsqlparser.statement.create.table.ColumnDefinition; import net.sf.jsqlparser.statement.select.SubSelect; import net.sf.jsqlparser.util.validation.ValidationCapability; import net.sf.jsqlparser.util.validation.metadata.NamedObject; @@ -499,7 +500,13 @@ public void visit(ValueListExpression valueList) { @Override public void visit(RowConstructor rowConstructor) { - validateOptionalExpressionList(rowConstructor.getExprList()); + if (rowConstructor.getColumnDefinitions().isEmpty()) { + validateOptionalExpressionList(rowConstructor.getExprList()); + } else { + for (ColumnDefinition columnDefinition: rowConstructor.getColumnDefinitions()) { + validateName(NamedObject.column, columnDefinition.getColumnName()); + } + } } @Override diff --git a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt index d338c6e31..bf3da555c 100644 --- a/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt +++ b/src/main/jjtree/net/sf/jsqlparser/parser/JSqlParserCC.jjt @@ -3169,7 +3169,8 @@ ExpressionList SimpleExpressionList(boolean outerBrackets) #ExpressionList: Expression expr = null; } { - expr=SimpleExpression() { expressions.add(expr); } ("," expr=SimpleExpression() { expressions.add(expr); })* + expr=SimpleExpression() { expressions.add(expr); } + ( LOOKAHEAD(2) "," expr=SimpleExpression() { expressions.add(expr); } )* { retval.setExpressions(expressions); return retval; @@ -3552,6 +3553,8 @@ Expression PrimaryExpression() #PrimaryExpression: | LOOKAHEAD(2) retval=CastExpression() + //| LOOKAHEAD(2) retval=RowConstructor() + // support timestamp expressions | (token= | token=) { retval = new TimeKeyExpression(token.image); } @@ -4096,15 +4099,23 @@ CastExpression CastExpression(): { CastExpression retval = new CastExpression(); ColDataType type = null; + RowConstructor rowConstructor = null; Expression expression = null; boolean useCastKeyword; } { - "(" expression=SimpleExpression() type=ColDataType() ")" { retval.setUseCastKeyword(true); } + + "(" + expression=SimpleExpression() + { retval.setUseCastKeyword(true); } + ( + LOOKAHEAD(3) rowConstructor = RowConstructor() { retval.setRowConstructor(rowConstructor); } + | type=ColDataType() { retval.setType(type); } + ) + ")" { retval.setLeftExpression(expression); - retval.setType(type); return retval; } } @@ -4168,16 +4179,20 @@ WhenClause WhenThenSearchCondition(): }*/ RowConstructor RowConstructor(): { - ExpressionList list = null; RowConstructor rowConstructor = new RowConstructor(); + ColumnDefinition columnDefinition = null; } { [ { rowConstructor.setName("ROW");} ] "(" - list = SimpleExpressionList(true) - ")" + columnDefinition = ColumnDefinition() { rowConstructor.addColumnDefinition(columnDefinition); } + ( + "," + columnDefinition = ColumnDefinition() { rowConstructor.addColumnDefinition(columnDefinition); } + )* + ")" + { - rowConstructor.setExprList(list); return rowConstructor; } } @@ -4541,7 +4556,7 @@ ColumnDefinition ColumnDefinition(): { colDataType = ColDataType() - ( parameter=CreateParameter() { columnSpecs.addAll(parameter); } )* + ( LOOKAHEAD(2) parameter=CreateParameter() { columnSpecs.addAll(parameter); } )* { coldef = new ColumnDefinition(); diff --git a/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java new file mode 100644 index 000000000..a0fdfd212 --- /dev/null +++ b/src/test/java/net/sf/jsqlparser/expression/CastExpressionTest.java @@ -0,0 +1,27 @@ +/*- + * #%L + * JSQLParser library + * %% + * Copyright (C) 2004 - 2021 JSQLParser + * %% + * Dual licensed under GNU LGPL 2.1 or Apache License 2.0 + * #L% + */ + +package net.sf.jsqlparser.expression; + +import net.sf.jsqlparser.JSQLParserException; +import net.sf.jsqlparser.test.TestUtils; +import org.junit.Test; + +/** + * + * @author Andreas Reichel + */ +public class CastExpressionTest { + @Test + public void testCastToRowConstructorIssue1267() throws JSQLParserException { + TestUtils.assertExpressionCanBeParsedAndDeparsed("CAST(ROW(dataid, value, calcMark) AS ROW(datapointid CHAR, value CHAR, calcMark CHAR))", true); + TestUtils.assertExpressionCanBeParsedAndDeparsed("CAST(ROW(dataid, value, calcMark) AS testcol)", true); + } +} diff --git a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java index 32cbb3016..e739d579f 100644 --- a/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java +++ b/src/test/java/net/sf/jsqlparser/expression/ExpressionVisitorAdapterTest.java @@ -256,4 +256,12 @@ public void testJsonAggregateFunction() throws JSQLParserException { .parseExpression("JSON_ARRAYAGG( a FORMAT JSON ABSENT ON NULL ) FILTER( WHERE name = 'Raj' ) OVER( PARTITION BY name )") .accept(adapter); } + + @Test + public void testRowConstructor() throws JSQLParserException { + ExpressionVisitorAdapter adapter = new ExpressionVisitorAdapter(); + CCJSqlParserUtil + .parseExpression("CAST(ROW(dataid, value, calcMark) AS ROW(datapointid CHAR, value CHAR, calcMark CHAR))") + .accept(adapter); + } } diff --git a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java index 350404464..7e7dc6409 100644 --- a/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java +++ b/src/test/java/net/sf/jsqlparser/statement/select/SelectTest.java @@ -4666,6 +4666,17 @@ public void testKeywordFilterIssue1255() throws JSQLParserException { } @Test + public void testUnionLimitOrderByIssue1268() throws JSQLParserException { + String sqlStr = "(SELECT __time FROM traffic_protocol_stat_log LIMIT 1) UNION ALL (SELECT __time FROM traffic_protocol_stat_log ORDER BY __time LIMIT 1)"; + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + + @Test + public void testCastToRowConstructorIssue1267() throws JSQLParserException { + String sqlStr = "SELECT CAST(ROW(dataid, value, calcMark) AS ROW(datapointid CHAR, value CHAR, calcMark CHAR)) AS datapoints"; + assertSqlCanBeParsedAndDeparsed(sqlStr, true); + } + public void testCollisionWithSpecialStringFunctionsIssue1284() throws JSQLParserException { assertSqlCanBeParsedAndDeparsed( "SELECT test( a in (1) AND 2=2) ", true); diff --git a/src/test/java/net/sf/jsqlparser/util/validation/ValidationTest.java b/src/test/java/net/sf/jsqlparser/util/validation/ValidationTest.java index f55f2030f..178843293 100644 --- a/src/test/java/net/sf/jsqlparser/util/validation/ValidationTest.java +++ b/src/test/java/net/sf/jsqlparser/util/validation/ValidationTest.java @@ -146,6 +146,14 @@ public void testFeatureSetName() { assertEquals("UPDATE + SELECT + feature set", FeaturesAllowed.UPDATE.copy().add(new FeaturesAllowed(Feature.commit)).getName()); } + + @Test + public void testRowConstructorValidation() throws JSQLParserException { + String stmt = "SELECT CAST(ROW(dataid, value, calcMark) AS ROW(datapointid CHAR, value CHAR, calcMark CHAR))"; + List errors = Validation.validate( + Arrays.asList(DatabaseType.ANSI_SQL, FeaturesAllowed.SELECT), stmt); + assertErrorsSize(errors, 0); + } } \ No newline at end of file