Skip to content

Conversation

@samwillis
Copy link
Collaborator

Summary

This PR ensures deterministic iteration order for collections and indexes when multiple items have equal sort values. It builds on #957 which added stable tie-breaking for ORDER BY operations in the query engine.

Changes:

  • SortedMap: Added key-based tie-breaking when values compare as equal, and optimized to skip value comparison entirely when no comparator is provided
  • BTreeIndex: Keys within the same indexed value are now returned in deterministic sorted order, with fast paths for empty/single-key sets
  • CollectionStateManager: Collections now always use SortedMap for syncedData, ensuring deterministic iteration even without a custom compare function
  • Shared utility: Extracted compareKeys to utils/comparison.ts for reuse across modules

Problem

When multiple rows share the same indexed value (e.g., same priority), their iteration order was previously non-deterministic:

  • SortedMap returned any position among equal values
  • BTreeIndex.valueMap used Set which iterates in insertion order
  • Collections without a compare function used Map with insertion-order iteration

This caused issues with live queries using orderBy + limit where window operations (setWindow) could produce inconsistent results.

Solution

All three components now use key-based tie-breaking to ensure deterministic ordering:

  • When values compare as equal, keys are compared using compareKeys(a, b)
  • Strings sort before numbers, then lexicographically/numerically within type

Test plan

  • Added comprehensive test suite (deterministic-ordering.test.ts) covering:
    • SortedMap ordering with equal values
    • BTreeIndex take()/takeReversed() with equal indexed values
    • Collection iteration with and without compare function
    • currentStateAsChanges with orderBy and limit
  • All 1747 existing tests pass

Note: This PR is stacked on #957 and should be merged after it.

@changeset-bot
Copy link

changeset-bot bot commented Dec 3, 2025

🦋 Changeset detected

Latest commit: 3181eee

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

This PR includes changesets to release 13 packages
Name Type
@tanstack/db Patch
@tanstack/angular-db Patch
@tanstack/db-collection-e2e 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 Dec 3, 2025

More templates

@tanstack/angular-db

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

@tanstack/db

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

@tanstack/db-ivm

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

@tanstack/electric-db-collection

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

@tanstack/offline-transactions

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

@tanstack/powersync-db-collection

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

@tanstack/query-db-collection

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

@tanstack/react-db

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

@tanstack/rxdb-db-collection

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

@tanstack/solid-db

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

@tanstack/svelte-db

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

@tanstack/trailbase-db-collection

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

@tanstack/vue-db

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

commit: 3181eee

@github-actions
Copy link
Contributor

github-actions bot commented Dec 3, 2025

Size Change: +176 B (+0.2%)

Total Size: 87.3 kB

Filename Size Change
./packages/db/dist/esm/collection/state.js 3.42 kB -8 B (-0.23%)
./packages/db/dist/esm/indexes/btree-index.js 1.93 kB +58 B (+3.1%)
./packages/db/dist/esm/SortedMap.js 1.3 kB +126 B (+10.72%) ⚠️
ℹ️ View Unchanged
Filename Size
./packages/db/dist/esm/collection/change-events.js 1.39 kB
./packages/db/dist/esm/collection/changes.js 977 B
./packages/db/dist/esm/collection/events.js 388 B
./packages/db/dist/esm/collection/index.js 3.24 kB
./packages/db/dist/esm/collection/indexes.js 1.1 kB
./packages/db/dist/esm/collection/lifecycle.js 1.67 kB
./packages/db/dist/esm/collection/mutations.js 2.31 kB
./packages/db/dist/esm/collection/subscription.js 2.55 kB
./packages/db/dist/esm/collection/sync.js 2.37 kB
./packages/db/dist/esm/deferred.js 207 B
./packages/db/dist/esm/errors.js 4.19 kB
./packages/db/dist/esm/event-emitter.js 748 B
./packages/db/dist/esm/index.js 2.66 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/indexes/reverse-index.js 513 B
./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 3.96 kB
./packages/db/dist/esm/query/builder/ref-proxy.js 917 B
./packages/db/dist/esm/query/compiler/evaluators.js 1.35 kB
./packages/db/dist/esm/query/compiler/expressions.js 430 B
./packages/db/dist/esm/query/compiler/group-by.js 1.8 kB
./packages/db/dist/esm/query/compiler/index.js 1.96 kB
./packages/db/dist/esm/query/compiler/joins.js 2 kB
./packages/db/dist/esm/query/compiler/order-by.js 1.25 kB
./packages/db/dist/esm/query/compiler/select.js 1.07 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.33 kB
./packages/db/dist/esm/query/live/collection-registry.js 264 B
./packages/db/dist/esm/query/live/collection-subscriber.js 1.74 kB
./packages/db/dist/esm/query/live/internal.js 130 B
./packages/db/dist/esm/query/optimizer.js 2.56 kB
./packages/db/dist/esm/query/predicate-utils.js 2.91 kB
./packages/db/dist/esm/query/subset-dedupe.js 921 B
./packages/db/dist/esm/scheduler.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 881 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 852 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 Dec 3, 2025

Size Change: 0 B

Total Size: 3.35 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.12 kB
./packages/react-db/dist/esm/useLiveSuspenseQuery.js 431 B
./packages/react-db/dist/esm/usePacedMutations.js 401 B

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

@samwillis samwillis force-pushed the fix/deterministic-collection-ordering branch 4 times, most recently from 0c47d64 to 5f4b251 Compare December 3, 2025 11:25
@kevin-dp kevin-dp self-requested a review December 4, 2025 13:25
Copy link
Contributor

@kevin-dp kevin-dp left a comment

Choose a reason for hiding this comment

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

Perfect! Love how focused this PR is with small surgical fixes.

@samwillis samwillis force-pushed the fix/stable-order-by-tiebreaker branch 2 times, most recently from 1047564 to 4c5cda6 Compare December 5, 2025 14:13
Base automatically changed from fix/stable-order-by-tiebreaker to main December 5, 2025 14:24
@samwillis samwillis force-pushed the fix/deterministic-collection-ordering branch from 5f4b251 to 60c1709 Compare December 5, 2025 14:52
…exes

- SortedMap: add key-based tie-breaking for deterministic ordering
- SortedMap: optimize to skip value comparison when no comparator provided
- BTreeIndex: sort keys within same indexed value for deterministic order
- BTreeIndex: add fast paths for empty/single-key sets
- CollectionStateManager: always use SortedMap for deterministic iteration
- Extract compareKeys utility to utils/comparison.ts
- Add comprehensive tests for deterministic ordering behavior
@samwillis samwillis force-pushed the fix/deterministic-collection-ordering branch 2 times, most recently from 88ec7fd to 7bfa01d Compare December 5, 2025 15:46
@samwillis samwillis merged commit 09da081 into main Dec 5, 2025
7 checks passed
@samwillis samwillis deleted the fix/deterministic-collection-ordering branch December 5, 2025 15:52
@github-actions github-actions bot mentioned this pull request Dec 5, 2025
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.

3 participants