[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
PPL/SQL long arithmetic (+, -, *) in eval/SELECT expressions silently wrapped on overflow in the Calcite engine (e.g. long_field * 999...9 wrapped to a negative value with HTTP 200). SqlStdOperatorTable.PLUS/MINUS/MULTIPLY generate plain Java +/-/* in the Enumerable code path, which wrap. Rewrite long +/-/* to their overflow-checked variants (CHECKED_PLUS / CHECKED_MINUS / CHECKED_MULTIPLY), which generate Math.addExact etc. and throw ArithmeticException on overflow. The exception is caught in QueryService and surfaced as a 4xx client error instead of wrapping or falling back to V2. Scoped to BIGINT operands only: narrower integer arithmetic (byte/short/int) is widened to a type that cannot overflow before this rewrite runs (PPLFuncImpTable promotes byte/short to INT and any int/long to BIGINT for +/-/*), so long — which has no wider integer type — is the sole remaining overflow case on the Calcite engine. Float/double/decimal follow IEEE 754 / decimal semantics and have no CHECKED_* runtime, so they are left untouched. The rewrite runs after the analytics-engine fork, so only the Calcite path is affected (the DataFusion backend handles its own overflow). The rewrite runs before pushdown so both coordinator-executed and pushed-down (script) arithmetic are checked; PPLAggregateConvertRule, OpenSearchRelOptUtil, and ExtendedRelJson recognize the CHECKED_* kinds so aggregate/sort pushdown and script serialization keep working. Adds a REST yaml test (issues/5164.yml) and updates the affected explain fixtures. Resolves opensearch-project#5164 Signed-off-by: Kai Huang <ahkcs@amazon.com>
PR Reviewer Guide 🔍(Review updated until commit f01ed11)Here are some key observations to aid the review process:
|
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>
634d9b6 to
f01ed11
Compare
|
Persistent review updated to latest commit f01ed11 |
PR Code Suggestions ✨Explore these optional code suggestions:
|
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.