[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 1 commit into
Conversation
PR Reviewer Guide 🔍(Review updated until commit 0a077e0)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 0a077e0 Explore these optional code suggestions:
Previous suggestionsSuggestions 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.
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 so the DSL-pushdown and 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 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 holds the true average without 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.Behavior notes (intentional trade-offs)
double, and(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; a genuine overflow lands far outside that boundary, and the in-memory path detects it exactly.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. Stacked on #5604 (reuses its
ArithmeticException→ 4xx routing); the diff will shrink once #5604 merges.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.