[BugFix] Detect BIGINT overflow in SUM and fix wrong AVG on the Calcite engine#5612
[BugFix] Detect BIGINT overflow in SUM and fix wrong AVG on the Calcite engine#5612ahkcs wants to merge 2 commits into
Conversation
PR Reviewer Guide 🔍(Review updated until commit 9eec01c)Here are some key observations to aid the review process:
|
634d9b6 to
f01ed11
Compare
|
Persistent review updated to latest commit f01ed11 |
PR Code Suggestions ✨Latest suggestions up to 9eec01c Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 0a077e0
Suggestions up to commit 31bbdf2
Suggestions up to commit 4d2c6fc
Suggestions up to commit f01ed11
|
| ### Overflow behavior | ||
|
|
||
| Integer and long arithmetic operations (`+`, `-`, `*`) in `eval` expressions detect overflow and return an error instead of silently wrapping. For example, `eval x = int_field + 1` where `int_field` is `2147483647` (integer max) returns an error rather than `-2147483648`. Floating-point (`float`, `double`) arithmetic follows IEEE 754 and does not produce overflow errors. | ||
|
|
There was a problem hiding this comment.
call-out alg difference between pushdown enabled vs disabled?
Two docs indexed: v = 4611686018427387904 (2^62) and v = 1. True sum = 4611686018427387905 (fits BIGINT exactly).
The no-pushdown PPL path (via widenBigintColumnToDecimal + CHECKED_LONG_NARROW) returns the exact bigint. The pushdown path silently loses precision because OpenSearch computes the sum in double, and 2^62 + 1 isn't representable
There was a problem hiding this comment.
Added this distinction to the overflow documentation.
f01ed11 to
4d2c6fc
Compare
|
Persistent review updated to latest commit 4d2c6fc |
4d2c6fc to
31bbdf2
Compare
|
Persistent review updated to latest commit 31bbdf2 |
SUM and AVG over a BIGINT (long) column near 2^63 silently produced wrong
results on the Calcite engine:
- SUM(long): the enumerable accumulator is a plain long, so a running sum
past 2^63 wrapped to a negative value (e.g. SUM(UserID) returned
-5594372458244005145 instead of erroring).
- AVG(long): AVG reduces to SUM(field)/COUNT(field); the intermediate long
SUM wrapped the same way, so AVG returned a wrong (often negative) result.
Fix, applied in the SQL-plugin lowering so both the DSL-pushdown and the
in-memory (no-pushdown) paths behave identically:
- SUM(long): the aggregate argument (a bare BIGINT column) is summed in
DECIMAL so it cannot wrap, and the result is narrowed back to BIGINT with
CHECKED_LONG_NARROW. Narrowing errors (HTTP 4xx) when the sum clearly
overflowed 2^63, and otherwise saturates. The output type stays bigint.
- AVG(long): the bare BIGINT argument is averaged in DOUBLE, which holds the
true average without an intermediate long wrap. The DOUBLE output type is
unchanged.
Only bare BIGINT column references are widened; expressions such as
sum(field + 1) are left untouched so PPLAggregateConvertRule can still rewrite
them to pushdown-friendly form. Narrower integer sums (byte/short/int) already
widen to a BIGINT accumulator and cannot overflow, so they are unchanged.
Boundary note: OpenSearch computes pushed-down sums in double, and
(double) Long.MAX_VALUE rounds to exactly 2^63, indistinguishable from a small
overflow. To avoid erroring on a legitimate near-Long.MAX_VALUE sum, only
magnitudes strictly beyond 2^63 are treated as overflow; a genuine overflow
lands far outside that boundary, and the in-memory path detects it exactly.
AVG pushdown note: casting the AVG argument to DOUBLE keeps native avg pushdown
in most cases, but an AVG with a preceding sort can fall back to an in-memory
sum/count reduction instead of a pushed-down native avg. The result stays
correct; only that narrow case loses aggregate pushdown.
Adds CalcitePPLAggregationIT coverage (overflow -> 4xx, avg correct, in-range
and Long.MAX sums), a REST yaml test (issues/5164_agg.yml), and regenerates the
affected explain fixtures.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
31bbdf2 to
0a077e0
Compare
|
Persistent review updated to latest commit 0a077e0 |
| * are left as-is so {@link org.opensearch.sql.calcite.plan.rule.PPLAggregateConvertRule} can | ||
| * still rewrite them to pushdown-friendly {@code sum(field) OP literal} form. | ||
| */ | ||
| void registerSumOperator() { |
There was a problem hiding this comment.
#5604 use Math.addExact to detect overflow. Why Sum choose another approach?
There was a problem hiding this comment.
#5604 handles scalar arithmetic such as a + b. Calcite represents that operation as a visible RexCall(PLUS, a, b), so we can replace it with CHECKED_PLUS, which executes using Math.addExact.
SUM is represented as an AggregateCall; its repeated additions happen internally inside Calcite’s aggregate accumulator, so there are no individual PLUS nodes for #5604’s rewrite to replace. Using Math.addExact would require a custom aggregate implementation, would not apply to native OpenSearch pushdown, and could report an order-dependent intermediate overflow even when the final mathematical sum fits.
Therefore, the no-pushdown path accumulates BIGINT values in a wider DECIMAL, then checks and narrows the final result back to BIGINT. Keeping the standard SUM operator also preserves the existing pushdown optimizations.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 9eec01c |
Description
SUM/AVGover a BIGINT (long) column near 2^63 silently produced wrong results on the Calcite engine:SUM(long)- the enumerable accumulator is a plainlong, so a running sum past 2^63 wrapped to a negative value (e.g.SUM(UserID)returned-5594372458244005145with HTTP 200).AVG(long)-AVGreduces toSUM(field)/COUNT(field); the intermediatelongsum wrapped the same way, soAVGreturned a wrong (often negative) result.Fix
Applied in the SQL-plugin lowering while preserving the pushdown and in-memory execution paths:
SUM(long)- the aggregate argument (a bare BIGINT column) is summed in DECIMAL so it cannot wrap, and the result is narrowed back to BIGINT via a newCHECKED_LONG_NARROWUDF. Narrowing raises a client error (4xx) when the sum clearly overflowed 2^63, and otherwise saturates. The output type staysbigint.AVG(long)- the bare BIGINT argument is averaged in DOUBLE, which avoids an intermediatelongwrap. The DOUBLE output type is unchanged.Only bare BIGINT column references are widened; expressions such as
sum(field + 1)are left untouched soPPLAggregateConvertRulecan still rewrite them to pushdown-friendly form. Narrower integer sums (byte/short/int) already widen to a BIGINT accumulator and cannot overflow, so they are unchanged.Design
#5604 handles scalar arithmetic because Calcite exposes each operation as a
RexCall:SUMis anAggregateCall; its internal additions are not visiblePLUSnodes, so the #5604 rewrite cannot intercept them. #5612 instead widens the accumulator and checks the final result:The DECIMAL path is exact and order-independent. Keeping the standard
SUMoperator also preserves existing aggregate optimization and pushdown rules. #5612 reuses #5604'sArithmeticException-to-4xx routing, but not its scalar-expression rewrite.CHECKED_PLUSreceives twolongoperands and performs the addition, so it can useMath.addExact(a, b).CHECKED_LONG_NARROWreceives one already-computed DECIMAL/double aggregate result and only converts it back tolong; there is no addition left forMath.addExactto perform. Using checked addition would require a separate customCHECKED_SUMaccumulator. It also would not recover precision already lost by OpenSearch's pushed-downdoublesum.For
AVG, the plan becomesAVG(CAST(field AS DOUBLE)), preventing Calcite's internalSUM/COUNTreduction from first overflowing along.Behavior notes (intentional trade-offs)
double. Large in-range sums can therefore lose low-order precision before narrowing; for example,2^62 + 1becomes2^62. Without pushdown, the DECIMAL accumulator returns the exact result.(double) Long.MAX_VALUErounds to exactly2^63, indistinguishable from a small overflow. To avoid erroring on a legitimate near-Long.MAX_VALUEsum, only magnitudes strictly beyond2^63are treated as overflow.avgpushdown in most cases, but anavgwith a precedingsortcan fall back to an in-memorysum/countreduction instead of a pushed-down nativeavg. The result stays correct; only that narrow case loses aggregate pushdown.Before / After
stats sum(long_field)(overflow)stats avg(long_field)(overflow)doublestats sum(long_field)(in range)bigint)stats sum(Long.MAX)Testing
CalcitePPLAggregationIT.testSumAvgLongOverflow- overflow -> 4xx,avgcorrect, in-range and single-Long.MAXsums; runs on both the pushdown path and the no-pushdown path (viaCalciteNoPushdownIT).issues/5164_agg.yml.expectedOutput/calcite/andexpectedOutput/calcite_no_pushdown/.Related
Addresses the aggregate half of #5164 and builds on #5604's checked-arithmetic error routing.
Check List
--signoff.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
For more information on following Developer Certificate of Origin and signing off your commits, please check here.