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

peter-toth pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/spark.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new 27ea0abe9728 [SPARK-58323][4.1][SQL] Materialize AliasAware output 
ordering and partitioning to avoid StackOverflowError
27ea0abe9728 is described below

commit 27ea0abe9728e43b1cb4e3c3d0353297020398e6
Author: Peter Toth <[email protected]>
AuthorDate: Sun Jul 26 09:59:20 2026 +0200

    [SPARK-58323][4.1][SQL] Materialize AliasAware output ordering and 
partitioning to avoid StackOverflowError
    
    ### What changes were proposed in this pull request?
    
    Backport of SPARK-58323 (#57502) to branch-4.1.
    
    `AliasAwareQueryOutputOrdering.outputOrdering` and 
`BroadcastHashJoinExec`'s output-partitioning expansion build their results 
with `multiTransform`, which returns a `LazyList`, and store the bounded result 
unforced. This forces them into strict collections with `.toList`, right after 
the existing `.take(...)` so the lazy short-circuit during candidate generation 
is preserved.
    
    Tailored for branch-4.1 (differs from the master PR):
    - The `PartitioningPreservingUnaryExecNode.outputPartitioning` change is 
omitted: on branch-4.1 that method already materializes a strict collection 
(`...iterator.flatMap(...).take(...).toSeq` over an `Iterator`), so there is no 
unforced `LazyList` to fix.
    - The `BroadcastJoinSuite` single-element assertion change is omitted: on 
branch-4.1 `expandOutputPartitioning` always returns a `PartitioningCollection` 
(no `case p :: Nil` unwrap), so the single-element output shape is unchanged.
    
    ### Why are the changes needed?
    
    Each plan node re-wraps the child ordering/partitioning's unforced 
`LazyList`, so across a deep projection chain the deferred nesting grows 
unbounded and overflows the stack when the ordering/partitioning is later 
serialized or deeply traversed. On master this was observed as a driver 
`StackOverflowError` at `DAGScheduler.submitMissingTasks` during task 
serialization of a `SortedMergeCoalescedRDD` (SPARK-55715) on a large bucketed 
`MERGE`. That RDD and code path do not exist on branch [...]
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    New test in `ProjectedOrderingAndPartitioningSuite` asserting 
`sameOrderExpressions` is strict (not a `LazyList`); it fails before this 
change and passes after. Existing `ProjectedOrderingAndPartitioningSuite` / 
`BroadcastJoinSuite` / `BroadcastJoinSuiteAE` pass.
    
    ### Was this patch authored or co-authored using generative AI tooling?
    
    Generated-by: Claude Opus 4.8
    
    Closes #57531 from 
peter-toth/SPARK-58323-aliasaware-strict-ordering-partitioning-4.1.
    
    Authored-by: Peter Toth <[email protected]>
    Signed-off-by: Peter Toth <[email protected]>
---
 .../sql/catalyst/plans/AliasAwareOutputExpression.scala |  6 ++++++
 .../sql/execution/joins/BroadcastHashJoinExec.scala     |  5 ++++-
 .../ProjectedOrderingAndPartitioningSuite.scala         | 17 +++++++++++++++++
 3 files changed, 27 insertions(+), 1 deletion(-)

diff --git 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
index e1a9e8b5ea81..e7d2ab8538a4 100644
--- 
a/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
+++ 
b/sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/plans/AliasAwareOutputExpression.scala
@@ -107,6 +107,12 @@ trait AliasAwareQueryOutputOrdering[T <: QueryPlan[T]]
           .flatMap(projectExpression)
           .filter(e => orderingSet.add(e.canonicalized))
           .take(aliasCandidateLimit)
+          // Materialize the bounded result into a strict collection. The 
`LazyList` above is only
+          // needed so `take` can short-circuit candidate generation; storing 
`sameOrderExpressions`
+          // as an unforced `LazyList` lets each plan node re-wrap the child 
ordering's lazy list,
+          // and across a deep projection chain that nesting overflows the 
stack when the ordering
+          // is later serialized or deeply traversed.
+          .toList
         if (sameOrderings.nonEmpty) {
           Some(sortOrder.copy(child = sameOrderings.head,
             sameOrderExpressions = sameOrderings.tail))
diff --git 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
index 944ee3b05909..c8212eeb071c 100644
--- 
a/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
+++ 
b/sql/core/src/main/scala/org/apache/spark/sql/execution/joins/BroadcastHashJoinExec.scala
@@ -117,7 +117,10 @@ case class BroadcastHashJoinExec private(
       case e: Expression if 
streamedKeyToBuildKeyMapping.contains(e.canonicalized) =>
         e +: streamedKeyToBuildKeyMapping(e.canonicalized)
     }.asInstanceOf[LazyList[HashPartitioningLike]]
-      .take(conf.broadcastHashJoinOutputPartitioningExpandLimit))
+      .take(conf.broadcastHashJoinOutputPartitioningExpandLimit)
+      // Materialize the bounded expansion into a strict `List` -- never an 
unforced `LazyList`,
+      // which chained across nodes can overflow the stack when serialized or 
deeply traversed.
+      .toList)
   }
 
   protected override def doExecute(): RDD[InternalRow] = {
diff --git 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
index ec13d48d45f8..d55e5bd68187 100644
--- 
a/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
+++ 
b/sql/core/src/test/scala/org/apache/spark/sql/execution/ProjectedOrderingAndPartitioningSuite.scala
@@ -84,6 +84,23 @@ class ProjectedOrderingAndPartitioningSuite
     }
   }
 
+  test("SPARK-58323: AliasAware output ordering is strict, not lazy") {
+    // A multi-alias projection makes `sameOrderExpressions` non-trivial. It 
must be a strict
+    // collection: the underlying `multiTransform` produces a `LazyList`, and 
storing it unforced
+    // lets each plan node re-wrap the child ordering's lazy list. Across a 
deep projection chain
+    // that nesting overflows the stack when the ordering is later serialized 
or deeply traversed.
+    withSQLConf(SQLConf.EXPRESSION_PROJECTION_CANDIDATE_LIMIT.key -> "5") {
+      // id -> {x, y, z} gives sameOrderExpressions.
+      val df = spark.range(2).orderBy($"id").selectExpr("id as x", "id as y", 
"id as z")
+      val outputOrdering = df.queryExecution.optimizedPlan.outputOrdering
+      assert(outputOrdering.head.sameOrderExpressions.nonEmpty)
+      outputOrdering.foreach { so =>
+        assert(!so.sameOrderExpressions.isInstanceOf[LazyList[_]],
+          s"sameOrderExpressions must be strict, was 
${so.sameOrderExpressions.getClass.getName}")
+      }
+    }
+  }
+
   test("SPARK-42049: Improve AliasAwareOutputExpression - ordering - 
multi-references") {
     val df = spark.range(2).selectExpr("id as a", "id as b")
       .orderBy($"a" + $"b").selectExpr("a as x", "b as y")


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

Reply via email to