fix: unify checked arithmetic and overflow semantics#25846
Conversation
Unify overflow checking across SQL arithmetic execution kernels to match MySQL 8.4 strict-mode behavior (ERROR 1690): - Merge 24 per-type integer overflow helpers into 6 generic checked operations; fix the signed-subtraction check that missed the v1 - MinInt wraparound (e.g. 0 - (-9223372036854775808)). - Add float32/float64 overflow checks for -, *, / (finite operands producing Inf now raise out-of-range, matching + which already had the check). - DIV: float operands now truncate through a checked float64 quotient; any quotient with magnitude >= 2^63 raises out-of-range, including MinInt64 DIV 1/-1, since MySQL computes DIV on unsigned magnitudes (verified against MySQL 8.4.8). - Map decimal ErrInvalidInput to ErrOutOfRange at the SQL arithmetic execution boundary. - specialTemplateForDivFunction now supports error-returning kernels. - Add characterization tests for all 8 integer type boundaries, float overflow, DIV/MOD boundaries, and div/mod template branches. Division-by-zero semantics (SQL-mode controlled NULL vs error) are unchanged and remain distinct from overflow errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- specialTemplateForDivFunction's vector/const branch only checked p1.WithAnyNullValue() and ignored rsAnyNull, so rows masked by selectList (e.g. short-circuited by CASE/IF) were still evaluated; with error-returning kernels this raised spurious overflow errors (e.g. a masked 1e308 / 1e-308 row). Add the rsAnyNull check, matching the const/vector branch, the basic case, and the mod template. Add regression tests covering divFn vector/const, integerDivFn vector/const, and divFn vector/vector with a masked overflow row (verified to fail without the fix). - Regenerate func_div_by_zero.result via mo-tester: MinInt64 DIV 1 now raises ERROR 1690 per MySQL 8.4 unsigned-magnitude DIV semantics instead of returning MinInt64. Audited the rest of test/distributed: uint64_div_overflow's overflow cases are already behind @bvt:issue and MaxUint64 DIV 1 already expects an error; no other case pins the old behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
integerDivSigned/integerDivUnsigned iterated over every row regardless of selectList, so rows short-circuited by CASE/IF were still evaluated: the MinInt64 DIV +/-1 check, the uint64 quotient > MaxInt64 check, and the strict-mode division-by-zero check could all raise errors for rows that must not be evaluated (the div-by-zero case predates this PR). Extract maskUnselectedRows to mark masked rows NULL up front (returning early when all rows are masked) and skip them in the per-row loops, matching the behavior of the template kernels. The template kernels are unaffected: opBinaryFixedFixedToFixedWithErrorCheck, the mod template, and decimalBatchArith (whose kernels skip rsnull rows) all already handle selectList. Add regression tests driving the real integer kernels: masked MinInt64 DIV 1 (vector/const), MinInt64 DIV -1 (vector/vector), MaxUint64 DIV 1 (vector/vector), and a masked zero divisor under forced strict mode; all verified to fail without the fix. Clarify that the earlier float64 DIV case covers the float branch only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Regenerate TimeToInt.result and decimal.result via mo-tester: decimal arithmetic overflow now raises ERROR 1690 (out of range) instead of the internal "invalid input" error, per the ErrInvalidInput -> ErrOutOfRange mapping at the SQL arithmetic execution boundary. This fixes the 9 e2e BVT failures in CI (1 in TimeToInt.sql, 8 in decimal.test). The regenerated files also refresh checkpoint metadata lines (display widths) to match current server output; those lines are not compared by mo-tester. Both files verified locally with mo-tester at 100%. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
XuPeng-SH
left a comment
There was a problem hiding this comment.
There is one blocking semantic gap: the NULL/division-by-zero fix is limited to integer DIV, while the same row-eligibility contract is still violated by float /, float DIV, MOD, and decimal //DIV/MOD.
In specialTemplateForDivFunction, the vector/constant branch checks v2 == 0 before merging the dividend's NULLs (baseTemplate.go:1704-1718). specialTemplateForModFunction does the same (baseTemplate.go:1447-1458), and the decimal vec/const kernels return ErrDivByZero before consulting rsnull (for example arith_decimal_fast.go:893-900 and 4895-4902). Consequently, in strict DML mode, an all-NULL dividend vector over a constant zero incorrectly errors instead of producing NULL.
I reproduced this deterministically at the PR head:
- float64
/,DIV, andMOD:[NULL] op const(0)all returneddivision by zero; - Decimal64
/,DIV, andMOD: an all-NULL bitmap with a constant-zero divisor all returneddivision by zero; - an independent MySQL 8.0.44 strict-DML check inserted NULL successfully for all six corresponding DOUBLE/DECIMAL expressions.
The newly added TestIntegerDivNullDividendByZeroStrictMode does not cover the failing shape: its divisor is a normal vector, and it exercises only integer DIV.
Please enforce the underlying rule before any error-producing arithmetic: only rows that are selected and have two non-NULL operands may raise overflow or division-by-zero. Then cover /, DIV, and MOD across integer/unsigned/float/Decimal64/128/256; const/const, const/vector, vector/const, vector/vector; all-NULL and mixed-NULL dividends; partial selection masks; and strict/non-strict modes. A shared early "no evaluable rows" check is preferable to more per-type special cases.
Other review gates passed locally: the full function package, focused -race tests, go build, go vet, and git diff --check. I found no separate concurrency, ownership, or unbounded-growth issue, and no material hot-path allocation regression.
gouhongshen
left a comment
There was a problem hiding this comment.
Codex automated review
Strict-mode NULL propagation remains broken for constant-zero divisors outside the newly fixed integer-DIV loops.
P1 - Check row eligibility before raising a constant-zero error (pkg/sql/plan/function/baseTemplate.go:1709)
For a vector dividend and non-NULL constant zero divisor, this branch calls checkDivisionByZeroBehavior before merging the dividend null bitmap (the merge is only at line 1717). Thus, in strict INSERT/UPDATE mode, [NULL] / 0 and [NULL] DIV 0 for FLOAT/DOUBLE return division-by-zero instead of NULL. The same premature constant-zero pattern remains in specialTemplateForModFunction (line 1451) and decimal vec/const kernels (for example arith_decimal_fast.go:895), affecting /, DIV, and MOD for DECIMAL as well. Only selected rows with two non-NULL operands may raise this error; centralize that check and add strict all-NULL/mixed-NULL vector/const coverage.
gouhongshen
left a comment
There was a problem hiding this comment.
Codex automated review
Found one blocking masked-row overflow regression and one batch-reuse NULL-bitmap regression. Focused tests were blocked because cgo/libmo.dylib is missing; the worktree is unchanged.
P1 - Masked vector/constant float overflow is still evaluated (pkg/sql/plan/function/baseTemplate.go:553)
When the second operand is constant and the first vector has no input NULLs, this branch ignores rsAnyNull. A partial select list marks masked rows in rsNull, but the else loop still calls resultFn for them. The PR routes float subtraction and multiplication through this error-checking template, so a masked row such as MaxFloat64 * 2 or MaxFloat64 - (-MaxFloat64) raises ErrOutOfRange and aborts a CASE/IF query instead of producing NULL. The branch must honor the current rsNull mask (or include rsAnyNull in its guarded path).
P2 - Generic arithmetic still writes select-list masks past the batch (pkg/sql/plan/function/baseTemplate.go:609)
FunctionExpressionExecutor only grows its reusable SelectList; a later smaller batch retains stale tail entries. This loop still ranges over the entire retained SelectList instead of length, so a three-row mask reused for a two-row batch can add a NULL bit at row 2, outside the result vector. Nulls.Add expands the bitmap and Count includes that bit; for example, order/window operators can see nullCnt >= Length and skip sorting. The new bounded loops cover decimal, DIV, and MOD paths, but the generic plus/minus/multiply templates remain affected.
XuPeng-SH
left a comment
There was a problem hiding this comment.
There is still one blocking select-list correctness gap in the Decimal128 addition path.
P1 - Preserve mask-derived NULLs after Decimal128 addition (pkg/sql/plan/function/arithmetic.go:240)
decimalBatchArith now correctly adds unselected rows to the result NULL bitmap, but the Decimal128-result special case in plusFn unconditionally calls result.GetResultVector().GetNulls().Reset() whenever both input vectors contain no NULLs. That condition says nothing about select-list eligibility, so it erases the NULLs just established for masked CASE/IF rows.
I reproduced this at the current head bc96aa17c9: two non-NULL Decimal128 vectors, FunctionSelectList{AnyNull: true, SelectList: []bool{true, false}}, and a two-row result. plusFn returns nil, but result.GetNulls().Contains(1) is false; row 1 is exposed as a valid value although it was not selected.
Please remove the caller-side bitmap reset or make it preserve all result NULL sources, and add Decimal128 + partial-mask coverage (including vector/vector and const/vector or vector/const). The underlying invariant is that result validity is the union of input NULLs and execution-ineligible rows; it cannot be reconstructed from input NULL state alone.
The latest head does close the previously reported masked-overflow and stale-tail issues, and the current CI is green. This remaining case is absent from the matrix: the addition test uses Decimal64, while Decimal128 is covered only for /, DIV, and MOD.
What type of PR is this?
Which issue(s) this PR fixes:
issue #25754
What this PR does / why we need it:
Unifies checked arithmetic and overflow semantics across the SQL arithmetic execution kernels to match MySQL 8.4 strict-mode behavior (ERROR 1690), as required by the subtask of #25705.
Integer arithmetic
addInt8WithOverflowCheck...mulUint64WithOverflowCheck) into 6 generic checked operations constrained bysignedInt/unsignedInt. Error messages (type name and(%d op %d)format) are byte-for-byte identical to the old helpers.v1 > 0and therefore missed thev1 - MinIntwraparound (e.g.0 - (-9223372036854775808)silently wrapped; MySQL raises ERROR 1690).Float arithmetic
-,*,/: finite operands producing Inf now raise out-of-range, consistent with+which already had the check.DIV
int64(v1/v2)silently truncated, soSELECT 1e308 DIV 1returned garbage instead of erroring.MinInt64 DIV 1andMinInt64 DIV -1now raise out-of-range. MySQL computes DIV on unsigned magnitudes, so any quotient with magnitude 2^63 errors even when the signed result would fit (verified against MySQL 8.4.8).Decimal
ErrInvalidInputfrom decimal kernels toErrOutOfRangeat the SQL arithmetic execution boundary.Infrastructure
specialTemplateForDivFunctionnow accepts error-returning kernels.Tests
All boundary assertions were validated against a local MySQL 8.4.8 instance:
arithmetic_int_overflow_test.go: boundary matrix for all 8 integer types (+, -, *), including0 - MinIntandMinInt * -1.arithmetic_float_overflow_test.go: float overflow helpers and decimal error mapping.arithmetic_overflow_boundary_test.go: DIV int64/uint64/float boundaries, MOD boundaries (MinInt64 MOD -1 = 0, matching MySQL), and div/mod template const/vector/null branch coverage.Known deviations documented in test comments (out of scope here):
MaxUint64 DIV 1: MySQL returns an UNSIGNED BIGINT; MO's DIV result type is fixed at signed int64, so it errors. Result type inference belongs to [Subtask]: add numeric resolver and prepared expression context #25751.🤖 Generated with Claude Code