Skip to content

fix(db): a key in BasicIndex/BTreeIndex lives in at most one bucket#1517

Open
kevin-dp wants to merge 3 commits intomainfrom
kevin/stale-index-bucket-repro
Open

fix(db): a key in BasicIndex/BTreeIndex lives in at most one bucket#1517
kevin-dp wants to merge 3 commits intomainfrom
kevin/stale-index-bucket-repro

Conversation

@kevin-dp
Copy link
Copy Markdown
Contributor

@kevin-dp kevin-dp commented May 5, 2026

Summary

Both BasicIndex and BTreeIndex could leave a key in two buckets at once whenever update/remove was called with an oldItem that evaluated to a value other than the one the index actually had recorded for that key — remove would silently no-op on the bucket map while the subsequent add placed the key in a new bucket. Calling add twice for the same key (e.g. when an upstream "update" change message arrives without a previousValue, so the collection treats it as an insert) had the same effect.

This is the underlying cause of the user-reported "stale auto-index after optimistic→synced update on the indexed column" bug, where a kanban card snaps back to its old column because the auto-index still has the row in the old stage's bucket.

The fix: each index now tracks every key's currently-indexed value internally and uses that on remove/update instead of trusting the caller-supplied oldItem. add also drops the key from its current bucket first if it's already indexed.

The first commit adds a regression test that demonstrates the invariant violation (and fails on main); the second commit lands the fix.

Test plan

  • New tests/index-key-bucket-tracking.test.ts fails on main and passes after the fix
  • All existing @tanstack/db index, auto-index, subscribe-changes tests still pass
  • Full @tanstack/db + @tanstack/offline-transactions + @tanstack/electric-db-collection suites still pass locally

🤖 Generated with Claude Code

Pin down the invariant that calling update or add on an index never
leaves the same key in two buckets at once, even when the caller-
supplied oldItem disagrees with what the index recorded.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 5, 2026

More templates

@tanstack/angular-db

npm i https://pkg.pr.new/@tanstack/angular-db@1517

@tanstack/browser-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/browser-db-sqlite-persistence@1517

@tanstack/capacitor-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/capacitor-db-sqlite-persistence@1517

@tanstack/cloudflare-durable-objects-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/cloudflare-durable-objects-db-sqlite-persistence@1517

@tanstack/db

npm i https://pkg.pr.new/@tanstack/db@1517

@tanstack/db-ivm

npm i https://pkg.pr.new/@tanstack/db-ivm@1517

@tanstack/db-sqlite-persistence-core

npm i https://pkg.pr.new/@tanstack/db-sqlite-persistence-core@1517

@tanstack/electric-db-collection

npm i https://pkg.pr.new/@tanstack/electric-db-collection@1517

@tanstack/electron-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/electron-db-sqlite-persistence@1517

@tanstack/expo-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/expo-db-sqlite-persistence@1517

@tanstack/node-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/node-db-sqlite-persistence@1517

@tanstack/offline-transactions

npm i https://pkg.pr.new/@tanstack/offline-transactions@1517

@tanstack/powersync-db-collection

npm i https://pkg.pr.new/@tanstack/powersync-db-collection@1517

@tanstack/query-db-collection

npm i https://pkg.pr.new/@tanstack/query-db-collection@1517

@tanstack/react-db

npm i https://pkg.pr.new/@tanstack/react-db@1517

@tanstack/react-native-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/react-native-db-sqlite-persistence@1517

@tanstack/rxdb-db-collection

npm i https://pkg.pr.new/@tanstack/rxdb-db-collection@1517

@tanstack/solid-db

npm i https://pkg.pr.new/@tanstack/solid-db@1517

@tanstack/svelte-db

npm i https://pkg.pr.new/@tanstack/svelte-db@1517

@tanstack/tauri-db-sqlite-persistence

npm i https://pkg.pr.new/@tanstack/tauri-db-sqlite-persistence@1517

@tanstack/trailbase-db-collection

npm i https://pkg.pr.new/@tanstack/trailbase-db-collection@1517

@tanstack/vue-db

npm i https://pkg.pr.new/@tanstack/vue-db@1517

commit: 72b31cc

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Size Change: +192 B (+0.17%)

Total Size: 114 kB

📦 View Changed
Filename Size Change
packages/db/dist/esm/indexes/basic-index.js 2.15 kB +102 B (+4.99%) 🔍
packages/db/dist/esm/indexes/btree-index.js 2.26 kB +90 B (+4.14%)
ℹ️ View Unchanged
Filename Size
packages/db/dist/esm/collection/change-events.js 1.39 kB
packages/db/dist/esm/collection/changes.js 1.38 kB
packages/db/dist/esm/collection/cleanup-queue.js 810 B
packages/db/dist/esm/collection/events.js 434 B
packages/db/dist/esm/collection/index.js 3.61 kB
packages/db/dist/esm/collection/indexes.js 1.99 kB
packages/db/dist/esm/collection/lifecycle.js 1.69 kB
packages/db/dist/esm/collection/mutations.js 2.47 kB
packages/db/dist/esm/collection/state.js 5.26 kB
packages/db/dist/esm/collection/subscription.js 3.74 kB
packages/db/dist/esm/collection/sync.js 2.88 kB
packages/db/dist/esm/collection/transaction-metadata.js 144 B
packages/db/dist/esm/deferred.js 207 B
packages/db/dist/esm/errors.js 4.92 kB
packages/db/dist/esm/event-emitter.js 748 B
packages/db/dist/esm/index.js 3 kB
packages/db/dist/esm/indexes/auto-index.js 830 B
packages/db/dist/esm/indexes/base-index.js 729 B
packages/db/dist/esm/indexes/index-registry.js 820 B
packages/db/dist/esm/indexes/reverse-index.js 538 B
packages/db/dist/esm/local-only.js 890 B
packages/db/dist/esm/local-storage.js 2.1 kB
packages/db/dist/esm/optimistic-action.js 359 B
packages/db/dist/esm/paced-mutations.js 496 B
packages/db/dist/esm/proxy.js 3.75 kB
packages/db/dist/esm/query/builder/functions.js 919 B
packages/db/dist/esm/query/builder/index.js 5.25 kB
packages/db/dist/esm/query/builder/ref-proxy.js 1.2 kB
packages/db/dist/esm/query/compiler/evaluators.js 1.62 kB
packages/db/dist/esm/query/compiler/expressions.js 430 B
packages/db/dist/esm/query/compiler/group-by.js 2.69 kB
packages/db/dist/esm/query/compiler/index.js 4.13 kB
packages/db/dist/esm/query/compiler/joins.js 2.34 kB
packages/db/dist/esm/query/compiler/order-by.js 1.72 kB
packages/db/dist/esm/query/compiler/select.js 1.11 kB
packages/db/dist/esm/query/effect.js 4.78 kB
packages/db/dist/esm/query/expression-helpers.js 1.43 kB
packages/db/dist/esm/query/ir.js 829 B
packages/db/dist/esm/query/live-query-collection.js 360 B
packages/db/dist/esm/query/live/collection-config-builder.js 7.88 kB
packages/db/dist/esm/query/live/collection-registry.js 264 B
packages/db/dist/esm/query/live/collection-subscriber.js 1.95 kB
packages/db/dist/esm/query/live/internal.js 145 B
packages/db/dist/esm/query/live/utils.js 1.64 kB
packages/db/dist/esm/query/optimizer.js 2.62 kB
packages/db/dist/esm/query/predicate-utils.js 2.97 kB
packages/db/dist/esm/query/query-once.js 359 B
packages/db/dist/esm/query/subset-dedupe.js 960 B
packages/db/dist/esm/scheduler.js 1.3 kB
packages/db/dist/esm/SortedMap.js 1.3 kB
packages/db/dist/esm/strategies/debounceStrategy.js 247 B
packages/db/dist/esm/strategies/queueStrategy.js 428 B
packages/db/dist/esm/strategies/throttleStrategy.js 246 B
packages/db/dist/esm/transactions.js 2.9 kB
packages/db/dist/esm/utils.js 927 B
packages/db/dist/esm/utils/array-utils.js 273 B
packages/db/dist/esm/utils/browser-polyfills.js 304 B
packages/db/dist/esm/utils/btree.js 5.61 kB
packages/db/dist/esm/utils/comparison.js 1.05 kB
packages/db/dist/esm/utils/cursor.js 457 B
packages/db/dist/esm/utils/index-optimization.js 1.54 kB
packages/db/dist/esm/utils/type-guards.js 157 B
packages/db/dist/esm/virtual-props.js 360 B

compressed-size-action::db-package-size

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Size Change: 0 B

Total Size: 4.24 kB

ℹ️ View Unchanged
Filename Size
packages/react-db/dist/esm/index.js 249 B
packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.32 kB
packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
packages/react-db/dist/esm/useLiveQueryEffect.js 355 B
packages/react-db/dist/esm/useLiveSuspenseQuery.js 567 B
packages/react-db/dist/esm/usePacedMutations.js 401 B

compressed-size-action::react-db-package-size

Both BasicIndex and BTreeIndex now keep a key->indexedValue map and use
it on remove/update instead of evaluating the caller-supplied oldItem.
add() also drops the key from its current bucket first so a missed
remove (or a second add) can't leave the key in two buckets at once.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kevin-dp kevin-dp changed the title test(db): a key in BasicIndex/BTreeIndex lives in at most one bucket fix(db): a key in BasicIndex/BTreeIndex lives in at most one bucket May 5, 2026
Use describe.each to run each test against both index implementations
instead of duplicating the BasicIndex and BTreeIndex variants by hand.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant