peter-toth commented on code in PR #59306:
URL: https://github.com/apache/spark/pull/59306#discussion_r4282260479


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -360,6 +390,7 @@ case class GroupPartitionsExec(
 
   override protected def doExecuteColumnar(): RDD[ColumnarBatch] = {
     checkChildStillMatches()
+    checkPlannedMergeRuns()

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44).
 `supportsColumnar` now follows the plan, `!(enableSortedMerge && 
hasCoalescing)`, and the columnar path fails for any planned merge. A new 
assertion covers a columnar child outside the whitelist. It fails if 
`supportsColumnar` reads `usesSortedMerge` again.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/exchange/EnsureRequirementsSuite.scala:
##########
@@ -2920,6 +2920,19 @@ class EnsureRequirementsSuite extends SharedSparkSession 
{
     }
   }
 
+  test("SPARK-60044: pushing join key positions into a merging node keeps its 
ordering") {
+    val keys = Seq(InternalRow(1, 1), InternalRow(1, 2))
+    val leaf = DummySparkPlan(

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44).
 The child is now `DummyLeafSafeForKWayMerge`, and the test also checks that 
the rebuilt node reports the frozen ordering.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -337,8 +352,23 @@ case class GroupPartitionsExec(
     }
   }
 
+  /**
+   * Fails a planned merge that would not run. The parent's required ordering 
was checked against
+   * the merge, so no sort was added above it. A rule after 
`EnsureRequirements` can still put a
+   * node outside the `SafeForKWayMerge` whitelist below this one. This node 
would then concatenate
+   * and hand the parent unsorted rows. No built-in rule does that, see 
`childIsSafeForKWayMerge`.
+   * Like `checkChildStillMatches`, this makes the failure explicit if one 
starts.
+   */
+  private def checkPlannedMergeRuns(): Unit = {
+    if (enableSortedMerge && hasCoalescing && !childIsSafeForKWayMerge) {
+      throw SparkException.internalError(
+        "GroupPartitionsExec was planned with a k-way merge that its child no 
longer allows")

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44).
 The message names the node and `SafeForKWayMerge`, and the `SafeForKWayMerge` 
scaladoc mentions this failure.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -144,21 +144,66 @@ class GroupPartitionsExecSuite extends SharedSparkSession 
{
   }
 
   test("SPARK-58324: k-way merge ordering drops sameOrderExpressions") {
-    // The child ordering carries sameOrderExpressions (planner metadata). The 
k-way merge
+    // The planned ordering carries sameOrderExpressions (planner metadata). 
The k-way merge
     // comparator only needs the sort key, so kWayMergeOrdering keeps 
child/direction/nullOrdering
     // but drops sameOrderExpressions, so LazyRowOrdering does not serialize 
them with the RDD.
-    val childOrdering = Seq(SortOrder(exprA, Ascending, Seq(exprB, exprC)))
+    val plannedOrdering = Seq(SortOrder(exprA, Ascending, Seq(exprB, exprC)))
     val child = DummySparkPlan(
-      outputPartitioning = KeyedPartitioning(Seq(exprA), Seq(row(1), row(2), 
row(1))),
-      outputOrdering = childOrdering)
-    val gpe = GroupPartitionsExec(child)
+      outputPartitioning = KeyedPartitioning(Seq(exprA), Seq(row(1), row(2), 
row(1))))
+    val gpe = GroupPartitionsExec(child, sortedMergeOrdering = 
Some(plannedOrdering))
 
-    assert(child.outputOrdering.head.sameOrderExpressions.nonEmpty, "test 
setup")
     val merged = gpe.kWayMergeOrdering
     assert(merged.map(so => (so.child, so.direction)) === Seq((exprA, 
Ascending)))
     assert(merged.forall(_.sameOrderExpressions.isEmpty))
   }
 
+  test("SPARK-60044: a merging node is not rebuilt over another child") {
+    // Its frozen ordering was checked against its own child, not against the 
new one.
+    val leaf = DummySparkPlan(
+      outputPartitioning = KeyedPartitioning(Seq(exprA), Seq(row(1), row(1))))
+    assert(GroupPartitionsExec(leaf).withKeyPositionsFor(leaf).isDefined, 
"test setup")
+    val merging =
+      GroupPartitionsExec(leaf, sortedMergeOrdering = 
Some(Seq(SortOrder(exprA, Ascending))))
+    assert(merging.withKeyPositionsFor(leaf).isEmpty)

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44),
 with a case over `InputAdapter(leaf)`.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/connector/KeyGroupedPartitioningSuite.scala:
##########
@@ -6793,6 +6797,98 @@ class KeyGroupedPartitioningSuite
     }
   }
 
+  /**
+   * The setup of the SPARK-60044 tests. Creates `items`, which reports no 
ordering, so the scan
+   * derives [id, name] from its partition keys and drops the transform 
between them. The splits
+   * are sorted by the full key, so within id 1 the 'bb' split comes before 
the 'aa' one. Returns a
+   * window query that groups by id and orders by name. Under 
`kWayMergeWindowConfs` a k-way merge
+   * delivers that ordering, so EnsureRequirements adds no SortExec. The query 
selects
+   * `arrive_time`, so the transform's column stays in the scan output.
+   */
+  private def createKWayMergeWindowQuery(): String = {
+    createTable(items, itemsColumns, Array(identity("id"), 
days("arrive_time"), identity("name")))
+    sql(s"INSERT INTO testcat.ns.$items VALUES " +
+      "(1, 'bb', 10.0, cast('2021-01-01' as timestamp)), " +
+      "(1, 'aa', 20.0, cast('2022-01-01' as timestamp)), " +
+      "(2, 'cc', 30.0, cast('2021-01-01' as timestamp))")
+    s"""SELECT id, name, arrive_time, ROW_NUMBER() OVER (PARTITION BY id ORDER 
BY name) AS rn
+       |FROM testcat.ns.$items
+       |""".stripMargin
+  }
+
+  private val kWayMergeWindowConfs = Seq(
+    SQLConf.V2_BUCKETING_ALLOW_KEYS_SUBSET_OF_PARTITION_KEYS.key -> "true",
+    SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key -> "true",
+    SQLConf.V2_BUCKETING_PRESERVE_ORDERING_ON_COALESCE_ENABLED.key -> "true")
+
+  private val kWayMergeWindowRows = Seq(
+    Row(1, "aa", Timestamp.valueOf("2022-01-01 00:00:00"), 1),
+    Row(1, "bb", Timestamp.valueOf("2021-01-01 00:00:00"), 2),
+    Row(2, "cc", Timestamp.valueOf("2021-01-01 00:00:00"), 1))
+
+  test("SPARK-60044: a planned k-way merge keeps its ordering after a later 
config change") {
+    // Turning partitionKeyOrdering off after planning empties the scan's 
ordering. The planned
+    // merge must still merge by [id, name]. Otherwise the window numbers the 
rows of id 1 in split
+    // order.
+    //
+    // Both AQE modes are covered, see the SPARK-59279 test above.
+    val query = createKWayMergeWindowQuery()
+    Seq(true, false).foreach { aqeEnabled =>
+      val confs = (SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> 
aqeEnabled.toString) +:
+        kWayMergeWindowConfs
+      withSQLConf(confs: _*) {
+        val df = sql(query)
+        // Force the plan without executing it, so the k-way merge is decided 
under this config.
+        val plan = df.queryExecution.executedPlan
+        val merging = 
collectAllGroupPartitions(plan).filter(_.enableSortedMerge)
+        assert(merging.length == 1, "expected one k-way merge")
+        assert(merging.head.outputOrdering.map(_.child.sql) == Seq("id", 
"name"))
+        assert(collect(plan) { case s: SortExec => s }.isEmpty,
+          "the k-way merge satisfies the window's ordering, so no SortExec 
should be added")
+
+        // Flip only the key ordering config. The plan above is already 
committed.
+        withSQLConf(SQLConf.V2_BUCKETING_PARTITION_KEY_ORDERING_ENABLED.key -> 
"false") {
+          assert(merging.head.child.outputOrdering.isEmpty,
+            "test setup: the scan no longer derives an ordering from its keys")
+          checkAnswer(df, kWayMergeWindowRows)
+          // Re-collected, because under AQE `executedPlan` only reaches the 
final plan's nodes
+          // once the query has run.
+          val executed = 
collectAllGroupPartitions(df.queryExecution.executedPlan)
+          assert(executed.map(_.outputOrdering.map(_.child.sql)) == 
Seq(Seq("id", "name")),
+            "the planned k-way merge must keep the ordering it was planned 
with")
+        }
+      }
+    }
+  }
+
+  test("SPARK-60044: the debug() helper runs a planned k-way merge") {
+    // Without AQE, `debug()` wraps every node of the executed plan in a 
`DebugExec`, including the
+    // nodes under a merging `GroupPartitionsExec`. That wrapper must not stop 
the planned merge.
+    import org.apache.spark.sql.execution.debug._
+    val query = createKWayMergeWindowQuery()
+    val confs = (SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") +: 
kWayMergeWindowConfs
+    withSQLConf(confs: _*) {
+      val df = sql(query)
+      val plan = df.queryExecution.executedPlan
+      assert(collectAllGroupPartitions(plan).exists(_.enableSortedMerge),
+        "test setup: the plan has a k-way merge")
+      df.debug()
+
+      // `debug()` returns nothing, so its wrapping is rebuilt here, the same 
way, to check that
+      // the merge runs.
+      val visited = mutable.HashSet.empty[TreeNodeRef]
+      val debugPlan = plan.transform {

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44).
 The test now wraps only the merging node's child in a `DebugExec`, and the two 
imports are gone.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExecSuite.scala:
##########
@@ -448,17 +492,22 @@ class GroupPartitionsExecSuite extends SharedSparkSession 
{
     assert(!GroupPartitionsExec(child).groupedPartitions.forall(_._2.size <= 
1),
       "expected coalescing")
     
withSQLConf(SQLConf.V2_BUCKETING_PRESERVE_KEY_ORDERING_ON_COALESCE_ENABLED.key 
-> "true") {
-      // Even though enableSortedMerge = true, the child is not safe for k-way 
merge,
-      // so only key-expression orders survive (non-key exprC is dropped).
-      val ordering = GroupPartitionsExec(child, enableSortedMerge = 
true).outputOrdering
+      val merging = GroupPartitionsExec(child, sortedMergeOrdering = 
Some(childOrdering))
+      val ordering = merging.outputOrdering
       assert(ordering.length === 1)
       assert(ordering.head.child === exprA)
+      // A planned merge that cannot run fails rather than concatenating 
(SPARK-60044). Both
+      // execute paths have their own check.
+      Seq(() => merging.execute(), () => merging.executeColumnar()).foreach { 
run =>
+        val error = intercept[SparkException](run())
+        assert(error.getMessage.contains("planned with a k-way merge"))
+      }
     }
   }
 
-  test("SPARK-55715: coalescing with enableSortedMerge = true returns full 
child ordering") {
+  test("SPARK-55715: coalescing with a planned merge returns full child 
ordering") {
     // Key 1 appears on partitions 0 and 2, causing coalescing. The child is a 
LeafExecNode so
-    // childIsSafeForKWayMerge = true. With enableSortedMerge = true the node 
performs the k-way
+    // childIsSafeForKWayMerge = true. With a planned merge ordering the node 
performs the k-way

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44).
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/datasources/v2/GroupPartitionsExec.scala:
##########
@@ -246,30 +250,41 @@ case class GroupPartitionsExec(
   // such change mints a fresh node through `withNewChildInternal`, so this is 
recomputed against
   // the new child. The correctness of this code relies on all such insertable 
nodes
   // (WholeStageCodegenExec, InputAdapter, ColumnarToRowExec) being in the 
SafeForKWayMerge
-  // whitelist so the evaluation stays consistent.
+  // whitelist so the evaluation stays consistent. So is the `DebugExec` that 
the `debug()` helper
+  // (`org.apache.spark.sql.execution.debug`) wraps every node in without AQE. 
A rule that breaks
+  // this under a planned merge fails the query at execution, see 
`checkPlannedMergeRuns`.
   @transient private lazy val childIsSafeForKWayMerge: Boolean =
     !child.exists {
       case _: SafeForKWayMerge => false
       case _ => true
     }
 
-  /** Whether a k-way merge would work at all, leaving aside whether it is 
switched on. */
-  @transient private lazy val kWayMergeIsFeasible: Boolean =
-    child.outputOrdering.nonEmpty && childIsSafeForKWayMerge
+  // A merge by an empty ordering would run but sort nothing.
+  require(sortedMergeOrdering.forall(_.nonEmpty), "A k-way merge needs an 
ordering to merge by")
 
-  /** Whether this node performs the k-way merge. No config term, see 
`enableSortedMerge`. */
+  /**
+   * Whether the k-way merge was planned for this node. See 
`sortedMergeOrdering`. Driver-only, like
+   * the other accessors of `@transient` fields: a deserialized copy holds no 
ordering.
+   */
+  def enableSortedMerge: Boolean = sortedMergeOrdering.isDefined
+
+  /** Whether this node performs the k-way merge. No config term, see 
`sortedMergeOrdering`. */
   @transient private lazy val usesSortedMerge: Boolean =
-    enableSortedMerge && hasCoalescing && kWayMergeIsFeasible
+    enableSortedMerge && hasCoalescing && childIsSafeForKWayMerge
 
   /**
    * Returns a copy of this node with k-way merge enabled, when the config is 
on, this node
-   * coalesces partitions and the merge is feasible. The only read of
-   * `preserveOrderingOnCoalesce`.
+   * coalesces partitions, the child reports an ordering and its subtree is 
safe for the merge. The
+   * only read of `preserveOrderingOnCoalesce`. The copy keeps the ordering 
checked here, see
+   * `sortedMergeOrdering`. A node that already merges is left as it is, so 
the merge is decided
+   * once.
    */
   def tryEnableSortedMerge(): Option[GroupPartitionsExec] = {
-    Option.when(conf.v2BucketingPreserveOrderingOnCoalesceEnabled && 
hasCoalescing &&
-        kWayMergeIsFeasible) {
-      val newGroupPartitions = copy(enableSortedMerge = true)
+    // Read once, so that the ordering frozen is the one checked.
+    lazy val ordering = child.outputOrdering

Review Comment:
   Done in 
[`34f2672de31`](https://github.com/apache/spark/pull/59306/commits/34f2672de316aad274f3f555af3c04b9cc745a44),
 with your shape.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to