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


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -98,6 +104,55 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
     }
   }
 
+  /**
+   * Whether substituting this definition into its references is as good as 
evaluating it once.
+   * True when it is referenced once, since substituting then evaluates it 
once either way, or when
+   * it is cheap enough to evaluate repeatedly and deterministic, so the 
repeated evaluations agree.
+   *
+   * `CollapseProject.isCheap` answers what one evaluation costs, not whether 
a second one is
+   * allowed: it admits a `PythonUDF`, which may be nondeterministic. 
Determinism is checked here
+   * rather than read into the cost test, so that a reader of either call site 
can see which of the
+   * two questions is being asked.
+   */
+  private def canSubstitute(
+      child: Expression,
+      id: CommonExpressionId,
+      commonExprIdSet: Set[CommonExpressionId]): Boolean = {
+    !commonExprIdSet.contains(id) || (CollapseProject.isCheap(child) && 
child.deterministic)
+  }
+
+  /**
+   * `w` with every definition that gains nothing from being memoized inlined 
into its references:
+   * one cheap enough to evaluate twice, and one that is referenced once 
anyway. This is the test
+   * the main rewrite already applies before it hoists a definition into a 
project.
+   *
+   * Inlining matters beyond the per-row bookkeeping it saves. A `With` is not 
foldable, so it hides
+   * whatever it wraps from `ConstantFolding`, `PushFoldableIntoBranches`, 
`SimplifyConditionals`
+   * and `ReplaceNullWithFalseInPredicate`, all of which run in later batches. 
Dropping the `With`
+   * once nothing is left to memoize keeps `CASE WHEN c THEN nullif(1, 1) END` 
folding as it did
+   * before this rule learned to leave one behind.
+   */
+  private def inlineDefsThatGainNothing(
+      w: With,
+      commonExprIdSet: Set[CommonExpressionId]): Expression = {
+    val (toInline, toKeep) = w.defs.partition { d =>
+      canSubstitute(d.child, d.id, commonExprIdSet)

Review Comment:
   **Blocking (P1):** `commonExprIdSet` describes the expression before this 
bottom-up rewrite, but rewriting an inner `With` can duplicate a reference 
owned by this outer `With`. With 
`spark.sql.optimizer.avoidCollapseUDFWithExpensiveExpr=false`, an outer 
nondeterministic Python UDF used once inside a cheap deterministic inner Python 
UDF becomes two outer references when that inner definition is substituted at 
two sites; this stale single-use decision then substitutes the nondeterministic 
definition twice, so the consumers can observe different values. Please base 
this classification on reference multiplicity in the rewritten `With` and add 
the nested regression.
   
   **Recommended change:** Recompute reference multiplicity for each rewritten 
With immediately before classifying its definitions, and add a focused 
nested-UDF regression.
   
   **Why this works:** Count CommonExpressionRef occurrences from the 
post-transform child and definitions so references introduced by inner 
substitution prevent an outer nondeterministic definition from being inlined.
   
   **Scope:** RewriteWithExpression and its focused Catalyst optimizer suite.
   
   **Compatibility:** No public API or data-format change; this only corrects 
optimizer behavior for nested With expressions.
   
   **Risks:** The recount must respect With binding boundaries while including 
references introduced into the rewritten node. More definitions may remain 
memoized in nested shapes where the original count was stale.
   
   **Constraints:** Preserve the existing cheap deterministic and true 
single-use substitutions when their post-rewrite counts permit them. Preserve 
branch-local laziness and optimizer idempotence.
   
   **Success:** The nested configuration invokes the outer nondeterministic 
definition once per reached With entry, and the optimizer's existing 
substitution and convergence tests continue to pass.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -207,6 +207,150 @@ class CodegenContext extends Logging {
       throw QueryExecutionErrors.lambdaVariableNotDefinedError(id))
   }
 
+  /**
+   * The slots a `CommonExpressionRef` reads: the value and its nullness, plus 
the flag saying
+   * whether this row has computed them yet, and the definition to compute 
them from.
+   */
+  case class CommonExprSlots(
+      value: ExprCode,
+      computed: String,
+      definition: Expression) {
+
+    // Not constructor parameters: every one of those takes part in a case 
class's `equals`,
+    // `hashCode`, `copy` and `toString`, so a mutable one would make a slot's 
hash change as it is
+    // filled, and would hand a `copy` the code generated for another scope.
+    private var fillCode: Option[Block] = None
+    private var filling: Boolean = false
+
+    /**
+     * The code that computes the definition into the slots and sets 
`computed`, which a reference
+     * emits behind that flag. Generated once and cached, so that every 
reference shares whatever
+     * mutable state the definition allocated, such as an RNG, rather than 
getting its own.
+     *
+     * A definition that references its own id would re-enter this while the 
first call is still
+     * generating it, since `fillCode` is set only after `definition.genCode` 
returns. `filling`
+     * turns that into the error `With.refsToBind` documents for the 
interpreted path, rather than a
+     * StackOverflowError from generating the same definition inside itself 
forever.
+     *
+     * The body goes into a method where it can and is worth it -- a 
definition that is or holds
+     * another `With`, or a body past the split threshold -- leaving a 
reference's own code a single
+     * call: such a definition would otherwise have its body pasted once per 
reference at every
+     * level. `Expression.reduceCodeSize` already keeps that from running 
away, by hoisting
+     * whichever node's code first passes the same threshold, so what a method 
here buys is the body
+     * once per scope rather than once per reference up to that threshold, and 
a bound that no
+     * longer grows with depth wherever the threshold sits. It is only 
possible where the definition
+     * reads the input row rather than local variables -- the condition 
`reduceCodeSize` splits
+     * under, and for the same reason. Whole-stage codegen generates the 
operators a `With` reaches
+     * with `currentVars` set, so neither applies there and the body is pasted 
per reference.
+     */
+    def fill: Block = {
+      if (fillCode.isEmpty) {
+        if (filling) {
+          throw SparkException.internalError(
+            "Cannot generate a common expression whose definition references 
it: " +
+              definition.toString)
+        }
+        filling = true
+        try {
+          fillCode = Some(build)
+        } finally {
+          filling = false
+        }
+      }
+      fillCode.get
+    }
+
+    private def build: Block = {
+      val defGen = definition.genCode(CodegenContext.this)
+      // Whether the isNull slot exists is decided by the definition, so it is 
read off the slot
+      // rather than off a reference's own `nullable`: taking it from both 
would let the two
+      // disagree, and either emit `false = <isNull>;`, which does not 
compile, or leave the slot
+      // holding the previous row's nullness.
+      val assignIsNull = if (value.isNull == FalseLiteral) {
+        ""
+      } else {
+        s"${value.isNull} = ${defGen.isNull};"
+      }
+      val body = code"""
+         |${defGen.code}
+         |$assignIsNull
+         |${value.value} = ${defGen.value};
+         |$computed = true;
+       """.stripMargin
+      val canPutInMethod = INPUT_ROW != null && currentVars == null
+      // A definition that is or holds another `With` is the shape whose code 
doubles per level,
+      // and what this is aimed at. It is not the only one -- a definition 
referencing a sibling
+      // definition of the same `With` doubles the same way, and codegen 
accepts that, since the
+      // sibling's slots are in scope while this definition is generated 
(`With.refsToBind` says
+      // why nothing builds that tree, and that evaluating one raises). What 
bounds those is not
+      // the length arm below: `body` is assembled after `definition.genCode` 
already ran
+      // `reduceCodeSize`, so the arm fires only in the band just under the 
threshold. It is
+      // `reduceCodeSize` itself, which hoists whichever node's code first 
passes the threshold as
+      // generation walks up, capping what one level contributes, so the code 
stays linear in the
+      // depth either way. The length arm just keeps the same body from being 
split once per
+      // reference, which leaves the methods small and the code as large.
+      val worthAMethod = definition.exists(_.isInstanceOf[With]) ||
+        body.length > SQLConf.get.methodSplitThreshold
+      if (canPutInMethod && worthAMethod) {
+        val funcName = freshName("computeCommonExpr")
+        val funcFullName = addNewFunction(funcName,
+          s"""
+             |private void $funcName(InternalRow $INPUT_ROW) {
+             |  $body
+             |}
+           """.stripMargin)
+        code"$funcFullName($INPUT_ROW);"
+      } else {
+        body
+      }
+    }
+  }
+
+  /**
+   * Holding a map of the common expressions of the `With` expressions 
currently being generated,
+   * the same way [[currentLambdaVars]] holds the variables of the enclosing 
lambdas.
+   */
+  var currentCommonExprs: mutable.Map[Long, CommonExprSlots] = 
mutable.HashMap.empty
+
+  /**
+   * Allocates a value slot and a `computed` flag per definition, generates 
`f` with them in scope,
+   * then takes them out of scope again. A reference generated inside `f` 
reads the slots back by
+   * id and fills them the first time it is reached on a row -- the enclosing 
`With` only clears the
+   * flags.
+   */
+  def withCommonExprs(defs: Seq[CommonExpressionDef])(f: Seq[CommonExprSlots] 
=> ExprCode)
+    : ExprCode = {
+    val slots = defs.map { d =>

Review Comment:
   **Non-blocking (P2):** Please add interpreted and generated execution 
coverage for one `With` containing two surviving, distinguishable definitions, 
each referenced twice. Every current runtime case has one definition, and the 
optimizer's two-definition case inlines one before evaluation, so a slot-index 
or cleanup bug that aliases these per-definition states would not be observed. 
Exact values and once-per-definition advancement across rows would cover both 
allocation and clearing.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/With.scala:
##########
@@ -162,7 +373,79 @@ case class CommonExpressionDef(child: Expression, id: 
CommonExpressionId = new C
  * referenced, so that we can determine the data type and nullable of the 
reference node.
  */
 case class CommonExpressionRef(id: CommonExpressionId, dataType: DataType, 
nullable: Boolean)
-  extends LeafExpression with Unevaluable {
+  extends LeafExpression {
   def this(exprDef: CommonExpressionDef) = this(exprDef.id, exprDef.dataType, 
exprDef.nullable)
+
+  /**
+   * The definition this reference names, and the cell holding its value for 
the current row. Both
+   * are wired by the enclosing [[With]] before it evaluates its child, and 
are left out of the case
+   * class parameters so that equality and canonicalization are unchanged -- 
and so that a rule
+   * comparing two references does not compare their cells.
+   */
+  private var definition: Expression = _
+  private var cell: CommonExpressionCell = _
+
+  private[expressions] def bindTo(exprDef: CommonExpressionDef): Unit = {
+    definition = exprDef.child
+    cell = exprDef.cell
+  }
+
+  private[expressions] def boundDefinition: Expression = definition
+  private[expressions] def boundCell: CommonExpressionCell = cell
+
+  private[expressions] def bindTo(
+      newDefinition: Expression,
+      newCell: CommonExpressionCell): Unit = {
+    definition = newDefinition
+    cell = newCell
+  }
+
   override val nodePatterns: Seq[TreePattern] = Seq(COMMON_EXPR_REF)
+
+  // The cell is cleared by the enclosing `With` on every entry, so this reads 
mutable state.
+  override def stateful: Boolean = true
+
+  /**
+   * A copy must not carry this reference's binding: the copy belongs to a 
different `With`, which
+   * wires it to its own cell. `LeafLike` returns `this` here, which would 
hand two `With`s one
+   * reference object and let whichever wires last decide what both of them 
read --
+   * `NamedLambdaVariable` overrides this for the same reason.
+   */
+  override def withNewChildrenInternal(
+      newChildren: IndexedSeq[Expression]): CommonExpressionRef = copy()
+
+  override def eval(input: InternalRow): Any = {
+    if (cell == null) {
+      throw SparkException.internalError(
+        s"Cannot evaluate a common expression reference outside its With: 
$this")
+    }
+    cell.get(definition, input)
+  }
+
+  /**
+   * Computes the definition into the shared slots if this row has not done so 
yet, then reads them.
+   * The code that computes it is emitted here rather than by the enclosing 
`With`, so it runs where
+   * the first reference is reached -- behind a short-circuiting operator or a 
nested conditional,
+   * if that is where the reference sits.
+   *
+   * A second reference emits the same code again, which never runs because 
the flag is set. What
+   * that code is depends on the definition: a call, where the definition can 
be put in a method, so
+   * that a definition that is or holds another `With` is not pasted once per 
reference at every
+   * level; the body itself otherwise, whose locals are declared inside each 
guard. No copy of one
+   * body encloses another -- for that, a definition would have to reference 
its own id, directly or
+   * through a sibling, which recurses in `fill` before any Java exists -- so 
repeating it declares
+   * nothing twice in one scope. See `CommonExprSlots.fill`, which also says 
what bounds the code
+   * when a method is not possible.
+   */
+  override protected def doGenCode(ctx: CodegenContext, ev: ExprCode): 
ExprCode = {
+    val slots = ctx.getCommonExpr(id.id)
+    ev.copy(
+      code = code"""
+         |if (!${slots.computed}) {

Review Comment:
   **Non-blocking (P2):** Could the generated path get a case that enters this 
`With` but takes a child branch containing no reference? The current 
no-reference checks use interpreted `Counter`, while the SQL skip case avoids 
entering the `With` altogether. A `GenerateMutableProjection` over a stateful 
definition with inputs `false, true, false, true` should return `-1, 0, -1, 1`; 
that would catch an eager fill here that consumes values on the false rows.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -181,15 +236,12 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
           rewriteWithExprAndInputPlans(
             _, inputPlans, commonExprsPerChild, commonExprIdSet, isNestedWith))
         val newExpr = c.withNewAlwaysEvaluatedInputs(newAlwaysEvaluatedInputs)
-        // Use transformUp to handle nested With.
+        // A `With` in a conditional branch cannot go into a project, which is 
always evaluated
+        // while the branch may not be. It stays where it is and memoizes its 
definition per row

Review Comment:
   **Nit (P3):** This memoization is per `With` entry/evaluation, not per row: 
`With` clears the cell or generated flag whenever it is entered, and the same 
expression object can occur at two positions for one row. Please use 
`per-entry` here (and in the corresponding test/PR description wording) so the 
documented lifetime matches `CommonExpressionCell` and `With.eval`.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala:
##########
@@ -98,6 +104,55 @@ object RewriteWithExpression extends Rule[LogicalPlan] {
     }
   }
 
+  /**
+   * Whether substituting this definition into its references is as good as 
evaluating it once.
+   * True when it is referenced once, since substituting then evaluates it 
once either way, or when
+   * it is cheap enough to evaluate repeatedly and deterministic, so the 
repeated evaluations agree.
+   *
+   * `CollapseProject.isCheap` answers what one evaluation costs, not whether 
a second one is
+   * allowed: it admits a `PythonUDF`, which may be nondeterministic. 
Determinism is checked here
+   * rather than read into the cost test, so that a reader of either call site 
can see which of the
+   * two questions is being asked.
+   */
+  private def canSubstitute(
+      child: Expression,
+      id: CommonExpressionId,
+      commonExprIdSet: Set[CommonExpressionId]): Boolean = {
+    !commonExprIdSet.contains(id) || (CollapseProject.isCheap(child) && 
child.deterministic)
+  }
+
+  /**
+   * `w` with every definition that gains nothing from being memoized inlined 
into its references:
+   * one cheap enough to evaluate twice, and one that is referenced once 
anyway. This is the test
+   * the main rewrite already applies before it hoists a definition into a 
project.
+   *
+   * Inlining matters beyond the per-row bookkeeping it saves. A `With` is not 
foldable, so it hides
+   * whatever it wraps from `ConstantFolding`, `PushFoldableIntoBranches`, 
`SimplifyConditionals`
+   * and `ReplaceNullWithFalseInPredicate`, all of which run in later batches. 
Dropping the `With`
+   * once nothing is left to memoize keeps `CASE WHEN c THEN nullif(1, 1) END` 
folding as it did
+   * before this rule learned to leave one behind.
+   */
+  private def inlineDefsThatGainNothing(
+      w: With,
+      commonExprIdSet: Set[CommonExpressionId]): Expression = {
+    val (toInline, toKeep) = w.defs.partition { d =>
+      canSubstitute(d.child, d.id, commonExprIdSet)
+    }
+    if (toInline.isEmpty) {
+      w
+    } else {
+      val refToExpr = toInline.map(d => d.id -> d.child).toMap
+      val newChild = 
w.child.transformWithPruning(_.containsPattern(COMMON_EXPR_REF)) {
+        // A ref of a definition kept here, or of an enclosing `With`, is left 
for its owner.
+        case ref: CommonExpressionRef if refToExpr.contains(ref.id) => 
refToExpr(ref.id)
+      }
+      // `copy` rather than `withNewChildren`, which requires the child count 
to be unchanged. The
+      // references of the kept definitions are carried over unbound; the new 
`With` binds them, and
+      // `w` is discarded here, so no two `With`s hold the same reference.

Review Comment:
   **Nit (P3):** Discarding this parent does not guarantee unique reference 
objects. If the same `With` instance is reused at two parent positions, each 
transform can build a distinct copy while retaining the same unchanged 
references for `toKeep`. The new bind/restore logic makes that DAG-shaped reuse 
safe, so could this comment describe that mechanism instead of claiming two 
`With` expressions cannot hold the same reference?



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/codegen/CodeGenerator.scala:
##########
@@ -207,6 +207,150 @@ class CodegenContext extends Logging {
       throw QueryExecutionErrors.lambdaVariableNotDefinedError(id))
   }
 
+  /**
+   * The slots a `CommonExpressionRef` reads: the value and its nullness, plus 
the flag saying
+   * whether this row has computed them yet, and the definition to compute 
them from.
+   */
+  case class CommonExprSlots(
+      value: ExprCode,
+      computed: String,
+      definition: Expression) {
+
+    // Not constructor parameters: every one of those takes part in a case 
class's `equals`,
+    // `hashCode`, `copy` and `toString`, so a mutable one would make a slot's 
hash change as it is
+    // filled, and would hand a `copy` the code generated for another scope.
+    private var fillCode: Option[Block] = None
+    private var filling: Boolean = false
+
+    /**
+     * The code that computes the definition into the slots and sets 
`computed`, which a reference
+     * emits behind that flag. Generated once and cached, so that every 
reference shares whatever
+     * mutable state the definition allocated, such as an RNG, rather than 
getting its own.
+     *
+     * A definition that references its own id would re-enter this while the 
first call is still
+     * generating it, since `fillCode` is set only after `definition.genCode` 
returns. `filling`
+     * turns that into the error `With.refsToBind` documents for the 
interpreted path, rather than a
+     * StackOverflowError from generating the same definition inside itself 
forever.
+     *
+     * The body goes into a method where it can and is worth it -- a 
definition that is or holds
+     * another `With`, or a body past the split threshold -- leaving a 
reference's own code a single
+     * call: such a definition would otherwise have its body pasted once per 
reference at every
+     * level. `Expression.reduceCodeSize` already keeps that from running 
away, by hoisting
+     * whichever node's code first passes the same threshold, so what a method 
here buys is the body
+     * once per scope rather than once per reference up to that threshold, and 
a bound that no
+     * longer grows with depth wherever the threshold sits. It is only 
possible where the definition
+     * reads the input row rather than local variables -- the condition 
`reduceCodeSize` splits
+     * under, and for the same reason. Whole-stage codegen generates the 
operators a `With` reaches

Review Comment:
   **Nit (P3):** Whole-stage mode does not always imply `currentVars` is 
populated. For example, `SortMergeJoinExec.createJoinKey` generates keys with 
`INPUT_ROW` set and `currentVars` cleared, which satisfies the helper-method 
predicate below. Please describe the exact `INPUT_ROW != null && currentVars == 
null` boundary and identify local-variable whole-stage `Project` paths as the 
case where extraction is unavailable.



##########
sql/core/src/test/scala/org/apache/spark/sql/ColumnExpressionSuite.scala:
##########
@@ -430,6 +430,136 @@ class ColumnExpressionSuite extends SharedSparkSession {
     checkAnswer(testData.filter($"a".between($"b", $"c")), expectAnswer)
   }
 
+  // Runs `f` on each of the three evaluation paths, since a `With` left in a 
conditional branch is
+  // evaluated by all three and the memoization is implemented separately for 
interpretation and for
+  // codegen.
+  private def onEachEvalPath(f: => Unit): Unit = {
+    withSQLConf(
+      SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "false",
+      SQLConf.CODEGEN_FACTORY_MODE.key -> "NO_CODEGEN")(f)
+    withSQLConf(
+      SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "false",
+      SQLConf.CODEGEN_FACTORY_MODE.key -> "CODEGEN_ONLY")(f)
+    withSQLConf(
+      SQLConf.WHOLESTAGE_CODEGEN_ENABLED.key -> "true",
+      SQLConf.CODEGEN_FACTORY_MODE.key -> "CODEGEN_ONLY")(f)
+  }
+
+  test("SPARK-58902: BETWEEN on a nondeterministic input inside a conditional 
branch") {
+    onEachEvalPath {
+      // `BETWEEN` reads its input twice. Inlining the common expression into 
a branch gave each
+      // read its own value, so the two comparisons saw two different ids and 
6 of the 10 rows came
+      // back true where 3 is correct. A single partition makes the id 
sequence 0, 1, 2, ...
+      val df = spark.range(0, 10, 1, 1)
+      checkAnswer(
+        df.selectExpr(
+          "CASE WHEN id < 0 THEN false ELSE monotonically_increasing_id() 
BETWEEN 3 AND 5 END"),
+        (0 until 10).map(i => Row(i >= 3 && i <= 5)))
+    }
+  }
+
+  test("SPARK-58902: a definition in a branch is evaluated only on the rows 
that reach it") {
+    onEachEvalPath {
+      // Rows 0 to 4 take the first branch, so the five rows that reach the 
ELSE see ids 0 to 4 --
+      // the same values they would see if the branch were the whole 
expression.
+      val df = spark.range(0, 10, 1, 1)
+      checkAnswer(
+        df.selectExpr(
+          "CASE WHEN id < 5 THEN NULL ELSE monotonically_increasing_id() 
BETWEEN 1 AND 2 END"),
+        (0 until 10).map { i =>
+          if (i < 5) Row(null) else Row(i - 5 >= 1 && i - 5 <= 2)
+        })
+    }
+  }
+
+  test("SPARK-58902: a branch condition that can raise is not evaluated on 
other rows") {
+    onEachEvalPath {
+      withSQLConf(SQLConf.ANSI_ENABLED.key -> "true") {
+        // Nothing is relocated, so a condition is evaluated only where it 
always was. `a` runs
+        // -2, -1, 0, 1, 2, 3: the first two take the third branch, 0 the 
first, 1 and 2 the second,
+        // and 3 falls through. `rand` is in [0, 1), so a row reaching a 
BETWEEN is true whatever it
+        // draws -- the answers here do not depend on memoization. What this 
rules out is the
+        // alternative that was measured and rejected: hoisting the definition 
into a `Project` and
+        // guarding that column with the branch condition, which puts `6 / a` 
outside conditional
+        // evaluation and raises on the `a = 0` row under ANSI.
+        val df = spark.range(0, 6, 1, 1).selectExpr("cast(id as int) - 2 as a")
+        checkAnswer(
+          df.selectExpr(
+            "CASE WHEN a = 0 THEN false " +
+              "WHEN 6 / a > 2 THEN rand(1) BETWEEN 0 AND 1 " +
+              "WHEN 6 / a < -2 THEN rand(2) BETWEEN 0 AND 1 " +
+              "ELSE false END"),
+          Seq(Row(true), Row(true), Row(false), Row(true), Row(true), 
Row(false)))
+      }
+    }
+  }
+
+  test("SPARK-58902: a nondeterministic input a branch cannot pre-evaluate is 
still read once") {
+    onEachEvalPath {
+      // Enough rows that the two copies an inlining implementation makes have 
to fall out of step:
+      // each copy owns its own generator seeded the same way, so they only 
differ once the first
+      // comparison has skipped the second copy on some row.
+      val df = spark.range(0, 20, 1, 1)
+      // `randstr` draws a new string per row, so inlining gives the two 
comparisons of the BETWEEN
+      // two draws, while memoizing gives them one and makes the answer inside 
a branch the same as
+      // outside one. Its value is a string rather than a primitive, so this 
also exercises a slot
+      // that cannot be an inlined field. `id` is selected alongside so that 
`checkAnswer`, which
+      // compares rows as a bag, is comparing per row rather than just 
counting trues.
+      val inBranch = df.selectExpr(
+        "id", "CASE WHEN id < 0 THEN false ELSE randstr(3, 0) BETWEEN 'a' AND 
'zzzz' END")
+      checkAnswer(
+        inBranch, df.selectExpr("id", "randstr(3, 0) BETWEEN 'a' AND 
'zzzz'").collect().toSeq)
+
+      // The same for a nondeterministic input wrapped in arithmetic. 
Comparing against the
+      // branch-free form under a fixed seed is what makes this bite: inlined, 
the second comparison
+      // draws again, so the row a given draw lands on shifts. The window also 
has to be one the
+      // first comparison can fail: `BETWEEN 0 AND 1` over `rand` is satisfied 
by every draw, which
+      // keeps the two copies in lockstep and makes the comparison hold either 
way. The assertion
+      // below is what keeps that true for these rows rather than merely 
intended.
+      val branchFree = df.selectExpr("id", "(rand(7) / 1.0) BETWEEN 0.3 AND 
0.6").collect()
+      assert(branchFree.exists(!_.getBoolean(1)) && 
branchFree.exists(_.getBoolean(1)),
+        s"the window is not selective over these rows: 
${branchFree.mkString(", ")}")
+      val inBranchRand = df.selectExpr(
+        "id", "CASE WHEN id < 0 THEN false ELSE (rand(7) / 1.0) BETWEEN 0.3 
AND 0.6 END")
+      checkAnswer(inBranchRand, branchFree.toSeq)
+    }
+  }
+
+  test("SPARK-58902: a nested With agrees across the evaluation paths") {
+    // The outer `nullif`'s definition is the inner `nullif`, which is itself 
a `With`, and neither
+    // one is cheap or read once, so both survive inside the branch. That 
nesting is where each
+    // reference's copy of the definition code matters: the code is generated 
once per definition
+    // and the text reused, so a nested `With` does not grow its code by a 
factor per level.

Review Comment:
   **Nit (P3):** This is true only when the generated body can be extracted 
into a helper method. In a whole-stage `Project`, `currentVars` prevents 
extraction and each reference pastes the cached inner Block, so the focused 
source-size test still measures `2, 4, 8, ...` growth. Could this comment 
distinguish generating the definition once for shared state from emitting it 
once, and scope the bounded-source claim to the row-based standalone path?



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