TanStack

Content temporarily unavailable

Eager indexed ordered-window repair oracle review

Reviewed source head: ff731ad1c on perf-ordered-loader, with the perf fix in 44c90e0ad and the null-ordering fix in ff731ad1c. The base is 65992aacd (main with #1650).

Contract and evidence

An ordered, limited live query over an eager source with an index on its leading order term must show the first limit rows after offset of the eligible source rows after every change. The authority is packages/db/src/query/live/ARCHITECTURE.md, "Ordered requests, continuation, and recovery" and "Atomic window publication", and the documented order in docs/guides/live-queries.md: nulls first by default, NaN greater than every non-null value, ties by key.

Work is a separate law. With an index, the source delivers at most 2 * (offset + limit) + tied rows + 1 rows per change. That is one reacquired prefix of the window, the tie group at its boundary, one refill of at most the window, and the changed row. The bound does not grow with source size. Each generated source holds 200 eligible filler rows outside every window, so a read that resends every matching row exceeds it. A NaN or null boundary cannot be expressed as a cursor (canExpressCursorOrder), so the documented full-source fallback applies and only the rows law is checked while such a rank is present. An unindexed source also falls back to full source.

The owner is the "eager indexed ordered windows" section of packages/db/tests/query/ordered-work-oracle.property.test.ts. Its model is a plain array sorted by the documented order. It shares no code with the loader, the comparator, or the index.

Original failure

On 65992aacd, a visible delete or an eligibility change delivered 202 to 206 rows against a bound of 5 to 8: "rows delivered by step 1: expected 202 to be less than or equal to 5". The ordered-prefix repair's local read was predicate-only, so it resent every matching row. In the rindle "list: newest 50 open" case this cost 1.107 ms per insert/delete pair at 10,000 issues.

Repair

  • requestSnapshot reads only the first limit matching local rows when the subscription has an order index, the request has orderBy and limit, and the source is eager. An on-demand source keeps the full delivery that its repair chain relies on.
  • An eager source's repair may settle synchronously, so its tie and refill steps finish inside one graph run. Without this, widening a window after a repair published twice: once after the tie request and again after the refill. That broke two pagination-oracle laws ("keeps a NaN window coherent through equal and changed source ranks" and "rebuilds the full boundary when widening after an out-of-window rank update").

Alternative (b), considered and rejected

Moving the bounded read into loadOrderedPrefixRepair through requestLimitedSnapshot gave 2.28 ms per pair, slower than main. The profile showed ascComparator at 27%, getPairOrNextLower at 25%, and the BTree index's take loop at 18.5% self time. The cause is semantic, not per-call setup: requestLimitedSnapshot skips keys already sent to the query, so after a repair it walks past the whole visible window, one tree descent per step, and loads the next limit rows instead of resending the prefix. That also grows the query's private state on every change.

Null ordering

findIndexForField served a descending, nulls-first query with an ascending, nulls-first index wrapped in ReverseIndex. Reversing the index moved its null group to the end. On 44c90e0ad the pinned case failed with "expected [ 1000 ] to deeply equal [ +0 ]", and both campaigns failed. ReverseIndex now takes the requested null placement and reads the null group from that end.

Mutant results

MutantOutcome
Original predicate-only repair read (65992aacd)Assertion failure, work law: 202 > 5.
Ordered read without its limit (M3)Assertion failure, work law: 202 > 5.
Bounded read for on-demand sources too (M4)Assertion failure in 2 pagination-oracle cases.
Never repair after a visible change (M5)Assertion failure, rows law: expected [ 2 ] to deeply equal [ 1 ].
Read limit + 1 rowsAssertion failure, work law: 4 > 3.
Read limit - 1 rows (M1)Survives. Equivalent at the public observation, see below.
Read in the reversed direction (M2)Survives. Equivalent at the public observation, see below.
Eager repair stays asynchronousAssertion failure in the same 2 pagination-oracle cases (split publication).
ReverseIndex ignores the requested null placementAssertion failure in 3 of 4 tests, rows law.

M1 and M2 deliver no more rows than the bound allows, and the refill and tie-boundary chain still reaches the correct window. A tighter work bound cannot reject them, because they deliver the same number of rows or fewer. The extra refill request they cause is an internal request count, not a public observation of this law. Both are equivalent within the tested domain.

Benchmark

scripts/bench/incremental-update.ts gains a "list: newest 50 open" case with a visible issue delete. With an index at 10,000 issues, its median per-write latency is 0.63–0.70 ms on main and 0.17–0.18 ms with the fix. Without an index it stays near 4 ms, because the full-source path is unchanged. Overall geomean is 0.95–0.96× against main, with a same-code noise run of 1.00×.

ORC-012 requirement audit

RequirementOutcome
ORC-001Applicable. The rows and work laws and their authority are above. The claim covers eager sources only. On-demand providers keep their own work laws in this owner.
ORC-002Applicable. The model sorts plain rows by the documented order with its own comparator. It does not import the loader, the index, or makeComparator.
ORC-003Applicable. The section's opening prose states both laws, the bound's derivation, the grammar, the production path, the checkpoint and the limits.
ORC-004Applicable. The grammar crosses sync and local-only writes, index present or absent, both directions, one or two order terms, limit 1–3, offset 0–1, and upserts and deletes over ids 0–29 with ranks 0–15, NaN and null. The pinned delete case and the pinned null case reconstruct the reported traces. The work law skips only steps whose boundary value is NaN or null; it asserts on 88.5% of indexed single-term steps. The reversed-index oracle covers nulls: 'last'. Excluded: on-demand sources and custom collation.
ORC-005Applicable. The driver runs a real live-query Collection over a real eager Collection. It counts rows a snapshot returns from currentStateAsChanges, rows the source delivers to the query's subscription (which include index refills), and rows entries() yields.
ORC-006Applicable. The original code and the mutants above reach the checkpoint. Each outcome is classified.
ORC-007Applicable. Each campaign runs with fixed seed 2044 and with a random or replay seed through oracleRandomParameters, property ordered-work.eager-indexed-window.
ORC-008Applicable. The model keeps only the current rows. Order is derived from them.
ORC-009Applicable. "Read rows", "delivered rows" and "scanned rows" are work observations, not production states.
ORC-010Applicable, with a gap. Cleanup runs in a finally block, so a cleanup error after an assertion failure would replace it. No run showed a cleanup failure.
ORC-011Inapplicable. No reviewer named a fault shared by the model and production.
ORC-012This record.
ORC-013Applicable. The work bound is a threshold law. The limit + 1 mutant exceeds it by one row (4 > 3).
ORC-014Inapplicable. No controlled provider supplies a premise.

Unresolved

  • Per-change work for on-demand sources is not bounded by this owner.
  • A reversed read gathers the whole nullish group on each read: it builds a set of the nullish keys, sorts them, and calls the filter once on each. So a read costs O(m log m) in the nullish group size m, even when it returns no nullish key. The work law bounds filter calls on non-null keys by n and allows one filter call per nullish key. Large nullish groups are rare in ordered windows, so the reader keeps this cost for simpler code. The bounded nullish read of d2bb4c3f0 was replaced by this simpler reader in the follow-up commit; a later index-API change that removed per-read group sorts was reverted (e0ab7e0f5). Reads of equal non-null values still sort each group, as on main.
  • An initial full-source ordered load over an eager source stays asynchronous, as on main. #1896 states a general rule for synchronous results, so this is a likely gap; it is the next change, not this one.

Review follow-up (2026-10-06)

A code review of this branch found eight issues. The probes, the RED output and the mutant runs are in the PR evidence; this section records the outcomes.

FindingOutcome
R1: a reversed index sorted and filtered every nullish key on each readFixed. The new reversed-index oracle (reverse-index-oracle.property.test.ts) bounds filter calls by the position of the n-th accepted key. RED on the branch: 5,000 filter calls for n = 50 among 5,000 nullish keys. A follow-up simplified the reader: each read now gathers the nullish group, and the law allows one filter call per nullish key while still bounding non-null filter calls by n (see Unresolved). A follow-up (afe19e442) also removed the per-read sort of an equal-value group, through a new index API and write-path upkeep. It was reverted in e0ab7e0f5: that sort is old behavior on main and not part of this regression, so the change did not earn its code weight. See Unresolved.
R2: a second order term took the bounded read into a full in-memory sortFixed. The bounded read applies only to one order term. The eager-window oracle gains a second order term and a scan law (one pass per change). RED: 400 rows scanned against a bound of 201. Removing the gate fails it.
R3: ARCHITECTURE.md contradicted the synchronous repairFixed. Both passages now name the eager ordered-prefix exception.
R4: the synchronous gate also matched full-source requestsFixed by narrowing the gate. A lifecycle witness checks that an eager fn.where ordered window is loading at creation; it fails on the widened gate and passes on main.
R5: index refills were not countedFixed. The oracle also counts rows delivered to the subscription and bounds the initial load. The unbounded-refill mutant now fails (200 > 3); before, the refill ran only during the initial load, which the law did not bound.
R6: the work law skipped every history with a NaN or null rankFixed. It skips only an inexpressible boundary and counts tie groups at the boundary, over a larger domain. A mutant that resends about 20 domain rows fails (23 > 3).
R7: the reader returns two tie-key ordersDocumented, not changed. The query breaks ties by ascending key, so the reviewer's fix would put nullish keys in the wrong order. The reversed-index oracle states the reader's order.
R8: sourceHoldsAllRows was optional and always setFixed. It is required.
R10: a null cursor in a nulls-first reversed readRefuted. A cursor at null is past the nullish group, so the non-null keys are the correct result. The order law passed on the original code at 20× runs.

After these fixes, the original predicate-only read, limit + 1, the resend-domain mutant and the unbounded refill all fail. limit - 1 (M1) still survives, as above.

Second review follow-up (2026-10-06)

A high-effort code review of 308bb12b9 raised nine findings. Each was checked with a probe on this branch and on main, or with a mutant.

FindingOutcome
1: a synchronous prefix repair holds no publication gate, so a later asynchronous step could publish a partial windowRefuted for both named paths. A repair whose boundary enters a NaN tie group falls back to a full-source read (204 rows delivered) and publishes [61,62] once, as on main. A blocked LEFT-joined filter publishes nothing while blocked and [2,3] once after release, as on main. The eager-window oracle now asserts one publication per change whose window equals the model, and pins the NaN tie-group repair. A mutant with main's asynchronous gate also passes this law: single-change histories publish once under both designs, and the pagination laws own window moves.
2: ReverseIndex requires nullsFirstLow. ReverseIndex is a read-time wrapper that findIndexForField builds; no caller outside this package constructs it. The parameter then defaulted to false; the external review below showed that default was wrong for a nulls-last index, and omission now means plain reversal.
3: a bounded read skips stale rows kept after a failed truncate replayRefuted. An eager collection never calls loadSubset, so it has no replay demand to fail (the probe recorded no calls). Source cleanup puts every dependent live query in its terminal error, on this branch and on main.
4: two-term and unindexed eager repairs settled synchronouslyFixed. The loader oracle pins that an eager prefix repair settles synchronously only for a bounded read. RED on 308bb12b9: the two-term and unindexed cases settled before loadMore returned. The gate now uses the same bounded-read fact as the subscription; the old gate fails 2 cases.
5: the source-holds-all-rows rule was coded twiceFixed. The loader computes the bounded-read fact once and passes it to requestSnapshot, which no longer checks the sync mode, the index or the term count. A subscription that ignores the flag reads 206 rows where the bound is 5.
6: the bounded read repeats index discovery and could fall back to a full sortPartly confirmed. Index discovery per repair costs one scan of the collection's index list and is kept. A silent fall back to a full sort was not caught: the scan law allowed one pass. A bounded step must now scan no more rows than its work bound; a mutant that disables the index read in getOrderedKeys fails (406 > 204).
7: each reversed read gathers the nullish groupAccepted design (see Unresolved).
8: two tie-key orders cut through a NaN tie groupDuplicate of R7. A NaN boundary cannot be a cursor, so the loader reads the full source whatever the tie order, and the window publishes once with the right rows.
9: sourceHoldsAllRows defaulted to true for an unknown sourceFixed. It reads the resolved source collection, which is never undefined there.

External review follow-up (2026-10-07)

An external review of 3dc581394 raised three findings, each with a reproduction against 9e8ed9978. All three were confirmed and fixed.

FindingOutcome
1 (P1): a bounded repair kept a distant joined row instead of the next eligible rowConfirmed, a regression of this change. The law is the eager-window rows law: the window equals the first limit eligible rows. A full window is evidence of that only when every row the source sends passes the filter; a LEFT-joined filter drops rows after the source's limit, and a row that an earlier live update sent can fill the window. The eager-window oracle gained a filter dimension (source predicate, function filter, LEFT-joined marker), a label-only touch command, a delete-visible command, and a campaign fixed to the joined premise (ordered-work.eager-joined-window). RED on 3dc581394 for the pinned history (root ranks 1..10, markers on even ids, limit 1, touch 10, delete 2): rows [10] where [4] is expected; 9e8ed9978 gives [4]. Fix: a plan with a joined filter keeps the full local read, so it also keeps main's asynchronous repair. Re-admitting joined plans fails the pinned case on both sync paths and the fixed-seed joined campaign (an assertion failure); the random joined campaign catches it on about a third of seeds. A function filter, residual predicates, inner joins, groupBy, distinct and custom collations already require the full source, so they never reach the bounded read; the function filter is in the grammar and passes.
2 (P2): a reversed read required an eq lookup that admission does not checkConfirmed. The reader now gathers the nullish group with equalityLookup, which every index implements. A range-only index witness reads in both null placements, and a public witness orders a descending snapshot and a limited live query through it. RED on 3dc581394: Unsupported operation: eq from every read; the public witness passes on 9e8ed9978. A mutant that calls lookup('eq') again fails 3 cases with that runtime failure.
3 (P2): the nullsFirst = false default changed plain reversal over a nulls-last indexConfirmed. Omitting the argument now reads the original index's reversed walk, as earlier releases did; findIndexForField always passes it. A legacy witness pins the earlier release's keys for BTree and Basic indexes (takeFromStart(3) is [1, 3, 2], take(3, null) is [3, 2]); RED on 3dc581394 returned [3, 2, 1]. The old default fails both cases.

Two observations made while extending the oracle are outside this change:

  • An unindexed or indexed joined plan reads its full source twice per change (402 rows scanned where the source holds 201), on 9e8ed9978 too. The scan law now covers source-filter plans only.
  • When rows tie on every orderBy term, a joined query returns them in the joined result key's order, not the source key's: for ties at rank 11 among ids 2 and 10 with limit 1, both commits return [10]. The live-queries guide promises order only by orderBy terms, so the joined window is compared as any valid first limit rows. The source-filter window still compares exact ids, which assumes ties by source key; that assumption is unsupported by the guide and is open.