TanStack

Content temporarily unavailable

One insert per key after a truncate

Pre-repair executable revision: da4e6b7cd (oracle extension on 201220b51). Repair executable revisions: 67b5f73a1, then 64da25f00.

Law

Each change message must be valid for a consumer that has applied every earlier message: an insert names an absent key, and an update or delete names a present one. Authority: the change-message contract in packages/db/tests/change-event-history-oracle.test.ts and issue #1901. A truncate batch is no exception. After its delete prefix, it must insert each visible key at most once.

Gap

A truncate commit re-applies the optimistic upserts and publishes each one as an insert. The reapply loop in state.ts meant to replace the server's insert for the same key. It ran before the changed-key loop that pushes that insert, so its search never matched. When a key was both re-applied and changed by the commit, the batch inserted it two times.

The optimistic-history oracle missed it because its only subscriber used includeInitialState: true. That subscription filters keys it already sent, so the second insert never reached the replica. The change-event oracle has a strict mirror but uses a local-only Collection, which never truncates.

CodeRabbit found it on PR #2004. The state-stack mutation round 2 had already listed the redundant insert push as reached in 72 cases on main, but no observation showed it there.

Repair and witnesses

  • The optimistic-history driver adds a raw subscriber that starts from the visible rows. Its batches pass the same event-semantics check as the main subscriber, and its replica must equal the model at each checkpoint.
  • The changed-key loop skips keys that the truncate already published.
  • collection-sync-reentrancy.test.ts pinned the duplicate in its trace [1, 2, 3, 1, 3, 1, 2]. The trace is now [1, 2, 3, 1, 3, 2].

Follow-up from review: 64da25f00

Ready callback during a truncate. A truncate marks the Collection ready before its changed-key loop runs. A ready callback that edits a replaced key adds an optimistic upsert that the batch has not published. The first repair read the live upsert map, so it skipped that key's insert, and the subscriber lost the row. The loop now skips only the keys that the reapply loop published, frozen before markReady. CodeRabbit found this on #2005. The optimistic-history grammar has no ready callbacks, so it could not reach this history. A witness in collection-sync-reentrancy.test.ts covers onFirstReady and status:change callbacks. It fails on 67b5f73a1 and passes on main and 64da25f00.

Accepted delete under an active insert. A random campaign of the extended oracle found a second invalid insert on main, with no truncate. A completed direct delete retires on the next sync commit. When an active optimistic insert covered the key, retirement added the key to the changed keys without the row that subscribers last saw, so the commit inserted it again. Upsert retirement already recorded that row. Both loops now share one helper. A pinned replay in optimistic-history-publication.test.ts fails on main for an immediate commit. A non-immediate commit waits for the active insert, so it passes on both.

ORC outcomes

  • ORC-002 independent judgment: the replica check uses the model's visible rows and the protocol rule, not production event code.
  • ORC-006 checker calibration: on da4e6b7cd the extended oracle fails 7 cases in the retention oracle, the settlement replays, and both random campaigns. A wrong repair that skips every changed key during a truncate fails 2 cases.
  • ORC-007 fixed and random campaigns: the fixed and random campaigns pass on 64da25f00, including three runs at 20 times the usual count. The @tanstack/db suite passes 236 files and 8,178 tests.

Limits

The raw subscriber has no where clause and no ordering. Filtered and limited raw subscriptions through a truncate remain with the WHERE predicate publication owner, which lists truncate as outside its scope.