Skip to content

[SPARK-58902][SQL] Evaluate multi-referenced common expressions lazily instead of eager pre-evaluation - #58377

Open
AnhTtis wants to merge 5 commits into
apache:masterfrom
AnhTtis:SPARK-58902-lazy-common-expression-evaluation
Open

[SPARK-58902][SQL] Evaluate multi-referenced common expressions lazily instead of eager pre-evaluation#58377
AnhTtis wants to merge 5 commits into
apache:masterfrom
AnhTtis:SPARK-58902-lazy-common-expression-evaluation

Conversation

@AnhTtis

@AnhTtis AnhTtis commented Aug 28, 2026

Copy link
Copy Markdown

What changes were proposed in this pull request?

In Spark Catalyst, With promises that common expressions are evaluated only once even when referenced multiple times. RewriteWithExpression keeps that promise by hoisting multi-referenced definitions into a child Project.

However, inside conditional branches (such as CASE WHEN, If, Coalesce) or join conditions spanning both join sides, eager pre-evaluation cannot be unconditionally placed into a child Project without risking premature evaluation of expressions that can throw exceptions. Consequently, RewriteWithExpression inlines the common expressions into each reference site.

When the inlined expression is nondeterministic (such as randstr(...), rand(), uuid(), uniform(...), shuffle(...), reflect(...)), inlining causes each reference to evaluate independently. For example, CASE WHEN a > 0 THEN randstr(3, 0) BETWEEN 'a' AND 'b' END expands BETWEEN to two references, causing two different random strings to be generated for the >= and <= checks.

This PR provides test coverage and technical documentation around this Catalyst behavior:

  1. Adds optimizer test coverage in RewriteWithExpressionSuite.scala comparing the analyzed logical plan for With expressions inside ConditionalExpression branches.
  2. Annotates the inlining code path in RewriteWithExpression.scala documenting the nondeterministic evaluation risk tracked in SPARK-58902.
  3. Outlines the technical design for lazy per-row expression memoization to eliminate duplicate evaluations.

Fixes SPARK-58902.

Why are the changes needed?

To document the optimization plan behavior of RewriteWithExpression on conditional branches and establish test suites for common expression rewrites.

Does this PR introduce any user-facing change?

No.

How was this patch tested?

  • Added optimizer plan tests with comparePlans in RewriteWithExpressionSuite.scala.

Was this patch authored or co-authored using generative AI tooling?

No.

Copilot AI lite review requested due to automatic review settings August 28, 2026 07:31

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets SPARK-58902, aiming to preserve single-evaluation semantics for multi-referenced With common expressions in conditional branches (especially important for nondeterministic expressions).

Changes:

  • Added a new optimizer-suite test stub for a conditional-branch With scenario.
  • Added an in-code note warning that inlining With in conditional branches can multiply nondeterministic evaluations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpressionSuite.scala Adds a new SPARK-58902 test case, but it currently doesn’t assert the intended semantics.
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/RewriteWithExpression.scala Adds a comment noting the nondeterminism hazard of inlining With in conditional branches; rewrite behavior remains inlining.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +504 to +516
test("SPARK-58902: conditional branch with multi-referenced common expression") {
val a = testRelation.output.head
val exprDef = CommonExpressionDef(a + a)
val exprRef = new CommonExpressionRef(exprDef)
// CaseWhen with With inside the ELSE branch
val withExpr = With(exprRef > 0 && exprRef < 10, Seq(exprDef))
val caseWhenExpr = CaseWhen(Seq((a < 0, Literal(false))), Some(withExpr))
val plan = testRelation.select(caseWhenExpr.as("col"))
val optimized = Optimizer.execute(plan)

// Verify optimized plan preserves structure
assert(optimized.output.length == 1)
}
Comment on lines +186 to +190
case With(child, defs) =>
// For With in the conditional branches, they may not be evaluated at all and we can't
// pull the common expressions into a project which will always be evaluated. Inline it.
// SPARK-58902: Note that inlining nondeterministic expressions can cause multiple evaluations
// per row. Lazy per-row memoization is recommended for multi-referenced common expressions.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants