From fe065d5fd67fc09a77b48a7099883df08d3f27cf Mon Sep 17 00:00:00 2001 From: Stamatis Zampetakis Date: Thu, 7 May 2026 20:11:50 +0200 Subject: [PATCH] [CALCITE-7507] NPE in ReleaseExtension. when building from sources --- .../src/main/codegen/includes/parserImpls.ftl | 23 ++++++-- .../apache/calcite/test/BabelParserTest.java | 57 ++++++++++++++++--- .../calcite/sql/fun/SqlCastOperator.java | 14 +++++ gradle.properties | 2 +- 4 files changed, 84 insertions(+), 12 deletions(-) diff --git a/babel/src/main/codegen/includes/parserImpls.ftl b/babel/src/main/codegen/includes/parserImpls.ftl index 7565303fa008..391022d796be 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 list, ExprContext exprContext, Span s) : { final SqlDataTypeSpec dt; + SqlNode e, p; } { { 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); } + ( + e = Expression(ExprContext.ACCEPT_SUB_QUERY) + { + SqlNode current = (SqlNode) list.remove(list.size() - 1); + list.add(SqlStdOperatorTable.ITEM.createCall(getPos(), current, e)); + } + | + + 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 a305586738e0..0d1372f182ea 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,11 +294,50 @@ 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 + * [CALCITE-7475] + * Babel parser allows postfix access after PostgreSQL-style {@code ::} infix cast. + * + *

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" @@ -313,9 +356,9 @@ 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. @@ -327,10 +370,10 @@ private void checkParseInfixCast(String sqlType) { // 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") + + " 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," 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 54fd68342054..a99f23e51738 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,18 @@ 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) { + final boolean needParens = leftPrec > getLeftPrec(); // true when ITEM/DOT is parent + if (needParens) { + writer.sep("("); + } + call.operand(0).unparse(writer, leftPrec, getLeftPrec()); + writer.keyword("::"); + call.operand(1).unparse(writer, getRightPrec(), rightPrec); + if (needParens) { + writer.sep(")"); + } + } @Override public RelDataType inferReturnType( SqlOperatorBinding opBinding) { return SqlStdOperatorTable.CAST.inferReturnType(opBinding); diff --git a/gradle.properties b/gradle.properties index 5e51d8df35ec..8c7e62d40aea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -46,7 +46,7 @@ org.checkerframework.version=0.5.16 com.github.autostyle.version=3.2 com.github.johnrengelman.shadow.version=5.1.0 com.github.spotbugs.version=2.0.0 -com.github.vlsi.vlsi-release-plugins.version=3.0.1 +com.github.vlsi.vlsi-release-plugins.version=3.0.2 com.google.protobuf.version=0.8.10 de.thetaphi.forbiddenapis.version=3.10 jacoco.version=0.8.14