Skip to content

perf(db): harden unchanged index updates#1

Merged
K-Mistele merged 1 commit into
K-Mistele:perf/skip-unchanged-index-updatesfrom
TanStack:codex/pr-1691-improvements
Jul 24, 2026
Merged

perf(db): harden unchanged index updates#1
K-Mistele merged 1 commit into
K-Mistele:perf/skip-unchanged-index-updatesfrom
TanStack:codex/pr-1691-improvements

Conversation

@KyleAMathews

Copy link
Copy Markdown

Summary

This stacked follow-up hardens TanStack#1691’s unchanged-index fast path and cuts more of its residual work. It repairs BasicIndex key bookkeeping after a failed removal, caches compiled index evaluators, avoids object normalization work for primitives, and adds model-based property tests for both index types.

Root cause

BasicIndex.remove() preserves the value bucket when expression evaluation fails, but removes the key from indexedKeys. A later same-value update saw the key in the bucket and returned early, so keyCount stayed wrong.

The no-op update path also recompiled the same index expression and sent primitive values through object-specific normalization checks on every update.

Approach

  • Require both bucket membership and indexedKeys membership before treating a BasicIndex update as a no-op.
  • Cache each index’s compiled expression evaluator.
  • Return primitive values from normalizeValue() before the object-specific checks.
  • Generate valid add, update, and delete sequences for BasicIndex and BTreeIndex, then compare their buckets, keys, ordering, lookups, range queries, and rebuilt state with a reference model after every action.
  • Preserve the existing fallback behavior when expression evaluation throws.

Key invariants

  • A no-op update may return early only when the key is present in both the right value bucket and the index’s key set.
  • keyCount, indexedKeysSet, value buckets, ordered entries, equality lookups, and range queries must agree.
  • Rebuilding an index from the final rows must produce the same observable index state.
  • Evaluation failures keep the behavior that perf(db): skip unchanged index updates TanStack/db#1691 inherited from the public remove/add fallback.

Non-goals

Trade-offs

The evaluator cache and primitive fast path overlap two small parts of TanStack#1645. Keeping them here makes the complete unchanged-update optimization reviewable and testable as one stack, at the cost of dropping those hunks when useful pieces of TanStack#1645 are cherry-picked later.

Verification

pnpm exec vitest run packages/db/tests/index-update.property.test.ts packages/db/tests/index-update-short-circuit.test.ts
pnpm --filter @tanstack/db test
pnpm --filter @tanstack/db build
pnpm exec eslint packages/db/src/indexes/base-index.ts packages/db/src/indexes/basic-index.ts packages/db/src/utils/comparison.ts packages/db/tests/index-update-short-circuit.test.ts packages/db/tests/index-update.property.test.ts

The focused suite passes 23 tests. The full package suite passes 2,483 tests with 5 skipped, and the package build passes.

Files changed

  • base-index.ts: cache the compiled index evaluator.
  • basic-index.ts: include key-set membership in the no-op guard.
  • comparison.ts: return primitives before object normalization.
  • index-update-short-circuit.test.ts: cover failed evaluation and bookkeeping repair.
  • index-update.property.test.ts: compare randomized valid operation sequences with a reference model.
  • quick-trees-rest.md: describe the full patch impact.

Stacked on TanStack#1691. Related: TanStack#1645 and TanStack#1517.

@K-Mistele
K-Mistele merged commit fdf567a into K-Mistele:perf/skip-unchanged-index-updates Jul 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants