From 95ae26c3799df2feeb54a1a7fbe351f9abbd930b Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:05:03 +0300 Subject: [PATCH 1/4] fix(db): require all or() branches to be index-optimizable --- .changeset/fix-or-partial-union.md | 5 ++ packages/db/src/utils/index-optimization.ts | 2 +- .../query/or-partial-optimization.test.ts | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-or-partial-union.md create mode 100644 packages/db/tests/query/or-partial-optimization.test.ts diff --git a/.changeset/fix-or-partial-union.md b/.changeset/fix-or-partial-union.md new file mode 100644 index 0000000000..ebbf80d5ae --- /dev/null +++ b/.changeset/fix-or-partial-union.md @@ -0,0 +1,5 @@ +--- +"@tanstack/db": patch +--- + +fix(db): require all or() branches to be index-optimizable before using index result diff --git a/packages/db/src/utils/index-optimization.ts b/packages/db/src/utils/index-optimization.ts index 81b111af56..48ff5bf2c8 100644 --- a/packages/db/src/utils/index-optimization.ts +++ b/packages/db/src/utils/index-optimization.ts @@ -477,7 +477,7 @@ function optimizeOrExpression( } } - if (results.length > 0) { + if (results.length === expression.args.length) { // Use unionSets utility for OR logic const allMatchingSets = results.map((r) => r.matchingKeys) const unionedKeys = unionSets(allMatchingSets) diff --git a/packages/db/tests/query/or-partial-optimization.test.ts b/packages/db/tests/query/or-partial-optimization.test.ts new file mode 100644 index 0000000000..58c7830a32 --- /dev/null +++ b/packages/db/tests/query/or-partial-optimization.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from 'vitest' +import { createCollection } from '../../src/collection/index.js' +import { BasicIndex } from '../../src/indexes/basic-index.js' +import { createLiveQueryCollection } from '../../src/query/live-query-collection' +import { eq, or } from '../../src/query/builder/functions' +import { mockSyncCollectionOptions } from '../utils' + +interface TestItem { + id: string + category: string + tag: string +} + +const testData: Array = [ + { id: `1`, category: `A`, tag: `x` }, + { id: `2`, category: `B`, tag: `y` }, + { id: `3`, category: `A`, tag: `z` }, + { id: `4`, category: `C`, tag: `x` }, +] + +describe(`or() with partially indexed branches`, () => { + it(`returns all matching rows when only one or() branch is indexed`, async () => { + const collection = createCollection( + mockSyncCollectionOptions({ + id: `or-partial-index`, + getKey: (item) => item.id, + initialData: testData, + autoIndex: `off`, + }), + ) + + await collection.stateWhenReady() + + collection.createIndex((row) => row.category, { + indexType: BasicIndex, + }) + + const liveQuery = createLiveQueryCollection({ + query: (q: any) => + q + .from({ item: collection }) + .where(({ item }: any) => + or(eq(item.category, `A`), eq(item.tag, `x`)), + ) + .select(({ item }: any) => ({ + id: item.id, + category: item.category, + tag: item.tag, + })), + startSync: true, + }) + + await liveQuery.stateWhenReady() + + const ids = liveQuery.toArray.map((r) => r.id).sort() + expect(ids).toEqual([`1`, `3`, `4`]) + }) +}) From 3e3d9eb7922f509c4b073de4b0d3a51e07b3bacd Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 10:52:56 +0300 Subject: [PATCH 2/4] fix: align canOptimizeOrExpression with all-branches requirement --- packages/db/src/utils/index-optimization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/db/src/utils/index-optimization.ts b/packages/db/src/utils/index-optimization.ts index 48ff5bf2c8..5fa717af48 100644 --- a/packages/db/src/utils/index-optimization.ts +++ b/packages/db/src/utils/index-optimization.ts @@ -498,8 +498,8 @@ function canOptimizeOrExpression< return false } - // If any argument can be optimized, we can gain some speedup - return expression.args.some((arg) => canOptimizeExpression(arg, collection)) + // All branches must be optimizable — partial OR optimization is unsound + return expression.args.every((arg) => canOptimizeExpression(arg, collection)) } /** From 3182b59d2f0c0091255e1f573bfe3ef60fa9de2d Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Mon, 25 May 2026 11:12:25 +0300 Subject: [PATCH 3/4] test: remove explicit any from or-optimization test --- packages/db/tests/query/or-partial-optimization.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/db/tests/query/or-partial-optimization.test.ts b/packages/db/tests/query/or-partial-optimization.test.ts index 58c7830a32..56d89990a3 100644 --- a/packages/db/tests/query/or-partial-optimization.test.ts +++ b/packages/db/tests/query/or-partial-optimization.test.ts @@ -36,13 +36,13 @@ describe(`or() with partially indexed branches`, () => { }) const liveQuery = createLiveQueryCollection({ - query: (q: any) => + query: (q) => q .from({ item: collection }) - .where(({ item }: any) => + .where(({ item }) => or(eq(item.category, `A`), eq(item.tag, `x`)), ) - .select(({ item }: any) => ({ + .select(({ item }) => ({ id: item.id, category: item.category, tag: item.tag, From 942fe1e247c431641e6d9cba96c389389df7df5a Mon Sep 17 00:00:00 2001 From: Anton Vozghrin Date: Wed, 27 May 2026 15:33:55 +0300 Subject: [PATCH 4/4] refactor: early return in optimizeOrExpression for non-optimizable branches --- packages/db/src/utils/index-optimization.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/db/src/utils/index-optimization.ts b/packages/db/src/utils/index-optimization.ts index 5fa717af48..30c87c4830 100644 --- a/packages/db/src/utils/index-optimization.ts +++ b/packages/db/src/utils/index-optimization.ts @@ -469,22 +469,17 @@ function optimizeOrExpression( const results: Array> = [] - // Try to optimize each part, keep the optimizable ones for (const arg of expression.args) { const result = optimizeQueryRecursive(arg, collection) - if (result.canOptimize) { - results.push(result) + if (!result.canOptimize) { + return { canOptimize: false, matchingKeys: new Set() } } + results.push(result) } - if (results.length === expression.args.length) { - // Use unionSets utility for OR logic - const allMatchingSets = results.map((r) => r.matchingKeys) - const unionedKeys = unionSets(allMatchingSets) - return { canOptimize: true, matchingKeys: unionedKeys } - } - - return { canOptimize: false, matchingKeys: new Set() } + const allMatchingSets = results.map((r) => r.matchingKeys) + const unionedKeys = unionSets(allMatchingSets) + return { canOptimize: true, matchingKeys: unionedKeys } } /**