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 -&gt; e &gt; 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");

Reply via email to