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


##########
sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala:
##########
@@ -690,19 +684,43 @@ class PlanMerger(
                        // rows are NULL-padded so f=NULL, causing FILTER 
(WHERE f) to incorrectly
                        // exclude rows that should contribute to the 
aggregate. Right-side
                        // attributes are also absent from semi/anti join 
output.
-                       (leftNPFilter.isEmpty && leftCPFilter.isEmpty  ||
+                       (leftNPFilter.isEmpty && leftCPFilter.isEmpty ||
                            filterSafeForJoin(fromLeft = true, cp.joinType)) &&
                        (rightNPFilter.isEmpty && rightCPFilter.isEmpty ||
                            filterSafeForJoin(fromLeft = false, cp.joinType)) =>
                   val npMapping = leftNPMapping ++ rightNPMapping
                   val mappedNPCondition = np.condition.map(mapAttributes(_, 
npMapping))
                   // Comparing the canonicalized form is required to ignore 
different forms of the
                   // same expression and `AttributeReference.qualifier`s in 
`cp.condition`.
-                  if (mappedNPCondition.map(_.canonicalized) == 
cp.condition.map(_.canonicalized)) {
-                    val npFilter = leftNPFilter.orElse(rightNPFilter)
-                    val cpFilter = leftCPFilter.orElse(rightCPFilter)
-                    Some(TryMergeResult(cp.withNewChildren(Seq(mergedLeft, 
mergedRight)), npMapping,
-                      npFilter, cpFilter, dsv2Merged = leftDsv2Merged || 
rightDsv2Merged))
+                  if (mappedNPCondition.map(_.canonicalized) ==
+                      cp.condition.map(_.canonicalized)) {
+                    val mergedJoin = cp.withNewChildren(Seq(mergedLeft, 
mergedRight))
+                    val (npFilter, npAlias) = (leftNPFilter, rightNPFilter) 
match {
+                      case (Some((left, _)), Some((right, _))) =>
+                        val alias = Alias(
+                          And(left, right), 
s"propagatedFilter_${PlanMerger.newId}")()

Review Comment:
   **Finding 1.** Folding `left` and `right` into this `And` means neither is 
carried to the `Project`s above the join. A later round can still reuse one of 
them on its own. Then the `Aggregate`'s `FILTER` references an attribute its 
child doesn't output.
   
   The reuse comes from the tagged `(Filter, Filter)` branch 
(`existingNPFilter`, line 546). It returns the attribute with `isNew = false`, 
so `mergeNamedExpressions` doesn't append it (line 1125). That relies on the 
round that created the attribute having carried it all the way up to the 
`Aggregate`. Once it went into this `And`, it wasn't.
   
   A shape that hits it, with the confs of `Repeatedly merge filters from both 
Join children` (symmetric and through-join propagation on):
   
   ```sql
   SELECT
     (SELECT sum(l.a) FROM testData2 l JOIN testData2 r ON l.a = r.a WHERE l.b 
= 1),
     (SELECT max(l.a) FROM testData2 l JOIN testData2 r ON l.a = r.a WHERE l.b 
= 2 AND r.b = 2),
     (SELECT count(*) FROM testData2 l JOIN testData2 r ON l.a = r.a WHERE l.b 
= 2)
   ```
   
   1. The second subquery merges into the first. The left child becomes a 
tagged `Filter` holding `f_l = (b = 2)`. Only `And(f_l, f_r)` is carried into 
the `Project` that `ColumnPruning` put between the join and the `Aggregate`, 
and `f_l` itself is not.
   2. The third subquery reuses `f_l` with `isNew = false`. Its `count(*) 
FILTER (WHERE f_l)` then sits above that `Project`, which doesn't output `f_l`.
   
   The expected answer is `Row(12, 3, 6)`. On master the second subquery 
doesn't merge, so the third one merges with the first and the query works.
   
   As far as I can trace, a post-join `Filter` can already fold a child's 
attribute this way on master (`newNPCondition = And(f, ...)`). So I'd fix it in 
`mergeNamedExpressions` rather than here, by appending the np filter whenever 
the merged list doesn't output it yet:
   
   ```scala
       newPlanFilter.foreach { case (f, _) =>
         if (!mergedExpressions.exists(_.exprId == f.exprId)) mergedExpressions 
+= f
       }
   ```
   



##########
sql/core/src/test/scala/org/apache/spark/sql/execution/planmerging/MergeSubplansSuite.scala:
##########
@@ -1859,10 +1859,7 @@ class MergeSubplansSuite extends PlanTest {
     }
   }
 
-  test("SPARK-56677: Do not merge subqueries when both join children have 
independent filters") {
-    // np has filters on BOTH left and right join children simultaneously. The 
guard in the
-    // Join case prevents this merge because combining two independent filter 
attributes would
-    // require ANDing them into a new alias, which is not yet supported.
+  test("SPARK-56677: Merge subqueries when both join children have independent 
filters") {

Review Comment:
   **Finding 3.** This renamed test and the four new ones (two below, two in 
`PlanMergingSuite`) test the SPARK-59897 change. They'd normally carry 
`SPARK-59897:` rather than `SPARK-56677:`.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala:
##########
@@ -115,9 +115,7 @@ object PlanMerger {
  * result of expressions like `coalesce(col, default)` in the aggregate: an 
originally unmatched row
  * would have contributed `default` via `coalesce(NULL, default)`, but in the 
merged plan it is
  * matched, its real column value fails the filter, and `FILTER (WHERE false)` 
discards it entirely.
- * Propagation is also skipped when both the left and right children 
simultaneously produce filter
- * attributes, as combining them would require an additional AND alias above 
the join (not yet
- * supported).
+ * Filters from both join children are combined with AND above the join.

Review Comment:
   **Finding 4.** The `tryMergePlans` scaladoc (lines 399-400) still says the 
join arm forwards propagation "so a filter difference on one child can still be 
merged". Worth saying "either or both children" there too.
   



##########
sql/core/src/main/scala/org/apache/spark/sql/execution/planmerging/PlanMerger.scala:
##########
@@ -676,11 +674,7 @@ class PlanMerger(
               tryMergePlans(np.right, cp.right, context.copy(filterAboveScan = 
false)).flatMap {
                 case TryMergeResult(mergedRight, rightNPMapping, 
rightNPFilter, rightCPFilter, _,
                     rightDsv2Merged)
-                    // If both children independently propagate filter 
attributes we would need to
-                    // AND them into a new alias above the join, which is not 
yet supported.
-                    if !(leftNPFilter.isDefined && rightNPFilter.isDefined) &&
-                       !(leftCPFilter.isDefined && rightCPFilter.isDefined) &&
-                       // Gate join-crossing filter propagation behind its own 
config flag.
+                    if // Gate join-crossing filter propagation behind its own 
config flag.

Review Comment:
   **Finding 2.** Removing this guard leaves `filterSafeForJoin` as the only 
check that keeps a filter pair off an outer join. For a `LeftOuter` join it's 
the right-side case that rejects the pair, and no test covers it. The existing 
negative tests use `RightOuter`, `FullOuter` and the right side of `LeftSemi`. 
A test next to the `RightOuter` one would pin it:
   
   ```scala
     test("SPARK-59897: Do not merge subqueries when both children of a 
LeftOuter join have filters") {
       val subquery1 = ScalarSubquery(
         testRelation.join(testRelation2, LeftOuter, Some($"a" === $"d"))
           .groupBy()(sum($"a").as("sum_a")))
       val subquery2 = ScalarSubquery(
         testRelation.where($"a" > 1).join(testRelation2.where($"d" > 1), 
LeftOuter,
           Some($"a" === $"d"))
           .groupBy()(max($"a").as("max_a")))
       val originalQuery = testRelation.select(subquery1, subquery2)
   
       
withSQLConf(SQLConf.MERGE_SUBPLANS_FILTER_PROPAGATION_THROUGH_JOIN_ENABLED.key 
-> "true") {
         comparePlans(Optimize.execute(originalQuery.analyze), 
originalQuery.analyze)
       }
     }
   ```
   



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