cloud-fan commented on code in PR #57901:
URL: https://github.com/apache/spark/pull/57901#discussion_r3758099639


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushdownPredicatesAndPruneColumnsForCTEDef.scala:
##########
@@ -156,6 +174,79 @@ object PushdownPredicatesAndPruneColumnsForCTEDef extends 
Rule[LogicalPlan] {
       }
   }
 
+  /**
+   * Removes the previous push-down filter (identified by its condition, 
`predicate`) from
+   * `plan`, wherever predicate push-down rules sharing the fixedPoint batches 
with this rule
+   * (e.g. `PushDownPredicates`) may have moved it. The descent mirrors the 
cases of
+   * `PushPredicateThroughNonJoin` and `PushPredicateThroughJoin`, translating 
`predicate` back
+   * the same way they translate the pushed condition (for `Project` and 
`Aggregate` it uses
+   * the very same `AliasHelper` utilities, so the two cannot drift apart): 
through projection
+   * and grouping-key aliases, positionally into each branch (`Union`), and 
unchanged
+   * through operators that pass the referenced attributes verbatim (`Join`, 
`Window`, and
+   * output-preserving unary nodes like `Filter`, `Sort`, `Repartition`). 
Descent stops at
+   * operators that remap attributes in other ways (e.g. `Generate`, 
`Expand`): if the filter
+   * cannot be located, the input plan is returned unchanged, and the caller 
re-pushes on top,
+   * which is redundant but always semantics-preserving.
+   */
+  private def removePushedDownFilter(plan: LogicalPlan, predicate: 
Expression): LogicalPlan = {
+    def remove(current: LogicalPlan, target: Expression): (LogicalPlan, 
Boolean) = current match {
+      case Filter(cond, inner) if cond.canonicalized == target.canonicalized =>
+        (inner, true)
+      case p: Project =>
+        // Mirror PushPredicateThroughNonJoin: translate the target through 
the projection's
+        // aliases with the same helper it uses to move the filter below the 
projection.
+        val translated = replaceAlias(target, getAliasMap(p))
+        val (newChild, removed) = remove(p.child, translated)
+        if (removed) (p.copy(child = newChild), true) else (p, false)
+      case j: Join =>
+        val (newLeft, removedFromLeft) = remove(j.left, target)
+        if (removedFromLeft) {
+          (j.copy(left = newLeft), true)
+        } else {
+          val (newRight, removedFromRight) = remove(j.right, target)
+          if (removedFromRight) (j.copy(right = newRight), true) else (j, 
false)
+        }
+      case u: Union =>
+        // PushDownPredicates copies the filter into every branch, mapping the 
union output
+        // attributes to each branch's output positionally; remove it from 
every branch where
+        // it is found.
+        var removedAny = false
+        val newChildren = u.children.map { branch =>
+          val branchTarget = target.transform {
+            case a: Attribute if u.output.exists(_.exprId == a.exprId) =>
+              branch.output(u.output.indexWhere(_.exprId == a.exprId))

Review Comment:
   **Non-blocking:**
   
   Please build an ExprId-to-output-index map once before iterating the 
branches. This currently scans `u.output` with both `exists` and `indexWhere` 
for every referenced attribute in every branch, which becomes unnecessarily 
quadratic for wide unions.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushdownPredicatesAndPruneColumnsForCTEDef.scala:
##########
@@ -156,6 +174,79 @@ object PushdownPredicatesAndPruneColumnsForCTEDef extends 
Rule[LogicalPlan] {
       }
   }
 
+  /**
+   * Removes the previous push-down filter (identified by its condition, 
`predicate`) from
+   * `plan`, wherever predicate push-down rules sharing the fixedPoint batches 
with this rule
+   * (e.g. `PushDownPredicates`) may have moved it. The descent mirrors the 
cases of
+   * `PushPredicateThroughNonJoin` and `PushPredicateThroughJoin`, translating 
`predicate` back
+   * the same way they translate the pushed condition (for `Project` and 
`Aggregate` it uses
+   * the very same `AliasHelper` utilities, so the two cannot drift apart): 
through projection
+   * and grouping-key aliases, positionally into each branch (`Union`), and 
unchanged
+   * through operators that pass the referenced attributes verbatim (`Join`, 
`Window`, and
+   * output-preserving unary nodes like `Filter`, `Sort`, `Repartition`). 
Descent stops at
+   * operators that remap attributes in other ways (e.g. `Generate`, 
`Expand`): if the filter
+   * cannot be located, the input plan is returned unchanged, and the caller 
re-pushes on top,
+   * which is redundant but always semantics-preserving.
+   */
+  private def removePushedDownFilter(plan: LogicalPlan, predicate: 
Expression): LogicalPlan = {
+    def remove(current: LogicalPlan, target: Expression): (LogicalPlan, 
Boolean) = current match {
+      case Filter(cond, inner) if cond.canonicalized == target.canonicalized =>
+        (inner, true)
+      case p: Project =>
+        // Mirror PushPredicateThroughNonJoin: translate the target through 
the projection's
+        // aliases with the same helper it uses to move the filter below the 
projection.
+        val translated = replaceAlias(target, getAliasMap(p))
+        val (newChild, removed) = remove(p.child, translated)
+        if (removed) (p.copy(child = newChild), true) else (p, false)

Review Comment:
   **Non-blocking:**
   
   Could we rebuild these ancestors with `withNewChildren` (or explicitly copy 
tags)? Direct case-class copies drop `TreeNode` tags; for example, a tagged 
`Project` loses `hiddenOutputTag`. The same applies to the direct `Join`, 
`Aggregate`, and `Window` copies, so a tagged-node regression case would make 
the invariant explicit.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2194,6 +2194,10 @@ object PushDownPredicates extends Rule[LogicalPlan] {
  * 2) the predicate is deterministic and the operator will not change any of 
rows.
  * 3) We don't add double evaluation OR double evaluation would be cheap OR 
we're configured to.
  *
+ * Note: if a new push-through case is added here, or the translation applied 
to pushed
+ * conditions changes (e.g. how aliases are substituted), also update
+ * [[PushdownPredicatesAndPruneColumnsForCTEDef.removePushedDownFilter]], 
which mirrors this

Review Comment:
   **Nit:**
   
   Please link `[[PushdownPredicatesAndPruneColumnsForCTEDef]]` and render 
`removePushedDownFilter` as code. The helper is private, so it is not a stable 
generated-Scaladoc member target.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushdownPredicatesAndPruneColumnsForCTEDef.scala:
##########
@@ -113,32 +114,49 @@ object PushdownPredicatesAndPruneColumnsForCTEDef extends 
Rule[LogicalPlan] {
    * In order to guarantee idempotency, we keep the predicates (if any) being 
pushed down by the
    * last iteration of this rule in a temporary field of `CTERelationDef`, so 
that on the current
    * iteration, we only push down predicates for a CTE def if there exists any 
new predicate that
-   * has not been pushed before. Also, since part of a new predicate might 
overlap with some
-   * existing predicate and it can be hard to extract only the non-overlapping 
part, we also keep
-   * the original CTE definition plan without any predicate push-down in that 
temporary field so
-   * that when we do a new predicate push-down, we can construct a new plan 
with all latest
-   * predicates over the original plan without having to figure out the exact 
predicate difference.
+   * has not been pushed before. When such a new predicate push-down happens, 
the CTE definition
+   * is rebuilt from its CURRENT child: the push-down filter this rule placed 
in the previous
+   * iteration is removed (wherever it sits) and the result is wrapped with 
the latest combined
+   * predicate. This preserves any change other rules made to the CTE 
definition's child in
+   * between (e.g. filters injected by `InferFiltersFromConstraints`, which 
runs in the `Once`
+   * batch sandwiched between the two fixedPoint batches containing this 
rule). If the previous
+   * push-down can no longer be found (another rule rewrote or merged it with 
other filters),
+   * the current child is used as-is: re-pushing the combined predicate is 
redundant but always
+   * semantics-preserving, since the disjunction of the reference predicates 
is valid for every
+   * row of the CTE definition.
    */
   private def pushdownPredicatesAndAttributes(
       plan: LogicalPlan,
       cteMap: CTEMap): LogicalPlan = plan.transformWithSubqueries {
     case cteDef @ CTERelationDef(child, id, originalPlanWithPredicates, _, _, 
_) =>
       val (_, _, newPreds, newAttrSet) = cteMap(id)
-      val originalPlan = originalPlanWithPredicates.map(_._1).getOrElse(child)
       val preds = originalPlanWithPredicates.map(_._2).getOrElse(Seq.empty)
       if (!isTruePredicate(newPreds) &&
           newPreds.exists(newPred => 
!preds.exists(_.semanticEquals(newPred)))) {
+        val basePlan = originalPlanWithPredicates match {
+          case Some((_, prevPreds)) if prevPreds.nonEmpty =>
+            // Remove the push-down filter this rule placed in the previous 
iteration. It is
+            // usually the top-level node of the child, but rules sharing the 
fixedPoint
+            // batches with this rule (e.g. `PushDownPredicates`) may have 
moved it deeper,
+            // possibly across attribute-renaming projections - hence the 
comparison is done
+            // on canonicalized conditions. If the previous push-down can no 
longer be found
+            // (another rule rewrote or merged it with other filters), the 
current child is
+            // used as-is: re-pushing the combined predicate is redundant but 
always
+            // semantics-preserving, since the disjunction of the reference 
predicates is
+            // valid for every row of the CTE definition.
+            removePushedDownFilter(child, prevPreds.reduce(Or))
+          case _ => child
+        }
         val newCombinedPred = newPreds.reduce(Or)
-        val newChild = if (needsPruning(originalPlan, newAttrSet)) {
-          Project(newAttrSet.toSeq, originalPlan)
+        val newChild = if (needsPruning(basePlan, newAttrSet)) {
+          Project(newAttrSet.toSeq, basePlan)
         } else {
-          originalPlan
+          basePlan
         }
         cteDef.copy(child = Filter(newCombinedPred, newChild),
-          originalPlanWithPredicates = Some((originalPlan, newPreds)))
+          originalPlanWithPredicates = Some((basePlan, newPreds)))

Review Comment:
   **Nit:**
   
   Please update the `CTERelationDef.originalPlanWithPredicates` parameter 
documentation with this semantic change. It now stores the current base plan 
after removing the previous pushed filter, not necessarily the original query 
plan before predicate pushdown.



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