--- id: pg-decide-stamp-arithmetic-side name: Decide which side guards pg's server-side stamp arithmetic (review 004 item 5, decision) status: completed depends_on: [] scope: narrow risk: low impact: component level: implementation tags: [review-004-fixes, postgres-engine, decision, contract] --- ## Description The decision that must precede the BIGINT-overflow fix, per `docs/reviews/004-pre-release-general-review.md` §"Findings recorded, not fixed" item 5: pg's claim, heartbeat, retry-reschedule, and retention-sweep statements compute server-side sums over consumer-supplied i64 values (`queue.rs:386/912/1000/714`, `lock.rs:91`) — one row stamped with `visibility_timeout_s = i64::MAX` (or retention `i64::MIN`) makes every claim/heartbeat UPDATE fail with "bigint out of range", and the failure is *sticky* per row. The client-side resolution is saturated (`resolution.rs`); the row-stamp arithmetic is not. Decide which side owns the guard: - **(a) Clamp/validate at the enqueue seam** — consumer i64s bounded at entry, matching `resolution.rs`'s client-side saturation posture and the sibling engines' stamp arithmetic (sqlite/mem saturate per review 004 inline fix 6 — the engine-uniformity argument). - **(b) `LEAST`/`GREATEST`-guard the server expressions** — defense at the arithmetic site; no entry-point change, but the stored stamps are silently clamped server-side. **Contract-adjacent — this is why it's its own task.** The choice changes which failures are reachable through the public surface, which touches ADR-023 §2's totality wording. Check the window state first: as of the decomposition (2026-10-11) no publish tag exists, so the class-1 window is presumed open — verify, and if open, land the decision with a dated ADR-023 §2-style annotation (the failure-set change is class-1-legal pre-publish, the review-004 inline fix 5 precedent). If the window has closed, raise with the user before pinning anything. The decision must name the guard form precisely: the clamp domain, which sites it applies to (enqueue vs the stamp sites), and what the observable behavior of an extreme-stamped row becomes. ## Acceptance Criteria - [x] The decision of record: which side owns the guard, with the ADR-023 §2 totality argument and the sibling-engine uniformity check written down - [x] The window state verified and recorded; a dated annotation landed if class-1, or user sign-off if post-publish - [x] The guard form is precise (clamp domain, sites, the extreme-stamped row's observable semantics) — enough for `pg-fix-bigint-stamp-overflow` to implement without re-deciding - [x] The decision recorded where the family keeps such records (ADR annotation / task Notes + contract text if the wording needs it) ## References - docs/reviews/004-pre-release-general-review.md §"Findings recorded, not fixed" item 5 (the finding, the which-side question, the contract-adjacency) - docs/architecture/decisions/023-fourth-review-round.md §2 (the totality wording the choice touches) - alkstore-postgres/src/resolution.rs (the client-side saturation posture), queue.rs / lock.rs (the stamp sites) ## Notes **The site set is six, not four** — the lock arithmetic is two expressions, and the review's `lock.rs:91` line ref was the try_lock insert only: `lock.rs:92` (insert) and `lock.rs:217` (renew) both sum `$3::bigint + $4::bigint` over the per-call `ttl`; and the retention sweep's subtraction is `queue.rs:715` (the `:714` ref was the `IS NOT NULL` line above it). The guard form must cover the *renew* arm too. **Per-call vs stamped arguments split the option-(a) story.** Four of the six arguments are per-call (`heartbeat(extend)`, the `retry(err, Some(d))` override, `try_lock`/`renew(ttl)` ×2) — there is no enqueue to clamp at. The other two ride enqueue/stamped sources (`visibility_timeout_s`, `dead_letter_retention_s` — `QueueOpts`, stamped verbatim per the review-002 disposition, which recorded *unvalidated* stamping as the posture, not a defect). So option (a) as written decomposes into two different entry rules — one of which (validating QueueOpts) contradicts the recorded review-002 posture, and the other (validating per-call boundary args) contradicts ADR-023 §2's totality wording itself ("no clamping, no rejection" — `heartbeat(i64::MAX)` is a value, not a violation). Option (b) needs neither posture change: it realizes what the pinned totality already computes. **The pinned arithmetic's form, not its totality, is what was wrong.** ADR-023 §2's 2026-10-11 annotation cites "saturating_add / plain SQL integer add" as the three engines' code of record and calls it total — correct for the *non-positive* values that annotation classified, but pg's plain bigint add was never total in the overflow domain (Postgres errors; SQLite's overflow→REAL approximates; Rust saturates). The decision therefore needs **no contract-text change** — the table pins the dispositions already; the annotation *corrects the form* on pg. ## Summary Decision of record: **(b), the server expression owns the guard**; clamp domain = the arithmetic *result*, saturated to the i64 domain; no input clamped, validated, or rejected at any seam. Guard form: every server-side stamp expression computes its sum in `numeric` and clamps before the bigint cast — `LEAST(GREATEST(, -9223372036854775808), 9223372036854775807)::bigint` — the SQL realization of the same saturating arithmetic the pinned code of record runs. Sites of record: pg `queue.rs:386/912/1000/715` (claim, heartbeat, retry-reschedule, retention sweep) and `lock.rs:92/217` (try_lock insert, renew); enqueue itself stays Rust-resolved (`resolution.rs`'s `saturating_add` — never a SQL-side sum). Extreme-stamped-row observables (all three engines uniform): `visibility_timeout_s = i64::MAX` → claim deadline `i64::MAX` (effectively never lapsing); `dead_letter_retention_s = i64::MIN` → swept-before `i64::MAX` (sweeps everything, matching mem's `saturating_sub`); negative-extreme `extend`/delay → a past deadline voided by the uniform validity predicate on the next op; `ttl = i64::MAX` → `i64::MAX` expiry. "bigint out of range" leaves pg's reachable surface; the sticky-poison class dies; the duration row's opaque rejection stays `ttl <= 0` only. Recorded as the dated 2026-10-11 annotation in `docs/architecture/decisions/023-fourth-review-round.md` §2 (amending the boundary-row annotation's arithmetic parenthetical in place — ADR-017 §2 class 1). **Window state verified open** at decision time (2026-10-11): no git tags exist, no `cargo publish` has occurred, and ADR-017 §2's termination of record pins the class permanently at the first core-crate publish — so the annotation landed pre-publish, legal. The guard form was validated against the live Postgres 16 harness server before pinning: clamping legs green (overflow both edges clamps; `1700000000 + i64::MIN` rides exact, matching Rust `saturating_add`; `now − i64::MIN` clamps to `i64::MAX`), so `pg-fix-bigint-stamp-overflow` can implement the form verbatim without re-deciding. No code changed (a decision task); no crate touched, so no build/test gates re-run — the doc-only diff carries no rustdoc surface.