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 c1fe98ad16 [CALCITE-7269] SqlValidator throws exception if lambda
parameter is struct
c1fe98ad16 is described below
commit c1fe98ad16e9d360a938ef229f5e264fea229159
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, 445 insertions(+), 44 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..ae095af32d 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 --------------------------------------------------------
@@ -229,6 +242,9 @@ private RelNode tryRestructure(RelNode root, RelNode
flattened) {
.projectNamed(structuringExps, resultFieldNames, true)
.build();
restructured = RelOptUtil.copyRelHints(flattened, restructured);
+ // REVIEW jvs 23-Mar-2005: How do we make sure that this
+ // implementation stays in Java? Fennel can't handle
+ // structured types.
return restructured;
} else {
return flattened;
@@ -971,6 +987,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");