[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 8 commits into
Conversation
PR Reviewer Guide 🔍(Review updated until commit 481819a)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 481819a Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 2166002
Suggestions up to commit aab660c
Suggestions up to commit aab660c
Suggestions up to commit 146adec
Suggestions up to commit 0deaa72
|
| ### 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 |
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 and this PR now use the same checked-addition primitive, but at different planner levels.
#5604 handles scalar arithmetic such as a + b. Calcite exposes it as RexCall(PLUS, a, b), so it can be rewritten to CHECKED_PLUS, which calls Math.addExact once.
SUM is an AggregateCall; its internal additions are not visible PLUS nodes, so the scalar rewrite cannot intercept them. We now solve that with a custom CHECKED_LONG_SUM aggregate:
- Non-pushdown:
CheckedLongSumAggFunctioncallsMath.addExact(accumulator, value)for every row. - Pushdown: native
checked_long_sumcallsMath.addExactduring shard accumulation and coordinator reduction, and transports exactlongvalues. TINYINT,SMALLINT,INTEGER, andBIGINTinputs all use this checked BIGINT accumulator.
This intentionally fails on an intermediate overflow even if later values would bring the final mathematical sum back into range. For example, Long.MAX_VALUE, 1, -1 throws at Long.MAX_VALUE + 1. We consider that fail-fast, order-dependent behavior acceptable and consistent with checked Java/Spark-style accumulation.
|
Persistent review updated to latest commit 9eec01c |
|
Persistent review updated to latest commit f5d8adc |
f5d8adc to
f2c52bd
Compare
|
Persistent review updated to latest commit f2c52bd |
|
Persistent review updated to latest commit 0deaa72 |
|
Persistent review updated to latest commit 146adec |
|
|
||
| @Override | ||
| public List<AggregationSpec> getAggregations() { | ||
| return List.of( |
There was a problem hiding this comment.
Add an new sum aggregation in DSL? I do not think it is required. I am OK post-processing and DSL-pushdown has percision difference (this is DSL existing feature).
There was a problem hiding this comment.
Agreed. I removed the custom DSL aggregation and getAggregations() registration. Pushdown now uses native OpenSearch sum with post-processing for BIGINT range validation, while accepting the existing native-double precision behavior. The non-pushdown path still uses Math.addExact during accumulation.
78015b4 to
aab660c
Compare
|
Persistent review updated to latest commit aab660c |
1 similar comment
|
Persistent review updated to latest commit aab660c |
aab660c to
2166002
Compare
|
Persistent review updated to latest commit 2166002 |
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>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
Signed-off-by: Kai Huang <ahkcs@amazon.com>
2166002 to
481819a
Compare
|
Persistent review updated to latest commit 481819a |
Description
SUM/AVGover integral columns near theBIGINTboundary could silently return wrong results on the Calcite engine:SUMcould wrap its runninglongaccumulator.AVG(BIGINT)could overflow its intermediate long sum before division.double, so pushed-down sums can lose low-order precision for large values.Fix
SUMinputs (TINYINT,SMALLINT,INTEGER, andBIGINT) useCHECKED_LONG_SUM.CheckedLongSumAggFunctioncallsMath.addExactduring every accumulation step.CHECKED_LONG_SUMreportsSqlKind.SUM, preserving SUM planner rewrites and native pushdown.sum(field)or scriptedsum(expression)aggregation. No new OpenSearch aggregation is registered.BIGINT; backend precision already lost indoublecannot be recovered.AVG(BIGINT)uses a reflectivedouble sum + long countaggregate locally and reportsSqlKind.AVG, allowing pushdown to remain nativeavg(field)without a cast-generated script.Architecture
This separates execution implementation from planner identity: Calcite invokes the reflective checked aggregates locally, while
SqlKind.SUM/AVGlets planner rules retain native OpenSearch pushdown.Precision behavior
The two execution paths intentionally have different precision guarantees:
Example:
At that magnitude, adjacent
longvalues cannot all be represented bydouble.CheckedLongSumParsercan detect a non-finite or out-of-BIGINT-range result, but it cannot reconstruct bits already rounded by the backend.AVGreturnsDOUBLEon both paths, so it follows double precision semantics. The local BIGINT-specific implementation prevents long overflow; it does not provide arbitrary-precision averaging.Overflow semantics
Local overflow is checked during accumulation, not only against the final mathematical result:
The later
-1is not processed. A sequence whose final mathematical sum fits can therefore fail when an intermediate running sum exceeds the BIGINT range. This fail-fast behavior is intentional and followsMath.addExactaccumulation semantics.Before / After
SUMoverflowSUMin rangeBIGINTSUMAVG(BIGINT)near long overflowDOUBLEresultAVG(BIGINT)avg(field)SUMTesting
Long.MAX_VALUE, intermediate overflow, null handling, and all integral input types.AGGREGATION->and bare field AVG no longer creates a script.CalciteExplainITtests with pushdown disabled.Related
Addresses the aggregate half of #5164 and builds on #5604's checked scalar arithmetic.
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.