Skip to content

Content temporarily unavailable

Collection key type and SortedMap restore review

Reviewed product head: c385f6eed162fca2f9a997f02ecababfff077294 on sortedmap-merge-restore, based on origin/main at b8d8ee07c95f51d84c880f6bb7371a84d2858394. Sources: PR #2266, an internal code review (10 findings), a code-simplifier pass (7 proposals), and two CodeRabbit reviews (one finding and one nitpick). A scratch ledger was kept during evaluation; this record keeps the verdict-critical oracle evidence in the repository.

Contract and scope

Two owners change, and one owner is new.

Grouped result keys (new owner). A Collection key is a string or a number: the TKey constraint of every Collection type, and the InvalidKeyError that optimistic mutations throw for any other key. A grouped live query that groups by one field used the raw group value as the key, so a null, undefined, boolean, or bigint group value produced a key outside that type. Grouping by a nullable field is a valid query. Maintainer decision (2026-10-27): the grouped key matches the declared key type; the type does not widen. Such a group value now keys its group in the namespace that normalizeValue reserves, which no string group value occupies. The law fixes the key's type, uniqueness, and stability, not its format. sync.write now throws UndefinedKeyError or InvalidKeyError for a key outside the type, through the check that mutations already used.

SortedMap (repaired owner). Maintainer decision: SortedMap promises what Collection state uses, not full Map behavior, and is no longer exported. Promised: Map ownership for Collection keys, including NaN as one key after every other number; key or comparator order with key tie-breaking; a row changed in place before restore sorts by its current value; after a placed row changes in place, each key stays in the views once with its current value, and a later write or delete affects only its own key; a deferred delete during iteration yields the starting order once; a restore of k deferred keys among n others makes at most k * (ceil(log2 k) + ceil(log2 (n + 1)) + 2) comparator calls, and none with nothing deferred. Not promised: immediate writes or clear() during iteration, the order after a placed row changes in place without a write, -0 keys, keys outside the key type, a throwing comparator, and the work of key order without a comparator.

Load-subset replay refinement (fixed regression added). A named replay of issue #2224's trace (a failed include demand during a marker replay) joins the existing campaign. PR #2229 had fixed that defect, so the replay is green; it needs no model change.

The keyed MultiSet.consolidate rewrite and the Collection commit-loop cleanup keep their observable behavior. The existing MultiSet consolidation oracle and the Collection oracles own them; this PR does not change those owners.

RED, GREEN, and mutant evidence

Grouped result key oracle (packages/db/tests/query/group-result-key-oracle.property.test.ts):

  • RED on the parent commit: all 5 tests fail at the first checkpoint with keys undefined, null, false, and 10n.
  • GREEN: 5 of 5.
  • Mutants, all assertion failures: an encoding by String(value) fails because null and "null" (and true and "true") share a key; an encoding of nullish values only fails because boolean keys remain. An encoding of only the internal D2 key, with the public key unchanged, passes every test: the Collection keys rows by the public group key, so the change is equivalent within this domain. That part of the repair was removed.
  • Reach in each campaign of 80 histories (fixed seed / random): a nullish group published 38 / 32; a group beside a string that prints like it 21 / 18; a key kept across a change 49 / 53.

Sync write key witness (packages/db/tests/collection.test.ts): RED 4 of 4 (undefined, null, boolean, bigint keys were staged); GREEN 4 of 4. The witness also found that InvalidKeyError threw a TypeError for a bigint key, because its development message used plain JSON.stringify; the message now shows the bigint.

SortedMap oracle (packages/db/tests/SortedMap-oracle.test.ts):

  • Before this PR's oracle repair, a mutant that sorted every key at each restore passed all 30 tests. The work law closes that gap.
  • A NaN key RED: with comparator order, rewriting a placed NaN key duplicated it and deleting it removed the last key, because the slot check used ===. This was a regression inside this PR; main spliced at the binary-search index. GREEN after slots compare by SameValueZero.
  • GREEN: 7 of 7, and 21 of 21 supplemental examples in SortedMap.test.ts.
  • Mutants, all assertion failures at the named checkpoint:
MutantRejection
Sort every key at each restore108 comparator calls against a bound of 7 (with delete-only restores made free)
Sort kept keys with the dirty keys appended34 calls against a bound of 7 (same control)
Sort again with nothing deferredcalls at a clean ordered read
No scan fallback for a changed rowa neighbour is deleted
Match slots by ===a placed NaN key removes the last key
Keep dirty keys' old slotsorder and membership mismatch
Apply deferred deletes immediatelya prune skips an entry
Place a deferred row by its value at the writeorder mismatch
Replace the key array instead of writing in placethe supplemental live-iterator example fails
  • Reach in each campaign of 100 histories (lowest across two local runs): burst restores 54; an entry yielded after a prune deleted one 30; a placed NaN key written without deferral 7; a restore whose bound is below n 19; a changed placed row later written 11; a deferred row changed before restore 11. The last three apply to comparator orders only.

Oracle guide audit

RequirementGrouped result key oracleSortedMap oracle
ORC-001Law, authority (TKey, InvalidKeyError, maintainer decision), and limits are in the opening prose.Law, authority (existing contract plus the maintainer's scope decision), and the not-promised list are in the opening prose.
ORC-002expectedGroups partitions plain rows by type and value with NaN equal to NaN. It does not import the compiler, value identity, or key encoding.A native Map of numbers and a fresh full sort derive every observation. The work bound is a formula in n and k, not production's algorithm.
ORC-003Literate file: contract, model, grammar, driver, and refinement check each have prose beside the code.Same structure; the three Collection-use laws sit in the opening prose and beside the driver branches that check them.
ORC-004Reconstruction: the probe that found the bug (nullish groups beside strings) and the bigint and undefined counterexamples are pinned or regenerate in the first runs. Ablation: without nullish values the nullish reach state and RED vanish; without lookalike strings the String(value) mutant survives; without booleans and bigints the nullish-only mutant survives; without changes the key-stability state is unreachable. Range: 1 to 6 rows over 16 group values, up to 4 changes; marginal cases include an empty source after deletes and a NaN group. Exclusion: a move or delete with no rows writes nothing; Dates, -0, and objects are outside the domain.Reconstruction: four fixed histories (main's original, a mutated neighbour, a deferred delete before a burst, a placed NaN rewrite) replay through the shared driver. Ablation: without mutate two reach states and two mutant kills vanish; without the work counter the full-sort mutants survive (shown on the earlier oracle); without prune the iteration law has no generated history; without NaN keys the === mutant survives. Range: keys -3..3 and NaN, values -3..3, up to 40 actions, bursts of 17 to 41 keys. Exclusion: mutate targets only present keys; deletes during iteration are always deferred; keys outside the key type are not generated, because sync.write rejects them.
ORC-005Public createLiveQueryCollection over a mock sync source; entries() and size after preload and after each committed change.SortedMap's own methods are the entry point (it is internal); every view, size, has, and get after each action or chosen checkpoint, comparator calls at each ordered read. Collection sync paths are pinned in SortedMap.test.ts.
ORC-006Mutants above, classified as assertion failures; one equivalent within the domain.Mutants above, all assertion failures; a calibration test feeds wrong orders, a lost tie, a stale value, duplicates, and a missing key to the comparison.
ORC-007Fixed seed 92604118 and an unseeded campaign share the property, driver, comparison, and budget; group-result-key.groups is registered for seed and path replay.The existing sorted-map.* fixed and unseeded campaigns and replay interface carry the new actions.
ORC-008Stateless partition; no model state.deferred, changedPlaced, and orderPromised are kept: a changed deferred row and a changed placed row look alike until the next ordered read, where one must sort exactly and the other only keeps membership. The write-only placed set was removed.
ORC-009Group value, key, and publication use glossary terms; "lookalike" is a model-only label for a string that prints like a non-string value."Deferred", "placed", and "changed in place" are local SortedMap terms with no glossary counterpart; sync transaction and publication keep glossary meanings.
ORC-010withOracleCleanup cleans both Collections and preserves the primary failure.No resources; fast-check reports the shrunk history.
ORC-011No shared semantic fault between the type-and-value partition and production's value identity was named inside the domain. Dates, whose identity is their timestamp, are excluded rather than modelled.No shared fault between a full sort and incremental restore was named.
ORC-012This record.This record.
ORC-013The lookalike strings distinguish a type-preserving encoding from String(value); the boolean and bigint values distinguish a complete encoding from a nullish-only one.The restore-bound-below-order-size premise is reached in 19 or more histories per campaign, and the full and presorted re-sorts exceed the bound there.
ORC-014The mock sync source is a controlled provider; the claim covers only the compiler's keying. Real adapters are not claimed.No controlled premise is carried to a real provider.
ORC-015Three declared states, counts above.Six declared states (three for comparator orders only), counts above.
ORC-016Pinned histories run the shared driver and comparison without their own expectations.Example tests moved to SortedMap.test.ts; the oracle file keeps the property, its fixed histories as inputs, and checker calibration.

Review items

ItemDisposition at the reviewed head
CR1 NaN unplaceFixed: slots compare by SameValueZero; generated NaN keys and the === mutant above. An interim scope note had wrongly called NaN keys unpromised.
CR2 O(k n) deferred deletesFixed: dirty keys leave by membership in one pass; the work law and supplemental splice bounds reject per-key work.
CR3 throwing comparatorNot promised (maintainer scope decision); a comparator that throws is a programming error.
CR4 rows mutated without a writeAccepted design (maintainer decision A): their order is not promised; membership is, and is generated.
CR5 test gap for NaN and workFixed by the NaN dimension and the comparator-call work law.
CR6 reach on pending runtime keysSuperseded: runtime nullish keys no longer reach SortedMap.
CR7 boolean and bigint keysFixed at the producer: grouped keys and sync.write keep keys in the key type.
CR8 per-operation allocationFixed: immediate writes keep the binary-insert path.
CR9 absent delete skips restoreFixed: delete restores order first.
CR10 removed 512 thresholdFixed: the threshold remains.
S1 vacuous work boundsFixed: the comparator-call law bounds the merge pass that the splice counter cannot see.
S2 drop the indexOf value parameterRefuted: immediate inserts and the restore pass values not yet in the searched array.
S3 model mirror of pending keysStale after the redesign.
S4 stale commentFixed.
S5 per-commit allocationDuplicate of CR8 and CR10.
S6 history narrative in the coverage mapFixed.
S7 clear() and live iteratorsSuperseded by the scope decision; main's live-iterator example remains.
CodeRabbit: unplace miss check for an undefined keyValid when raised; its premise is removed, because an undefined key no longer reaches SortedMap.
CodeRabbit: extend an oracle for invalid sync keysPartly valid: SortedMap is not the owner and the no-effect assertion already existed; the coverage map now states why this is a fail-fast invariant witness.

Siblings found during evaluation: a -0 key reported as -0 (now not promised), an immediate write that lost its slot when the comparator threw (not promised), and live iterators that skipped or repeated entries across immediate writes (superseded by the scope decision). The removed forEach and public export were approved by the maintainer.

Loss audit: 17 raw review items = 10 fixed, 1 accepted design, 1 not promised by decision, 2 superseded, 1 refuted, 1 stale, and 1 duplicate; plus 2 CodeRabbit items (1 premise removed, 1 partly valid and documented) and 3 siblings (2 not promised, 1 superseded). No source claim is omitted.

Limits

The grouped key owner does not cover Dates, -0, objects, or multi-field groups, which the aggregate select oracle owns, nor real sync adapters. SortedMap does not promise the items listed under scope. Shipped-byte effects were measured with a local esbuild approximation of pnpm size:compare, because a registry 403 prevented the install that size:compare needs; they are not part of this oracle verdict.

Update: merge with origin/main at 7e46f81ad

The branch merged origin/main at 7e46f81ad in 26896c42a. That change (#2237) requires each registered property to name its owner, so group-result-key.groups now has an owner entry and a replay test name in oracle-replay-manifest.ts. No law, model, grammar, or comparison changed. The replay manifest tests (40) and both oracles pass after the merge.

A third CodeRabbit finding, on ea4cf8792, was valid: the guide note did not say that a string with the reserved \u0000tanstack-db: prefix gets an escaped key, or that a live query's getKey replaces grouped keys. The note now says both.

Shipped bytes: pnpm size:compare against the merge-base b8d8ee07c now runs, with a local-only override for one dependency that the registry proxy blocks. The gzip difference is -56 bytes for app, -38 to -61 bytes for the other scenarios, and -2 bytes for the @tanstack/db package bundle. These numbers replace the esbuild approximation in the limits above.