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 0c28ff9627 [CALCITE-7654] Lambda functions handle incorrectly field 
accesses
0c28ff9627 is described below

commit 0c28ff962775c2d03273b7094fc92589bfb3ae6a
Author: Mihai Budiu <[email protected]>
AuthorDate: Mon Jul 13 20:12:12 2026 -0700

    [CALCITE-7654] Lambda functions handle incorrectly field accesses
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../org/apache/calcite/sql/fun/SqlDotOperator.java |  8 ++++
 .../calcite/sql/validate/SqlLambdaScope.java       | 21 +++++-----
 .../calcite/sql/validate/SqlValidatorImpl.java     |  8 ++++
 .../calcite/sql/validate/SqlValidatorUtil.java     | 21 ++++++++++
 .../org/apache/calcite/test/SqlValidatorTest.java  | 49 ++++++++++++++++++++++
 5 files changed, 97 insertions(+), 10 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java 
b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
index 7c9a231579..9d9749c3e5 100644
--- a/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
+++ b/core/src/main/java/org/apache/calcite/sql/fun/SqlDotOperator.java
@@ -107,6 +107,14 @@ public class SqlDotOperator extends SqlSpecialOperator {
       return validator.getTypeFactory().createTypeWithNullability(nodeType, 
true);
     }
 
+    if (nodeType.getSqlTypeName() == SqlTypeName.ANY
+        && SqlValidatorUtil.inLambdaWithUntypedParameters(scope)) {
+      // The lambda parameters' types are not known until the enclosing call
+      // has inferred its operand types.  Field resolution happens when the
+      // lambda type checker re-validates the lambda body.
+      return validator.getTypeFactory().createTypeWithNullability(nodeType, 
true);
+    }
+
     if (!nodeType.isStruct()) {
       throw SqlUtil.newContextException(operand.getParserPosition(),
           Static.RESOURCE.incompatibleTypes());
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
index 0db4b45711..a4ba93d56a 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlLambdaScope.java
@@ -62,16 +62,17 @@ public boolean isParameter(SqlIdentifier id) {
   }
 
   @Override public SqlQualified fullyQualify(SqlIdentifier identifier) {
-    if (identifier.isSimple()) {
-      final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
-      final String name = identifier.getSimple();
-      boolean found = lambdaExpr.getParameters()
-          .stream()
-          .anyMatch(param ->
-              nameMatcher.matches(((SqlIdentifier) param).getSimple(), name));
-      if (found) {
-        return SqlQualified.create(this, 1, null, identifier);
-      }
+    final SqlNameMatcher nameMatcher = validator.catalogReader.nameMatcher();
+    final String name = identifier.names.get(0);
+    boolean found = lambdaExpr.getParameters()
+        .stream()
+        .anyMatch(param ->
+            nameMatcher.matches(((SqlIdentifier) param).getSimple(), name));
+    if (found) {
+      // If the first component names a parameter, in a compound identifier
+      // such as 'x.name' the remaining components are fields of the
+      // parameter's struct.
+      return SqlQualified.create(this, 1, null, identifier);
     }
     if (!validator.config().conformance().allowLambdaClosure()) {
       throw validator.newValidationError(identifier,
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
index 5d667fe502..023de1b77f 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorImpl.java
@@ -8034,6 +8034,14 @@ private class DeriveTypeVisitor implements 
SqlVisitor<RelDataType> {
 
       // Resolve rest of identifier
       for (; i < id.names.size(); i++) {
+        if (type.getSqlTypeName() == SqlTypeName.ANY
+            && SqlValidatorUtil.inLambdaWithUntypedParameters(scope)) {
+          // The lambda parameters' types are not known until the enclosing
+          // call has inferred its operand types.  Field resolution happens
+          // when the lambda type checker re-validates the lambda body.
+          return typeFactory.createTypeWithNullability(
+              typeFactory.createSqlType(SqlTypeName.ANY), true);
+        }
         String name = id.names.get(i);
         final RelDataTypeField field;
         if (name.isEmpty()) {
diff --git 
a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java 
b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
index 1c5bfcf9a6..3b67e5bbc3 100644
--- a/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
+++ b/core/src/main/java/org/apache/calcite/sql/validate/SqlValidatorUtil.java
@@ -57,6 +57,7 @@
 import org.apache.calcite.sql.SqlUtil;
 import org.apache.calcite.sql.fun.SqlStdOperatorTable;
 import org.apache.calcite.sql.parser.SqlParserPos;
+import org.apache.calcite.sql.type.SqlTypeName;
 import org.apache.calcite.sql.type.SqlTypeUtil;
 import org.apache.calcite.util.ImmutableBitSet;
 import org.apache.calcite.util.Litmus;
@@ -1678,6 +1679,26 @@ private static class ExplicitTableSchema extends 
AbstractSchema {
     }
   }
 
+  /** Returns whether {@code scope} is enclosed in the scope of a lambda
+   * expression whose parameter types have not yet been inferred.
+   *
+   * <p>Un-inferred parameters have type ANY, and so does any expression
+   * computed from them, whatever its shape; field resolution on such
+   * values must be deferred until the enclosing call has inferred the
+   * parameter types and re-validates the lambda body (see
+   * {@link SqlValidator#validateLambda}). */
+  public static boolean inLambdaWithUntypedParameters(SqlValidatorScope scope) 
{
+    for (SqlValidatorScope s = scope; s instanceof DelegatingScope;
+        s = ((DelegatingScope) s).getParent()) {
+      if (s instanceof SqlLambdaScope
+          && ((SqlLambdaScope) s).getParameterTypes().values().stream()
+              .anyMatch(t -> t.getSqlTypeName() == SqlTypeName.ANY)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   /** Flattens any FILTER, WITHIN DISTINCT, WITHIN GROUP surrounding a call to
    * an aggregate function. */
   public static class FlatAggregate {
diff --git a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java 
b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
index 228d90026b..cecb57385e 100644
--- a/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
+++ b/core/src/test/java/org/apache/calcite/test/SqlValidatorTest.java
@@ -8776,6 +8776,55 @@ void testGroupExpressionEquivalenceParams() {
             + "reference to 'EMP.DEPTNO' from enclosing scope");
   }
 
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7654";>[CALCITE-7654]
+   * Lambda functions handle incorrectly field accesses</a>.  */
+  @Test void testLambdaStructFieldAccess() {
+    SqlOperatorTable chain =
+        SqlOperatorTables.chain(
+            SqlOperatorTables.of(SqlLibraryOperators.EXISTS),
+            SqlStdOperatorTable.instance());
+    final SqlValidatorFixture s = fixture().withOperatorTable(chain);
+    // EMPLOYEES is ARRAY<ROW(EMPNO, ENAME, DETAIL)>.
+    // Parenthesized form: parses as DOT(e, empno).
+    s.withSql("select \"EXISTS\"(employees, e -> (e).empno > 0)\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+    // Unparenthesized form: parses as compound identifier e.empno.
+    s.withSql("select \"EXISTS\"(employees, e -> e.empno > 0)\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+    // Nested field access.
+    s.withSql("select \"EXISTS\"(employees, e -> (e).detail.skills is not 
null)\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+    // Field access through an array index: DETAIL.SKILLS is
+    // ARRAY<ROW(TYPE, DESC, OTHERS)>.
+    s.withSql("select \"EXISTS\"(employees,\n"
+            + "  e -> (e).detail.skills[1].\"TYPE\" = 'a')\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+    // Same, starting from a compound identifier.
+    s.withSql("select \"EXISTS\"(employees,\n"
+            + "  e -> e.detail.skills[1].\"TYPE\" = 'a')\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+    // A field that does not exist in the parameter's type.
+    s.withSql("select \"EXISTS\"(employees, e -> e.^bad^ > 0)\n"
+            + "from dept_nested")
+        .fails("Unknown field 'BAD'");
+    // An unknown field behind an array index produces an error.
+    s.withSql("select \"EXISTS\"(employees,\n"
+            + "  e -> e.detail.skills[1].^bad^ = 'a')\n"
+            + "from dept_nested")
+        .fails("Unknown field 'BAD'");
+    // Field access on an expression that is not a simple access chain.
+    s.withSql("select \"EXISTS\"(employees,\n"
+            + "  e -> coalesce((e).detail, e.detail).skills is not null)\n"
+            + "from dept_nested")
+        .columnType("BOOLEAN");
+  }
+
   /** Test case for <a 
href="https://issues.apache.org/jira/browse/CALCITE-7193";>[CALCITE-7193]
    * In an aggregation validator treats lambda variable names as column 
names</a>. */
   @Test void testGroupByLambda() {

Reply via email to