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


##########
sql/core/src/test/scala/org/apache/spark/sql/CTEInlineSuite.scala:
##########
@@ -451,6 +451,29 @@ abstract class CTEInlineSuiteBase
     }
   }
 
+  test("SPARK-59434: non-deterministic predicates are not pushed into a CTE 
def") {

Review Comment:
   **Finding 2.** The new filter has two outcomes and this test covers one of 
them.
   
   - Every predicate at a reference is non-deterministic. `filteredPredicates` 
ends up empty, the combined predicate becomes `Literal.TrueLiteral`, and the 
definition loses push-down entirely. That is this test.
   - Some conjuncts are deterministic. Those are still pushed, while the 
non-deterministic ones stay at the reference. Nothing covers this.
   
   I ran the second shape on this branch:
   
   ```sql
   with v as (select c1, c2, rand(1) r from t)
   select c1 from v where c1 > 0 and rand(2) < 0.5
   union all
   select c1 from v where c1 < 2
   ```
   
   The definition gets `Filter ((_1 > 0) OR (_1 < 2))` and one `rand` filter 
stays at the reference. On `master` the definition gets `Filter (((_1 > 0) AND 
(rand(2) < 0.5)) OR (_1 < 2))`, so an assertion on the pushed condition fails 
there.
   
   Worth a second test. Without it, narrowing the fix to "skip the whole 
reference when any predicate is non-deterministic" would pass the suite while 
quietly losing this push-down.
   



##########
sql/core/src/test/scala/org/apache/spark/sql/CTEInlineSuite.scala:
##########
@@ -451,6 +451,29 @@ abstract class CTEInlineSuiteBase
     }
   }
 
+  test("SPARK-59434: non-deterministic predicates are not pushed into a CTE 
def") {
+    withTempView("t") {
+      Seq(0, 1, 2).toDF("c1").createOrReplaceTempView("t")
+      // The CTE def is non-deterministic and referenced twice, so it is not 
inlined and the
+      // references' predicates get OR-merged and pushed into the shared def. 
Each reference
+      // keeps its own predicate, so pushing a non-deterministic one down 
evaluates it twice.
+      val df = sql(
+        """with v as (select c1, rand(1) r from t)
+          |select c1 from v where rand(2) < 0.5
+          |union all
+          |select c1 from v where rand(3) < 0.5
+          |""".stripMargin)
+      assert(
+        
df.queryExecution.optimizedPlan.exists(_.isInstanceOf[RepartitionOperation]),
+        "Non-deterministic With-CTE with multiple references should be not 
inlined.")
+      val randFilters = df.queryExecution.optimizedPlan.collect {
+        case f: Filter if f.condition.exists(_.isInstanceOf[Rand]) => f
+      }
+      assert(randFilters.length == 2,

Review Comment:
   **Finding 3.** This is a wrong-results fix, but nothing in the test looks at 
a row. The filter count is a proxy, and it would still pass if a later change 
kept two filters and dropped rows another way.
   
   A deterministic row assertion is available for this shape. 
`monotonically_increasing_id()` is non-deterministic and counts rows inside a 
partition, so the second evaluation is directly visible:
   
   ```scala
   withSQLConf(SQLConf.SHUFFLE_PARTITIONS.key -> "1") {
     withTempView("t") {
       spark.range(0, 6, 1, 1).selectExpr("cast(id as int) 
c1").createOrReplaceTempView("t")
       val df = sql(
         """with v as (select c1, rand(1) r from t)
           |select c1 from v where monotonically_increasing_id() > 0
           |union all
           |select c1 from v where monotonically_increasing_id() > 0
           |""".stripMargin)
       checkAnswer(df, ((1 to 5) ++ (1 to 5)).map(Row(_)))
     }
   }
   ```
   
   I ran it in both suites, AQE on and off. It passes on this branch and fails 
on `master`, which returns 8 rows instead of 10. `rand(1)` is only there to 
stop `InlineCTE` from inlining the definition. Column pruning then removes it, 
so it never reaches a filter.
   
   Worth adding alongside the plan-shape assertion.
   



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/PushdownPredicatesAndPruneColumnsForCTEDef.scala:
##########
@@ -57,8 +57,8 @@ object PushdownPredicatesAndPruneColumnsForCTEDef extends 
Rule[LogicalPlan] with
 
   /**
    * Gather all the predicates and referenced attributes on different points 
of CTE references
-   * using pattern `ScanOperation` (which takes care of determinism) and 
combine those predicates
-   * and attributes that belong to the same CTE definition.
+   * using pattern `PhysicalOperation` and combine those predicates and 
attributes that belong
+   * to the same CTE definition.

Review Comment:
   **Finding 1.** The scaladoc correction is right, but the story behind it in 
the description is not. `ScanOperation` never rejected a lone non-deterministic 
filter either, so SPARK-39764 did not regress anything here.
   
   At `175e429cca2`, the SPARK-37670 commit that added this rule, 
`ScanOperation` ran with `legacyMode = false`, and that path admits the first 
filter whatever it is:
   
   ```scala
   fields.forall(_.forall(_.deterministic)) && {
     filters.isEmpty || (filters.forall(_.deterministic) && 
condition.deterministic)
   } && canCollapseExpressions(Seq(condition), aliases, alwaysInline)
   ```
   
   Today's `ScanOperation` still hands a lone non-deterministic filter back as 
pushable, at 
`sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/planning/patterns.scala:152`:
   
   ```scala
   } else {
     val filtersCanPushDown = splitConjunctivePredicates(filters.head)
     val filtersStayUp = filters.drop(1)
   ```
   
   I measured this. Swapping the rule back to `ScanOperation(projects, _, 
predicates, ref)` and removing the new `filter(_.deterministic)` leaves your 
test failing exactly as on `master`, with four `rand` filters.
   
   SPARK-37670 also first shipped in 3.4.0, so "Affects 3.4.0 and later" stays 
correct. What changes is the cause. The rule was born with this bug, and the 
old scaladoc's "which takes care of determinism" was never true. Worth 
rewording, since the description becomes the commit message.
   
   The scaladoc could carry the reason too:
   
   ```suggestion
      * to the same CTE definition. `PhysicalOperation` still returns a single 
filter even when it
      * is non-deterministic, so such predicates are excluded below.
   ```
   



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