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 a27c69b220 [CALCITE-7671] EXISTS fails typechecking nested lambda 
EXIST(a -> EXISTS(a, b -> b > 2))
a27c69b220 is described below

commit a27c69b22042c8d01836a1466fb5b7bdf4030a59
Author: Mihai Budiu <[email protected]>
AuthorDate: Fri Jul 24 16:50:48 2026 -0700

    [CALCITE-7671] EXISTS fails typechecking nested lambda EXIST(a -> EXISTS(a, 
b -> b > 2))
    
    Signed-off-by: Mihai Budiu <[email protected]>
---
 .../org/apache/calcite/sql/type/OperandTypes.java  | 57 ++++++++++++++++++++--
 .../org/apache/calcite/test/SqlOperatorTest.java   | 17 +++++++
 2 files changed, 71 insertions(+), 3 deletions(-)

diff --git a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java 
b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
index 99c623865c..114b11cf16 100644
--- a/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
+++ b/core/src/main/java/org/apache/calcite/sql/type/OperandTypes.java
@@ -1417,11 +1417,28 @@ public static SqlSingleOperandTypeChecker same(int 
operandCount,
             SqlCallBinding callBinding,
             boolean throwOnFailure) {
           // The first operand must be an array type
-          ARRAY.checkSingleOperandType(callBinding, callBinding.operand(0), 0, 
throwOnFailure);
+          if (!ARRAY.checkSingleOperandType(callBinding, 
callBinding.operand(0), 0,
+              throwOnFailure)) {
+            return false;
+          }
           final RelDataType arrayType =
               SqlTypeUtil.deriveType(callBinding, callBinding.operand(0));
-          final RelDataType componentType =
-              requireNonNull(arrayType.getComponentType(), "componentType");
+          RelDataType componentType = arrayType.getComponentType();
+          if (componentType == null) {
+            // The ARRAY family check above accepts operands of type ANY and
+            // untyped NULL literals, which have no component type.
+            if (arrayType.getSqlTypeName() == SqlTypeName.ANY) {
+              // This is probably a parameter of an enclosing lambda, whose 
type has
+              // not been inferred yet.  Accept for now; the enclosing 
function's checker
+              // will re-validated the lambda body with concrete parameter 
types, which runs
+              // this checker again.
+              return true;
+            }
+            // Untyped NULL literal: an unknown array whose elements are also
+            // NULL; the call returns NULL.
+            componentType =
+                callBinding.getTypeFactory().createSqlType(SqlTypeName.NULL);
+          }
 
           // The second operand is a function(array_element_type)->boolean type
           LambdaRelOperandTypeChecker lambdaChecker =
@@ -1948,6 +1965,40 @@ private static class LambdaRelOperandTypeChecker
   /**
    * Abstract base class for type-checking strategies involving lambda 
expressions.
    * This class provides common functionality for checking the type of lambda 
expression.
+   *
+   * <p>Lambda expressions are validated in two passes, and operand checkers of
+   * functions that accept lambda operands (higher-order functions) must be
+   * written with both passes in mind.  For example, consider validating
+   *
+   * <blockquote>{@code
+   * EXISTS(array[array[1, 2]], a -> EXISTS(a, b -> b > 1))
+   * }</blockquote>
+   *
+   * <ol>
+   * <li>Deriving the type of the outer call starts by deriving the types of
+   * its operands.  The type of the outer lambda is derived by validating its
+   * body with parameter {@code a} set to the nullable ANY type, the default
+   * that {@link org.apache.calcite.sql.validate.SqlLambdaScope} assigns while
+   * the parameter types are still unknown.  During this pass the checker of
+   * the <em>inner</em> {@code EXISTS} call runs and sees its array operand
+   * {@code a} typed as ANY.  It cannot check anything meaningful yet, so it
+   * must accept the call provisionally instead of failing; definitive checking
+   * happens in the second pass.
+   *
+   * <li>The checker of the <em>outer</em> {@code EXISTS} then computes the
+   * concrete type of {@code a} from the component type of its array operand
+   * {@code array[array[1, 2]]}, namely {@code INTEGER ARRAY}; stores it in the
+   * lambda's {@code SqlLambdaScope}; discards the types derived during the
+   * first pass (see {@code TypeRemover}); and calls
+   * {@link org.apache.calcite.sql.validate.SqlValidator#validateLambda} to
+   * re-validate the body.  This re-runs the checker of the inner
+   * {@code EXISTS}, which now sees {@code a} as {@code INTEGER ARRAY} and
+   * repeats the same protocol for the inner lambda, typing {@code b} as
+   * {@code INTEGER}.
+   * </ol>
+   *
+   * <p>See {@link OperandTypes#EXISTS} for a checker that follows this
+   * protocol.
    */
   private abstract static class LambdaOperandTypeChecker
       implements SqlSingleOperandTypeChecker {
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 8ddefaa32b..91f20af681 100644
--- a/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
+++ b/testkit/src/main/java/org/apache/calcite/test/SqlOperatorTest.java
@@ -9208,6 +9208,23 @@ void checkArrayReverseFunc(SqlOperatorFixture f0, 
SqlFunction function,
     f.checkNull("\"EXISTS\"(array[null, 3], x -> cast(null as boolean))");
     f.checkNull("\"EXISTS\"(array[null, 3], x -> x = null)");
     f.checkNull("\"EXISTS\"(cast(null as integer array), x -> x > 2)");
+    f.checkNull("\"EXISTS\"(null, x -> x > 2)");
+  }
+
+  /** Test case for
+   * <a 
href="https://issues.apache.org/jira/browse/CALCITE-7671";>[CALCITE-7671]
+   * EXISTS fails typechecking nested lambda
+   * EXIST(a -&gt; EXISTS(a, b -&gt; b &gt; 2))</a>. */
+  @Test void testNestedExistsFunc() {
+    final SqlOperatorFixture f = fixture()
+        .setFor(SqlLibraryOperators.EXISTS)
+        .withLibrary(SqlLibrary.SPARK);
+    f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]],"
+            + " a -> \"EXISTS\"(a, b -> b > 3))", true, "BOOLEAN");
+    f.checkScalar("\"EXISTS\"(array[array[1, 2], array[3, 4]],"
+            + " a -> \"EXISTS\"(a, b -> b > 4))", false, "BOOLEAN");
+    f.checkNull("\"EXISTS\"(cast(null as integer array array),"
+        + " a -> \"EXISTS\"(a, b -> b > 3))");
   }
 
   /** Tests {@code MAP_CONCAT} function from Spark. */

Reply via email to