diff --git a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java index f458a174da6c..baf6c0135807 100644 --- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java +++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java @@ -15,7 +15,10 @@ * limitations under the License. */ package org.apache.calcite.test; +import org.apache.calcite.sql.SqlCall; import org.apache.calcite.sql.SqlDialect; +import org.apache.calcite.sql.SqlKind; +import org.apache.calcite.sql.SqlSelect; import org.apache.calcite.sql.dialect.MysqlSqlDialect; import org.apache.calcite.sql.dialect.PostgresqlSqlDialect; import org.apache.calcite.sql.dialect.SparkSqlDialect; @@ -25,6 +28,7 @@ import org.apache.calcite.sql.parser.SqlParserTest; import org.apache.calcite.sql.parser.StringAndPos; import org.apache.calcite.sql.parser.babel.SqlBabelParserImpl; +import org.apache.calcite.sql.validate.SqlAbstractConformance; import org.apache.calcite.tools.Hoist; import com.google.common.base.Throwables; @@ -53,6 +57,10 @@ class BabelParserTest extends SqlParserTest { .withConfig(c -> c.withParserFactory(SqlBabelParserImpl.FACTORY)); } + @Override protected boolean allowsDoubleColonInColonFieldAccessMode() { + return true; + } + /** Tests that the Babel parser correctly parses a CAST to INTERVAL type * in PostgreSQL dialect. */ @Test void testCastToInterval() { @@ -301,6 +309,38 @@ private void checkParseInfixCast(String sqlType) { sql(sql).ok(expected); } + @Test void testInfixCastBracketAccessNeedsParentheses() { + sql("select v::variant^[^1] from t") + .fails("(?s).*Encountered \"\\[\".*"); + sql("select (v::variant)[1], (v::integer array)[1] from t") + .node( + customMatches("select list", node -> { + final SqlSelect select = (SqlSelect) node; + assertThat(select.getSelectList().get(0).getKind(), is(SqlKind.ITEM)); + assertThat(((SqlCall) select.getSelectList().get(0)).operand(0).getKind(), + is(SqlKind.CAST)); + assertThat(select.getSelectList().get(1).getKind(), is(SqlKind.ITEM)); + assertThat(((SqlCall) select.getSelectList().get(1)).operand(0).getKind(), + is(SqlKind.CAST)); + })); + } + @Test void testColonFieldAccessWithInfixCast() { + final SqlParserFixture f = + fixture().withConformance(new SqlAbstractConformance() { + @Override public boolean isColonFieldAccessAllowed() { + return true; + } + }); + f.sql("select v:field::integer, arr[1]:field::varchar, " + + "v:field.field2::integer, v:field[2]::integer from t") + .ok("SELECT (`V`.`FIELD`) :: INTEGER, " + + "(`ARR`[1].`FIELD`) :: VARCHAR, " + + "((`V`.`FIELD`).`FIELD2`) :: INTEGER, " + + "(`V`.`FIELD`)[2] :: INTEGER\n" + + "FROM `T`"); + f.sql("select v::variant^:^field from t") + .fails("(?s).*Encountered \":.*\".*"); + } /** Tests parsing MySQL-style "<=>" equal operator. */ @Test void testParseNullSafeEqual() { // x <=> y diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index d87339e6602a..923125c723e8 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -3774,6 +3774,24 @@ SqlNode Expression(ExprContext exprContext) : list = Expression2(exprContext) { return SqlParserUtil.toTree(list); } } +void AddRegularPostfixes(List list) : +{ + SqlNode ext; +} +{ + ( + LOOKAHEAD(2) + ext = RowExpressionExtension() { + list.add( + new SqlParserUtil.ToTreeListItem( + SqlStdOperatorTable.DOT, getPos())); + list.add(ext); + } + | + AddBracketPostfix(list) + )* +} + void AddExpression2b(List list, ExprContext exprContext) : { SqlNode e; @@ -3791,13 +3809,61 @@ void AddExpression2b(List list, ExprContext exprContext) : e = Expression3(exprContext) { list.add(e); } + AddRegularPostfixes(list) + [ + LOOKAHEAD(2, SimpleIdentifier(), + { this.conformance.isColonFieldAccessAllowed() }) + + ext = SimpleIdentifier() { + list.add( + new SqlParserUtil.ToTreeListItem( + SqlStdOperatorTable.DOT, getPos())); + list.add(ext); + } + AddRegularPostfixes(list) + | + LOOKAHEAD(2, , + { this.conformance.isColonFieldAccessAllowed() }) + + AddBracketAccess(list) + AddRegularPostfixes(list) + ] +} + +void AddBracketAccess(List list) : +{ + SqlNode e; + SqlOperator itemOp; +} +{ + + ( { itemOp = SqlLibraryOperators.OFFSET; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } + | { itemOp = SqlLibraryOperators.ORDINAL; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } + | { itemOp = SqlLibraryOperators.SAFE_OFFSET; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } + | { itemOp = SqlLibraryOperators.SAFE_ORDINAL; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } + | { itemOp = SqlStdOperatorTable.ITEM; } e = Expression(ExprContext.ACCEPT_SUB_QUERY) + ) + { + list.add( + new SqlParserUtil.ToTreeListItem( + itemOp, getPos())); + list.add(e); + } +} + +void AddBracketPostfix(List list) : +{ + SqlIdentifier p; +} +{ + AddBracketAccess(list) ( LOOKAHEAD(2) - ext = RowExpressionExtension() { + p = SimpleIdentifier() { list.add( new SqlParserUtil.ToTreeListItem( SqlStdOperatorTable.DOT, getPos())); - list.add(ext); + list.add(p); } )* } @@ -3823,9 +3889,7 @@ List Expression2(ExprContext exprContext) : final List list3 = new ArrayList(); SqlNodeList nodeList; SqlNode e; - SqlOperator itemOp; SqlOperator op; - SqlIdentifier p; final Span s = span(); } { @@ -3962,29 +4026,6 @@ List Expression2(ExprContext exprContext) : list.add(new SqlParserUtil.ToTreeListItem(op, getPos())); } AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) - | - - ( { itemOp = SqlLibraryOperators.OFFSET; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } - | { itemOp = SqlLibraryOperators.ORDINAL; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } - | { itemOp = SqlLibraryOperators.SAFE_OFFSET; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } - | { itemOp = SqlLibraryOperators.SAFE_ORDINAL; } { e = Expression(ExprContext.ACCEPT_SUB_QUERY); } - | { itemOp = SqlStdOperatorTable.ITEM; } e = Expression(ExprContext.ACCEPT_SUB_QUERY) - ) - { - list.add( - new SqlParserUtil.ToTreeListItem( - itemOp, getPos())); - list.add(e); - } - ( - LOOKAHEAD(2) - p = SimpleIdentifier() { - list.add( - new SqlParserUtil.ToTreeListItem( - SqlStdOperatorTable.DOT, getPos())); - list.add(p); - } - )* | { checkNonQueryExpression(exprContext); @@ -7012,13 +7053,15 @@ List JsonNameAndValue() : | { - if (kvMode) { + if (kvMode || this.conformance.isColonFieldAccessAllowed()) { throw SqlUtil.newContextException(getPos(), RESOURCE.illegalComma()); } } | { - if (kvMode) { + // If ':' is used for field access, JSON constructors must use + // VALUE syntax instead of ':'. + if (kvMode || this.conformance.isColonFieldAccessAllowed()) { throw SqlUtil.newContextException(getPos(), RESOURCE.illegalColon()); } } diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java index 5e652be9b839..a5060eea90b0 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformance.java @@ -260,6 +260,19 @@ enum SelectAliasLookup { */ boolean allowHyphenInUnquotedTableName(); + /** + * Whether {@code :} is allowed as a field/item access operator. + * + *

If true, expressions such as {@code v:field}, {@code v:['x']} and + * {@code arr[1]:field} are valid. In this mode, JSON constructors must use + * {@code VALUE} syntax rather than {@code :} or comma-pair syntax. + * + *

Among the built-in conformance levels, false for all. + */ + default boolean isColonFieldAccessAllowed() { + return false; + } + /** * Whether the bang-equal token != is allowed as an alternative to <> in * the parser. diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java index 8d7d0b530608..047f05981a16 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlConformanceEnum.java @@ -232,6 +232,10 @@ public enum SqlConformanceEnum implements SqlConformance { } } + @Override public boolean isColonFieldAccessAllowed() { + return false; + } + @Override public boolean isBangEqualAllowed() { switch (this) { case LENIENT: diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java index 00a77b0ee042..25f8d7e03747 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlDelegatingConformance.java @@ -87,6 +87,10 @@ protected SqlDelegatingConformance(SqlConformance delegate) { return delegate.allowHyphenInUnquotedTableName(); } + @Override public boolean isColonFieldAccessAllowed() { + return delegate.isColonFieldAccessAllowed(); + } + @Override public boolean isBangEqualAllowed() { return delegate.isBangEqualAllowed(); } diff --git a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java index cfea5b6ce0e9..d2a1d7e3bc7d 100644 --- a/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java +++ b/testkit/src/main/java/org/apache/calcite/sql/parser/SqlParserTest.java @@ -43,6 +43,7 @@ import org.apache.calcite.sql.validate.SqlAbstractConformance; import org.apache.calcite.sql.validate.SqlConformance; import org.apache.calcite.sql.validate.SqlConformanceEnum; +import org.apache.calcite.sql.validate.SqlDelegatingConformance; import org.apache.calcite.test.IntervalTest; import org.apache.calcite.tools.Hoist; import org.apache.calcite.util.Bug; @@ -630,6 +631,12 @@ public class SqlParserTest { SqlDialect.DatabaseProduct.POSTGRESQL.getDialect(); private static final SqlDialect REDSHIFT = SqlDialect.DatabaseProduct.REDSHIFT.getDialect(); + private static final SqlConformance COLON_FIELD = + new SqlDelegatingConformance(SqlConformanceEnum.DEFAULT) { + @Override public boolean isColonFieldAccessAllowed() { + return true; + } + }; /** Creates the test fixture that determines the behavior of tests. * Sub-classes that, say, test different parser implementations should @@ -646,6 +653,10 @@ protected SqlParserFixture expr(String sql) { return sql(sql).expression(true); } + protected boolean allowsDoubleColonInColonFieldAccessMode() { + return false; + } + /** Converts a string to linux format (LF line endings rather than CR-LF), * except if disabled in {@link SqlParserFixture#convertToLinux}. */ static UnaryOperator linux(boolean convertToLinux) { @@ -2310,6 +2321,82 @@ void checkPeriodPredicate(Checker checker) { .ok("(`FOO`(`A`, `B`).`C`)"); } + @Test void testColonFieldAccessMode() { + assumeFalse(fixture().tester.isUnparserTest()); + final SqlParserFixture expr = fixture().withConformance(COLON_FIELD).expression(); + final SqlParserFixture sql = fixture().withConformance(COLON_FIELD); + expr("v^:^field") + .fails("(?s).*Encountered \":.*\".*"); + expr.sql("v:field") + .ok("(`V`.`FIELD`)"); + expr.sql("v:field.nested") + .ok("((`V`.`FIELD`).`NESTED`)"); + expr.sql("v:['field name']") + .ok("`V`['field name']"); + expr.sql("arr[1]:field") + .ok("(`ARR`[1].`FIELD`)"); + expr.sql("obj['x']:nested") + .ok("(`OBJ`['x'].`NESTED`)"); + sql.sql( + "select v:field, v:['field name'], arr[1]:field, obj['x']:nested from t") + .ok("SELECT (`V`.`FIELD`), `V`['field name'], (`ARR`[1].`FIELD`), " + + "(`OBJ`['x'].`NESTED`)\n" + + "FROM `T`"); + if (!allowsDoubleColonInColonFieldAccessMode()) { + expr.sql("v^:^:field") + .fails("(?s).*Encountered \":.*\".*"); + } + } + + @Test void testColonFieldAccessEdgeCases() { + assumeFalse(fixture().tester.isUnparserTest()); + final SqlParserFixture expr = fixture().withConformance(COLON_FIELD).expression(); + expr.sql("v:field['leaf']") + .ok("(`V`.`FIELD`)['leaf']"); + expr.sql("v:['field name'].leaf") + .ok("(`V`['field name'].`LEAF`)"); + expr.sql("v:[OFFSET(1)]") + .ok("`V`[OFFSET(1)]"); + expr.sql("v:[ORDINAL(1)]") + .ok("`V`[ORDINAL(1)]"); + expr.sql("v:[SAFE_OFFSET(1)]") + .ok("`V`[SAFE_OFFSET(1)]"); + expr.sql("v:[SAFE_ORDINAL(1)]") + .ok("`V`[SAFE_ORDINAL(1)]"); + expr.sql("v:[i + 1]") + .ok("`V`[(`I` + 1)]"); + expr.sql("v:[OFFSET(1)].field") + .ok("(`V`[OFFSET(1)].`FIELD`)"); + expr.sql("v:[OFFSET(1)][SAFE_ORDINAL(2)]") + .ok("`V`[OFFSET(1)][SAFE_ORDINAL(2)]"); + expr.sql("v:field[OFFSET(1)]") + .ok("(`V`.`FIELD`)[OFFSET(1)]"); + expr.sql("arr[1]:field[2]") + .ok("(`ARR`[1].`FIELD`)[2]"); + expr.sql("v.field:nested") + .ok("(`V`.`FIELD`.`NESTED`)"); + expr.sql("obj['x']:nested['y']") + .ok("(`OBJ`['x'].`NESTED`)['y']"); + expr.sql("a = b:field") + .ok("(`A` = (`B`.`FIELD`))"); + expr.sql("a + b:field") + .ok("(`A` + (`B`.`FIELD`))"); + expr.sql("a * arr[1]:field") + .ok("(`A` * (`ARR`[1].`FIELD`))"); + expr.sql("foo(v:field, arr[1]:field, obj['x']:nested['y'])") + .ok("`FOO`((`V`.`FIELD`), (`ARR`[1].`FIELD`), (`OBJ`['x'].`NESTED`)['y'])"); + expr.sql("v:field^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + expr.sql("v:['field name']^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + expr.sql("v:[i + 1]^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + if (!allowsDoubleColonInColonFieldAccessMode()) { + expr.sql("v^:^:(field)") + .fails("(?s).*Encountered \":.*\".*"); + } + } + @Test void testFunctionInFunction() { expr("ln(power(2,2))") .ok("LN(POWER(2, 2))"); @@ -6688,7 +6775,9 @@ private IntervalTest.Fixture2 getFixture2(SqlParserFixture f2, + "Was expecting one of:\n" + " \n" + " \"\\(\" \\.\\.\\.\n" - + " \"\\.\" \\.\\.\\..*"); + + " \"\\[\" \\.\\.\\.\n" + + " \"\\.\" \\.\\.\\.\n" + + ".*"); expr("interval '1-2' year ^to^ day") .fails(ANY); expr("interval '1-2' year ^to^ hour") @@ -9138,6 +9227,23 @@ private static Consumer> checkWarnings( .ok("JSON_OBJECT(KEY `KEY` VALUE `VALUE` NULL ON NULL)"); } + @Test void testJsonObjectInColonFieldAccessMode() { + assumeFalse(fixture().tester.isUnparserTest()); + final SqlParserFixture expr = fixture().withConformance(COLON_FIELD).expression(); + expr.sql("json_object(key v:field value arr[1]:field)") + .ok("JSON_OBJECT(KEY (`V`.`FIELD`) VALUE (`ARR`[1].`FIELD`) NULL ON NULL)"); + expr.sql("json_object(key v:field.field value col)") + .ok("JSON_OBJECT(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); + expr.sql("json_object(v:field value 1)") + .ok("JSON_OBJECT(KEY (`V`.`FIELD`) VALUE 1 NULL ON NULL)"); + expr.sql("json_object(v:field^,^ 1)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + expr.sql("json_object('foo': col^,^ 1)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + expr.sql("json_object('foo'^,^ 'bar')") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + } + @Test void testJsonType() { expr("json_type('11.56')") .ok("JSON_TYPE('11.56')"); @@ -9208,6 +9314,24 @@ private static Consumer> checkWarnings( + "FORMAT JSON NULL ON NULL)"); } + @Test void testJsonObjectAggInColonFieldAccessMode() { + assumeFalse(fixture().tester.isUnparserTest()); + final SqlParserFixture expr = fixture().withConformance(COLON_FIELD).expression(); + expr.sql("json_objectagg(key v:[SAFE_OFFSET(1)] value obj['x']:nested['y'])") + .ok("JSON_OBJECTAGG(KEY `V`[SAFE_OFFSET(1)] VALUE " + + "(`OBJ`['x'].`NESTED`)['y'] NULL ON NULL)"); + expr.sql("json_objectagg(key v:field.field value col)") + .ok("JSON_OBJECTAGG(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); + expr.sql("json_objectagg(v:field value col)") + .ok("JSON_OBJECTAGG(KEY (`V`.`FIELD`) VALUE `COL` NULL ON NULL)"); + expr.sql("json_objectagg(v:field^,^ col)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + expr.sql("json_objectagg('k': [1]^,^ v)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + expr.sql("json_objectagg('k'^,^ 1)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + } + /** Test case for * [CALCITE-6003] * JSON_ARRAY() with no arguments does not unparse correctly. */