From 361bd6b6c0d1a9511d1d8c1ddbd4332475385594 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Jan 2026 00:10:04 +0000 Subject: [PATCH 1/2] test: add failing test for OR with mixed indexed/non-indexed fields This test demonstrates a bug where using `or()` with one indexed field and one non-indexed field causes items matching the non-indexed condition to be silently dropped. Example: `or(eq(element.id, itemId), eq(element.group_id, itemId))` where 'id' has an index but 'group_id' does not. Expected: Items matching EITHER condition should be returned Actual: Only items matching the indexed condition are returned Root cause is in `packages/db/src/utils/index-optimization.ts`: - `canOptimizeOrExpression` returns true if ANY operand can be optimized - `optimizeOrExpression` only collects results from optimizable branches - Non-optimizable branches are silently dropped The fix should change OR optimization to require ALL branches be optimizable, falling back to full scan otherwise. https://claude.ai/code/session_01R16FFtQbQr1VXjEVCYAeUk --- packages/db/tests/collection-indexes.test.ts | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/db/tests/collection-indexes.test.ts b/packages/db/tests/collection-indexes.test.ts index a8e3896fd..df20d2bc7 100644 --- a/packages/db/tests/collection-indexes.test.ts +++ b/packages/db/tests/collection-indexes.test.ts @@ -788,6 +788,42 @@ describe(`Collection Indexes`, () => { }) }) + it(`should return all matching items for OR with mixed indexed/non-indexed fields`, () => { + // BUG: When using OR with one indexed field and one non-indexed field, + // items matching the non-indexed condition are silently dropped. + // This test demonstrates the bug reported by users: + // or(eq(element.id, itemId), eq(element.group_id, itemId)) + // where 'id' has an index but 'group_id' does not. + // + // Expected: Items matching EITHER condition should be returned + // Actual (bug): Only items matching the indexed condition are returned + + withIndexTracking(collection, (_tracker) => { + // age has an index (created in beforeEach), name does NOT have an index + // Query: Find items where age=25 OR name='Bob' + // Expected results: + // - Alice (age=25) - matches via indexed field + // - Bob (name='Bob') - should match via non-indexed field but gets DROPPED due to bug + const result = collection.currentStateAsChanges({ + where: or( + eq(new PropRef([`age`]), 25), // Indexed - Alice matches + eq(new PropRef([`name`]), `Bob`), // NOT indexed - Bob should match + ), + })! + + // This is the EXPECTED behavior - both Alice AND Bob should be returned + // The bug causes only Alice to be returned (Bob is silently dropped) + expect(result).toHaveLength(2) // Should be Alice AND Bob + const names = result.map((r) => r.value.name).sort() + expect(names).toEqual([`Alice`, `Bob`]) + + // Note: With the bug fixed, this should fall back to full scan + // since not all OR branches can be optimized with indexes. + // The correct behavior for OR is: if ANY branch can't be optimized, + // fall back to full scan to ensure all matching items are found. + }) + }) + it(`should optimize inArray queries using indexes`, () => { withIndexTracking(collection, (tracker) => { const result = collection.currentStateAsChanges({ From adc0aeac73d1302883c6d783fdfc08ca4c206904 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 28 Jan 2026 00:29:19 +0000 Subject: [PATCH 2/2] test: add failing test for auto-indexing inside OR expressions Auto-indexing only recurses into AND expressions, not OR expressions. This means fields used inside OR don't get auto-indexed, which combined with the optimizer bug (dropping non-indexed OR branches), causes silent data loss in queries. This test demonstrates what SHOULD happen: auto-indexing should create indexes for all fields in OR expressions. https://claude.ai/code/session_01R16FFtQbQr1VXjEVCYAeUk --- .../db/tests/collection-auto-index.test.ts | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/db/tests/collection-auto-index.test.ts b/packages/db/tests/collection-auto-index.test.ts index f40db1e61..effa68a39 100644 --- a/packages/db/tests/collection-auto-index.test.ts +++ b/packages/db/tests/collection-auto-index.test.ts @@ -387,6 +387,56 @@ describe(`Collection Auto-Indexing`, () => { subscription.unsubscribe() }) + it(`should create auto-indexes for fields inside OR expressions`, async () => { + // BUG: Auto-indexing only recurses into AND expressions, not OR expressions. + // This means fields used inside OR don't get auto-indexed, which combined with + // the optimizer bug (dropping non-indexed OR branches), causes silent data loss. + // + // This test demonstrates what SHOULD happen: auto-indexing should create indexes + // for all fields in OR expressions so queries return correct results. + + const autoIndexCollection = createCollection({ + getKey: (item) => item.id, + autoIndex: `eager`, + startSync: true, + sync: { + sync: ({ begin, write, commit, markReady }) => { + begin() + for (const item of testData) { + write({ + type: `insert`, + value: item, + }) + } + commit() + markReady() + }, + }, + }) + + await autoIndexCollection.stateWhenReady() + + // Subscribe with OR expression containing two DIFFERENT fields + // Expected: Both fields should get auto-indexed + // Actual (bug): Neither field gets auto-indexed because OR isn't handled + const subscription = autoIndexCollection.subscribeChanges(() => {}, { + whereExpression: or(eq(row.status, `active`), eq(row.name, `Bob`)), + }) + + // EXPECTED: Should have created auto-indexes for BOTH fields in the OR + // This ensures the query can be properly evaluated + expect(autoIndexCollection.indexes.size).toBe(2) + + const indexPaths = Array.from(autoIndexCollection.indexes.values()).map( + (index) => (index.expression as any).path, + ) + + expect(indexPaths).toContainEqual([`status`]) + expect(indexPaths).toContainEqual([`name`]) + + subscription.unsubscribe() + }) + it(`should create auto-indexes for complex AND expressions with multiple fields`, async () => { const autoIndexCollection = createCollection({ getKey: (item) => item.id,