viirya commented on code in PR #59944:
URL: https://github.com/apache/spark/pull/59944#discussion_r4421485633


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2821,15 +2817,10 @@ object PushPredicateThroughNonJoin extends 
Rule[LogicalPlan] with PredicateHelpe
     // come from grandchild.
     // TODO: non-deterministic predicates could be pushed through some 
operators that do not change
     // the rows.
-    val (candidates, nonDeterministic) =
-      splitConjunctivePredicates(filter.condition).partition(_.deterministic)
-
-    val (pushDown, rest) = candidates.partition { cond =>
-      cond.references.subsetOf(grandchild.outputSet)
+    val (pushDown, stayUp) = splitPushableConjuncts(filter.condition) { cond =>
+      cond.deterministic && cond.references.subsetOf(grandchild.outputSet)
     }

Review Comment:
   Thanks, confirmed: your `explode(filter(array(id), x => x > 5))` query fails 
today. Fixed in 6228e8b3a66. The generic case no longer pushes a throwable 
condition through a `Generate` without `outer` or a `ScriptTransformation`, 
even when it is written first (`canDropRows`), so a throwable condition stops 
the `span` there. An outer `Generate` still gets it, since it outputs a row for 
every input row, and so do the other operators of `canPushThrough`, which 
output at least one row per input row (`BinBy` included). Added a plan test for 
both `outer` values and your query to the `DataFrameSuite` test.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/FilterPushdownSuite.scala:
##########
@@ -750,6 +750,120 @@ class FilterPushdownSuite extends PlanTest {
     assertTopFilterConjuncts(Optimize.execute(originalQuery), 
Seq(nonDeterministic, unplaced))
   }
 
+  // Raises for the rows with a <= 5, so it must only see the rows the 
conditions before it kept.
+  private val throwableOnA = attrA > 5 || IsNull(RaiseError(Literal("boom")))
+
+  test("SPARK-60633: a throwable condition is not pushed through a Python UDF 
evaluation " +
+    "ahead of the conditions before it") {
+    Seq(PythonEvalType.SQL_BATCHED_UDF, 
PythonEvalType.SQL_SCALAR_PANDAS_UDF).foreach { evalType =>
+      withClue(s"eval type $evalType: ") {
+        val pythonUDF = PythonUDF(
+          "pythonUDF", null, IntegerType, Seq(attrA), evalType, 
udfDeterministic = true)
+        val f = AttributeReference("f", IntegerType)()
+        def evalPython(child: LogicalPlan): LogicalPlan =
+          if (evalType == PythonEvalType.SQL_BATCHED_UDF) {
+            BatchEvalPython(Seq(pythonUDF), Seq(f), child)
+          } else {
+            ArrowEvalPython(Seq(pythonUDF), Seq(f), child, evalType)
+          }
+
+        // f > 5 cannot be pushed, so the throwable condition after it stays 
above too. b > 1
+        // is still pushed, as that only reduces the rows the throwable 
condition sees.
+        val originalQuery = Filter(f > 5 && throwableOnA && attrB > 1, 
evalPython(testRelation))
+        val correctAnswer =
+          Filter(f > 5 && throwableOnA, evalPython(Filter(attrB > 1, 
testRelation)))
+        comparePlans(Optimize.execute(originalQuery), correctAnswer)
+
+        // With nothing left above before it, a throwable condition is still 
pushed.
+        val throwableFirst = Filter(throwableOnA && f > 5, 
evalPython(testRelation))
+        val throwableFirstAnswer = Filter(f > 5, 
evalPython(Filter(throwableOnA, testRelation)))
+        comparePlans(Optimize.execute(throwableFirst), throwableFirstAnswer)
+
+        // The conditions left above keep their written order.
+        val nonDeterministic = Rand(10) > 0.5
+        val mixed = Filter(nonDeterministic && f > 5 && throwableOnA, 
evalPython(testRelation))
+        assertTopFilterConjuncts(
+          Optimize.execute(mixed), Seq(nonDeterministic, f > 5, throwableOnA))
+      }
+    }
+  }
+
+  test("SPARK-60633: a throwable condition is not pushed through a sort ahead 
of a " +
+    "non-deterministic condition before it") {
+    val originalQuery = testRelation.orderBy($"b".asc)
+      .where(Rand(10) > 0.5 && throwableOnA && $"b" > 1)
+      .analyze
+    val Seq(nonDeterministic, throwable, _) = splitConjunctivePredicates(
+      originalQuery.asInstanceOf[Filter].condition)
+    val correctAnswer = testRelation.where($"b" > 1).orderBy($"b".asc)
+      .where(Rand(10) > 0.5 && throwableOnA)
+      .analyze
+
+    val optimized = Optimize.execute(originalQuery)
+    comparePlans(optimized, correctAnswer)
+    assertTopFilterConjuncts(optimized, Seq(nonDeterministic, throwable))
+  }
+
+  test("SPARK-60633: a throwable condition is not pushed through an aggregate 
ahead of the " +
+    "conditions before it") {
+    val originalQuery = testRelation.groupBy($"a")($"a", count($"b") as "c")
+      .where($"c" > 1 && throwableOnA && $"a" > 1)
+      .analyze
+    val correctAnswer = testRelation.where($"a" > 1).groupBy($"a")($"a", 
count($"b") as "c")
+      .where($"c" > 1 && throwableOnA)
+      .analyze
+    comparePlans(Optimize.execute(originalQuery), correctAnswer)
+
+    // With nothing left above before it, a throwable condition is still 
pushed.
+    val throwableFirst = testRelation.groupBy($"a")($"a", count($"b") as "c")
+      .where(throwableOnA && $"c" > 1)
+      .analyze
+    val throwableFirstAnswer = testRelation.where(throwableOnA)
+      .groupBy($"a")($"a", count($"b") as "c")
+      .where($"c" > 1)
+      .analyze
+    comparePlans(Optimize.execute(throwableFirst), throwableFirstAnswer)
+  }
+
+  test("SPARK-60633: a throwable condition is not pushed through a window 
ahead of the " +
+    "conditions before it") {
+    val winExpr = windowExpr(count($"b"),
+      windowSpec($"a" :: Nil, $"b".asc :: Nil, UnspecifiedFrame))
+    val originalQuery = testRelation
+      .window(winExpr.as("window") :: Nil, $"a" :: Nil, $"b".asc :: Nil)
+      .where($"window" > 1 && throwableOnA && $"a" > 1)
+      .analyze
+    val correctAnswer = testRelation.where($"a" > 1)
+      .window(winExpr.as("window") :: Nil, $"a" :: Nil, $"b".asc :: Nil)
+      .where($"window" > 1 && throwableOnA)
+      .analyze
+    comparePlans(Optimize.execute(originalQuery), correctAnswer)

Review Comment:
   Added in 6228e8b3a66 with your snippet. On the merge base, `rest ++ 
nonDeterministic` makes the top filter `window > 1` then `rand(10) > 0.5`, so 
the check would catch a revert.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2724,15 +2725,10 @@ object PushPredicateThroughNonJoin extends 
Rule[LogicalPlan] with PredicateHelpe
       if w.partitionSpec.forall(_.isInstanceOf[AttributeReference]) =>
       val partitionAttrs = AttributeSet(w.partitionSpec.flatMap(_.references))
 
-      val (candidates, nonDeterministic) =
-        splitConjunctivePredicates(condition).partition(_.deterministic)
-
-      val (pushDown, rest) = candidates.partition { cond =>
-        cond.references.subsetOf(partitionAttrs)
+      val (pushDown, stayUp) = splitPushableConjuncts(condition) { cond =>
+        cond.deterministic && cond.references.subsetOf(partitionAttrs)
       }

Review Comment:
   Updated in 6228e8b3a66 to the wording you suggested.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2842,6 +2833,22 @@ object PushPredicateThroughNonJoin extends 
Rule[LogicalPlan] with PredicateHelpe
     }
   }
 
+  /**
+   * Splits `condition` into the conjuncts to push below an operator and the 
conjuncts to keep
+   * above it, where `canPush` tells whether the operator lets a conjunct 
through. Every conjunct
+   * written before the first one that cannot be pushed is pushed. After it, 
only the conjuncts
+   * that are not throwable (e.g. `raise_error`) are pushed, so a throwable 
conjunct is not
+   * evaluated on rows the conjuncts before it would have filtered out. Moving 
a non-throwable
+   * conjunct below a throwable one is fine, as it can only reduce the rows 
the throwable one
+   * sees. The conjuncts to keep above are returned in the order they were 
written.
+   */
+  private def splitPushableConjuncts(condition: Expression)(
+      canPush: Expression => Boolean): (Seq[Expression], Seq[Expression]) = {

Review Comment:
   Done in 6228e8b3a66. `splitPushable[T]` takes `canPush` and `throwable`, and 
the `Project` case uses it with `replaced.throwable`. `splitPushableConjuncts` 
is now a wrapper for the cases that split a plain condition. The `Aggregate` 
case also uses `splitPushable` directly now, to check `replaced.throwable`, as 
the bot review asked.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2842,6 +2833,22 @@ object PushPredicateThroughNonJoin extends 
Rule[LogicalPlan] with PredicateHelpe
     }
   }
 
+  /**
+   * Splits `condition` into the conjuncts to push below an operator and the 
conjuncts to keep
+   * above it, where `canPush` tells whether the operator lets a conjunct 
through. Every conjunct
+   * written before the first one that cannot be pushed is pushed. After it, 
only the conjuncts
+   * that are not throwable (e.g. `raise_error`) are pushed, so a throwable 
conjunct is not
+   * evaluated on rows the conjuncts before it would have filtered out. Moving 
a non-throwable
+   * conjunct below a throwable one is fine, as it can only reduce the rows 
the throwable one
+   * sees. The conjuncts to keep above are returned in the order they were 
written.
+   */
+  private def splitPushableConjuncts(condition: Expression)(
+      canPush: Expression => Boolean): (Seq[Expression], Seq[Expression]) = {
+    val (leading, rest) = splitConjunctivePredicates(condition).span(canPush)
+    val (pushedRest, stayUp) = rest.partition(cond => canPush(cond) && 
!cond.throwable)

Review Comment:
   Done in 6228e8b3a66, in the shared helper.



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