From faff15a0a6366f4158e2f168e8b35be3589b44b1 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:05:42 +0000 Subject: [PATCH 1/2] test: assert BTreeIndex.take handles undefined indexed values correctly (issue #1186) Co-Authored-By: Claude (claude-opus-4-5) --- .../db/tests/deterministic-ordering.test.ts | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/db/tests/deterministic-ordering.test.ts b/packages/db/tests/deterministic-ordering.test.ts index f25e3db93..09027251f 100644 --- a/packages/db/tests/deterministic-ordering.test.ts +++ b/packages/db/tests/deterministic-ordering.test.ts @@ -109,6 +109,40 @@ describe(`Deterministic Ordering`, () => { }) describe(`BTreeIndex`, () => { + it(`should handle undefined indexed values with take and limit`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index`, + ) + + // Add items where priority is undefined + index.add(`a`, { priority: undefined }) + index.add(`b`, { priority: undefined }) + index.add(`c`, { priority: 1 }) + + // take() with a limit should return results without hanging + const keys = index.take(2) + expect(keys).toEqual([`a`, `b`]) + }) + + it(`should handle undefined indexed values with takeReversed and limit`, () => { + const index = new BTreeIndex( + 1, + new PropRef([`priority`]), + `priority_index`, + ) + + // Add items where priority is undefined + index.add(`a`, { priority: undefined }) + index.add(`b`, { priority: undefined }) + index.add(`c`, { priority: 1 }) + + // takeReversed() with a limit should return results without hanging + const keys = index.takeReversed(2) + expect(keys).toEqual([`c`, `b`]) + }) + it(`should return keys in deterministic order when indexed values are equal`, () => { const index = new BTreeIndex( 1, From f4bb14da4672d38ceca6d07fb275a09997912384 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 16:29:33 +0000 Subject: [PATCH 2/2] test: add reproduction test for infinite loop with undefined + orderBy + limit Add tests that properly reproduce issue #1186 where currentStateAsChanges hangs when using orderBy + limit with undefined indexed values. The infinite loop occurs because: 1. nextHigherPair(undefined) returns the minimum pair [undefined, ...] 2. When the minimum key IS undefined, calling nextHigherPair(undefined) again with inclusive=false still returns the same pair, creating a loop Co-authored-by: Kevin --- .../db/tests/deterministic-ordering.test.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/packages/db/tests/deterministic-ordering.test.ts b/packages/db/tests/deterministic-ordering.test.ts index 09027251f..9e98ac161 100644 --- a/packages/db/tests/deterministic-ordering.test.ts +++ b/packages/db/tests/deterministic-ordering.test.ts @@ -428,5 +428,71 @@ describe(`Deterministic Ordering`, () => { const keys = changes?.map((c) => c.key) expect(keys).toEqual([`a`, `b`, `c`]) }) + + it(`should handle undefined indexed values with orderBy and limit without hanging`, () => { + type Item = { id: string; priority: number | undefined } + + const options = mockSyncCollectionOptions({ + id: `test-undefined-priority`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `a`, priority: undefined } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: undefined } }) + options.utils.commit() + + // This should hang on the base branch due to infinite loop in takeInternal + // when nextHigherPair(undefined) returns the same pair repeatedly + const changes = collection.currentStateAsChanges({ + orderBy: [ + { + expression: new PropRef([`priority`]), + compareOptions: { direction: `asc`, nulls: `last` }, + }, + ], + limit: 1, + }) + + // Should return the first item without hanging + const keys = changes?.map((c) => c.key) + expect(keys).toEqual([`a`]) + }) + + it(`should handle mixed undefined and defined values with orderBy and limit`, () => { + type Item = { id: string; priority: number | undefined } + + const options = mockSyncCollectionOptions({ + id: `test-mixed-undefined-priority`, + getKey: (item) => item.id, + initialData: [], + }) + + const collection = createCollection(options) + + options.utils.begin() + options.utils.write({ type: `insert`, value: { id: `a`, priority: undefined } }) + options.utils.write({ type: `insert`, value: { id: `b`, priority: 1 } }) + options.utils.write({ type: `insert`, value: { id: `c`, priority: undefined } }) + options.utils.write({ type: `insert`, value: { id: `d`, priority: 2 } }) + options.utils.commit() + + // With nulls: last, defined values should come first, then undefined values + const changes = collection.currentStateAsChanges({ + orderBy: [ + { + expression: new PropRef([`priority`]), + compareOptions: { direction: `asc`, nulls: `last` }, + }, + ], + limit: 3, + }) + + const keys = changes?.map((c) => c.key) + expect(keys).toEqual([`b`, `d`, `a`]) + }) }) })