Skip to content

Patch add operations on BigInt attributes add instead of overwriting, and object sub-types validate the sub-object - #3577

Merged
kriszyp merged 7 commits into
mainfrom
fix/validate-patch-envelope-coercion
Oct 27, 2026
Merged

kriszyp merged 7 commits into
mainfrom
fix/validate-patch-envelope-coercion

Conversation

@kriszyp

@kriszyp kriszyp commented Oct 26, 2026 •

Copy link
Copy Markdown
Member
An engineer swaps the coin inside a sealed plus envelope on a conveyor

⊙ Problem

A patch like { balance: { __op__: 'add', value: 25 } } on a BigInt attribute overwrites the stored value with 25n instead of adding to it. Table.validate wrote the coerced operand over the whole add operation. Object sub-type validation had three more bugs:

  • a coerced 0n was dropped;
  • the computed-property check read the outer record;
  • an array passed as an object; a primitive gave extra errors or a TypeError.

🤔 Is the requirement right?

Yes. The task asked that a null add on an object sub-type "not throw". It now fails with a 400 ValidationError, not a TypeError: an add on an object has no meaning.

💡 Solution

🤔 Unmeasured probe on untyped PATCH fields

A plain object value (not an operation) on an untyped or Any attribute now costs one __op__ read per PATCH. Typed primitives no longer read it. Not benchmarked: the shared bench lock was busy all session.

⚖️ Alternatives

  • Convert at apply (crdt.addValues) — an add onto an absent field would store a number in a BigInt column.
  • Per-type fix in the BigInt and Date branches — four branches coerce, and nested envelopes would still be stored literally.

🤔 Operations are checked only at the top level

Planning review (Framing-Verdict: better-alternative-exists) suggested checking envelopes in the recursive validateValue. Adopted, then narrowed to the top-level loop: the commit applies __op__ only there, so a nested envelope is now a type error.

🔧 Changes

✅ Verification

  • validation.test.js on RocksDB and LMDB: 11/11 pass (5 new cases fail against base Table.ts).
  • test:unit:resources: 4905 passing. subscribeReadGateCallerImpact (fixed on main in 8bf7d1e06) and subscribeExpiredCurrent pass alone.
  • test:unit:main timed out on a saturated box after 1158 passing, 0 failing; CI covers it and test:integration:all.

PR QA: Do concurrent REST PATCH add operations on a BigInt attribute sum exactly across HTTP workers? — queued @ 9056b77, qa-pr-harper-3577-9056b77
Docs: not needed — numeric __op__ patches already add per the documented CRDT semantics; this stops a wrong overwrite.

🤖 Generated with Claude Code — Claude Opus 5.5

Release target: main — a validation bug fix; no customer need for a backport is known
Related PRs: #3430 overlaps, 76 others independent
Complexity: complicated

Deferred findings

Origin — the dispatch brief this PR was written from

Table.validate / validateValue (resources/Table.ts, called from _writeUpdate) has four defects: (A) for a patch op envelope ({op, value}) the coerced scalar is written back over the envelope (BigInt counters lose the add), and a null-op on a nested sub-type dereferences null; (B) if (updated) truthiness writebacks drop 0n; (C) the nested computed/relationship check uses record.hasOwnProperty; (D) arrays are accepted for object sub-types and addError falls through into the child loop. When patch is true and the value is an op envelope, coerce .value in place and return undefined (skip further validation if the unwrapped value is null); reject arrays for object sub-types and return instead of iterating; use Object.hasOwn(value, name); change both writebacks to !== undefined. Related only: #1372.

Acceptance

Tests on rocksdb and lmdb (the QA-1949 suite shape): BigInt addTo/subtractFrom patch on a BigInt attribute applies the delta; 0n persists; array for an object sub-type is rejected with one error; null-op on nested sub-type does not throw; Int-delta patches unchanged. Show each new test failing on origin/main sources (forced rebuild) before the fix.

Dispatch: task harper-validate-patch-envelope-bigint · queued by finding-triage/wave-40 · ran by claude/opus/high · worker kzyp-xps-1

Review-Coverage: authored=gemini; ran=cursor-composer,codex,cursor-grok,claude; late=cursor-grok:adjudicated; adjudicated=domain; declined=muse; rounds=8; full=2 @ 685fbc9

Review-Attention: study ~20m (critical: Table.ts; decisions: budget-via-reflow, reject-string-operands-untyped, top-level-only-operations, operand-validity-split, strict-object-subtype-arrays) @ 685fbc9

@kriszyp kriszyp added this to the v5.4 milestone Oct 26, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request updates the validation logic in Table.ts to ensure that top-level patch operations (such as { __op__: 'add', value }) are preserved during validation rather than being replaced by their coerced scalar values, while still coercing their operands in place. It also refactors object sub-type validation to correctly handle computed properties and pending changes, and adds comprehensive unit tests to verify these behaviors. Feedback on the changes suggests using object spread when copying a frozen patch operation envelope to preserve any existing custom properties or metadata.

Comment thread resources/Table.ts Resolved
kriszyp and others added 7 commits October 26, 2026 17:55
…date sub-objects on the sub-object

Table.validate replaced a patch's `{ __op__: 'add', value }` envelope with its
coerced operand, so a BigInt counter patched with a number or string delta was
overwritten instead of incremented. The operand is now validated and coerced
inside the envelope, add operations are limited to numeric properties with a
non-null operand, and a frozen envelope is replaced rather than mutated.

Also: coerced falsy values (0n) are written back in sub-objects and arrays;
computed/relationship checks of a sub-type read the sub-object (Object.hasOwn)
instead of the outer record; an array or primitive for an object sub-type is
one type error and no longer falls through into the child checks.

Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ones, with numeric operands

The commit applies `__op__` envelopes on top-level properties only, so one
nested in a sub-object or array element is now a type error instead of being
stored literally. Unknown operation names and non-numeric operands on untyped
or Any properties are rejected before the commit. A computed property assigned
on a tracked sub-object (held in its pending changes) is rejected too.

Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…etChanges only when callable

Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…der untyped fields are data

Dispatch-Task: harper-validate-patch-envelope-bigint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…neighbour to meet the line budget

Dispatch-Task: pr-maint-25376f6ce7c0222ac51338fa18370905
Co-Authored-By: Gemini <noreply@google.com>
@kriszyp
kriszyp force-pushed the fix/validate-patch-envelope-coercion branch from 9056b77 to 685fbc9 Compare October 27, 2026 00:21
@kriszyp
kriszyp marked this pull request as ready for review October 27, 2026 14:44
@kriszyp
kriszyp merged commit 0dae467 into main Oct 27, 2026
67 of 73 checks passed
@kriszyp
kriszyp deleted the fix/validate-patch-envelope-coercion branch October 27, 2026 14:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant