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
50 changes: 50 additions & 0 deletions packages/db/tests/collection-auto-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TestItem, string>({
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<TestItem, string>({
getKey: (item) => item.id,
Expand Down
36 changes: 36 additions & 0 deletions packages/db/tests/collection-indexes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading