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:
| Mutant | Rejection |
|---|---|
| Sort every key at each restore | 108 comparator calls against a bound of 7 (with delete-only restores made free) |
| Sort kept keys with the dirty keys appended | 34 calls against a bound of 7 (same control) |
| Sort again with nothing deferred | calls at a clean ordered read |
| No scan fallback for a changed row | a neighbour is deleted |
| Match slots by === | a placed NaN key removes the last key |
| Keep dirty keys' old slots | order and membership mismatch |
| Apply deferred deletes immediately | a prune skips an entry |
| Place a deferred row by its value at the write | order mismatch |
| Replace the key array instead of writing in place | the 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#
| Requirement | Grouped result key oracle | SortedMap oracle |
|---|---|---|
| ORC-001 | Law, 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-002 | expectedGroups 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-003 | Literate 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-004 | Reconstruction: 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-005 | Public 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-006 | Mutants 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-007 | Fixed 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-008 | Stateless 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-009 | Group 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-010 | withOracleCleanup cleans both Collections and preserves the primary failure. | No resources; fast-check reports the shrunk history. |
| ORC-011 | No 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-012 | This record. | This record. |
| ORC-013 | The 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-014 | The 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-015 | Three declared states, counts above. | Six declared states (three for comparator orders only), counts above. |
| ORC-016 | Pinned 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#
| Item | Disposition at the reviewed head |
|---|---|
| CR1 NaN unplace | Fixed: 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 deletes | Fixed: dirty keys leave by membership in one pass; the work law and supplemental splice bounds reject per-key work. |
| CR3 throwing comparator | Not promised (maintainer scope decision); a comparator that throws is a programming error. |
| CR4 rows mutated without a write | Accepted design (maintainer decision A): their order is not promised; membership is, and is generated. |
| CR5 test gap for NaN and work | Fixed by the NaN dimension and the comparator-call work law. |
| CR6 reach on pending runtime keys | Superseded: runtime nullish keys no longer reach SortedMap. |
| CR7 boolean and bigint keys | Fixed at the producer: grouped keys and sync.write keep keys in the key type. |
| CR8 per-operation allocation | Fixed: immediate writes keep the binary-insert path. |
| CR9 absent delete skips restore | Fixed: delete restores order first. |
| CR10 removed 512 threshold | Fixed: the threshold remains. |
| S1 vacuous work bounds | Fixed: the comparator-call law bounds the merge pass that the splice counter cannot see. |
| S2 drop the indexOf value parameter | Refuted: immediate inserts and the restore pass values not yet in the searched array. |
| S3 model mirror of pending keys | Stale after the redesign. |
| S4 stale comment | Fixed. |
| S5 per-commit allocation | Duplicate of CR8 and CR10. |
| S6 history narrative in the coverage map | Fixed. |
| S7 clear() and live iterators | Superseded by the scope decision; main's live-iterator example remains. |
| CodeRabbit: unplace miss check for an undefined key | Valid when raised; its premise is removed, because an undefined key no longer reaches SortedMap. |
| CodeRabbit: extend an oracle for invalid sync keys | Partly 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.