LuciferYang commented on code in PR #59209:
URL: https://github.com/apache/spark/pull/59209#discussion_r4204439260


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -67,6 +67,16 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         // changed anything: `mapExpressions` and `withNewChildren` preserve 
reference equality when
         // they rewrite nothing, which is what makes this detectable.
         if ((rewrittenAgg eq agg) && (rewrittenProj eq proj)) p else 
rewrittenProj
+      // The children of a `CreateVariable` are the `ResolvedIdentifier`s 
naming the variables, not
+      // rows to evaluate the default over, and `V2CommandStrategy` plans the 
command only while
+      // they stay so. `CreateVariableExec` evaluates the default once, so a 
`With` in it is left in
+      // place to memoize its definition, as one in a conditional branch is.
+      case c: CreateVariable if c.defaultExpr.containsPattern(WITH_EXPRESSION) 
=>
+        val default = c.defaultExpr.child
+        val rewritten = 
default.transformUpWithPruning(_.containsPattern(WITH_EXPRESSION)) {
+          case w: With => inlineDefsThatGainNothing(w)
+        }
+        if (rewritten eq default) c else c.copy(defaultExpr = 
c.defaultExpr.copy(child = rewritten))
       case p if p.expressions.exists(_.containsPattern(WITH_EXPRESSION)) =>
         applyInternal(p)

Review Comment:
   Fixed by #59217; `DataSourceV2SQLSuiteV1Filter` there now gets 
`_LEGACY_ERROR_TEMP_1110`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -67,6 +67,16 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         // changed anything: `mapExpressions` and `withNewChildren` preserve 
reference equality when
         // they rewrite nothing, which is what makes this detectable.
         if ((rewrittenAgg eq agg) && (rewrittenProj eq proj)) p else 
rewrittenProj
+      // The children of a `CreateVariable` are the `ResolvedIdentifier`s 
naming the variables, not
+      // rows to evaluate the default over, and `V2CommandStrategy` plans the 
command only while
+      // they stay so. `CreateVariableExec` evaluates the default once, so a 
`With` in it is left in
+      // place to memoize its definition, as one in a conditional branch is.
+      case c: CreateVariable if c.defaultExpr.containsPattern(WITH_EXPRESSION) 
=>
+        val default = c.defaultExpr.child
+        val rewritten = 
default.transformUpWithPruning(_.containsPattern(WITH_EXPRESSION)) {
+          case w: With => inlineDefsThatGainNothing(w)
+        }
+        if (rewritten eq default) c else c.copy(defaultExpr = 
c.defaultExpr.copy(child = rewritten))
       case p if p.expressions.exists(_.containsPattern(WITH_EXPRESSION)) =>

Review Comment:
   #59217 now covers `Call` too: the case matches `Command | Call`, and this 
CALL returns 6 (new test in `ProcedureSuite`). The nondeterministic form gets 
`args must be foldable`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -67,6 +67,16 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         // changed anything: `mapExpressions` and `withNewChildren` preserve 
reference equality when
         // they rewrite nothing, which is what makes this detectable.
         if ((rewrittenAgg eq agg) && (rewrittenProj eq proj)) p else 
rewrittenProj
+      // The children of a `CreateVariable` are the `ResolvedIdentifier`s 
naming the variables, not
+      // rows to evaluate the default over, and `V2CommandStrategy` plans the 
command only while

Review Comment:
   Reproduced, with and without this PR: `DEFAULT py_udf(1)` and `DEFAULT 
nullif(py_udf(1), '3')` fail with `No plan for CreateVariable` over a 
`BatchEvalPython` wrapping the `ResolvedIdentifier`, while `SELECT py_udf(1)` 
and `SET VAR v = py_udf(1)` work. Filed as SPARK-60030.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -67,6 +67,16 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         // changed anything: `mapExpressions` and `withNewChildren` preserve 
reference equality when
         // they rewrite nothing, which is what makes this detectable.
         if ((rewrittenAgg eq agg) && (rewrittenProj eq proj)) p else 
rewrittenProj
+      // The children of a `CreateVariable` are the `ResolvedIdentifier`s 
naming the variables, not
+      // rows to evaluate the default over, and `V2CommandStrategy` plans the 
command only while
+      // they stay so. `CreateVariableExec` evaluates the default once, so a 
`With` in it is left in
+      // place to memoize its definition, as one in a conditional branch is.
+      case c: CreateVariable if c.defaultExpr.containsPattern(WITH_EXPRESSION) 
=>
+        val default = c.defaultExpr.child
+        val rewritten = 
default.transformUpWithPruning(_.containsPattern(WITH_EXPRESSION)) {
+          case w: With => inlineDefsThatGainNothing(w)
+        }
+        if (rewritten eq default) c else c.copy(defaultExpr = 
c.defaultExpr.copy(child = rewritten))

Review Comment:
   Done.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -67,6 +67,16 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
         // changed anything: `mapExpressions` and `withNewChildren` preserve 
reference equality when
         // they rewrite nothing, which is what makes this detectable.
         if ((rewrittenAgg eq agg) && (rewrittenProj eq proj)) p else 
rewrittenProj
+      // The children of a `CreateVariable` are the `ResolvedIdentifier`s 
naming the variables, not
+      // rows to evaluate the default over, and `V2CommandStrategy` plans the 
command only while
+      // they stay so. `CreateVariableExec` evaluates the default once, so a 
`With` in it is left in
+      // place to memoize its definition, as one in a conditional branch is.
+      case c: CreateVariable if c.defaultExpr.containsPattern(WITH_EXPRESSION) 
=>
+        val default = c.defaultExpr.child
+        val rewritten = 
default.transformUpWithPruning(_.containsPattern(WITH_EXPRESSION)) {
+          case w: With => inlineDefsThatGainNothing(w)
+        }

Review Comment:
   Done, as `inlineWithsThatGainNothing`, shared with the conditional-branch 
path.



##########
sql/core/src/test/scala/org/apache/spark/sql/ColumnExpressionSuite.scala:
##########
@@ -645,6 +645,38 @@ class ColumnExpressionSuite extends SharedSparkSession {
     }
   }
 
+  test("SPARK-59954: a With in a DECLARE VARIABLE default is evaluated once") {
+    // `CreateVariableExec` evaluates the default on the driver, so the 
accumulator needs no job to
+    // read. `plus_one_counted` is deterministic but not cheap, so its 
definition is worth
+    // memoizing, and the first two defaults below each read it twice.
+    val calls = sparkContext.longAccumulator("calls")
+    spark.udf.register("plus_one_counted", (i: Int) => { calls.add(1); i + 1 })
+    spark.udf.register("plus_one_nondet",
+      udf((i: Int) => { calls.add(1); i + 1 }).asNondeterministic())
+    withUserDefinedFunction("plus_one_counted" -> true, "plus_one_nondet" -> 
true) {
+      withSessionVariable("v1", "v2", "v3", "v4") {
+        sql("DECLARE OR REPLACE VARIABLE v1 INT DEFAULT 
nullif(plus_one_counted(1), 3)")
+        assert(calls.value == 1, s"the NULLIF default called the UDF 
${calls.value} times")
+        checkAnswer(sql("SELECT v1"), Row(2))
+
+        calls.reset()
+        sql("DECLARE OR REPLACE VARIABLE v2 BOOLEAN DEFAULT 
plus_one_counted(1) BETWEEN 1 AND 3")
+        assert(calls.value == 1, s"the BETWEEN default called the UDF 
${calls.value} times")
+        checkAnswer(sql("SELECT v2"), Row(true))
+
+        // A nondeterministic definition is evaluated once as well.
+        calls.reset()
+        sql("DECLARE OR REPLACE VARIABLE v3 INT DEFAULT 
nullif(plus_one_nondet(1), 3)")
+        assert(calls.value == 1, s"the nondeterministic default called it 
${calls.value} times")
+        checkAnswer(sql("SELECT v3"), Row(2))
+
+        // A seeded form of the SPARK-59954 repro; its `Rand` is initialized 
through the `With`.
+        sql("DECLARE OR REPLACE VARIABLE v4 DOUBLE DEFAULT nullif(rand(5), 
0.5)")
+        checkAnswer(sql("SELECT v4 IS NULL OR (v4 >= 0 AND v4 < 1)"), 
Row(true))

Review Comment:
   Done; the comment now says it checks planning and `Rand` initialization, not 
the value.



##########
sql/core/src/test/scala/org/apache/spark/sql/ColumnExpressionSuite.scala:
##########
@@ -645,6 +645,38 @@ class ColumnExpressionSuite extends SharedSparkSession {
     }
   }
 
+  test("SPARK-59954: a With in a DECLARE VARIABLE default is evaluated once") {
+    // `CreateVariableExec` evaluates the default on the driver, so the 
accumulator needs no job to
+    // read. `plus_one_counted` is deterministic but not cheap, so its 
definition is worth
+    // memoizing, and the first two defaults below each read it twice.
+    val calls = sparkContext.longAccumulator("calls")
+    spark.udf.register("plus_one_counted", (i: Int) => { calls.add(1); i + 1 })
+    spark.udf.register("plus_one_nondet",
+      udf((i: Int) => { calls.add(1); i + 1 }).asNondeterministic())
+    withUserDefinedFunction("plus_one_counted" -> true, "plus_one_nondet" -> 
true) {

Review Comment:
   Done.



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