Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-or-partial-union.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

fix(db): require all or() branches to be index-optimizable before using index result
6 changes: 3 additions & 3 deletions packages/db/src/utils/index-optimization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ function optimizeOrExpression<T extends object, TKey extends string | number>(
}
}

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)
Expand All @@ -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))
}

/**
Expand Down
58 changes: 58 additions & 0 deletions packages/db/tests/query/or-partial-optimization.test.ts
Original file line number Diff line number Diff line change
@@ -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<TestItem> = [
{ 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<TestItem>({
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) =>
q
.from({ item: collection })
.where(({ item }) =>
or(eq(item.category, `A`), eq(item.tag, `x`)),
)
.select(({ item }) => ({
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`])
})
})