This is an automated email from the ASF dual-hosted git repository. mihaibudiu pushed a commit to branch issue7269 in repository https://gitbox.apache.org/repos/asf/calcite.git
commit 568b4ee3d90cbf51f3631295aafa2368b7d84ebe Author: Mihai Budiu <[email protected]> AuthorDate: Mon Aug 17 16:32:43 2026 -0700 [CALCITE-7269] SqlValidator throws exception if lambda parameter is struct Signed-off-by: Mihai Budiu <[email protected]> --- .../adapter/enumerable/RexToLixTranslator.java | 4 +- .../org/apache/calcite/sql/type/SqlTypeUtil.java | 30 +--- .../sql2rel/RelStructuredTypeFlattener.java | 58 +++++--- .../apache/calcite/test/SqlToRelConverterTest.java | 154 +++++++++++++++++++++ .../apache/calcite/test/SqlToRelConverterTest.xml | 138 ++++++++++++++++++ core/src/test/resources/sql/lambda.iq | 100 +++++++++++++ .../org/apache/calcite/test/SqlOperatorTest.java | 5 + 7 files changed, 443 insertions(+), 46 deletions(-) diff --git a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java index 35373aca3e..57ac8495c8 100644 --- a/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java +++ b/core/src/main/java/org/apache/calcite/adapter/enumerable/RexToLixTranslator.java @@ -1492,7 +1492,7 @@ private static Expression scaleValue( @Override public Result visitLambdaRef(RexLambdaRef ref) { final ParameterExpression valueVariable = Expressions.parameter( - typeFactory.getJavaClass(ref.getType()), ref.getName()); + javaVariableType(ref.getType()), ref.getName()); // Generate one line of code to check whether lambdaRef is null, e.g., // "final boolean input_isNull = $0 == null;" @@ -1943,7 +1943,7 @@ private Result toInnerStorageType(Result result, Type storageType) { // unlike javac which supports effectively-final variables. parameterExpressions[i] = Expressions.parameter( - Modifier.FINAL, typeFactory.getJavaClass(rexLambdaRef.getType()), + Modifier.FINAL, javaVariableType(rexLambdaRef.getType()), rexLambdaRef.getName()); } diff --git a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java index 5de00a50b4..c62fd58045 100644 --- a/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java +++ b/core/src/main/java/org/apache/calcite/sql/type/SqlTypeUtil.java @@ -1304,34 +1304,10 @@ private static boolean flattenFields( field.getType(), list, null); - } else if (field.getType().getComponentType() != null) { - nested = true; - - // TODO jvs 14-Feb-2005: generalize to any kind of - // collection type - RelDataType flattenedCollectionType = - typeFactory.createMultisetType( - flattenRecordType( - typeFactory, - getComponentTypeOrThrow(field.getType()), - null), - -1); - if (field.getType() instanceof ArraySqlType) { - flattenedCollectionType = - typeFactory.createArrayType( - flattenRecordType( - typeFactory, - getComponentTypeOrThrow(field.getType()), - null), - -1); - } - field = - new RelDataTypeFieldImpl( - field.getName(), - field.getIndex(), - flattenedCollectionType); - list.add(field); } else { + // Collection fields are kept as they are, including their element + // types: flattening never rewrites the collection values, so a + // flattened element type would not describe them. list.add(field); } } diff --git a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java index 3ab73813eb..1a7b6cec75 100644 --- a/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java +++ b/core/src/main/java/org/apache/calcite/sql2rel/RelStructuredTypeFlattener.java @@ -61,6 +61,7 @@ import org.apache.calcite.rex.RexCorrelVariable; import org.apache.calcite.rex.RexFieldAccess; import org.apache.calcite.rex.RexInputRef; +import org.apache.calcite.rex.RexLambdaRef; import org.apache.calcite.rex.RexLiteral; import org.apache.calcite.rex.RexLocalRef; import org.apache.calcite.rex.RexNode; @@ -113,32 +114,44 @@ * normal optimizer rules. This approach has the benefit that real optimizer and * codegen rules never have to deal with structured types. * - * <p>As an example, suppose we have a structured type <code>ST(A1 smallint, A2 - * bigint)</code>, a table <code>T(c1 ST, c2 double)</code>, and a query <code> - * select t.c2, t.c1.a2 from t</code>. After SqlToRelConverter executes, the - * unflattened tree looks like: + * <p>As an example, take the query * * <blockquote><pre><code> - * LogicalProject(C2=[$1], A2=[$0.A2]) - * LogicalTableScan(table=[T]) + * select t.s from + * (select ROW(1, 2) as r, 'a' as s from (values (0))) as t + * where t.r."EXPR$0" = 1 * </code></pre></blockquote> * - * <p>After flattening, the resulting tree looks like + * <p>After SqlToRelConverter executes, the unflattened tree looks like: * * <blockquote><pre><code> - * LogicalProject(C2=[$3], A2=[$2]) - * FtrsIndexScanRel(table=[T], index=[clustered]) + * LogicalProject(S=[$1]) + * LogicalFilter(condition=[=($0.EXPR$0, 1)]) + * LogicalProject(R=[ROW(1, 2)], S=['a']) + * LogicalValues(tuples=[[{ 0 }]]) * </code></pre></blockquote> * - * <p>The index scan produces a flattened row type <code>(boolean, smallint, - * bigint, double)</code> (the boolean is a null indicator for c1), and the - * projection picks out the desired attributes (omitting <code>$0</code> and - * <code>$1</code> altogether). After optimization, the projection might be - * pushed down into the index scan, resulting in a final tree like + * <p>After flattening, the resulting tree looks like * * <blockquote><pre><code> - * FtrsIndexScanRel(table=[T], index=[clustered], projection=[3, 2]) + * LogicalProject(S=[$2]) + * LogicalFilter(condition=[=($0, 1)]) + * LogicalValues(tuples=[[{ 1, 2, 'a' }]]) * </code></pre></blockquote> + * + * <p>The column <code>R</code> of type <code>ROW(INTEGER, INTEGER)</code> has + * become two columns of type <code>INTEGER</code>, the field access + * <code>$0.EXPR$0</code> has become the reference <code>$0</code>, and the + * references of the outer projection have been renumbered. + * + * <p>WARNING: this rewrite does not modify collection element types. + * + * <p>This rewrite does not alter the signature of a user-defined function + * or lambda expressions. Currently all SQL functions that take lambda + * expressions as arguments (e.g., <code>EXIST(a, e -> e > 1)</code>) apply these lambda + * expressions to collection elements, whose types are not rewritten. + * But this rewrite may not be correct in the future if other kinds of + * higher-order functions are introduced. */ public class RelStructuredTypeFlattener implements ReflectiveVisitor { //~ Instance fields -------------------------------------------------------- @@ -228,8 +241,7 @@ private RelNode tryRestructure(RelNode root, RelNode flattened) { RelNode restructured = relBuilder.push(flattened) .projectNamed(structuringExps, resultFieldNames, true) .build(); - restructured = RelOptUtil.copyRelHints(flattened, restructured); - return restructured; + return RelOptUtil.copyRelHints(flattened, restructured); } else { return flattened; } @@ -971,6 +983,18 @@ private RelDataType removeDistinct(RelDataType type) { newRefExp = rexBuilder.makeFieldAccess(newRefExp, ord); } return newRefExp; + } else if (refExp instanceof RexLambdaRef) { + // refExp references a parameter of an enclosing RexLambda, e.g. + // X in "(X) -> X.EXPR$1". Such a parameter is typed from the + // element type of a collection operand of the call, which + // flattening leaves as it is, so the access path is kept. A + // parameter typed from something that flattening does rewrite + // would need the access path to be re-addressed. + RexNode newRefExp = refExp; + for (Integer ord : accessOrdinals) { + newRefExp = rexBuilder.makeFieldAccess(newRefExp, ord); + } + return newRefExp; } else if (refExp instanceof RexFieldAccess) { fieldAccess = (RexFieldAccess) refExp; } else { diff --git a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java index d67a982464..8b75a495b7 100644 --- a/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java +++ b/core/src/test/java/org/apache/calcite/test/SqlToRelConverterTest.java @@ -202,6 +202,160 @@ public static void checkActualAndReferenceFiles() { .ok(); } + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. */ + @Test void testLambdaExpressionWithRowParameter() { + final String sql = "select \"EXISTS\"(array(ROW(true, false)), x -> x.\"EXPR$1\")"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A lambda that captures a field access on a struct column of the + * enclosing query. */ + @Test void testLambdaExpressionWithStructCapture() { + final String sql = "select \"EXISTS\"(array[f0.c0, f1.c0], x -> x = f1.c2)\n" + + "from struct.t"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A nested ROW value used both inside a lambda (as a capture) and outside + * of it (to extract a field); the two uses must see the same shape. */ + @Test void testLambdaExpressionStructUsedInsideAndOutside() { + final String sql = "select \"EXISTS\"(array(1, 2)," + + " x -> x = t.r.\"EXPR$0\".\"EXPR$1\") as e,\n" + + " t.r.\"EXPR$0\".\"EXPR$0\" as v\n" + + "from (select ROW(ROW(1, 2), 3) as r) as t"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A nested field access in the lambda. */ + @Test void testLambdaExpressionWithNestedRowParameter() { + final String sql = "select \"EXISTS\"(array(ROW(ROW(1, 2), 3))," + + " x -> x.\"EXPR$0\".\"EXPR$1\" > 1)"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A lambda over an array of nested structs. */ + @Test void testLambdaExpressionOverStructArrayColumn() { + final String sql = "select \"EXISTS\"(employees, e -> e.empno = 1)\n" + + "from dept_nested"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. */ + @Test void testLambdaExpressionNestedAccessOverStructArrayColumn() { + final String sql = "select \"EXISTS\"(employees," + + " e -> cardinality(e.detail.skills) > 0)\n" + + "from dept_nested"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A lambda over an ARRAY sub-query: the flattener splits the sub-query's + * struct column into flat columns, and restructures them back under the + * {@code Collect} so that the element type, and with it the lambda, are + * unchanged. */ + @Test void testLambdaExpressionOverArrayQuery() { + final String sql = "select \"EXISTS\"(array(select ROW(ROW(1, 2), 3)" + + " from (values (0))), x -> x.\"EXPR$0\".\"EXPR$1\" = 2)"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .ok(); + } + + /** Test case for + * <a href="https://issues.apache.org/jira/browse/CALCITE-7269">[CALCITE-7269] + * SqlValidator throws exception if lambda parameter is struct</a>. + * A lambda over a correlated array column. */ + @Test void testLambdaExpressionOverCorrelatedStructArrayColumn() { + final String sql = "select (select \"EXISTS\"(d.employees," + + " e -> cardinality(e.detail.skills) > 0) from (values (0)))\n" + + "from dept_nested as d"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .withDecorrelate(false) + .withTrim(true) + .ok(); + } + + /** As {@link #testLambdaExpressionOverCorrelatedStructArrayColumn}, but + * the parameter {@code e} is captured inside a nested lambda whose own + * parameter {@code f} has the same index and type. */ + @Test void testLambdaExpressionCaptureInNestedLambda() { + final String sql = "select (select \"EXISTS\"(d.employees," + + " e -> \"EXISTS\"(t.employees," + + " f -> f.empno = e.empno and cardinality(e.detail.skills) > 0))\n" + + "from dept_nested t)\n" + + "from dept_nested as d"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .withDecorrelate(false) + .withTrim(true) + .ok(); + } + + /** As {@link #testLambdaExpressionOverCorrelatedStructArrayColumn}, but + * the array's element has three levels of struct nesting; the element + * type and the access path pass through flattening unchanged. */ + @Test void testLambdaExpressionOverCorrelatedDeeplyNestedArray() { + final String sql = "select (select \"EXISTS\"(t.arr," + + " x -> x.\"EXPR$0\".\"EXPR$0\".\"EXPR$1\" = 2) from (values (0)))\n" + + "from (select array(ROW(ROW(ROW(1, 2), 3), 4)) as arr) as t"; + fixture() + .withFactory(c -> + c.withOperatorTable(t -> SqlValidatorTest.operatorTableFor(SqlLibrary.SPARK))) + .withSql(sql) + .withDecorrelate(false) + .withTrim(true) + .ok(); + } + /** Test case for * <a href="https://issues.apache.org/jira/browse/CALCITE-6116">[CALCITE-6116] * Add EXISTS function (enabled in Spark library)</a>. */ diff --git a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml index 10171218a5..ee3a20d4cd 100644 --- a/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/SqlToRelConverterTest.xml @@ -5155,6 +5155,24 @@ LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION2(1, () -> -1)]) <![CDATA[ LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION($7, (X, DEPTNO) -> +(DEPTNO, 1))]) LogicalTableScan(table=[[CATALOG, SALES, EMP]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionCaptureInNestedLambda"> + <Resource name="sql"> + <![CDATA[select (select "EXISTS"(d.employees, e -> "EXISTS"(t.employees, f -> f.empno = e.empno and cardinality(e.detail.skills) > 0)) +from dept_nested t) +from dept_nested as d]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[$1]) + LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}]) + LogicalProject(EMPLOYEES=[$3]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]]) + LogicalAggregate(group=[{}], agg#0=[SINGLE_VALUE($0)]) + LogicalProject(EXPR$0=[EXISTS($cor0.EMPLOYEES, (E) -> EXISTS($3, (F) -> AND(=(F.EMPNO, E.EMPNO), >(CARDINALITY(E.DETAIL.SKILLS), 0))))]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]]) ]]> </Resource> </TestCase> @@ -5166,6 +5184,126 @@ LogicalProject(EXPR$0=[HIGHER_ORDER_FUNCTION($7, (X, DEPTNO) -> +(DEPTNO, 1))]) <![CDATA[ LogicalProject(EXPR$0=[EXISTS(ARRAY(1, 2, 3, 4), (N) -> OR(=(N, 1), =(N, 3)))]) LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionNestedAccessOverStructArrayColumn"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(employees, e -> cardinality(e.detail.skills) > 0) +from dept_nested]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS($3, (E) -> >(CARDINALITY(E.DETAIL.SKILLS), 0))]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionOverArrayQuery"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(array(select ROW(ROW(1, 2), 3) from (values (0))), x -> x."EXPR$0"."EXPR$1" = 2)]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS($1, (X) -> =(X.EXPR$0.EXPR$1, 2))]) + LogicalJoin(condition=[true], joinType=[inner]) + LogicalValues(tuples=[[{ 0 }]]) + Collect(field=[EXPR$0]) + LogicalProject(EXPR$0=[ROW(ROW($0, $1), $2)]) + LogicalValues(tuples=[[{ 1, 2, 3 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionOverCorrelatedDeeplyNestedArray"> + <Resource name="sql"> + <![CDATA[select (select "EXISTS"(t.arr, x -> x."EXPR$0"."EXPR$0"."EXPR$1" = 2) from (values (0))) +from (select array(ROW(ROW(ROW(1, 2), 3), 4)) as arr) as t]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[$1]) + LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}]) + LogicalProject(ARR=[ARRAY(ROW(ROW(ROW(1, 2), 3), 4))]) + LogicalValues(tuples=[[{ 0 }]]) + LogicalAggregate(group=[{}], agg#0=[SINGLE_VALUE($0)]) + LogicalProject(EXPR$0=[EXISTS($cor0.ARR, (X) -> =(X.EXPR$0.EXPR$0.EXPR$1, 2))]) + LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionOverCorrelatedStructArrayColumn"> + <Resource name="sql"> + <![CDATA[select (select "EXISTS"(d.employees, e -> cardinality(e.detail.skills) > 0) from (values (0))) +from dept_nested as d]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[$1]) + LogicalCorrelate(correlation=[$cor0], joinType=[left], requiredColumns=[{0}]) + LogicalProject(EMPLOYEES=[$3]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]]) + LogicalAggregate(group=[{}], agg#0=[SINGLE_VALUE($0)]) + LogicalProject(EXPR$0=[EXISTS($cor0.EMPLOYEES, (E) -> >(CARDINALITY(E.DETAIL.SKILLS), 0))]) + LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionOverStructArrayColumn"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(employees, e -> e.empno = 1) +from dept_nested]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS($3, (E) -> =(E.EMPNO, 1))]) + LogicalTableScan(table=[[CATALOG, SALES, DEPT_NESTED]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionStructUsedInsideAndOutside"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(array(1, 2), x -> x = t.r."EXPR$0"."EXPR$1") as e, + t.r."EXPR$0"."EXPR$0" as v +from (select ROW(ROW(1, 2), 3) as r) as t]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(E=[EXISTS(ARRAY(1, 2), (X) -> =(X, ROW(ROW(1, 2), 3).EXPR$0.EXPR$1))], V=[ROW(ROW(1, 2), 3).EXPR$0.EXPR$0]) + LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionWithNestedRowParameter"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(array(ROW(ROW(1, 2), 3)), x -> x."EXPR$0"."EXPR$1" > 1)]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS(ARRAY(ROW(ROW(1, 2), 3)), (X) -> >(X.EXPR$0.EXPR$1, 1))]) + LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionWithRowParameter"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(array(ROW(true, false)), x -> x."EXPR$1")]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS(ARRAY(ROW(true, false)), (X) -> X.EXPR$1)]) + LogicalValues(tuples=[[{ 0 }]]) +]]> + </Resource> + </TestCase> + <TestCase name="testLambdaExpressionWithStructCapture"> + <Resource name="sql"> + <![CDATA[select "EXISTS"(array[f0.c0, f1.c0], x -> x = f1.c2) +from struct.t]]> + </Resource> + <Resource name="plan"> + <![CDATA[ +LogicalProject(EXPR$0=[EXISTS(ARRAY($4, $5), (X) -> =(X, $7))]) + LogicalTableScan(table=[[CATALOG, STRUCT, T]]) ]]> </Resource> </TestCase> diff --git a/core/src/test/resources/sql/lambda.iq b/core/src/test/resources/sql/lambda.iq index accc60b4e8..b51632ca82 100644 --- a/core/src/test/resources/sql/lambda.iq +++ b/core/src/test/resources/sql/lambda.iq @@ -103,6 +103,106 @@ select "EXISTS"(array[array[1, 2], array[3, 4]], x -> x[1] = 1); !ok +# [CALCITE-7269] SqlValidator throws exception if lambda parameter is struct +select "EXISTS"(array(ROW(true, false)), x -> x."EXPR$1"); ++--------+ +| EXPR$0 | ++--------+ +| false | ++--------+ +(1 row) + +!ok + +select "EXISTS"(array(ROW(1, 2)), x -> x."EXPR$1" > 1); ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + +# Nested access path within the lambda +select "EXISTS"(array(ROW(ROW(1, 2), 3)), x -> x."EXPR$0"."EXPR$1" > 1); ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + + +# Deeply nested element accessed through a correlated array +select (select "EXISTS"(t.arr, x -> x."EXPR$0"."EXPR$0"."EXPR$1" = 2) from (values (0))) +from (select array(ROW(ROW(ROW(1, 2), 3), 4)) as arr) as t; ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + +# Same access without correlation +select "EXISTS"(array(ROW(ROW(ROW(1, 2), 3), 4)), x -> x."EXPR$0"."EXPR$0"."EXPR$1" = 2); ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + +# Lambda over an ARRAY(subquery) whose element is a nested struct; the +# flattener splits the subquery columns and restructures them back under +# the Collect, so the element type and the lambda are unchanged +select "EXISTS"(array(select ROW(ROW(1, 2), 3) from (values (0))), + x -> x."EXPR$0"."EXPR$1" = 2); ++--------+ +| EXPR$0 | ++--------+ +| true | ++--------+ +(1 row) + +!ok + +# The same nested ROW value used inside a lambda (capture) and outside of +# it (field extraction); both uses must see the same shape +select "EXISTS"(array(1, 2), x -> x = t.r."EXPR$0"."EXPR$1") as e, + t.r."EXPR$0"."EXPR$0" as v +from (select ROW(ROW(1, 2), 3) as r) as t; ++------+---+ +| E | V | ++------+---+ +| true | 1 | ++------+---+ +(1 row) + +!ok + +# The same array used inside a lambda and outside of it; both uses must +# see the same element shape +select "EXISTS"(a.arr, x -> x."EXPR$0"."EXPR$1" = 2) as e, + cardinality(a.arr) as c, + a.arr[1]."EXPR$0"."EXPR$1" as v +from (select array(ROW(ROW(1, 2), 3)) as arr) as a; ++------+---+---+ +| E | C | V | ++------+---+---+ +| true | 1 | 2 | ++------+---+---+ +(1 row) + +!ok + + # [CALCITE-6242] Enhance lambda closure parsing select * from (select array(1, 2, 3) as arr) as t1 inner join diff --git a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java index 6082c27638..90aa54b222 100644 --- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java +++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java @@ -9368,6 +9368,11 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, SqlFunction function, f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]], x -> x[1] = 1)", true, "BOOLEAN"); f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]], x -> x[1] = 5)", false, "BOOLEAN"); + // array of structs; test case for + // [CALCITE-7269] SqlValidator throws exception if lambda parameter is struct + f.checkScalar("\"EXISTS\"(array(ROW(1, 2)), x -> x.\"EXPR$1\" > 1)", true, "BOOLEAN"); + f.checkScalar("\"EXISTS\"(array(ROW(1, 2)), x -> x.\"EXPR$1\" > 2)", false, "BOOLEAN"); + // test for null f.checkScalar("\"EXISTS\"(array[null, 3], x -> x > 2 or x < 4)", true, "BOOLEAN"); f.checkScalar("\"EXISTS\"(array[null, 3], x -> x is null)", true, "BOOLEAN");
