Describe the bug
Comet's native decimal SUM validates the accumulator against the sum buffer's precision on every update and latches the failure. Spark does not check intermediate values at all in several common buffer layouts, so it can recover from a temporary overflow and return a result that fits.
Where the two disagree, Comet returns NULL in legacy mode and raises ARITHMETIC_OVERFLOW under ANSI, while Spark returns a value. The aggregate runs entirely in Comet, so no mixed Comet/Spark plan is needed to hit this.
Steps to reproduce
One partition, no GROUP BY, whole-stage codegen enabled:
CREATE TABLE t AS
SELECT CAST(v AS DECIMAL(38,38)) AS v FROM VALUES ('0.6'), ('0.6'), ('-0.6') AS s(v);
SELECT SUM(v) FROM t;
DECIMAL(38,38) matters because Sum.resultType is DecimalType.bounded(p + 10, s), which saturates at (38, 38) and leaves no headroom. The running sum reaches 1.2, which does not fit in that precision, and the third value brings it back to 0.6, which does.
Expected behavior
Spark returns 0.6 in both legacy and ANSI mode.
Comet returns NULL in legacy mode and throws ARITHMETIC_OVERFLOW under ANSI.
Additional context
Why Spark differs. Sum.add uses DecimalAddNoOverflowCheck for decimal inputs. The doc comment on that expression says the overflow check is deliberately skipped because UnsafeRowWriter will perform it when the aggregation buffer is written. That only holds when the buffer actually is an UnsafeRow, and it is not in at least three places:
- ungrouped
HashAggregateExec under whole-stage codegen, which keeps the buffer in local Decimal variables (doProduceWithoutKeys),
- window frames, where
AggregateProcessor buffers into a SpecificInternalRow,
ObjectHashAggregateExec, which buffers the same way.
In all three Spark keeps the wide intermediate and only CheckOverflowInSum at evaluate time decides. Grouped HashAggregateExec does write an UnsafeRow, so Spark nulls there too and Comet agrees.
Comet side. SumDecimalAccumulator::update_single (native/spark-expr/src/agg_funcs/sum_decimal.rs, around line 202) and SumDecimalGroupsAccumulator::update_single (around line 421) both call Decimal128Type::is_valid_decimal_precision per row, then either set the sum to None permanently or raise immediately under ANSI. Once latched, a later cancelling value cannot recover the sum.
Scope. This needs a decimal whose sum precision has little headroom, in practice an input precision of 28 or more, together with an intermediate that exceeds the buffer precision and later inputs that bring it back into range. CometWindowExec has no guard for the ever-expanding decimal SUM case either.
Related work.
Suggested fix. Either match Spark's deferred check by keeping an unbounded intermediate and validating once at evaluate, or add a precision guard mirroring the one in #5420, covering the ungrouped aggregate, the ever-expanding window frame, and ObjectHashAggregate.
Describe the bug
Comet's native decimal
SUMvalidates the accumulator against the sum buffer's precision on every update and latches the failure. Spark does not check intermediate values at all in several common buffer layouts, so it can recover from a temporary overflow and return a result that fits.Where the two disagree, Comet returns
NULLin legacy mode and raisesARITHMETIC_OVERFLOWunder ANSI, while Spark returns a value. The aggregate runs entirely in Comet, so no mixed Comet/Spark plan is needed to hit this.Steps to reproduce
One partition, no
GROUP BY, whole-stage codegen enabled:DECIMAL(38,38)matters becauseSum.resultTypeisDecimalType.bounded(p + 10, s), which saturates at(38, 38)and leaves no headroom. The running sum reaches1.2, which does not fit in that precision, and the third value brings it back to0.6, which does.Expected behavior
Spark returns
0.6in both legacy and ANSI mode.Comet returns
NULLin legacy mode and throwsARITHMETIC_OVERFLOWunder ANSI.Additional context
Why Spark differs.
Sum.addusesDecimalAddNoOverflowCheckfor decimal inputs. The doc comment on that expression says the overflow check is deliberately skipped becauseUnsafeRowWriterwill perform it when the aggregation buffer is written. That only holds when the buffer actually is anUnsafeRow, and it is not in at least three places:HashAggregateExecunder whole-stage codegen, which keeps the buffer in localDecimalvariables (doProduceWithoutKeys),AggregateProcessorbuffers into aSpecificInternalRow,ObjectHashAggregateExec, which buffers the same way.In all three Spark keeps the wide intermediate and only
CheckOverflowInSumat evaluate time decides. GroupedHashAggregateExecdoes write anUnsafeRow, so Spark nulls there too and Comet agrees.Comet side.
SumDecimalAccumulator::update_single(native/spark-expr/src/agg_funcs/sum_decimal.rs, around line 202) andSumDecimalGroupsAccumulator::update_single(around line 421) both callDecimal128Type::is_valid_decimal_precisionper row, then either set the sum toNonepermanently or raise immediately under ANSI. Once latched, a later cancelling value cannot recover the sum.Scope. This needs a decimal whose sum precision has little headroom, in practice an input precision of 28 or more, together with an intermediate that exceeds the buffer precision and later inputs that bring it back into range.
CometWindowExechas no guard for the ever-expanding decimalSUMcase either.Related work.
AVG, by keeping global and expanding-window decimal AVG in Spark once the sum precision reachesDecimalType.MAX_PRECISION. DecimalSUMhas no equivalent guard in either the aggregate or the window path.0.6, 0.6, -0.6behaviour, but as justification for an exclusion inside an enhancement about recovering native partial aggregate coverage. It does not track the wrong answer itself.DECIMAL(38,38)case, run against a matching native library, is in fix: revert unsafe partial aggregates after final fallback #5421 (comment).Suggested fix. Either match Spark's deferred check by keeping an unbounded intermediate and validating once at
evaluate, or add a precision guard mirroring the one in #5420, covering the ungrouped aggregate, the ever-expanding window frame, andObjectHashAggregate.