This is an automated email from the ASF dual-hosted git repository.

sunchao pushed a commit to branch branch-4.x
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.x by this push:
     new f4f3e79b8af9 [SPARK-37019][SQL][FOLLOWUP] Resolve nested higher-order 
function arguments first
f4f3e79b8af9 is described below

commit f4f3e79b8af9349adc358305755dc41066b388c8
Author: Chao Sun <[email protected]>
AuthorDate: Fri Jun 19 16:53:34 2026 -0700

    [SPARK-37019][SQL][FOLLOWUP] Resolve nested higher-order function arguments 
first
    
    ### What changes were proposed in this pull request?
    
    Resolve each higher-order function's argument expressions before checking 
their data types and binding its lambda functions.
    
    The analyzer now follows this order:
    
    1. Resolve the argument expressions using the current outer lambda scope.
    2. Rebuild the higher-order function with those resolved arguments.
    3. If the arguments are ready and valid, bind the lambda functions 
immediately.
    4. Otherwise, resolve only the function expressions and defer binding.
    
    This matches the established sequence in the single-pass 
`HigherOrderFunctionResolver`, so both analyzer paths now resolve arguments 
before binding lambdas.
    
    This is intentionally narrow. It does not change `ArrayAggregate` 
accumulator types, casts, code generation, or runtime execution.
    
    The PR also adds a focused regression test for the nested `transform` / 
`filter` / `aggregate` expression that exposed the bug.
    
    ### Why are the changes needed?
    
    `ResolveLambdaVariables` previously bound a higher-order function only when 
its arguments were already resolved at the start of the visit. If nested 
argument expressions became resolved during that visit, Spark still walked and 
rebuilt the remaining expression tree without binding the current lambda 
functions.
    
    For complex nested types, that ordering could inspect a field extraction 
whose lambda variable was still unresolved and fail analysis with:
    
    ```
    Invalid call to dataType on unresolved object
    ```
    
    In short:
    
    ```
    Before: check readiness -> resolve nested arguments -> wait for another 
analyzer pass
    After:  resolve nested arguments -> check readiness -> bind lambdas in the 
same pass
    ```
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Valid queries with nested higher-order functions that previously 
failed during analysis can now be analyzed and executed.
    
    There is no public API, configuration, or intended runtime behavior change 
for queries that already worked.
    
    ### How was this patch tested?
    
    - Added `ArrayAggregate resolves nested lambda arguments before inspecting 
their types` to reproduce the production-shaped failure and verify the result.
    - `ResolveLambdaVariablesSuite`: 6 tests passed.
    - `DataFrameComplexTypeSuite`: 18 tests passed.
    - Catalyst and SQL test Scalastyle: 0 errors and 0 warnings.
    - `git diff --check` passed.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Codex (GPT-5)
    
    Closes #56507 from sunchao/dev/chao/codex/hof-gate-analyzer-behavior-oss.
    
    Authored-by: Chao Sun <[email protected]>
    Signed-off-by: Chao Sun <[email protected]>
    (cherry picked from commit 3fa2bea78b1e48b439efd412990798af91d9b94b)
    Signed-off-by: Chao Sun <[email protected]>
---
 .../catalyst/analysis/higherOrderFunctions.scala   | 25 ++++++++++--
 .../spark/sql/DataFrameComplexTypeSuite.scala      | 45 ++++++++++++++++++++--
 2 files changed, 64 insertions(+), 6 deletions(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/higherOrderFunctions.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/higherOrderFunctions.scala
index 9c94d045ae86..107345dda379 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/higherOrderFunctions.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/analysis/higherOrderFunctions.scala
@@ -52,9 +52,28 @@ object ResolveLambdaVariables extends Rule[LogicalPlan] {
   private def resolve(e: Expression, parentLambdaMap: LambdaVariableMap): 
Expression = e match {
     case _ if e.resolved => e
 
-    case h: HigherOrderFunction if h.argumentsResolved && 
h.checkArgumentDataTypes().isSuccess =>
-      SubqueryExpressionInLambdaOrHigherOrderFunctionValidator(e)
-      h.bind(LambdaBinder(_, _)).mapChildren(resolve(_, parentLambdaMap))
+    case h: HigherOrderFunction =>
+      // An argument can contain lambda variables from an outer scope. Resolve 
those variables
+      // before inspecting the argument type, which may otherwise access an 
unresolved extractor.
+      // Keep this higher-order function's own lambdas unbound until its 
argument types are ready.
+      val resolvedArguments = h.arguments.map(resolve(_, parentLambdaMap))
+      // HigherOrderFunction children are ordered as arguments followed by 
functions.
+      val resolvedHigherOrderFunction = h
+        .withNewChildren(resolvedArguments ++ h.functions)
+        .asInstanceOf[HigherOrderFunction]
+      if (resolvedHigherOrderFunction.argumentsResolved &&
+          resolvedHigherOrderFunction.checkArgumentDataTypes().isSuccess) {
+        
SubqueryExpressionInLambdaOrHigherOrderFunctionValidator(resolvedHigherOrderFunction)
+        // Arguments are already resolved; this walk resolves the newly bound 
lambda bodies.
+        resolvedHigherOrderFunction.bind(LambdaBinder(_, _))
+          .mapChildren(resolve(_, parentLambdaMap))
+      } else {
+        // The arguments were already visited above. Resolve only the 
functions here so nested
+        // arguments are not traversed twice in the same analyzer pass.
+        val resolvedFunctions = resolvedHigherOrderFunction.functions
+          .map(resolve(_, parentLambdaMap))
+        resolvedHigherOrderFunction.withNewChildren(resolvedArguments ++ 
resolvedFunctions)
+      }
 
     case l: LambdaFunction if !l.bound =>
       SubqueryExpressionInLambdaOrHigherOrderFunctionValidator(e)
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala 
b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
index 8dda985030af..f65b5a61b6a2 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/DataFrameComplexTypeSuite.scala
@@ -30,7 +30,9 @@ import org.apache.spark.sql.catalyst.util.DateTimeUtils
 import org.apache.spark.sql.functions._
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.test.SharedSparkSession
-import org.apache.spark.sql.types.{ArrayType, BooleanType, Decimal, 
DoubleType, IntegerType, MapType, StringType, StructField, StructType}
+import org.apache.spark.sql.types.{
+  ArrayType, BooleanType, Decimal, DoubleType, IntegerType, LongType, MapType, 
StringType,
+  StructField, StructType}
 import org.apache.spark.unsafe.types.CalendarInterval
 
 /**
@@ -235,6 +237,45 @@ class DataFrameComplexTypeSuite extends SharedSparkSession 
{
     }
   }
 
+  test("ArrayAggregate resolves nested lambda arguments before inspecting 
their types") {
+    val positionType = StructType(Seq(
+      StructField("turn_idx", LongType, nullable = false),
+      StructField("turn_kind", StringType, nullable = false),
+      StructField("start_message_index", LongType, nullable = false),
+      StructField("previous_start_message_index", LongType, nullable = true)))
+    val positionsType = ArrayType(positionType, containsNull = false)
+    val messages = array(struct(
+      lit("user").as("turn_kind"),
+      lit(0L).as("message_index")))
+    val candidates = filter(
+      transform(messages, message => struct(
+        message.getField("turn_kind").as("turn_kind"),
+        message.getField("message_index").as("message_index"))),
+      candidate => candidate.getField("turn_kind").isNotNull)
+    // The aggregate first sees `candidates` with its transform/filter lambdas 
still unbound. Those
+    // argument expressions must resolve before the aggregate inspects their 
complex data types.
+    val lastPosition = aggregate(
+      candidates,
+      array().cast(positionsType),
+      (positions, candidate) => {
+        val previousPosition = try_element_at(positions, lit(-1))
+        when(
+          
previousPosition.getField("turn_kind").eqNullSafe(candidate.getField("turn_kind")),
+          positions).otherwise(
+          concat(positions, array(struct(
+            (size(positions).cast(LongType) + 1L).as("turn_idx"),
+            candidate.getField("turn_kind").as("turn_kind"),
+            candidate.getField("message_index").as("start_message_index"),
+            previousPosition.getField("start_message_index")
+              .as("previous_start_message_index")))))
+      },
+      positions => try_element_at(positions, lit(-1)))
+
+    checkAnswer(
+      spark.range(1).select(lastPosition),
+      Row(Row(1L, "user", 0L, null)))
+  }
+
   test("SPARK-31552: array encoder with different types") {
     // primitives
     val booleans = Array(true, false)
@@ -394,5 +435,3 @@ extends DefinedByConstructorParams
 case class S100_5(
   s1: S100 = new S100(), s2: S100 = new S100(), s3: S100 = new S100(),
   s4: S100 = new S100(), s5: S100 = new S100())
-
-


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to