[BugFix] Detect BIGINT overflow in SUM and fix wrong AVG on the Calcite engine - #5612
Conversation
PR Reviewer Guide 🔍(Review updated until commit 5c7fbc2)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 5c7fbc2 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit 5c7fbc2
Suggestions up to commit 47145b5
Suggestions up to commit 481819a
Suggestions up to commit 2166002
Suggestions up to commit aab660c
|
| ### 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 |
2166002 to
481819a
Compare
|
Persistent review updated to latest commit 481819a |
| if (!Double.isFinite(value) || value > TWO_POW_63 || value < -TWO_POW_63) { | ||
| throw new ArithmeticException("BIGINT overflow in SUM"); | ||
| } |
There was a problem hiding this comment.
Added unit and IT coverage for finite/infinite values, both boundaries, and positive/negative overflow.
| AVG, | ||
| (distinct, field, argList, ctx) -> ctx.relBuilder.avg(distinct, null, field), | ||
| (distinct, field, argList, ctx) -> { | ||
| if (field instanceof RexInputRef |
There was a problem hiding this comment.
Why limit to RexInputRef? field could be RexCall, e.g. avg(toInt(value)).
There was a problem hiding this comment.
Agreed, removed it. All BIGINT expressions now use BIGINT_AVG, with RexCall IT coverage.
| private static boolean isIntegral(SqlTypeName typeName) { | ||
| return switch (typeName) { | ||
| case TINYINT, SMALLINT, INTEGER, BIGINT -> true; | ||
| default -> false; | ||
| }; | ||
| } |
There was a problem hiding this comment.
Use "SqlTypeName.INT_TYPES.contains(...)"
| SqlOperandTypeChecker innerTypeChecker = extractTypeCheckerFromUDF(SqlStdOperatorTable.SUM); | ||
| PPLTypeChecker typeChecker = wrapSqlOperandTypeChecker(innerTypeChecker, SUM.name(), true); | ||
| AggHandler handler = | ||
| (distinct, field, argList, ctx) -> { | ||
| List<RexNode> newArgList = | ||
| argList.stream().map(PlanUtils::derefMapCall).collect(Collectors.toList()); | ||
| boolean checkedLongSum = isIntegral(field.getType().getSqlTypeName()); | ||
| SqlAggFunction sumOperator = | ||
| checkedLongSum ? PPLBuiltinOperators.CHECKED_LONG_SUM : SqlStdOperatorTable.SUM; | ||
| return UserDefinedFunctionUtils.makeAggregateCall( | ||
| sumOperator, List.of(field), newArgList, ctx.relBuilder); | ||
| }; |
There was a problem hiding this comment.
Code is duplicate with registerOperator. simplify it.
|
Persistent review updated to latest commit 47145b5 |
47145b5 to
e247144
Compare
Integral SUM used Calcite's unchecked long accumulator and could silently wrap. BIGINT AVG could likewise overflow its intermediate long sum before division. Use CHECKED_LONG_SUM for integral inputs and Math.addExact during enumerable accumulation. Preserve SqlKind.SUM so planner rewrites and native OpenSearch sum pushdown remain unchanged. Range-check and narrow pushed results while retaining the backend's double semantics. Use a double sum and long count for enumerable BIGINT AVG. Preserve SqlKind.AVG and native avg pushdown. Add unit, integration, REST, documentation, and explain coverage for both execution paths. Signed-off-by: Kai Huang <ahkcs@amazon.com>
e247144 to
5c7fbc2
Compare
|
Persistent review updated to latest commit 5c7fbc2 |
1 similar comment
|
Persistent review updated to latest commit 5c7fbc2 |
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.