sarutak commented on code in PR #59940:
URL: https://github.com/apache/spark/pull/59940#discussion_r4429291189


##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/FilterPushdownSuite.scala:
##########
@@ -750,6 +750,314 @@ class FilterPushdownSuite extends PlanTest {
     assertTopFilterConjuncts(Optimize.execute(originalQuery), 
Seq(nonDeterministic, unplaced))
   }
 
+  test("SPARK-60730: a cheap condition that can fail is not pushed ahead of an 
expensive one") {
+    Seq(true, false).foreach { split =>
+      withSQLConf(SQLConf.SPLIT_PROJECTION_FOR_EXPENSIVE_FILTERS.key -> 
split.toString) {
+        val originalQuery = testStringRelation
+          .select($"a", $"e".rlike("magic") as "f", $"b")

Review Comment:
   Good point. The new tests now use `expensiveRLike(tag)`, with the same 
definition and at the same place as in #59879, so they hold with either 
heuristic. The Python UDF test uses a pattern that is not a constant too. 
`evaluatedHere` now replaces a Python UDF with an attribute, as 
`ExtractPythonUDFs` does, instead of a NULL literal, so `e RLIKE py(a)` stays 
expensive with #59879's `!right.foldable`. So I do not think the two PRs need 
an order. Until #59879 lands, every `RLIKE` alias over a column counts as 
expensive, as before this PR, so the hold-back applies to those aliases too.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/ExprUtils.scala:
##########
@@ -266,13 +269,22 @@ object ExprUtils extends EvalHelper with QueryErrorsBase {
     e.deterministic && !e.containsPattern(PLAN_EXPRESSION) &&
       canEvaluateUnconditionallyInternal(e)
 
+  // Only the numeric pairs are taken from `Cast.canUpCast`, which also covers 
e.g. LONG to
+  // TIMESTAMP and casts to STRING or between complex types: widening a number 
never fails.
+  private def isNumericUpCast(from: DataType, to: DataType): Boolean = (from, 
to) match {
+    case (_: NumericType, _: NumericType) => Cast.canUpCast(from, to)
+    case _ => false
+  }
+
   private def canEvaluateUnconditionallyInternal(e: Expression): Boolean = e 
match {
     case _: AttributeReference | _: Literal => true
     case _: GetStructField | _: GetArrayStructFields | _: GetMapValue =>
       e.children.forall(canEvaluateUnconditionallyInternal)
     case _: And | _: Or | _: Not | _: BinaryComparison | _: IsNull | _: 
IsNotNull |
-         _: IsNaN | _: NullIf | _: Coalesce | _: In | _: InSet =>
+         _: IsNaN | _: NullIf | _: Coalesce | _: In | _: InSet | _: Size =>
       e.children.forall(canEvaluateUnconditionallyInternal)

Review Comment:
   Added `StartsWith`, `EndsWith` and `Contains`, but not `Like` or `RLike`, 
and also `Length` and `OctetLength`, since `LikeSimplification` turns `LIKE 
'ab%c'` into `octet_length(s) >= 3 AND StartsWith(s, 'ab') AND EndsWith(s, 
'c')` (`length` with `UTF8_LCASE`), which was still rejected; so every `LIKE` 
without `_` over a column that it simplifies is pushed now. They can not fail 
for any collation: the ICU path returns before searching for an empty string, 
and an `ExprUtilsSuite` test evaluates them over binary, lowercase, ICU and 
trim collations on empty, ignorable and invalid UTF-8 strings. A 
`GetStructField` over a `CreateNamedStruct` is accepted when every field of the 
struct passes, since the whole struct is evaluated until 
`SimplifyExtractValueOps` reduces it, and that rule can be excluded. So your 
`struct(a, b) AS s` with `s.a > 1` is pushed now, while a struct with a field 
that can fail, e.g. `struct(id AS a, id * 2 AS b)`, stays rejected, since `id * 
2` can overflow in ANSI
  mode. `FilterExec`, `StreamedSideJoinCondition` and 
`RewriteDistinctAggregates` accept them too; their suites and the TPC-DS plan 
stability suites pass with no plan changes.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/Optimizer.scala:
##########
@@ -2652,8 +2681,20 @@ object PushPredicateThroughNonJoin extends 
Rule[LogicalPlan] with PredicateHelpe
         // non-deterministic expression sees. Throwable conditions, or 
conditions over throwable
         // aliases, stay there too: ordering by cost could evaluate them (or 
compute the aliases
         // in a lower projection) ahead of the conditions written before them.
+        // For the same reason, there is no split when a condition after the 
first one held back
+        // above is rejected by `canPushAhead`, with only the cheap aliases 
substituted (it can
+        // tell nothing about an expensive one such as a UDF): e.g. a division 
over an expensive
+        // alias, or a cheap condition held back above, which needs no 
expensive alias and so
+        // would go lowest. The filter then stays above the projection as 
written, as without
+        // the split.
+        val cheapAliasMap = AttributeMap(aliasMap.filter { case (_, a) => 
!a.child.expensive })
+        val keepsWrittenOrder = 
!SQLConf.get.legacyPushFilterAheadOfExpensiveConjunct &&
+          expensiveHeld.exists {
+            case (cond, _, _) => !canPushAhead(replaceAlias(cond, 
cheapAliasMap))
+          }

Review Comment:
   Done: `keepsWrittenOrder`, which builds the map, is a `lazy val` now, the 
split config is checked first, and the map is only built when `expensiveHeld` 
is non-empty. `evaluatedHere` checks `alias.child.expensive` before copying the 
tree.



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