Skip to content

fix: unify checked arithmetic and overflow semantics#25846

Merged
XuPeng-SH merged 15 commits into
matrixorigin:mainfrom
ck89119:issue-25754-main
Jul 20, 2026
Merged

fix: unify checked arithmetic and overflow semantics#25846
XuPeng-SH merged 15 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 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.

@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

No blocking correctness regression found. One non-blocking performance regression remains; focused tests were blocked by missing CGo headers.

P2 - Avoid unconditional null-bitmap scan for decimal batches (pkg/sql/plan/function/baseTemplate.go:427)

decimalBatchArith calls hasEvaluableRows even when there are no nulls or select-list masks. This adds an O(length) bitmap scan before the decimal kernel, which already scans the batch, for decimal +, -, *, /, DIV, and MOD. Add an empty-null-bitmap fast path or make the scan conditional.

@aunjgr aunjgr 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.

Re-reviewed current head. Decimal128 checked arithmetic now preserves the output NULL mask for vector/vector, constant/vector, and vector/constant paths. I found no remaining blocker.

@aunjgr aunjgr 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.

Re-reviewed current head. Decimal128 checked arithmetic now preserves the output NULL mask for vector/vector, constant/vector, and vector/constant paths. I found no remaining blocker.

@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.

Re-reviewed current head e847902 from first principles. The previously blocking Decimal128 mask-loss issue is fixed, and row eligibility is now consistently enforced before overflow/division-by-zero checks across integer, float, and decimal arithmetic. The test matrix covers type families, all const/vector shapes, NULL and selection masks, strict/non-strict modes, stale select-list tails, and result reuse. I also verified that hasEvaluableRows does not add an O(n) cost on the normal no-NULL/no-mask decimal path: Nulls.IsEmpty is an O(1) bitmap-count check. A synthetic merge with latest main was conflict-free; go build, go vet, the full pkg/sql/plan/function test suite, focused race tests, and CI all pass. No blocking correctness, performance, concurrency, ownership, or unbounded-growth issue remains.

@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

No blocking or non-blocking regression found. Reviewed arithmetic semantics, overflow boundaries, null/select-list reuse, decimal error mapping, type resolution, and updated BVT baselines. git diff --check passes. Focused Go tests could not compile because this checkout lacks required MatrixOne CGo artifacts/headers.

@mergify

mergify Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • Entered queue2026-07-20 09:19 UTC · Rule: main · triggered by rule Automatic queue on approval for main
  • 🟠 Checks running · in-place
  • 🚫 Left the queue2026-07-20 13:15 UTC · at 51b87089737536b69007018b327fdc7f7ba6ff85

This pull request spent 3 hours 56 minutes 49 seconds in the queue, with no time running CI.

Waiting for
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
    • check-skipped = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
    • check-success = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
    • check-skipped = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
    • check-success = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
    • check-skipped = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
    • check-success = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone CI / UT Test on Ubuntu/x86
    • check-skipped = Matrixone CI / UT Test on Ubuntu/x86
    • check-success = Matrixone CI / UT Test on Ubuntu/x86
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone Utils CI / Coverage
    • check-skipped = Matrixone Utils CI / Coverage
    • check-success = Matrixone Utils CI / Coverage
  • any of: [🛡 GitHub branch protection]
    • check-neutral = Matrixone CI / SCA Test on Linux/arm64
    • check-skipped = Matrixone CI / SCA Test on Linux/arm64
    • check-success = Matrixone CI / SCA Test on Linux/arm64
All conditions
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
    • check-skipped = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
    • check-success = Matrixone Compose CI / multi cn e2e bvt test docker compose(PESSIMISTIC)
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
    • check-skipped = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
    • check-success = Matrixone Standlone CI / Multi-CN e2e BVT Test on Linux/x64(LAUNCH, PROXY)
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
    • check-skipped = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
    • check-success = Matrixone Standlone CI / e2e BVT Test on Linux/x64(LAUNCH, PESSIMISTIC)
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone CI / UT Test on Ubuntu/x86
    • check-skipped = Matrixone CI / UT Test on Ubuntu/x86
    • check-success = Matrixone CI / UT Test on Ubuntu/x86
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone Utils CI / Coverage
    • check-skipped = Matrixone Utils CI / Coverage
    • check-success = Matrixone Utils CI / Coverage
  • any of [🛡 GitHub branch protection]:
    • check-neutral = Matrixone CI / SCA Test on Linux/arm64
    • check-skipped = Matrixone CI / SCA Test on Linux/arm64
    • check-success = Matrixone CI / SCA Test on Linux/arm64
  • #review-threads-unresolved = 0 [🛡 GitHub branch protection]
  • github-review-approved [🛡 GitHub branch protection] (documentation)
  • github-review-decision = APPROVED [🛡 GitHub branch protection] (documentation)

Reason

Pull request #25846 has been dequeued

Pull request from fork cannot be queued. This pull request comes from a fork, and Mergify needs the author's permission to update its branch.

The author needs to enable "Allow edits from maintainers" on this pull request.

Hint

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.
If you do update this pull request, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Tick the box to put this pull request back in the merge queue (same as @mergifyio queue).

  • Requeue this pull request

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

Labels

dequeued 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.

6 participants