Repository navigation
addTo/subtractFrom accumulation bypasses declared column type bounds (Int 32-bit, Long ±2^53) at commit-merge #1372
Description
Activity
- addedarea:storageStorage engine, LMDB/RocksDB, compactionStorage engine, LMDB/RocksDB, compaction
on Jun 18, 2026 Re-confirmed on main v5.1.11 + the
SUM/Longread-side companionRe-confirmed on main
541a3d33d(v5.1.11) thataddToaccumulation crosses the declaredLongbound and silently rounds:npm run test:integration -- "integrationTests/qa-scratch/qa214-numeric-precision.test.ts"- addTo wedge: seeded
9007199254740987,addTo10, expected9007199254740997, got9007199254740992— drift −5 ((prev||0)+deltarounds in double at the 2^53 boundary). - Round-trip reject confirms the validator's stated bound is exactly ±2^53: inserting
9007199254741000→ 400"Value … must be an integer (from -9007199254740992 to 9007199254740992)". SoLongbehaves as ±2^53, not true 64-bit.
Companion read-side leg (same root, JS-double accumulation): SQL
SUMover aLongcolumn drifts past 2^53 — 200 rows ×MAX_SAFE_INTEGERreturned a total off by −100 vs the BigInt oracle. That accumulator leg is also noted on the SQL umbrella #1397; flagging here since it shares the root cause with theaddTobound bypass this issue tracks.
QA-explorer campaign (scenario QA-214). Filed for @kris.
— Claude (Opus 4.8, 1M)
- addTo wedge: seeded
An exploratory-QA run added the write-path control this issue was missing, which sharpens it from "numeric coercion never consults the bound" into a path-asymmetry statement:
- Direct write path enforces the bound loudly and verifiably.
2^53+2on aLong→ HTTP 400, then 404 on read-back (so nothing was stored). A server-side 64-bit BigInt is rejected, not truncated. - Atomic path does not.
addTo(2^53)twice on aLongcommits2^54;IntMAX+1commits2147483648.
Same value, same declared column, opposite outcomes depending on which path wrote it. That is a cleaner one-line statement of the defect than the coercion framing, and it names the fix site directly:
addTomust run the same bound check the write path already runs.Re-verified on
origin/main(93752ad79) — the code moved since this issue was filed but the gap is unchanged:resources/tracked.tsAddition.update()now delegates toresources/crdt.tsaddValues(previousValue, value), which is pure arithmetic (previous + value, or a BigInt add) with no bound or type check against the declared column type.resources/Table.tsvalidateValue()— the only bound-enforcing validator — unwrapsif (patch && value.__op__) value = value.valueand therefore validates only the delta, never the post-merge sum. The merge itself happens later at commit time inupdateAndFreeze, with no re-validation.
Posted from finding-triage (QA finding QA-769), 2026-09-02.
- Direct write path enforces the bound loudly and verifiably.
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Summary
Incremental
addTo/subtractFromoperations silently commit values that exceed a column's declared type bound. A single large delta that itself exceeds the bound IS correctly rejected — but the accumulated running total escapes validation entirely.Confirmed affected types (both engines, no divergence):
Int(32-bit):addTo(1)on a column holding2^31−1→ commits2147483648with no error, no wrapIntunderflow:subtractFrom(1)on−2^31+1→ commits−2147483649Long(±2^53): twoaddTo(2^53)calls → commits2^54 = 18014398509481984Correct (unaffected): SQL
UPDATE col = col + 1correctly rejects the out-of-range resolved value (callsset()with the final computed value).Root cause
Addition.update(prev) { return (+prev||0) + this.value }atresources/tracked.ts:590returns the summed result directly to the storage layer without routing throughset(). The per-delta validator inset()(≈82–97) validatesvalue.__op__ ? value.value : value— i.e. the delta — which passes for any single in-range increment. The commit-merge path is unguarded.Impact
Any fleet using a typed
Intcolumn to cap a counter or quota (e.g. 32-bit sequence, rate-limit counter) or relying on theLong±2^53 bound underaddTo/subtractFromworkloads does not get that cap. Values silently exceed the declared range with no error and no data corruption (values stay exact JS-safe integers). The only safe workaround today is using SQL arithmetic UPDATE (which enforces correctly) or an open/untyped column.Fix
Re-validate the resolved sum against the column's type bound in
tracked.ts:590— call the column validator with the committed result after merge, or inline the bound check.Repro
Harper commit:
7aaa5a152(branchkris/repl-no-revalidate-1302-core; root cause intracked.tsis branch-independent).Found by the automated QA explorer campaign (waves 33–34). 🤖 Generated with Claude Code