TanStack

Content temporarily unavailable

Invalid index comparison review

Production/test head: the commit that adds this record. This record is a documentation-only follow-up to that head.

Law and boundary

A custom index comparator must return a number that is not NaN. A comparator that returns NaN, a boolean, or any non-number cannot order the index's storage. Per AGENTS.md ("Separate Valid Edge Cases from Contract Contradictions"), a broken comparator is a caller programming error, so the index crashes: the operation that receives an invalid result throws. NaN values ordered by the default comparator remain valid and supported.

makeCheckedComparator in packages/db/src/utils/comparison.ts wraps a user-supplied compareFn and throws a TypeError on any result that is not a non-NaN number. BaseIndex builds every index's comparator through it, so both BasicIndex and BTreeIndex share one enforcement point. Because the comparator now guarantees a valid order, the vendored B+ tree no longer needs its own NaN/invalid-result branch, and the post-split child placement follows the already-known insertion index instead of a redundant comparison. Net production code shrinks.

The index owner (packages/db/tests/index-update.property.test.ts) states the refinement law: for each index type, comparator, accepted numeric prefix, and probe operation, the operation throws exactly when it receives an invalid comparison result, and never when every comparison it makes is valid. The model counts invalid results independently of the production check. An empty prefix is included: the first write compares nothing and must succeed, and the next comparing operation must throw. The BTree Map owner (packages/db/tests/btree-map-oracle.test.ts) keeps two split histories that prove split placement does not consult a post-mutation comparison.

A rejected index add or remove throws before it changes the index, so the index still refines its accepted rows. Index update and build are not atomic. createIndex builds before it registers the index, so a failed build never reaches a collection.

Collection boundary

An external review showed that the first version of this change was worse than main at the Collection boundary. The Collection writes rows before it updates indexes and publishes change events. An index throw between those steps left rows that subscribers were never told about, while the collection stayed usable and reported the write as failed. That is the partial success AGENTS.md forbids. main stored and published those rows; only its index order was wrong.

The repair crashes the collection. CollectionIndexesManager.updateIndexes calls the existing lifecycle.markError and rethrows. The collection status becomes error, and the next mutation throws CollectionInErrorStateError. No recovery path, generation, or rollback is added. The index owner states this law for both index types and both write paths (optimistic insert and sync commit).

Remaining limits:

  • A sync source can still begin and commit writes on a collection in error state. Those rows are stored but not published. This is existing markError behavior for any crashed collection, not specific to comparators.
  • A user who relied on main's silently misordered index now gets a crashed collection at the first invalid comparison. The changeset states this.
  • Comparator transitivity is not validated.

Conformance evidence

RequirementOutcome
ORC-001The index comparator contract and AGENTS.md fail-fast policy supply the law; the limits above bound it.
ORC-002The refinement model counts invalid comparator results with its own recording wrapper. It does not call makeCheckedComparator or any production classifier.
ORC-003The opening comment states the law and limits; the probe/comparator/prefix tables supply inputs; public add/update/lookup/take/build drive production; the recording step judges throw-vs-success.
ORC-004Bounded enumeration crosses two index types, three comparators, seven probe operations, and seven prefixes including the empty prefix. The empty prefix reconstructs the first-write-never-compares case. The signed infinity comparator is the marginal valid case; it must never throw.
ORC-005Public index entry points execute, and the Collection cells drive collection.insert and a sync commit. Observations: throw vs. success, accepted-row refinement after a rejected add/remove, and collection status plus the next mutation after a rejected write.
ORC-006On unmodified origin/main, the BasicIndex law and the reported-string case fail (85 cases). Mutants that leave either index's comparator unchecked, that reject only NaN (accepting booleans), or that also reject infinite results each fail at the intended checkpoint. All were classified as assertion failures.
ORC-007These are bounded enumerations, not an important generated property; no fixed/random campaign parity is required. The package's existing generated index campaign is unchanged.
ORC-008No stateful reference model is introduced; the check recomputes throw-vs-success per operation.
ORC-009Accepted rows and index operations retain existing vocabulary.
ORC-010Checks are synchronous and retain no resources.
ORC-011Both index types run through the same law, distinguishing an index-type-specific fault from a shared one.
ORC-012This record names the reviewed head and each applicable outcome. It makes no universal bug-class closure claim; the sync-writes-after-crash limit above stays open.

Mutant kills (recorded at review time)

Run: vitest run tests/index-update.property.test.ts -t "invalid comparator results|NaN keys|reported string".

MutantResult
origin/main production (no check)85 assertion failures (all BasicIndex; BTreeIndex passed the law but not the error class).
BTreeIndex comparator unchecked84 assertion failures.
BasicIndex comparator unchecked84 assertion failures.
Check rejects only NaN, accepts booleans84 assertion failures.
Check also rejects ±Infinity82 assertion failures (the signed infinity valid control now throws).
Shipped fix0 failures.

Collection and atomicity cells, run with -t "invalid comparator|rejected remove":

MutantResult
PR head before this repair12 assertion failures: 4 Collection cells (status stayed ready), 6 BasicIndex add cells, 2 remove cells.
updateIndexes rethrows without markError4 assertion failures (Collection cells).
BasicIndex add writes valueMap before its sorted search6 assertion failures.
BTreeIndex remove changes maps before the tree delete1 assertion failure.
Shipped fix0 failures.

The full @tanstack/db suite passes: 223 files, 7,429 tests, no type errors. Production code is net negative against the merge base. Tests and this record grow separately.