Skip to content

fix: unify checked arithmetic and overflow semantics#25846

Open
ck89119 wants to merge 11 commits into
matrixorigin:mainfrom
ck89119:issue-25754-main
Open

fix: unify checked arithmetic and overflow semantics#25846
ck89119 wants to merge 11 commits into
matrixorigin:mainfrom
ck89119:issue-25754-main

Conversation

@ck89119

@ck89119 ck89119 commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

  • API-change
  • BUG
  • Improvement
  • Documentation
  • Feature
  • Test and CI
  • Code Refactoring

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

  • Merges the 24 per-type integer overflow helpers (addInt8WithOverflowCheck ... mulUint64WithOverflowCheck) into 6 generic checked operations constrained by signedInt / unsignedInt. Error messages (type name and (%d op %d) format) are byte-for-byte identical to the old helpers.
  • Fixes the signed-subtraction check, which previously required v1 > 0 and therefore missed the v1 - MinInt wraparound (e.g. 0 - (-9223372036854775808) silently wrapped; MySQL raises ERROR 1690).

Float arithmetic

  • Adds overflow checks for float32/float64 -, *, /: finite operands producing Inf now raise out-of-range, consistent with + which already had the check.

DIV

  • Float operands: the quotient is computed in float64 and truncated; any quotient with magnitude >= 2^63 (or NaN) raises out-of-range. Previously int64(v1/v2) silently truncated, so SELECT 1e308 DIV 1 returned garbage instead of erroring.
  • MinInt64 DIV 1 and MinInt64 DIV -1 now 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

  • Maps ErrInvalidInput from decimal kernels to ErrOutOfRange at the SQL arithmetic execution boundary.

Infrastructure

  • specialTemplateForDivFunction now accepts error-returning kernels.
  • Division-by-zero semantics (SQL-mode-controlled NULL vs. error) are unchanged and remain distinct from overflow errors.

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 (+, -, *), including 0 - MinInt and MinInt * -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.
  • MySQL converts float DIV operands through DECIMAL (~16 significant digits) before dividing; MO truncates in the double domain, so the low digits of extreme values can differ. Per-row decimal conversion is explicitly disallowed by the issue.

🤖 Generated with Claude Code

ck89119 and others added 2 commits July 17, 2026 17:23
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>
ck89119 and others added 2 commits July 17, 2026 22:20
- 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 XuPeng-SH left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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, and MOD: [NULL] op const(0) all returned division by zero;
  • Decimal64 /, DIV, and MOD: an all-NULL bitmap with a constant-zero divisor all returned division 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 gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@matrix-meow matrix-meow added size/XXL Denotes a PR that changes 2000+ lines and removed size/XL Denotes a PR that changes [1000, 1999] lines labels Jul 19, 2026

@gouhongshen gouhongshen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 XuPeng-SH left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/bug Something isn't working size/XXL Denotes a PR that changes 2000+ lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants