From 2e9384d48bf6ca7a08e0d5961d33377a991d2f99 Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Mon, 23 Mar 2026 13:07:30 +0100 Subject: [PATCH 1/7] [CALCITE-7448] Refactor expression postfix parsing --- core/src/main/codegen/templates/Parser.jj | 56 +++++++++++++---------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index d87339e6602a..be0988a68436 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -3802,6 +3802,37 @@ void AddExpression2b(List list, ExprContext exprContext) : )* } +void AddBracketPostfix(List list) : +{ + SqlNode e; + SqlOperator itemOp; + SqlIdentifier p; +} +{ + + ( { 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); + } + )* +} + /** * Parses a binary row expression, or a parenthesized expression of any * kind. @@ -3823,9 +3854,7 @@ List Expression2(ExprContext exprContext) : final List list3 = new ArrayList(); SqlNodeList nodeList; SqlNode e; - SqlOperator itemOp; SqlOperator op; - SqlIdentifier p; final Span s = span(); } { @@ -3963,28 +3992,7 @@ List Expression2(ExprContext exprContext) : } 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); - } - )* + AddBracketPostfix(list) | { checkNonQueryExpression(exprContext); From 23e023e2ef2dcffb32315b1dfb18232252c4236b Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Tue, 24 Mar 2026 08:52:47 +0100 Subject: [PATCH 2/7] [CALCITE-7448] Add conformance hook for colon field access --- .../calcite/sql/validate/SqlAbstractConformance.java | 4 ++++ .../apache/calcite/sql/validate/SqlConformance.java | 12 ++++++++++++ .../calcite/sql/validate/SqlConformanceEnum.java | 4 ++++ .../sql/validate/SqlDelegatingConformance.java | 4 ++++ 4 files changed, 24 insertions(+) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java index 39799a733cad..6a30f3536399 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java @@ -81,6 +81,10 @@ public abstract class SqlAbstractConformance implements SqlConformance { return SqlConformanceEnum.DEFAULT.allowHyphenInUnquotedTableName(); } + @Override public boolean isColonFieldAccessAllowed() { + return SqlConformanceEnum.DEFAULT.isColonFieldAccessAllowed(); + } + @Override public boolean isBangEqualAllowed() { return SqlConformanceEnum.DEFAULT.isBangEqualAllowed(); } 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..18ad7d743489 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,18 @@ 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. + * + *

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(); } From 9e79114ebd576d5be02125b6e73e843e29df67f3 Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Tue, 24 Mar 2026 08:53:35 +0100 Subject: [PATCH 3/7] [CALCITE-7448] Add conformance-gated colon field access --- .../apache/calcite/test/BabelParserTest.java | 25 ++++++ core/src/main/codegen/templates/Parser.jj | 50 +++++++++-- .../calcite/sql/parser/SqlParserTest.java | 87 ++++++++++++++++++- 3 files changed, 155 insertions(+), 7 deletions(-) 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..dae8a1471df0 100644 --- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java +++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java @@ -301,6 +301,31 @@ private void checkParseInfixCast(String sqlType) { sql(sql).ok(expected); } + @Test void testParseInfixCastWithBracketAccess() { + sql("select v::variant[1], (v::variant)[1], " + + "v::integer array[1], (v::integer array)[1] from t") + .ok("SELECT `V` :: VARIANT[1], `V` :: VARIANT[1], " + + "`V` :: INTEGER ARRAY[1], `V` :: INTEGER ARRAY[1]\n" + + "FROM `T`"); + } + + @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 be0988a68436..183608790dad 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,22 +3809,31 @@ void AddExpression2b(List list, ExprContext exprContext) : e = Expression3(exprContext) { list.add(e); } - ( - LOOKAHEAD(2) - ext = RowExpressionExtension() { + 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 AddBracketPostfix(List list) : +void AddBracketAccess(List list) : { SqlNode e; SqlOperator itemOp; - SqlIdentifier p; } { @@ -3822,6 +3849,14 @@ void AddBracketPostfix(List list) : itemOp, getPos())); list.add(e); } +} + +void AddBracketPostfix(List list) : +{ + SqlIdentifier p; +} +{ + AddBracketAccess(list) ( LOOKAHEAD(2) p = SimpleIdentifier() { @@ -3992,6 +4027,9 @@ List Expression2(ExprContext exprContext) : } AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) | + // Keep bracket access here so late-added operators such as + // Babel's "::" cast can continue into the same item-access + // forms that baseline accepted. AddBracketPostfix(list) | { 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..19a37c1684b5 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 @@ -646,6 +646,26 @@ protected SqlParserFixture expr(String sql) { return sql(sql).expression(true); } + protected SqlConformance colonFieldConformance() { + return new SqlAbstractConformance() { + @Override public boolean isColonFieldAccessAllowed() { + return true; + } + }; + } + + protected SqlParserFixture colonFieldFixture() { + return fixture().withConformance(colonFieldConformance()); + } + + protected SqlParserFixture colonFieldSql(String sql) { + return colonFieldFixture().sql(sql); + } + + protected SqlParserFixture colonFieldExpr(String sql) { + return colonFieldSql(sql).expression(true); + } + /** 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 +2330,69 @@ void checkPeriodPredicate(Checker checker) { .ok("(`FOO`(`A`, `B`).`C`)"); } + @Test void testColonFieldAccessMode() { + expr("v^:^field") + .fails("(?s).*Encountered \":.*\".*"); + colonFieldExpr("v:field") + .ok("(`V`.`FIELD`)"); + colonFieldExpr("v:field.nested") + .ok("((`V`.`FIELD`).`NESTED`)"); + colonFieldExpr("v:['field name']") + .ok("`V`['field name']"); + colonFieldExpr("arr[1]:field") + .ok("(`ARR`[1].`FIELD`)"); + colonFieldExpr("obj['x']:nested") + .ok("(`OBJ`['x'].`NESTED`)"); + colonFieldSql( + "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`"); + colonFieldExpr("v^:^:field") + .fails("(?s).*Encountered \":.*\".*"); + } + + @Test void testColonFieldAccessEdgeCases() { + colonFieldExpr("v:field['leaf']") + .ok("(`V`.`FIELD`)['leaf']"); + colonFieldExpr("v:['field name'].leaf") + .ok("(`V`['field name'].`LEAF`)"); + colonFieldExpr("v:[OFFSET(1)]") + .ok("`V`[OFFSET(1)]"); + colonFieldExpr("v:[ORDINAL(1)]") + .ok("`V`[ORDINAL(1)]"); + colonFieldExpr("v:[SAFE_OFFSET(1)]") + .ok("`V`[SAFE_OFFSET(1)]"); + colonFieldExpr("v:[SAFE_ORDINAL(1)]") + .ok("`V`[SAFE_ORDINAL(1)]"); + colonFieldExpr("v:[i + 1]") + .ok("`V`[(`I` + 1)]"); + colonFieldExpr("v:[OFFSET(1)].field") + .ok("(`V`[OFFSET(1)].`FIELD`)"); + colonFieldExpr("v:[OFFSET(1)][SAFE_ORDINAL(2)]") + .ok("`V`[OFFSET(1)][SAFE_ORDINAL(2)]"); + colonFieldExpr("v:field[OFFSET(1)]") + .ok("(`V`.`FIELD`)[OFFSET(1)]"); + colonFieldExpr("arr[1]:field[2]") + .ok("(`ARR`[1].`FIELD`)[2]"); + colonFieldExpr("obj['x']:nested['y']") + .ok("(`OBJ`['x'].`NESTED`)['y']"); + colonFieldExpr("a + b:field") + .ok("(`A` + (`B`.`FIELD`))"); + colonFieldExpr("a * arr[1]:field") + .ok("(`A` * (`ARR`[1].`FIELD`))"); + colonFieldExpr("foo(v:field, arr[1]:field, obj['x']:nested['y'])") + .ok("`FOO`((`V`.`FIELD`), (`ARR`[1].`FIELD`), (`OBJ`['x'].`NESTED`)['y'])"); + colonFieldExpr("v:field^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + colonFieldExpr("v:['field name']^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + colonFieldExpr("v:[i + 1]^:^leaf") + .fails("(?s).*Encountered \":.*\".*"); + colonFieldExpr("v^:^:(field)") + .fails("(?s).*Encountered \":.*\".*"); + } + @Test void testFunctionInFunction() { expr("ln(power(2,2))") .ok("LN(POWER(2, 2))"); @@ -6688,7 +6771,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") From a158419a5f5a16b0f91f28267d434934ab9e870a Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Tue, 24 Mar 2026 08:55:45 +0100 Subject: [PATCH 4/7] [CALCITE-7448] Disambiguate JSON constructors from colon field access --- core/src/main/codegen/templates/Parser.jj | 6 ++-- .../calcite/sql/validate/SqlConformance.java | 3 +- .../calcite/sql/parser/SqlParserTest.java | 31 +++++++++++++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index 183608790dad..e9b3fd96c794 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -7058,13 +7058,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 18ad7d743489..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 @@ -264,7 +264,8 @@ enum SelectAliasLookup { * 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. + * {@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. */ 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 19a37c1684b5..11df8a36fa82 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 @@ -9223,6 +9223,21 @@ private static Consumer> checkWarnings( .ok("JSON_OBJECT(KEY `KEY` VALUE `VALUE` NULL ON NULL)"); } + @Test void testJsonObjectInColonFieldAccessMode() { + colonFieldExpr("json_object(key v:field value arr[1]:field)") + .ok("JSON_OBJECT(KEY (`V`.`FIELD`) VALUE (`ARR`[1].`FIELD`) NULL ON NULL)"); + colonFieldExpr("json_object(key v:field.field value col)") + .ok("JSON_OBJECT(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); + colonFieldExpr("json_object(v:field value 1)") + .ok("JSON_OBJECT(KEY (`V`.`FIELD`) VALUE 1 NULL ON NULL)"); + colonFieldExpr("json_object(v:field^,^ 1)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + colonFieldExpr("json_object('foo': col^,^ 1)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + colonFieldExpr("json_object('foo'^,^ 'bar')") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + } + @Test void testJsonType() { expr("json_type('11.56')") .ok("JSON_TYPE('11.56')"); @@ -9293,6 +9308,22 @@ private static Consumer> checkWarnings( + "FORMAT JSON NULL ON NULL)"); } + @Test void testJsonObjectAggInColonFieldAccessMode() { + colonFieldExpr("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)"); + colonFieldExpr("json_objectagg(v:field value col)") + .ok("JSON_OBJECTAGG(KEY (`V`.`FIELD`) VALUE `COL` NULL ON NULL)"); + colonFieldExpr("json_objectagg(key v:field.field value col)") + .ok("JSON_OBJECTAGG(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); + colonFieldExpr("json_objectagg(v:field^,^ col)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + colonFieldExpr("json_objectagg('k': [1]^,^ v)") + .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); + colonFieldExpr("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. */ From a0dec2ccf12980189fa3436422b894207f8d7c71 Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Tue, 24 Mar 2026 17:08:16 +0100 Subject: [PATCH 5/7] [CALCITE-7448] Refine parser coverage and parenthesize infix cast bracket access --- .../apache/calcite/test/BabelParserTest.java | 28 +++- core/src/main/codegen/templates/Parser.jj | 5 - .../calcite/sql/parser/SqlParserTest.java | 126 ++++++++++-------- 3 files changed, 90 insertions(+), 69 deletions(-) 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 dae8a1471df0..80ad147c4cc4 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,14 +309,24 @@ private void checkParseInfixCast(String sqlType) { sql(sql).ok(expected); } - @Test void testParseInfixCastWithBracketAccess() { - sql("select v::variant[1], (v::variant)[1], " - + "v::integer array[1], (v::integer array)[1] from t") - .ok("SELECT `V` :: VARIANT[1], `V` :: VARIANT[1], " - + "`V` :: INTEGER ARRAY[1], `V` :: INTEGER ARRAY[1]\n" + @Test void testParseParenthesizedInfixCastWithBracketAccess() { + sql("select (v::variant)[1], (v::integer array)[1] from t") + .ok("SELECT `V` :: VARIANT[1], `V` :: INTEGER ARRAY[1]\n" + "FROM `T`"); } + @Test void testInfixCastBracketAccessNeedsParentheses() { + sql("select v::variant^[^1] from t") + .fails("(?s).*Encountered \"\\[\".*"); + sql("select (v::variant)[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)); + })); + } @Test void testColonFieldAccessWithInfixCast() { final SqlParserFixture f = fixture().withConformance(new SqlAbstractConformance() { diff --git a/core/src/main/codegen/templates/Parser.jj b/core/src/main/codegen/templates/Parser.jj index e9b3fd96c794..923125c723e8 100644 --- a/core/src/main/codegen/templates/Parser.jj +++ b/core/src/main/codegen/templates/Parser.jj @@ -4026,11 +4026,6 @@ List Expression2(ExprContext exprContext) : list.add(new SqlParserUtil.ToTreeListItem(op, getPos())); } AddExpression2b(list, ExprContext.ACCEPT_SUB_QUERY) - | - // Keep bracket access here so late-added operators such as - // Babel's "::" cast can continue into the same item-access - // forms that baseline accepted. - AddBracketPostfix(list) | { checkNonQueryExpression(exprContext); 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 11df8a36fa82..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,24 +653,8 @@ protected SqlParserFixture expr(String sql) { return sql(sql).expression(true); } - protected SqlConformance colonFieldConformance() { - return new SqlAbstractConformance() { - @Override public boolean isColonFieldAccessAllowed() { - return true; - } - }; - } - - protected SqlParserFixture colonFieldFixture() { - return fixture().withConformance(colonFieldConformance()); - } - - protected SqlParserFixture colonFieldSql(String sql) { - return colonFieldFixture().sql(sql); - } - - protected SqlParserFixture colonFieldExpr(String sql) { - return colonFieldSql(sql).expression(true); + protected boolean allowsDoubleColonInColonFieldAccessMode() { + return false; } /** Converts a string to linux format (LF line endings rather than CR-LF), @@ -2331,66 +2322,79 @@ void checkPeriodPredicate(Checker checker) { } @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 \":.*\".*"); - colonFieldExpr("v:field") + expr.sql("v:field") .ok("(`V`.`FIELD`)"); - colonFieldExpr("v:field.nested") + expr.sql("v:field.nested") .ok("((`V`.`FIELD`).`NESTED`)"); - colonFieldExpr("v:['field name']") + expr.sql("v:['field name']") .ok("`V`['field name']"); - colonFieldExpr("arr[1]:field") + expr.sql("arr[1]:field") .ok("(`ARR`[1].`FIELD`)"); - colonFieldExpr("obj['x']:nested") + expr.sql("obj['x']:nested") .ok("(`OBJ`['x'].`NESTED`)"); - colonFieldSql( + 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`"); - colonFieldExpr("v^:^:field") - .fails("(?s).*Encountered \":.*\".*"); + if (!allowsDoubleColonInColonFieldAccessMode()) { + expr.sql("v^:^:field") + .fails("(?s).*Encountered \":.*\".*"); + } } @Test void testColonFieldAccessEdgeCases() { - colonFieldExpr("v:field['leaf']") + assumeFalse(fixture().tester.isUnparserTest()); + final SqlParserFixture expr = fixture().withConformance(COLON_FIELD).expression(); + expr.sql("v:field['leaf']") .ok("(`V`.`FIELD`)['leaf']"); - colonFieldExpr("v:['field name'].leaf") + expr.sql("v:['field name'].leaf") .ok("(`V`['field name'].`LEAF`)"); - colonFieldExpr("v:[OFFSET(1)]") + expr.sql("v:[OFFSET(1)]") .ok("`V`[OFFSET(1)]"); - colonFieldExpr("v:[ORDINAL(1)]") + expr.sql("v:[ORDINAL(1)]") .ok("`V`[ORDINAL(1)]"); - colonFieldExpr("v:[SAFE_OFFSET(1)]") + expr.sql("v:[SAFE_OFFSET(1)]") .ok("`V`[SAFE_OFFSET(1)]"); - colonFieldExpr("v:[SAFE_ORDINAL(1)]") + expr.sql("v:[SAFE_ORDINAL(1)]") .ok("`V`[SAFE_ORDINAL(1)]"); - colonFieldExpr("v:[i + 1]") + expr.sql("v:[i + 1]") .ok("`V`[(`I` + 1)]"); - colonFieldExpr("v:[OFFSET(1)].field") + expr.sql("v:[OFFSET(1)].field") .ok("(`V`[OFFSET(1)].`FIELD`)"); - colonFieldExpr("v:[OFFSET(1)][SAFE_ORDINAL(2)]") + expr.sql("v:[OFFSET(1)][SAFE_ORDINAL(2)]") .ok("`V`[OFFSET(1)][SAFE_ORDINAL(2)]"); - colonFieldExpr("v:field[OFFSET(1)]") + expr.sql("v:field[OFFSET(1)]") .ok("(`V`.`FIELD`)[OFFSET(1)]"); - colonFieldExpr("arr[1]:field[2]") + expr.sql("arr[1]:field[2]") .ok("(`ARR`[1].`FIELD`)[2]"); - colonFieldExpr("obj['x']:nested['y']") + expr.sql("v.field:nested") + .ok("(`V`.`FIELD`.`NESTED`)"); + expr.sql("obj['x']:nested['y']") .ok("(`OBJ`['x'].`NESTED`)['y']"); - colonFieldExpr("a + b:field") + expr.sql("a = b:field") + .ok("(`A` = (`B`.`FIELD`))"); + expr.sql("a + b:field") .ok("(`A` + (`B`.`FIELD`))"); - colonFieldExpr("a * arr[1]:field") + expr.sql("a * arr[1]:field") .ok("(`A` * (`ARR`[1].`FIELD`))"); - colonFieldExpr("foo(v:field, arr[1]:field, obj['x']:nested['y'])") + expr.sql("foo(v:field, arr[1]:field, obj['x']:nested['y'])") .ok("`FOO`((`V`.`FIELD`), (`ARR`[1].`FIELD`), (`OBJ`['x'].`NESTED`)['y'])"); - colonFieldExpr("v:field^:^leaf") - .fails("(?s).*Encountered \":.*\".*"); - colonFieldExpr("v:['field name']^:^leaf") + expr.sql("v:field^:^leaf") .fails("(?s).*Encountered \":.*\".*"); - colonFieldExpr("v:[i + 1]^:^leaf") + expr.sql("v:['field name']^:^leaf") .fails("(?s).*Encountered \":.*\".*"); - colonFieldExpr("v^:^:(field)") + expr.sql("v:[i + 1]^:^leaf") .fails("(?s).*Encountered \":.*\".*"); + if (!allowsDoubleColonInColonFieldAccessMode()) { + expr.sql("v^:^:(field)") + .fails("(?s).*Encountered \":.*\".*"); + } } @Test void testFunctionInFunction() { @@ -9224,17 +9228,19 @@ private static Consumer> checkWarnings( } @Test void testJsonObjectInColonFieldAccessMode() { - colonFieldExpr("json_object(key v:field value arr[1]:field)") + 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)"); - colonFieldExpr("json_object(key v:field.field value col)") + expr.sql("json_object(key v:field.field value col)") .ok("JSON_OBJECT(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); - colonFieldExpr("json_object(v:field value 1)") + expr.sql("json_object(v:field value 1)") .ok("JSON_OBJECT(KEY (`V`.`FIELD`) VALUE 1 NULL ON NULL)"); - colonFieldExpr("json_object(v:field^,^ 1)") + expr.sql("json_object(v:field^,^ 1)") .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); - colonFieldExpr("json_object('foo': col^,^ 1)") + expr.sql("json_object('foo': col^,^ 1)") .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); - colonFieldExpr("json_object('foo'^,^ 'bar')") + expr.sql("json_object('foo'^,^ 'bar')") .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); } @@ -9309,18 +9315,20 @@ private static Consumer> checkWarnings( } @Test void testJsonObjectAggInColonFieldAccessMode() { - colonFieldExpr("json_objectagg(key v:[SAFE_OFFSET(1)] value obj['x']:nested['y'])") + 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)"); - colonFieldExpr("json_objectagg(v:field value col)") - .ok("JSON_OBJECTAGG(KEY (`V`.`FIELD`) VALUE `COL` NULL ON NULL)"); - colonFieldExpr("json_objectagg(key v:field.field value col)") + expr.sql("json_objectagg(key v:field.field value col)") .ok("JSON_OBJECTAGG(KEY ((`V`.`FIELD`).`FIELD`) VALUE `COL` NULL ON NULL)"); - colonFieldExpr("json_objectagg(v:field^,^ col)") + 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'.*"); - colonFieldExpr("json_objectagg('k': [1]^,^ v)") + expr.sql("json_objectagg('k': [1]^,^ v)") .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); - colonFieldExpr("json_objectagg('k'^,^ 1)") + expr.sql("json_objectagg('k'^,^ 1)") .fails("(?s).*Unexpected symbol ','. Was expecting 'VALUE'.*"); } From de84cfdc178021b13fb18a38d26cae34e22f6c65 Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Wed, 25 Mar 2026 09:45:13 +0100 Subject: [PATCH 6/7] [CALCITE-7448] Clarify infix cast bracket parser coverage --- .../java/org/apache/calcite/test/BabelParserTest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 80ad147c4cc4..baf6c0135807 100644 --- a/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java +++ b/babel/src/test/java/org/apache/calcite/test/BabelParserTest.java @@ -309,22 +309,19 @@ private void checkParseInfixCast(String sqlType) { sql(sql).ok(expected); } - @Test void testParseParenthesizedInfixCastWithBracketAccess() { - sql("select (v::variant)[1], (v::integer array)[1] from t") - .ok("SELECT `V` :: VARIANT[1], `V` :: INTEGER ARRAY[1]\n" - + "FROM `T`"); - } - @Test void testInfixCastBracketAccessNeedsParentheses() { sql("select v::variant^[^1] from t") .fails("(?s).*Encountered \"\\[\".*"); - sql("select (v::variant)[1] from t") + 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() { From 9562ed9bbeb61ed4fa5ad4680fa25d45a5429d87 Mon Sep 17 00:00:00 2001 From: Tamas Mate Date: Wed, 25 Mar 2026 10:26:45 +0100 Subject: [PATCH 7/7] [CALCITE-7448] Remove redundant colon field conformance override --- .../apache/calcite/sql/validate/SqlAbstractConformance.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java index 6a30f3536399..39799a733cad 100644 --- a/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java +++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlAbstractConformance.java @@ -81,10 +81,6 @@ public abstract class SqlAbstractConformance implements SqlConformance { return SqlConformanceEnum.DEFAULT.allowHyphenInUnquotedTableName(); } - @Override public boolean isColonFieldAccessAllowed() { - return SqlConformanceEnum.DEFAULT.isColonFieldAccessAllowed(); - } - @Override public boolean isBangEqualAllowed() { return SqlConformanceEnum.DEFAULT.isBangEqualAllowed(); }