This is an automated email from the ASF dual-hosted git repository.

mihaibudiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/calcite.git


The following commit(s) were added to refs/heads/main by this push:
     new 03d333693e [CALCITE-7475] Babel parser allows postfix access after 
PostgreSQL-style :: infix cast
03d333693e is described below

commit 03d333693e1797116cdc160d0471dbf8926ec743
Author: Stamatis Zampetakis <[email protected]>
AuthorDate: Thu May 7 20:11:50 2026 +0200

    [CALCITE-7475] Babel parser allows postfix access after PostgreSQL-style :: 
infix cast
---
 babel/src/main/codegen/includes/parserImpls.ftl    | 23 ++++--
 .../org/apache/calcite/test/BabelParserTest.java   | 83 ++++++++++++++++------
 .../apache/calcite/sql/fun/SqlCastOperator.java    |  9 +++
 3 files changed, 91 insertions(+), 24 deletions(-)

diff --git a/babel/src/main/codegen/includes/parserImpls.ftl 
b/babel/src/main/codegen/includes/parserImpls.ftl
index 7565303fa0..391022d796 100644
--- a/babel/src/main/codegen/includes/parserImpls.ftl
+++ b/babel/src/main/codegen/includes/parserImpls.ftl
@@ -197,17 +197,32 @@ SqlCreate SqlCreateTable(Span s, boolean replace) :
 void InfixCast(List<Object> list, ExprContext exprContext, Span s) :
 {
     final SqlDataTypeSpec dt;
+    SqlNode e, p;
 }
 {
     <INFIX_CAST> {
         checkNonQueryExpression(exprContext);
     }
     dt = DataType() {
-        list.add(
-            new SqlParserUtil.ToTreeListItem(SqlLibraryOperators.INFIX_CAST,
-                s.pos()));
-        list.add(dt);
+        SqlNode leftOperand = SqlParserUtil.toTree(list);
+        list.clear();
+        SqlNode castNode = SqlLibraryOperators.INFIX_CAST.createCall(
+        s.pos(), leftOperand, dt);
+        list.add(castNode);
     }
+    (   <LBRACKET>
+        e = Expression(ExprContext.ACCEPT_SUB_QUERY)
+        <RBRACKET> {
+            SqlNode current = (SqlNode) list.remove(list.size() - 1);
+            list.add(SqlStdOperatorTable.ITEM.createCall(getPos(), current, 
e));
+        }
+    |
+        <DOT>
+        p = SimpleIdentifier() {
+            SqlNode current = (SqlNode) list.remove(list.size() - 1);
+            list.add(SqlStdOperatorTable.DOT.createCall(getPos(), current, p));
+        }
+    )*
 }
 
 /** Parses the NULL-safe "<=>" equal operator used in MySQL. */
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 a305586738..d566642227 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,11 @@
  * limitations under the License.
  */
 package org.apache.calcite.test;
+import org.apache.calcite.sql.SqlBasicCall;
 import org.apache.calcite.sql.SqlDialect;
+import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.SqlNode;
+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;
@@ -290,14 +294,53 @@ class BabelParserTest extends SqlParserTest {
     final String sql = "select -('12' || '.34')::VARCHAR(30)::INTEGER as x\n"
         + "from t";
     final String expected = ""
-        + "SELECT (- ('12' || '.34') :: VARCHAR(30) :: INTEGER) AS `X`\n"
+        + "SELECT (((- ('12' || '.34')) :: VARCHAR(30)) :: INTEGER) AS `X`\n"
         + "FROM `T`";
     sql(sql).ok(expected);
   }
 
+  /**
+   * Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7475";>[CALCITE-7475]
+   * Babel parser allows postfix access after PostgreSQL-style {@code ::} 
infix cast</a>.
+   *
+   * <p>Verifies that PostgreSQL-style infix cast ({@code ::}) correctly binds
+   * tighter than postfix access operators such as array indexing ({@code []})
+   * and field access ({@code .}).
+   */
+  @Test void testParseInfixCastWithPostfixAccess() {
+    final String sql = "select 'test'::varchar array[1].field";
+
+    // 1. Verify the unparsed SQL string.
+    // Calcite's unparser adds parentheses to reflect the correct AST 
precedence.
+    final String expected = "SELECT (('test' :: VARCHAR ARRAY)[1].`FIELD`)";
+    sql(sql).ok(expected);
+
+    // 2. Verify the internal AST structure.
+    SqlNode node = sql(sql).node();
+    SqlSelect select = (SqlSelect) node;
+    SqlNode firstItem = select.getSelectList().get(0);
+
+    // The top-level operator should be DOT (.)
+    assertThat(firstItem.getKind(), is(SqlKind.DOT));
+    SqlBasicCall dotCall = (SqlBasicCall) firstItem;
+
+    // The left operand of DOT should be ITEM ([])
+    SqlNode dotLeft = dotCall.operand(0);
+    assertThat(((SqlBasicCall) dotLeft).getOperator().getName(), is("ITEM"));
+
+    // The left operand of ITEM should be the INFIX_CAST (::)
+    SqlNode itemLeft = ((SqlBasicCall) dotLeft).operand(0);
+    assertThat(itemLeft.getKind(), is(SqlKind.CAST));
+
+    // The right operand of CAST should be exactly 'VARCHAR ARRAY' without any 
subscripts.
+    SqlNode castRight = ((SqlBasicCall) itemLeft).operand(1);
+    assertThat(castRight, hasToString("VARCHAR ARRAY"));
+  }
+
   private void checkParseInfixCast(String sqlType) {
     String sql = "SELECT x::" + sqlType + " FROM (VALUES (1, 2)) as tbl(x,y)";
-    String expected = "SELECT `X` :: " + sqlType.toUpperCase(Locale.ROOT) + 
"\n"
+    String expected = "SELECT (`X` :: " + sqlType.toUpperCase(Locale.ROOT) + 
")\n"
         + "FROM (VALUES (ROW(1, 2))) AS `TBL` (`X`, `Y`)";
     sql(sql).ok(expected);
   }
@@ -313,46 +356,46 @@ private void checkParseInfixCast(String sqlType) {
     // Without parentheses, trailing postfixes after :: are consumed as part of
     // the type.
     sql("select v::varchar array[1].field from t")
-        .ok("SELECT `V` :: (VARCHAR ARRAY[1].`FIELD`)\nFROM `T`");
+        .ok("SELECT ((`V` :: VARCHAR ARRAY)[1].`FIELD`)\nFROM `T`");
     f.sql("select v:field::varchar array[1].field2 from t")
-        .ok("SELECT (`V`:`field`) :: (VARCHAR ARRAY[1].`FIELD2`)\nFROM `T`");
+        .ok("SELECT (((`V`:`field`) :: VARCHAR ARRAY)[1].`FIELD2`)\nFROM `T`");
 
     // Parenthesizing the cast lets the same postfixes apply to the cast
     // result instead.
     sql("select (v::varchar array)[1].field from t")
-        .ok("SELECT (`V` :: VARCHAR ARRAY[1].`FIELD`)\nFROM `T`");
+        .ok("SELECT ((`V` :: VARCHAR ARRAY)[1].`FIELD`)\nFROM `T`");
     f.sql("select (v:field::varchar array)[1].field2 from t")
-        .ok("SELECT ((`V`:`field`) :: VARCHAR ARRAY[1].`FIELD2`)\nFROM `T`");
+        .ok("SELECT (((`V`:`field`) :: VARCHAR ARRAY)[1].`FIELD2`)\nFROM `T`");
 
     // Postfix access is also accepted directly after :: in ordinary field/item
     // chains.
     sql("select v.field::integer,\n"
-            + "  arr[1].field::varchar,\n"
-            + "  v.field.field2::integer,\n"
-            + "  v.field[2]::integer\n"
-            + "from t")
-        .ok("SELECT `V`.`FIELD` :: INTEGER,"
-            + " (`ARR`[1].`FIELD`) :: VARCHAR,"
-            + " `V`.`FIELD`.`FIELD2` :: INTEGER,"
-            + " `V`.`FIELD`[2] :: INTEGER\n"
+        + "  arr[1].field::varchar,\n"
+        + "  v.field.field2::integer,\n"
+        + "  v.field[2]::integer\n"
+        + "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:field::integer,\n"
             + "  arr[1]:field::varchar,\n"
             + "  v:field.field2::integer,\n"
             + "  v:field[2]::integer\n"
             + "from t")
-        .ok("SELECT (`V`:`field`) :: INTEGER,"
-            + " (`ARR`[1]:`field`) :: VARCHAR,"
-            + " (`V`:`field`.`field2`) :: INTEGER,"
-            + " (`V`:`field`[2]) :: INTEGER\n"
+        .ok("SELECT ((`V`:`field`) :: INTEGER),"
+            + " ((`ARR`[1]:`field`) :: VARCHAR),"
+            + " ((`V`:`field`.`field2`) :: INTEGER),"
+            + " ((`V`:`field`[2]) :: INTEGER)\n"
             + "FROM `T`");
 
     // Deep mixed path: dotted identifiers, string-bracket key, integer-bracket
     // index, and a trailing infix cast in a single expression.
     f.sql("select v:a.b['c'][0]::integer from t")
-        .ok("SELECT (`V`:`a`.`b`['c'][0]) :: INTEGER\nFROM `T`");
+        .ok("SELECT ((`V`:`a`.`b`['c'][0]) :: INTEGER)\nFROM `T`");
     f.sql("select v:['a'].b[0]['c']::varchar from t")
-        .ok("SELECT (`V`:['a'].`b`[0]['c']) :: VARCHAR\nFROM `T`");
+        .ok("SELECT ((`V`:['a'].`b`[0]['c']) :: VARCHAR)\nFROM `T`");
 
     // Double-colon followed by colon field access is not allowed
     f.sql("select v::variant^:^field from t")
diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastOperator.java
index 54fd683420..723bb014a5 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlCastOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlCastOperator.java
@@ -18,10 +18,12 @@
 
 import org.apache.calcite.rel.type.RelDataType;
 import org.apache.calcite.sql.SqlBinaryOperator;
+import org.apache.calcite.sql.SqlCall;
 import org.apache.calcite.sql.SqlCallBinding;
 import org.apache.calcite.sql.SqlKind;
 import org.apache.calcite.sql.SqlOperandCountRange;
 import org.apache.calcite.sql.SqlOperatorBinding;
+import org.apache.calcite.sql.SqlWriter;
 import org.apache.calcite.sql.type.InferTypes;
 import org.apache.calcite.sql.validate.SqlMonotonicity;
 
@@ -46,6 +48,13 @@ class SqlCastOperator extends SqlBinaryOperator {
     super("::", SqlKind.CAST, 94, true, null, InferTypes.FIRST_KNOWN, null);
   }
 
+  @Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec, 
int rightPrec) {
+    writer.sep("(");
+    call.operand(0).unparse(writer, 0, 0);
+    writer.keyword("::");
+    call.operand(1).unparse(writer, 0, 0);
+    writer.sep(")");
+  }
   @Override public RelDataType inferReturnType(
       SqlOperatorBinding opBinding) {
     return SqlStdOperatorTable.CAST.inferReturnType(opBinding);

Reply via email to