Skip to content

Conversation

@kevin-dp
Copy link
Contributor

@kevin-dp kevin-dp commented Jan 29, 2026

Fix: Infinite loop in BTreeIndex.takeInternal when indexed values are undefined (fixes #1186)

Problem

When a collection contains items where an indexed field evaluates to undefined, and the query uses both .orderBy() and .limit(), the BTreeIndex.takeInternal method enters an infinite loop.

Root Cause:

The underlying BTree implementation uses undefined as a special parameter value meaning "start from the beginning" (for nextHigherPair) or "start from the end" (for nextLowerPair). This creates an ambiguity when the actual indexed value is undefined:

  1. takeInternal calls nextHigherPair(undefined) to get the first element
  2. The BTree returns the minimum pair, which has key undefined (the actual indexed value)
  3. takeInternal sets key = pair[0] which is undefined
  4. On the next iteration, it calls nextHigherPair(undefined)
  5. The BTree interprets this as "start from the beginning" again
  6. The same pair is returned → infinite loop

Solution

1. New explicit methods for starting from beginning/end

Added two new methods to clearly distinguish between "start from a value" and "start from the beginning/end":

  • takeFromStart(n, filterFn?) - Returns the first n items from the beginning
  • takeReversedFromEnd(n, filterFn?) - Returns the last n items from the end

The existing take(n, from, filterFn?) and takeReversed(n, from, filterFn?) methods now require a from value (which can be undefined as an actual indexed value).

2. Sentinel value for undefined

Since the BTree cannot store undefined as a key (it has special meaning), we normalize undefined values to a sentinel string __TS_DB_BTREE_UNDEFINED_VALUE__:

  • When adding/removing/looking up values in the index, undefined is converted to the sentinel
  • This allows the BTree to distinguish between "no key provided" (undefined parameter) and "the key is the undefined value" (sentinel string)

3. Comparison function wrapper

The BTree's comparison function is wrapped to convert the sentinel back to undefined before comparison. This ensures that the sentinel compares exactly as undefined would (respecting nulls-first/nulls-last ordering).

Changes

  • comparison.ts: Added UNDEFINED_SENTINEL constant, normalizeValueForBTree now converts undefined to sentinel, added denormalizeUndefined function
  • btree-index.ts: Added takeFromStart and takeReversedFromEnd methods, wrapped comparator to denormalize sentinel before comparison
  • base-index.ts: Updated interface and abstract class with new method signatures
  • reverse-index.ts: Added new methods that delegate appropriately
  • Updated call sites in subscription.ts and change-events.ts to use the new methods where appropriate

claude and others added 6 commits January 27, 2026 17:31
…lues (issue #1186)

This test demonstrates the infinite loop bug that occurs when calling
take() on a BTreeIndex containing items with undefined indexed values.

The bug is in takeInternal() where nextHigherPair(undefined) returns the
minimum pair [undefined, undefined], then key is set to pair[0] (undefined),
causing the same pair to be returned infinitely since the while condition
(pair !== undefined) is always true for arrays.

https://claude.ai/code/session_01RKBKXMoKVe1hSXGy3VVEFo
… from the start/end. Also introduce a sentinel such that we never store undefined as a key in the btree.
@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: 57b91d5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 12 packages
Name Type
@tanstack/db Patch
@tanstack/angular-db Patch
@tanstack/electric-db-collection Patch
@tanstack/offline-transactions Patch
@tanstack/powersync-db-collection Patch
@tanstack/query-db-collection Patch
@tanstack/react-db Patch
@tanstack/rxdb-db-collection Patch
@tanstack/solid-db Patch
@tanstack/svelte-db Patch
@tanstack/trailbase-db-collection Patch
@tanstack/vue-db Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 29, 2026

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 57b91d5

@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

Size Change: +391 B (+0.43%)

Total Size: 91.3 kB

Filename Size Change
./packages/db/dist/esm/collection/change-events.js 1.39 kB +3 B (+0.22%)
./packages/db/dist/esm/collection/subscription.js 3.64 kB +19 B (+0.52%)
./packages/db/dist/esm/indexes/btree-index.js 2.17 kB +244 B (+12.65%) ⚠️
./packages/db/dist/esm/indexes/reverse-index.js 538 B +25 B (+4.87%) 🔍
./packages/db/dist/esm/utils/comparison.js 952 B +100 B (+11.74%) ⚠️
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/changes.js 1.19 kB
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.32 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.68 kB
./packages/db/dist/esm/collection/mutations.js 2.34 kB
./packages/db/dist/esm/collection/state.js 3.49 kB
./packages/db/dist/esm/collection/sync.js 2.41 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.7 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.69 kB
./packages/db/dist/esm/indexes/auto-index.js 742 B
./packages/db/dist/esm/indexes/base-index.js 766 B
./packages/db/dist/esm/indexes/lazy-index.js 1.1 kB
./packages/db/dist/esm/local-only.js 837 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 733 B
./packages/db/dist/esm/query/builder/index.js 4.09 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 1.05 kB
./packages/db/dist/esm/query/compiler/evaluators.js 1.42 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.81 kB
./packages/db/dist/esm/query/compiler/index.js 2.02 kB
./packages/db/dist/esm/query/compiler/joins.js 2.07 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.45 kB
./packages/db/dist/esm/query/compiler/select.js 1.06 kB
./packages/db/dist/esm/query/expression-helpers.js 1.43 kB
./packages/db/dist/esm/query/ir.js 673 B
./packages/db/dist/esm/query/live-query-collection.js 360 B
./packages/db/dist/esm/query/live/collection-config-builder.js 5.42 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 1.93 kB
./packages/db/dist/esm/query/live/internal.js 145 B
./packages/db/dist/esm/query/optimizer.js 2.56 kB
./packages/db/dist/esm/query/predicate-utils.js 2.97 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 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 924 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/cursor.js 457 B
./packages/db/dist/esm/utils/index-optimization.js 1.51 kB
./packages/db/dist/esm/utils/type-guards.js 157 B

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

@github-actions
Copy link
Contributor

github-actions bot commented Jan 29, 2026

Size Change: 0 B

Total Size: 3.7 kB

ℹ️ View Unchanged
Filename Size
./packages/react-db/dist/esm/index.js 225 B
./packages/react-db/dist/esm/useLiveInfiniteQuery.js 1.17 kB
./packages/react-db/dist/esm/useLiveQuery.js 1.34 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 559 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

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

Copy link
Collaborator

@KyleAMathews KyleAMathews left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@KyleAMathews KyleAMathews merged commit 43c7c9d into main Jan 29, 2026
7 checks passed
@KyleAMathews KyleAMathews deleted the kevin/fix-infinite-takeInternal-loop branch January 29, 2026 16:29
@github-actions github-actions bot mentioned this pull request Jan 29, 2026
@github-actions
Copy link
Contributor

🎉 This PR has been released!

Thank you for your contribution!

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.

Infinite loop in BTreeIndex.takeInternal when using orderBy and limit with undefined indexed values

4 participants