From 7bfa01d800f8738df32b974650df5a2fb9043f5e Mon Sep 17 00:00:00 2001 From: Sam Willis Date: Fri, 5 Dec 2025 14:48:23 +0000 Subject: [PATCH 1/2] fix(db): ensure deterministic iteration order for collections and indexes - 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 --- .../deterministic-collection-ordering.md | 23 + packages/db/src/SortedMap.ts | 79 ++-- packages/db/src/collection/state.ts | 11 +- packages/db/src/indexes/btree-index.ts | 14 +- .../db/tests/deterministic-ordering.test.ts | 398 ++++++++++++++++++ .../tests/powersync.test.ts | 33 +- packages/react-db/tests/useLiveQuery.test.tsx | 11 +- 7 files changed, 504 insertions(+), 65 deletions(-) create mode 100644 .changeset/deterministic-collection-ordering.md create mode 100644 packages/db/tests/deterministic-ordering.test.ts diff --git a/.changeset/deterministic-collection-ordering.md b/.changeset/deterministic-collection-ordering.md new file mode 100644 index 000000000..2c7ec3f7f --- /dev/null +++ b/.changeset/deterministic-collection-ordering.md @@ -0,0 +1,23 @@ +--- +"@tanstack/db": patch +--- + +Ensure deterministic iteration order for collections and indexes. + +**SortedMap improvements:** + +- Added key-based tie-breaking when values compare as equal, ensuring deterministic ordering +- Optimized to skip value comparison entirely when no comparator is provided (key-only sorting) +- Extracted `compareKeys` utility to `utils/comparison.ts` for reuse + +**BTreeIndex improvements:** + +- Keys within the same indexed value are now returned in deterministic sorted order +- Optimized with fast paths for empty sets and single-key sets to avoid unnecessary allocations + +**CollectionStateManager changes:** + +- Collections now always use `SortedMap` for `syncedData`, ensuring deterministic iteration order +- When no `compare` function is provided, entries are sorted by key only + +This ensures that live queries with `orderBy` and `limit` produce stable, deterministic results even when multiple rows have equal sort values. diff --git a/packages/db/src/SortedMap.ts b/packages/db/src/SortedMap.ts index 86bc9a552..34c7375af 100644 --- a/packages/db/src/SortedMap.ts +++ b/packages/db/src/SortedMap.ts @@ -1,62 +1,81 @@ +import { compareKeys } from "@tanstack/db-ivm" + /** * A Map implementation that keeps its entries sorted based on a comparator function - * @template TKey - The type of keys in the map + * @template TKey - The type of keys in the map (must be string | number) * @template TValue - The type of values in the map */ -export class SortedMap { +export class SortedMap { private map: Map private sortedKeys: Array - private comparator: (a: TValue, b: TValue) => number + private comparator: ((a: TValue, b: TValue) => number) | undefined /** * Creates a new SortedMap instance * - * @param comparator - Optional function to compare values for sorting + * @param comparator - Optional function to compare values for sorting. + * If not provided, entries are sorted by key only. */ constructor(comparator?: (a: TValue, b: TValue) => number) { this.map = new Map() this.sortedKeys = [] - this.comparator = comparator || this.defaultComparator - } - - /** - * Default comparator function used when none is provided - * - * @param a - First value to compare - * @param b - Second value to compare - * @returns -1 if a < b, 1 if a > b, 0 if equal - */ - private defaultComparator(a: TValue, b: TValue): number { - if (a < b) return -1 - if (a > b) return 1 - return 0 + this.comparator = comparator } /** * Finds the index where a key-value pair should be inserted to maintain sort order. - * Uses binary search to find the correct position based on the value. - * Hence, it is in O(log n) time. + * Uses binary search to find the correct position based on the value (if comparator provided), + * with key-based tie-breaking for deterministic ordering when values compare as equal. + * If no comparator is provided, sorts by key only. + * Runs in O(log n) time. * - * @param key - The key to find position for - * @param value - The value to compare against + * @param key - The key to find position for (used as tie-breaker or primary sort when no comparator) + * @param value - The value to compare against (only used if comparator is provided) * @returns The index where the key should be inserted */ - private indexOf(value: TValue): number { + private indexOf(key: TKey, value: TValue): number { let left = 0 let right = this.sortedKeys.length + // Fast path: no comparator means sort by key only + if (!this.comparator) { + while (left < right) { + const mid = Math.floor((left + right) / 2) + const midKey = this.sortedKeys[mid]! + const keyComparison = compareKeys(key, midKey) + if (keyComparison < 0) { + right = mid + } else if (keyComparison > 0) { + left = mid + 1 + } else { + return mid + } + } + return left + } + + // With comparator: sort by value first, then key as tie-breaker while (left < right) { const mid = Math.floor((left + right) / 2) const midKey = this.sortedKeys[mid]! const midValue = this.map.get(midKey)! - const comparison = this.comparator(value, midValue) + const valueComparison = this.comparator(value, midValue) - if (comparison < 0) { + if (valueComparison < 0) { right = mid - } else if (comparison > 0) { + } else if (valueComparison > 0) { left = mid + 1 } else { - return mid + // Values are equal, use key as tie-breaker for deterministic ordering + const keyComparison = compareKeys(key, midKey) + if (keyComparison < 0) { + right = mid + } else if (keyComparison > 0) { + left = mid + 1 + } else { + // Same key (shouldn't happen during insert, but handle for lookups) + return mid + } } } @@ -74,12 +93,12 @@ export class SortedMap { if (this.map.has(key)) { // Need to remove the old key from the sorted keys array const oldValue = this.map.get(key)! - const oldIndex = this.indexOf(oldValue) + const oldIndex = this.indexOf(key, oldValue) this.sortedKeys.splice(oldIndex, 1) } // Insert the new key at the correct position - const index = this.indexOf(value) + const index = this.indexOf(key, value) this.sortedKeys.splice(index, 0, key) this.map.set(key, value) @@ -106,7 +125,7 @@ export class SortedMap { delete(key: TKey): boolean { if (this.map.has(key)) { const oldValue = this.map.get(key) - const index = this.indexOf(oldValue!) + const index = this.indexOf(key, oldValue!) this.sortedKeys.splice(index, 1) return this.map.delete(key) } diff --git a/packages/db/src/collection/state.ts b/packages/db/src/collection/state.ts index 18a015e1b..f183aa84e 100644 --- a/packages/db/src/collection/state.ts +++ b/packages/db/src/collection/state.ts @@ -43,7 +43,7 @@ export class CollectionStateManager< public pendingSyncedTransactions: Array< PendingSyncedTransaction > = [] - public syncedData: Map | SortedMap + public syncedData: SortedMap public syncedMetadata = new Map() // Optimistic state tracking - make public for testing @@ -69,12 +69,9 @@ export class CollectionStateManager< a.compareCreatedAt(b), ) - // Set up data storage with optional comparison function - if (config.compare) { - this.syncedData = new SortedMap(config.compare) - } else { - this.syncedData = new Map() - } + // Set up data storage - always use SortedMap for deterministic iteration. + // If a custom compare function is provided, use it; otherwise entries are sorted by key only. + this.syncedData = new SortedMap(config.compare) } setDeps(deps: { diff --git a/packages/db/src/indexes/btree-index.ts b/packages/db/src/indexes/btree-index.ts index b0a22d29a..2392fbcad 100644 --- a/packages/db/src/indexes/btree-index.ts +++ b/packages/db/src/indexes/btree-index.ts @@ -1,3 +1,4 @@ +import { compareKeys } from '@tanstack/db-ivm' import { BTree } from '../utils/btree.js' import { defaultComparator, normalizeValue } from '../utils/comparison.js' import { BaseIndex } from './base-index.js' @@ -262,6 +263,7 @@ export class BTreeIndex< nextPair: (k?: any) => [any, any] | undefined, from?: any, filterFn?: (key: TKey) => boolean, + reversed: boolean = false, ): Array { const keysInResult: Set = new Set() const result: Array = [] @@ -271,10 +273,12 @@ export class BTreeIndex< while ((pair = nextPair(key)) !== undefined && result.length < n) { key = pair[0] const keys = this.valueMap.get(key) - if (keys) { - const it = keys.values() - let ks: TKey | undefined - while (result.length < n && (ks = it.next().value)) { + if (keys && keys.size > 0) { + // Sort keys for deterministic order, reverse if needed + const sorted = Array.from(keys).sort(compareKeys) + if (reversed) sorted.reverse() + for (const ks of sorted) { + if (result.length >= n) break if (!keysInResult.has(ks) && (filterFn?.(ks) ?? true)) { result.push(ks) keysInResult.add(ks) @@ -309,7 +313,7 @@ export class BTreeIndex< filterFn?: (key: TKey) => boolean, ): Array { const nextPair = (k?: any) => this.orderedEntries.nextLowerPair(k) - return this.takeInternal(n, nextPair, from, filterFn) + return this.takeInternal(n, nextPair, from, filterFn, true) } /** diff --git a/packages/db/tests/deterministic-ordering.test.ts b/packages/db/tests/deterministic-ordering.test.ts new file mode 100644 index 000000000..ef6409453 --- /dev/null +++ b/packages/db/tests/deterministic-ordering.test.ts @@ -0,0 +1,398 @@ +import { describe, expect, it } from "vitest" +import { SortedMap } from "../src/SortedMap" +import { BTreeIndex } from "../src/indexes/btree-index" +import { createCollection } from "../src/collection/index.js" +import { PropRef } from "../src/query/ir" +import { mockSyncCollectionOptions } from "./utils" + +/** + * These tests verify deterministic ordering behavior when values compare as equal. + * + * The issue: When multiple items have the same "sort value" (e.g., same priority), + * their relative ordering should be deterministic and stable based on their key. + * Without key-based tie-breaking, the order depends on insertion order, which + * can vary between page loads, sync operations, etc. + */ + +describe(`Deterministic Ordering`, () => { + describe(`SortedMap`, () => { + it(`should maintain deterministic order when values are equal`, () => { + // All values are the same (priority = 1), so they compare as equal + const map = new SortedMap( + (a, b) => a.priority - b.priority + ) + + // Insert in "random" order + map.set(`c`, { priority: 1 }) + map.set(`a`, { priority: 1 }) + map.set(`b`, { priority: 1 }) + + // With key-based tie-breaking, should always iterate in key order: a, b, c + const keys = Array.from(map.keys()) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should maintain deterministic order with mixed equal and different values`, () => { + const map = new SortedMap( + (a, b) => a.priority - b.priority + ) + + // Mix of equal and different priorities + map.set(`d`, { priority: 2 }) + map.set(`c`, { priority: 1 }) + map.set(`a`, { priority: 1 }) + map.set(`e`, { priority: 2 }) + map.set(`b`, { priority: 1 }) + + // Expected: priority 1 items (a, b, c) sorted by key, then priority 2 items (d, e) sorted by key + const keys = Array.from(map.keys()) + expect(keys).toEqual([`a`, `b`, `c`, `d`, `e`]) + }) + + it(`should maintain deterministic order with numeric keys`, () => { + const map = new SortedMap( + (a, b) => a.priority - b.priority + ) + + map.set(30, { priority: 1 }) + map.set(10, { priority: 1 }) + map.set(20, { priority: 1 }) + + const keys = Array.from(map.keys()) + expect(keys).toEqual([10, 20, 30]) + }) + + it(`should maintain deterministic order after updates`, () => { + const map = new SortedMap( + (a, b) => a.priority - b.priority + ) + + map.set(`c`, { priority: 1 }) + map.set(`a`, { priority: 1 }) + map.set(`b`, { priority: 1 }) + + // Update 'b' with same priority + map.set(`b`, { priority: 1 }) + + const keys = Array.from(map.keys()) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should maintain deterministic order after delete and re-insert`, () => { + const map = new SortedMap( + (a, b) => a.priority - b.priority + ) + + map.set(`c`, { priority: 1 }) + map.set(`a`, { priority: 1 }) + map.set(`b`, { priority: 1 }) + + map.delete(`b`) + map.set(`b`, { priority: 1 }) + + const keys = Array.from(map.keys()) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should use key as tie-breaker even without custom comparator`, () => { + // When no comparator is provided, all items have "equal" sort value (default behavior) + // They should still be ordered by key + const map = new SortedMap() + + map.set(`c`, { name: `Charlie` }) + map.set(`a`, { name: `Alice` }) + map.set(`b`, { name: `Bob` }) + + const keys = Array.from(map.keys()) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + }) + + describe(`BTreeIndex`, () => { + it(`should return keys in deterministic order when indexed values are equal`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + // All have same priority + index.add(`c`, { priority: 1 }) + index.add(`a`, { priority: 1 }) + index.add(`b`, { priority: 1 }) + + // take() should return keys in key-sorted order when priorities are equal + const keys = index.take(3) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should return keys in deterministic order with mixed equal and different values`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + index.add(`d`, { priority: 2 }) + index.add(`c`, { priority: 1 }) + index.add(`a`, { priority: 1 }) + index.add(`e`, { priority: 2 }) + index.add(`b`, { priority: 1 }) + + // take() should return priority 1 keys sorted by key, then priority 2 keys sorted by key + const keys = index.take(5) + expect(keys).toEqual([`a`, `b`, `c`, `d`, `e`]) + }) + + it(`should return keys in deterministic order with numeric keys`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + index.add(30, { priority: 1 }) + index.add(10, { priority: 1 }) + index.add(20, { priority: 1 }) + + const keys = index.take(3) + expect(keys).toEqual([10, 20, 30]) + }) + + it(`should return keys in deterministic order for takeReversed`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + index.add(`c`, { priority: 1 }) + index.add(`a`, { priority: 1 }) + index.add(`b`, { priority: 1 }) + + // takeReversed should return keys in reverse key order when priorities are equal + const keys = index.takeReversed(3) + expect(keys).toEqual([`c`, `b`, `a`]) + }) + + it(`should maintain deterministic order after remove and re-add`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + index.add(`c`, { priority: 1 }) + index.add(`a`, { priority: 1 }) + index.add(`b`, { priority: 1 }) + + index.remove(`b`, { priority: 1 }) + index.add(`b`, { priority: 1 }) + + const keys = index.take(3) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should return keys in deterministic order with take from cursor across different values`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index` + ) + + // Add keys with different priorities + index.add(`e`, { priority: 2 }) + index.add(`c`, { priority: 1 }) + index.add(`a`, { priority: 1 }) + index.add(`f`, { priority: 2 }) + index.add(`d`, { priority: 2 }) + index.add(`b`, { priority: 1 }) + + // First batch - should get priority 1 keys in key order + const firstBatch = index.take(3) + expect(firstBatch).toEqual([`a`, `b`, `c`]) + + // Continue from cursor value 1 (exclusive) - should get priority 2 keys in key order + const secondBatch = index.take(3, 1) + expect(secondBatch).toEqual([`d`, `e`, `f`]) + }) + }) + + describe(`Collection iteration`, () => { + it(`should iterate in deterministic order when compare function returns equal`, () => { + type Item = { id: string; priority: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection({ + ...options, + // Compare by priority only - items with same priority compare as equal + compare: (a, b) => a.priority - b.priority, + }) + + // Insert via sync in "random" order + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `c`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `a`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.commit() + + // Should iterate in key order when priorities are equal + const keys = [...collection.keys()] + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should iterate in deterministic order with mixed priorities`, () => { + type Item = { id: string; priority: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection-mixed`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection({ + ...options, + compare: (a, b) => a.priority - b.priority, + }) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `d`, priority: 2 } }) + options.utils.write({ type: `insert`, value: { id: `c`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `a`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `e`, priority: 2 } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.commit() + + // Priority 1 items sorted by key, then priority 2 items sorted by key + const keys = [...collection.keys()] + expect(keys).toEqual([`a`, `b`, `c`, `d`, `e`]) + }) + + it(`should maintain deterministic order after incremental sync`, () => { + type Item = { id: string; priority: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection-incremental`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection({ + ...options, + compare: (a, b) => a.priority - b.priority, + }) + + // First sync batch + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `c`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `a`, priority: 1 } }) + options.utils.commit() + + // Second sync batch (simulating incremental load) + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.commit() + + // Order should be deterministic regardless of sync batch order + const keys = [...collection.keys()] + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should maintain deterministic order when collection has no compare function`, () => { + // Even without a compare function, iteration order should be deterministic (by key) + type Item = { id: string; name: string } + + const options = mockSyncCollectionOptions({ + id: `test-collection-no-compare`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ + type: `insert`, + value: { id: `c`, name: `Charlie` }, + }) + options.utils.write({ type: `insert`, value: { id: `a`, name: `Alice` } }) + options.utils.write({ type: `insert`, value: { id: `b`, name: `Bob` } }) + options.utils.commit() + + // Without compare function, should still iterate in key order + const keys = [...collection.keys()] + expect(keys).toEqual([`a`, `b`, `c`]) + }) + }) + + describe(`Collection currentStateAsChanges with orderBy`, () => { + it(`should return changes in deterministic order when orderBy values are equal`, () => { + type Item = { id: string; priority: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection-changes`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `c`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `a`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.commit() + + const changes = collection.currentStateAsChanges({ + orderBy: [ + { + expression: new PropRef([`priority`]), + compareOptions: { direction: `asc`, nulls: `last` }, + }, + ], + }) + + const keys = changes?.map((c) => c.key) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + + it(`should return changes in deterministic order with limit`, () => { + type Item = { id: string; priority: number } + + const options = mockSyncCollectionOptions({ + id: `test-collection-changes-limit`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `e`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `c`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `a`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `d`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.commit() + + const changes = collection.currentStateAsChanges({ + orderBy: [ + { + expression: new PropRef([`priority`]), + compareOptions: { direction: `asc`, nulls: `last` }, + }, + ], + limit: 3, + }) + + // First 3 in key order: a, b, c + const keys = changes?.map((c) => c.key) + expect(keys).toEqual([`a`, `b`, `c`]) + }) + }) +}) diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index c1712753b..4bdfc337f 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -81,10 +81,11 @@ describe(`PowerSync Integration`, () => { // Verify the collection state contains our items expect(collection.size).toBe(3) - expect(collection.toArray.map((entry) => entry.name)).deep.equals([ + // Sort by name since keys are random UUIDs + expect(collection.toArray.map((entry) => entry.name).sort()).deep.equals([ `one`, - `two`, `three`, + `two`, ]) }) @@ -110,12 +111,10 @@ describe(`PowerSync Integration`, () => { await vi.waitFor( () => { expect(collection.size).toBe(4) - expect(collection.toArray.map((entry) => entry.name)).deep.equals([ - `one`, - `two`, - `three`, - `four`, - ]) + // Sort by name since keys are random UUIDs + expect( + collection.toArray.map((entry) => entry.name).sort() + ).deep.equals([`four`, `one`, `three`, `two`]) }, { timeout: 1000 }, ) @@ -129,11 +128,10 @@ describe(`PowerSync Integration`, () => { await vi.waitFor( () => { expect(collection.size).toBe(3) - expect(collection.toArray.map((entry) => entry.name)).deep.equals([ - `one`, - `three`, - `four`, - ]) + // Sort by name since keys are random UUIDs + expect( + collection.toArray.map((entry) => entry.name).sort() + ).deep.equals([`four`, `one`, `three`]) }, { timeout: 1000 }, ) @@ -148,11 +146,10 @@ describe(`PowerSync Integration`, () => { await vi.waitFor( () => { expect(collection.size).toBe(3) - expect(collection.toArray.map((entry) => entry.name)).deep.equals([ - `updated`, - `three`, - `four`, - ]) + // Sort by name since keys are random UUIDs + expect( + collection.toArray.map((entry) => entry.name).sort() + ).deep.equals([`four`, `three`, `updated`]) }, { timeout: 1000 }, ) diff --git a/packages/react-db/tests/useLiveQuery.test.tsx b/packages/react-db/tests/useLiveQuery.test.tsx index b33227b9f..cbaf7fe97 100644 --- a/packages/react-db/tests/useLiveQuery.test.tsx +++ b/packages/react-db/tests/useLiveQuery.test.tsx @@ -2247,17 +2247,18 @@ describe(`Query Collections`, () => { expect(result.current.collection).toBeDefined() expect(result.current.status).toBeDefined() + // Results are in deterministic key order (id: 1 before id: 2) expect(result.current.data).toMatchObject([ - { - id: `2`, - name: `Jane Doe`, - age: 25, - }, { id: `1`, name: `John Doe`, age: 30, }, + { + id: `2`, + name: `Jane Doe`, + age: 25, + }, ]) // Switch to using pre-created collection From 3181eee09c9ee2b1432eafc1a42885d79a40bff8 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 5 Dec 2025 15:47:24 +0000 Subject: [PATCH 2/2] ci: apply automated fixes --- .../deterministic-collection-ordering.md | 2 +- packages/db/src/SortedMap.ts | 2 +- .../db/tests/deterministic-ordering.test.ts | 34 +++++++++---------- .../tests/powersync.test.ts | 6 ++-- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.changeset/deterministic-collection-ordering.md b/.changeset/deterministic-collection-ordering.md index 2c7ec3f7f..d420c751c 100644 --- a/.changeset/deterministic-collection-ordering.md +++ b/.changeset/deterministic-collection-ordering.md @@ -1,5 +1,5 @@ --- -"@tanstack/db": patch +'@tanstack/db': patch --- Ensure deterministic iteration order for collections and indexes. diff --git a/packages/db/src/SortedMap.ts b/packages/db/src/SortedMap.ts index 34c7375af..3d59ad687 100644 --- a/packages/db/src/SortedMap.ts +++ b/packages/db/src/SortedMap.ts @@ -1,4 +1,4 @@ -import { compareKeys } from "@tanstack/db-ivm" +import { compareKeys } from '@tanstack/db-ivm' /** * A Map implementation that keeps its entries sorted based on a comparator function diff --git a/packages/db/tests/deterministic-ordering.test.ts b/packages/db/tests/deterministic-ordering.test.ts index ef6409453..f25e3db93 100644 --- a/packages/db/tests/deterministic-ordering.test.ts +++ b/packages/db/tests/deterministic-ordering.test.ts @@ -1,9 +1,9 @@ -import { describe, expect, it } from "vitest" -import { SortedMap } from "../src/SortedMap" -import { BTreeIndex } from "../src/indexes/btree-index" -import { createCollection } from "../src/collection/index.js" -import { PropRef } from "../src/query/ir" -import { mockSyncCollectionOptions } from "./utils" +import { describe, expect, it } from 'vitest' +import { SortedMap } from '../src/SortedMap' +import { BTreeIndex } from '../src/indexes/btree-index' +import { createCollection } from '../src/collection/index.js' +import { PropRef } from '../src/query/ir' +import { mockSyncCollectionOptions } from './utils' /** * These tests verify deterministic ordering behavior when values compare as equal. @@ -19,7 +19,7 @@ describe(`Deterministic Ordering`, () => { it(`should maintain deterministic order when values are equal`, () => { // All values are the same (priority = 1), so they compare as equal const map = new SortedMap( - (a, b) => a.priority - b.priority + (a, b) => a.priority - b.priority, ) // Insert in "random" order @@ -34,7 +34,7 @@ describe(`Deterministic Ordering`, () => { it(`should maintain deterministic order with mixed equal and different values`, () => { const map = new SortedMap( - (a, b) => a.priority - b.priority + (a, b) => a.priority - b.priority, ) // Mix of equal and different priorities @@ -51,7 +51,7 @@ describe(`Deterministic Ordering`, () => { it(`should maintain deterministic order with numeric keys`, () => { const map = new SortedMap( - (a, b) => a.priority - b.priority + (a, b) => a.priority - b.priority, ) map.set(30, { priority: 1 }) @@ -64,7 +64,7 @@ describe(`Deterministic Ordering`, () => { it(`should maintain deterministic order after updates`, () => { const map = new SortedMap( - (a, b) => a.priority - b.priority + (a, b) => a.priority - b.priority, ) map.set(`c`, { priority: 1 }) @@ -80,7 +80,7 @@ describe(`Deterministic Ordering`, () => { it(`should maintain deterministic order after delete and re-insert`, () => { const map = new SortedMap( - (a, b) => a.priority - b.priority + (a, b) => a.priority - b.priority, ) map.set(`c`, { priority: 1 }) @@ -113,7 +113,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) // All have same priority @@ -130,7 +130,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) index.add(`d`, { priority: 2 }) @@ -148,7 +148,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) index.add(30, { priority: 1 }) @@ -163,7 +163,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) index.add(`c`, { priority: 1 }) @@ -179,7 +179,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) index.add(`c`, { priority: 1 }) @@ -197,7 +197,7 @@ describe(`Deterministic Ordering`, () => { const index = new BTreeIndex( 1, new PropRef([`priority`]), - `priority_index` + `priority_index`, ) // Add keys with different priorities diff --git a/packages/powersync-db-collection/tests/powersync.test.ts b/packages/powersync-db-collection/tests/powersync.test.ts index 4bdfc337f..de946be38 100644 --- a/packages/powersync-db-collection/tests/powersync.test.ts +++ b/packages/powersync-db-collection/tests/powersync.test.ts @@ -113,7 +113,7 @@ describe(`PowerSync Integration`, () => { expect(collection.size).toBe(4) // Sort by name since keys are random UUIDs expect( - collection.toArray.map((entry) => entry.name).sort() + collection.toArray.map((entry) => entry.name).sort(), ).deep.equals([`four`, `one`, `three`, `two`]) }, { timeout: 1000 }, @@ -130,7 +130,7 @@ describe(`PowerSync Integration`, () => { expect(collection.size).toBe(3) // Sort by name since keys are random UUIDs expect( - collection.toArray.map((entry) => entry.name).sort() + collection.toArray.map((entry) => entry.name).sort(), ).deep.equals([`four`, `one`, `three`]) }, { timeout: 1000 }, @@ -148,7 +148,7 @@ describe(`PowerSync Integration`, () => { expect(collection.size).toBe(3) // Sort by name since keys are random UUIDs expect( - collection.toArray.map((entry) => entry.name).sort() + collection.toArray.map((entry) => entry.name).sort(), ).deep.equals([`four`, `three`, `updated`]) }, { timeout: 1000 },