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);