cloud-fan commented on code in PR #59954:
URL: https://github.com/apache/spark/pull/59954#discussion_r4444272165
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala:
##########
@@ -273,19 +273,23 @@ object ConstantPropagation extends Rule[LogicalPlan] {
* Reorder associative integral-type operators and fold all constants into one.
*/
object ReorderAssociativeOperator extends Rule[LogicalPlan] {
+ // Flatten only operators evaluated in the same mode as the root: rebuilding
a TRY-mode Add or
+ // Multiply (try_add, try_multiply) in the enclosing mode would change its
overflow behavior.
private def flattenAdd(
expression: Expression,
- groupSet: ExpressionSet): Seq[Expression] = expression match {
- case expr @ Add(l, r, _) if !groupSet.contains(expr) =>
- flattenAdd(l, groupSet) ++ flattenAdd(r, groupSet)
+ groupSet: ExpressionSet,
+ evalContext: NumericEvalContext): Seq[Expression] = expression match {
+ case expr @ Add(l, r, c) if c == evalContext && !groupSet.contains(expr) =>
Review Comment:
**Non-blocking (P2):** `c == evalContext` compares the whole
`NumericEvalContext`, which holds `allowDecimalPrecisionLoss` as well as
`evalMode`. That flag only affects decimal result types (`resultDecimalType`),
and this rule only rewrites integral chains, so rebuilding such an operator in
the root's context is exact. Master folded chains whose operators share the
eval mode but differ only in that flag; this PR keeps them apart. Example:
create a view under `spark.sql.decimalOperations.allowPrecisionLoss=false` that
defines `x = a + 2` (views capture modified configs). Then run `SELECT x + 1
FROM v` in a session with the default setting. The optimized plan now keeps `(a
+ 2) + 1` instead of `a + 3`. Multiply, SQL function bodies, and Datasets built
across a change to that config behave the same way. Results are unchanged, but
each such boundary adds one integral operation per row.
Comparing only the mode (`c.evalMode == evalContext.evalMode`, here and in
`flattenMultiply`) keeps the cross-mode fix and restores this folding. It also
makes the code match the comment above, which says "same mode".
**Verification:**
- **Regression:** An ANSI-mode Add of (a + 1) built with
allowDecimalPrecisionLoss=false, nested under an ANSI-mode + 2 built with
allowDecimalPrecisionLoss=true, is optimized to a + 3; the Multiply analogue (a
* 2) * 3 is optimized to a * 6.
- **Behavior:** The TRY, ANSI, and LEGACY boundary cases for Add and
Multiply under both ANSI settings still keep the different-mode operand and
fold only literals of the root's mode.
##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/optimizer/expressions.scala:
##########
@@ -273,19 +273,23 @@ object ConstantPropagation extends Rule[LogicalPlan] {
* Reorder associative integral-type operators and fold all constants into one.
*/
object ReorderAssociativeOperator extends Rule[LogicalPlan] {
+ // Flatten only operators evaluated in the same mode as the root: rebuilding
a TRY-mode Add or
+ // Multiply (try_add, try_multiply) in the enclosing mode would change its
overflow behavior.
private def flattenAdd(
expression: Expression,
- groupSet: ExpressionSet): Seq[Expression] = expression match {
- case expr @ Add(l, r, _) if !groupSet.contains(expr) =>
- flattenAdd(l, groupSet) ++ flattenAdd(r, groupSet)
+ groupSet: ExpressionSet,
+ evalContext: NumericEvalContext): Seq[Expression] = expression match {
+ case expr @ Add(l, r, c) if c == evalContext && !groupSet.contains(expr) =>
Review Comment:
**Blocking (P1):** This guard keeps a TRY-mode chain as a single operand of
an ANSI or LEGACY root. But `transformExpressionsDown` then visits that operand
as its own root (`case a @ Add(_, _, f)` with `f = TRY`). There, `flattenAdd`
recurses through the TRY descendants, and the rule rebuilds the chain with the
non-literal operands first and the folded literal last. TRY arithmetic is not
associative: each step returns NULL only when its own operation overflows, so
regrouping changes which rows are NULL.
Example: a table row with `a = 2147483647, b = 1`, and `SELECT
try_add(try_add(a, -5), b) + 1 FROM t`. As written, the steps give 2147483642,
then 2147483643, then 2147483644. This PR rewrites the TRY chain to
`try_add(try_add(a, b), -5)`. `a + b` overflows, so the query returns NULL. On
master, the outer `+` absorbed the chain and rebuilt it in its own mode. With
`spark.sql.ansi.enabled=false` that wrapped back to the correct 2147483644;
with ANSI on it raised ARITHMETIC_OVERFLOW. Either way, this PR turns those
rows into a silent NULL. Multiply has the same problem: with ANSI off,
`try_multiply(try_multiply(a, -1), b) * 3` with `a = 1073741824, b = 2` returns
-2147483648 on master and NULL with this PR. Folding literals is affected too.
Under ANSI, `-5 + try_add(try_add(a, 2147483647), 1)` with `a = -10` should
return 2147483633, but the TRY chain now folds `try_add(2147483647, 1)` into
NULL for every row.
Top-level TRY chains such as `try_add(try_add(a, -5), b)` were already
regrouped this way on master, so their results also stay wrong. ANSI and LEGACY
chains are not affected. The new test has no TRY chain that mixes a non-literal
operand with literals of different signs, so it does not catch this.
**Recommended change:** In ReorderAssociativeOperator, exclude TRY-mode
roots from the Add and Multiply cases, so the rule leaves a chain of try_add or
try_multiply exactly as written. ANSI and LEGACY roots keep folding their
own-mode operands and keep a TRY operand as one unit. Update the comment above
the flatten helpers to say that TRY arithmetic is never regrouped, because a
TRY step returns NULL on its own overflow and regrouping changes which step
overflows. Extend the associative-reordering optimizer tests, and add SQL query
coverage for try arithmetic results, with nested and top-level TRY chains that
mix non-literal operands and literals of both signs.
**Why this works:** A TRY chain's result depends on which intermediate step
overflows. With TRY roots excluded, the rule never changes the grouping or
literal folding of TRY operations. The PR's mode guard already keeps TRY
operands out of ANSI and LEGACY chains, so no path left in the rule regroups
TRY arithmetic.
**Scope:** Stop ReorderAssociativeOperator from regrouping TRY-mode
Add/Multiply chains, and cover nested and top-level TRY chains in the optimizer
and SQL result tests.
**Compatibility:** Folding and regrouping of ANSI and LEGACY chains, the
zero and one simplifications, and the grouping-expression guard for Aggregate
behave as on the base branch.
**Risks:** Chains made only of TRY operations, such as try_add(try_add(a,
1), 2), are no longer folded into try_add(a, 3), so they evaluate one more TRY
operation per row and their optimized plans change. This is the cost of correct
results and can be rolled back by removing the TRY exclusion.
**Constraints:** ANSI and LEGACY roots must keep folding their own-mode
literals around a kept TRY operand, as the PR's new optimizer test expects (for
example, a TRY add plus 1 plus 2 still becomes the TRY add plus 3).
**Exclusions:**
- Regrouping inside ANSI-only chains, which can move or suppress an overflow
error, stays as it is on the base branch, where the existing SPARK-49915 and
SPARK-50380 optimizer tests assert it; changing it is a separate decision
outside this fix.
**Success:** The rule leaves every TRY-mode Add or Multiply chain with its
written grouping and literals. In queries that combine nested try_add or
try_multiply with regular arithmetic, each TRY operation returns NULL exactly
when its own written step overflows, under both ANSI settings. ANSI and LEGACY
chains still fold their own-mode literals and keep different-mode operands as
single units.
**Verification:**
- **Regression:** Under both ANSI settings, try_add(try_add(a, -5), b) + 1
over a table row with a = 2147483647 and b = 1 returns 2147483644, and the
optimized plan keeps the TRY chain in its written order.
- **Regression:** A top-level try_add(try_add(a, 2147483647), 1) over a
table row with a = -10 returns 2147483638 rather than NULL, and
try_add(try_add(a, 5), -3) with a = 2147483643 returns NULL.
- **Regression:** In a session with ANSI disabled,
try_multiply(try_multiply(a, -1), b) * 3 over a table row with a = 1073741824
and b = 2 returns -2147483648.
- **Behavior:** For Add and Multiply under both ANSI settings, ANSI and
LEGACY roots still fold their own-mode literals around a kept TRY operand and
keep each operand in its own mode.
- **Compatibility:** Existing try arithmetic SQL query results and the
existing associative-reordering plan expectations for ANSI and LEGACY chains,
including the zero and one simplifications and the Aggregate grouping guard,
are unchanged.
--
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]