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


##########
sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala:
##########
@@ -2597,6 +2653,70 @@ class MergeSubplansSuite extends PlanTest {
     }
   }
 
+  gridTest("SPARK-60106: Merge over a merged DSv2 filter on a join child")(
+      for {
+        v2OnLeft <- Seq(true, false)
+        shape <- Seq("dropped", "extended", "reused", "firstMerge")
+      } yield (v2OnLeft, shape)) { case (v2OnLeft, shape) =>
+    // sub1 and sub2 filter the DSv2 scan differently and merge under the DSv2 
exemption into a
+    // merged Filter. A subplan that also filters the other join child would 
join a child that the
+    // cached plans filter with a child that it filters, so without symmetric 
filter propagation
+    // it stays apart. The shapes:
+    // - dropped: sub3 filters only the other child, so its merge drops the 
merged Filter.
+    // - extended: sub3 filters the DSv2 scan too, so its merge extends the 
merged Filter.
+    // - reused: sub3 repeats sub1's filter on the DSv2 scan, so sub1 covers 
sub3's rows there and
+    //   the merge builds no extra pairs. It merges without symmetric filter 
propagation.
+    // - firstMerge: sub2 itself filters the other child too, so the first 
merge declines.
+    // The DSv2 scan is tried as both the left and the right join child.
+    def join(v2: LogicalPlan, other: LogicalPlan): LogicalPlan = if (v2OnLeft) 
{
+      v2.join(other, Inner, Some($"a" === $"d"))
+    } else {
+      other.join(v2, Inner, Some($"a" === $"d"))
+    }
+    val sub1 = ScalarSubquery(
+      join(v2ScanReading("a", "b").where($"a" > 1), testRelation2)
+        .groupBy()(sum($"a").as("sum_a")))
+    val other2 = if (shape == "firstMerge") testRelation2.where($"e" > 3) else 
testRelation2
+    val sub2 = ScalarSubquery(
+      join(v2ScanReading("a", "b").where($"b" > 2), other2)
+        .groupBy()(max($"a").as("max_a")))
+    val v2In3 = shape match {
+      case "extended" => v2ScanReading("a", "b").where($"a" < 0)
+      case "reused" => v2ScanReading("a", "b").where($"a" > 1)
+      case _ => v2ScanReading("a", "b")
+    }
+    val sub3 = ScalarSubquery(
+      join(v2In3, testRelation2.where($"e" > 3))
+        .groupBy()(min($"a").as("min_a")))
+    val originalQuery = if (shape == "firstMerge") {
+      testRelation.select(sub1, sub2)
+    } else {
+      testRelation.select(sub1, sub2, sub3)
+    }
+
+    def mergedAggregates(symmetric: Boolean): Seq[Seq[String]] = withSQLConf(

Review Comment:
   The `reused` case now also checks each aggregate's `FILTER`, with the 
propagated aliases inlined. For example, `min_a` has to get `((a > 1) AND (e > 
3))`. It fails if the reuse picks the wrong alias 
([0fb8bcce413](https://github.com/apache/spark/pull/59321/commits/0fb8bcce413b83ac6c4ab82e9a8e30e513f2d271)).
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala:
##########
@@ -430,27 +448,30 @@ class PlanMerger(
       (newPlan, cachedPlan) match {
         case (np: Project, cp: Project) =>
           tryMergePlans(np.child, cp.child, context).map {
-            case TryMergeResult(mergedChild, npMapping, npFilter, cpFilter, 
deferred, dsv2Merged) =>
+            case TryMergeResult(mergedChild, npMapping, npFilter, cpFilter, 
deferred, dsv2Merged,

Review Comment:
   Done, the three Project arms use `copy` now 
([0fb8bcce413](https://github.com/apache/spark/pull/59321/commits/0fb8bcce413b83ac6c4ab82e9a8e30e513f2d271)).
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala:
##########
@@ -660,18 +688,23 @@ class PlanMerger(
                   mergedChild.output.toList :+ newCPFilterAlias,
                   mergedChild)
                 TryMergeResult(project, npMapping, npFilter, Some(newCPFilter),
-                  dsv2Merged = dsv2Merged)
+                  dsv2Merged = dsv2Merged, cachedSideFiltered = true)
               }
           }
 
         case (np: Join, cp: Join) if np.joinType == cp.joinType && np.hint == 
cp.hint =>
           tryMergePlans(np.left, cp.left, context.copy(filterAboveScan = 
false)).flatMap {
             case TryMergeResult(mergedLeft, leftNPMapping, leftNPFilter, 
leftCPFilter, _,
-                leftDsv2Merged) =>
+                leftDsv2Merged, leftCPSideFiltered) =>
               tryMergePlans(np.right, cp.right, context.copy(filterAboveScan = 
false)).flatMap {
                 case TryMergeResult(mergedRight, rightNPMapping, 
rightNPFilter, rightCPFilter, _,
-                    rightDsv2Merged)
-                    if // Gate join-crossing filter propagation behind its own 
config flag.
+                    rightDsv2Merged, rightCPSideFiltered)
+                    if // When the two plans filter different children, the 
merge is symmetric
+                       // filter propagation and needs its config, see the 
class doc.
+                       (symmetricFilterPropagationEnabled ||

Review Comment:
   I added parentheses around the two `&&` terms. I'd keep the check in the 
guard, because it only protects this case, like the other clauses there with 
their own comments 
([0fb8bcce413](https://github.com/apache/spark/pull/59321/commits/0fb8bcce413b83ac6c4ab82e9a8e30e513f2d271)).
   



-- 
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