[Analytics Engine] Error on BIGINT arithmetic overflow instead of wrapping - #22378
[Analytics Engine] Error on BIGINT arithmetic overflow instead of wrapping#22378ahkcs wants to merge 5 commits into
Conversation
PR Reviewer Guide 🔍(Review updated until commit b39f770)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to b39f770 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit ed05653
Suggestions up to commit ea95017
Suggestions up to commit 9e28428
Suggestions up to commit 59b9d22
Suggestions up to commit 1e1d666
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #22378 +/- ##
============================================
+ Coverage 73.45% 73.46% +0.01%
- Complexity 76424 76440 +16
============================================
Files 6101 6101
Lines 346486 346486
Branches 49870 49870
============================================
+ Hits 254496 254548 +52
+ Misses 71740 71696 -44
+ Partials 20250 20242 -8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…pping
BIGINT (Int64) +, -, * overflow on the DataFusion backend silently wrapped
(arrow's wrapping kernels) and returned HTTP 200 with a wrong value. Rewrite
every Int64-operand +/-/* logical BinaryExpr into an overflow-checked scalar
UDF (checked_{add,sub,mul}_i64) that calls arrow's checked kernels and errors
on overflow, surfaced to the client as HTTP 400.
- checked_arith UDFs (Immutable, return Int64) re-wrap the arrow overflow into
a stable "BIGINT arithmetic overflow" DataFusionError.
- checked_arith_rewrite walks the logical plan (map_expressions + Expr
transform_up), gated to Int64-both-operands so float/decimal/uint/narrow-int
arithmetic is untouched; NamePreserver keeps aggregate measure names stable.
- Applied on every executor's executed plan and the indexed-path filter
extraction plan (so where-clause arithmetic feeding parquet pushdown is
checked too); nested inside comparisons only, so delegation is unaffected.
- NativeErrorConverter maps the overflow to a 400 OpenSearchStatusException;
AnalyticsTransportErrors tags it INVALID_ARGUMENT across Flight so the 400
survives the distributed-aggregate reduce instead of degrading to a 500.
Calcite/v2 long-overflow parity is tracked separately.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
1e1d666 to
59b9d22
Compare
|
Persistent review updated to latest commit 59b9d22 |
|
Persistent review updated to latest commit 9e28428 |
The purge thread runs on a fixed interval (200ms, set in setUp via init(0, 200) with threshold=0 = always purge). testPurgeOnlyFiresAboveThreshold raised the threshold to Long.MAX_VALUE and immediately sampled the purge count, but a purge cycle already in flight — started while the threshold was still 0, before the Rust thread observed the new threshold over FFI — could still fire once, yielding "expected:<0> but was:<1>". Wait past one full check interval after raising the threshold, before sampling the baseline count, so the thread finishes any in-flight cycle and observes the new threshold first. This mirrors the existing guard in testPurgeDisabledWhenIntervalIsZero. Test-only change; no production behavior change. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit ea95017 |
|
Persistent review updated to latest commit ed05653 |
|
❌ Gradle check result for ed05653: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
|
Persistent review updated to latest commit b39f770 |
|
❌ Gradle check result for b39f770: FAILURE Please examine the workflow log, locate, and copy-paste the failure(s) below, then iterate to green. Is the failure a flaky test unrelated to your change? |
Description
BIGINT (
long/ Int64) arithmetic (+,-,*) on the analytics-engine DataFusion backend silently wraps on 64-bit overflow instead of erroring. For exampleeval x = long_col * long_colwhere the product exceeds9,223,372,036,854,775,807returns a wrapped negative value with HTTP 200, rather than a clear client error. DataFusion lowers integerPlus/Minus/Multiplyto arrow's wrapping kernels (add_wrapping/sub_wrapping/mul_wrapping) by default.This makes BIGINT overflow an explicit HTTP 400 on the DataFusion route.
Mechanism
A logical-plan rewrite (
checked_arith_rewrite) runs betweenfrom_substrait_planandcreate_physical_planon every executor. It walks the plan withLogicalPlan::map_expressions+Expr::transform_upand rewrites everyBinaryExpr { op: Plus | Minus | Multiply }whose both operands resolve toInt64into a call to an overflow-checked scalar UDF (checked_add_i64/checked_sub_i64/checked_mul_i64). The UDFs invoke arrow's checked numeric kernels (add/sub/mul), which return an error on overflow. The error carries a stable, self-authored phrase (BIGINT arithmetic overflow) thatNativeErrorConvertermaps to anIllegalArgumentException→ HTTP 400.Why a logical rewrite (rather than flipping DataFusion's physical
BinaryExpr::fail_on_overflow): walking the logical plan reaches arithmetic in every node uniformly — projection exprs, filter predicates, aggregate group/argument exprs (sum(a*b)), window args, sort keys, join conditions — because those are all first-class top-levelExprs at the logical level. The physical-plan approach would depend on per-node expression accessors that do not uniformly surface aggregate/window argument expressions, leaving silent overflow gaps.Type gate (what is and isn't affected)
Only
Int64 op Int64is rewritten. Everything else passes through unchanged:Int64{+,-,*}Int64Float32/Float64Decimal*Int8/16/32),UInt64/,%+/-/*)The analytics scan boundary already narrows the only
UInt64source (unsigned_long) toInt64before planning (schema_coerce), and the SQL-plugin widening (opensearch-project/sql#5603) widens all sub-BIGINT integer arithmetic toInt64before the engine fork — soInt64 op Int64is the only integer arithmetic that reaches DataFusion and can still overflow. The gate is therefore both correct and sufficient.Type resolution failures (e.g. a correlated column absent from the merged input schema) fall through to "leave unchanged" rather than propagate, so the rewrite can never turn a previously-working query into a planning error.
The UDFs are
Volatility::Immutable, and theirreturn_typeisInt64— identical to theBinaryExprthey replace — so plan schemas are byte-identical (norecompute_schema, norelabel_execchurn), and predicate pushdown / CSE still apply. Const-folding an overflowing literal pair does not lose the error: DataFusion'sConstEvaluatorpreserves the original expr on a UDF runtime error (it only fails-fast forCAST), so overflow still surfaces at execution with the stable message; a non-overflowing constant simply folds to its exact value. Each rewritten expression's output name is preserved (NamePreserver) so a laterrecompute_schemacan't rename an aggregate measure likesum(a * b)and break a parent node that references it by the derived name.Distributed / aggregate path (transport)
Flight transport does not serialize the Java exception type — only a
StreamErrorCode+ message survive. A client-errorIllegalArgumentExceptionwith no HTTP status would cross asINTERNALand be redacted to a generic 500 on the coordinator (this is whatstats sum(a*b)overflow originally did).AnalyticsTransportErrorsnow tags the overflow asStreamErrorCode.INVALID_ARGUMENTon the data-node send side and rebuilds it as aBAD_REQUEST(400)OpenSearchStatusExceptionon the coordinator receive side — mirroring the existing breaker→429 and cancel→CANCELLED mappings — so the 400 survives the distributed reduce.Indexed path
On the data-node indexed path the rewrite is applied to both the executed plan and the plan used for filter extraction, so arithmetic inside a
WHEREpredicate that seeds the parquet pushdown predicate is checked too (otherwisewhere long1 * long2 > kwould be scan-filtered with wrapping arithmetic and drop overflowing rows silently). Checked arithmetic only ever appears nested inside a comparison operand — never as a top-levelAND/ORor delegation marker — so predicate delegation classification is unaffected.Scope / divergence
This is the DataFusion-backend fix for the residual
long × longoverflow that operand-widening cannot address (there is no wider primitive thanlong). The Calcite/v2 engine still wrapslongoverflow; unifying the two engines'long-overflow behavior is tracked separately (opensearch-project/sql#5604). The AE-errors-while-V2-wraps divergence is intentional and scoped here to the analytics route.Resolves the reported analytics-engine
long-arithmetic overflow (see opensearch-project/sql#5164).Testing
checked_arithRust UDF unit tests (overflow errors, null-propagation, exact values, batch-fail)checked_arith_rewriteRust unit tests (Int64+/-/*rewritten; float/mixed/divide untouched; nested bottom-up; Filter + Sort node coverage)analytics-backend-datafusionRust lib suite (regression)NativeErrorConverterTests(raw + controlledBIGINT arithmetic overflow→IllegalArgumentException)AnalyticsTransportErrorsTests(BIGINT overflow →INVALID_ARGUMENT→ 400 round-trip)BigintOverflowITQA (column*, literal+column,where-arith,stats sum(arith)→ 400; non-overflow → 200; double → 200 no-error)Check List
--signoffor-s.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.