Skip to content

fix(db): require all or() branches to be index-optimizable#1550

Open
v-anton wants to merge 3 commits into
TanStack:mainfrom
v-anton:fix/optimize-or-partial-union
Open

fix(db): require all or() branches to be index-optimizable#1550
v-anton wants to merge 3 commits into
TanStack:mainfrom
v-anton:fix/optimize-or-partial-union

Conversation

@v-anton
Copy link
Copy Markdown

@v-anton v-anton commented May 25, 2026

🎯 Changes

Fixes #1502.

optimizeOrExpression returned canOptimize: true when only some or() branches had matching indexes, producing a partial union that silently dropped rows from unindexed branches.

The fix requires all branches to be optimizable (results.length === expression.args.length) before returning the index result. If any branch can't be optimized, fall back to full scan.

✅ Checklist

  • I have tested this code locally with pnpm test:pr.
  • I have followed the steps in the Contributing guide.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes
    • OR-style queries now only use index results when every branch is index-optimizable, preventing partial index use that could produce incomplete query results.
  • Tests
    • Added tests to validate live-query behavior for OR queries when only some branches are indexed, ensuring correct and consistent results.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 25, 2026

📝 Walkthrough

Walkthrough

Tightens OR-expression index optimization: optimizer now requires every OR branch be index-optimizable before using unioned index results; otherwise it falls back to a full scan. Adds a test exercising a partially indexed OR and a changeset for a patch release.

Changes

Index optimization fix for OR expressions

Layer / File(s) Summary
Fix requires all OR branches to be index-optimizable
.changeset/fix-or-partial-union.md, packages/db/src/utils/index-optimization.ts
optimizeOrExpression and canOptimizeOrExpression now require all OR operands to be optimizable before returning canOptimize: true and unioned matching keys; changeset records a patch release.
Test coverage for partial index optimization
packages/db/tests/query/or-partial-optimization.test.ts
New Vitest suite creates a collection with an index only on category, runs a live or(eq(category,'A'), eq(tag,'x')) query, and asserts the returned ids include matches from both branches.

Sequence Diagram

sequenceDiagram
  participant Client
  participant Optimizer
  participant IndexStore
  Client->>Optimizer: optimizeOrExpression(or(branch1, branch2, ...))
  Optimizer->>IndexStore: optimizeQueryRecursive(branch1)
  Optimizer->>IndexStore: optimizeQueryRecursive(branch2)
  IndexStore-->>Optimizer: branch optimization results (canOptimize, matchingKeys)
  Optimizer->>Client: return canOptimize = true AND union(matchingKeys) only if ALL branches canOptimize
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped through indexes, sniffed each OR-lined trail,

One branch hid blossoms while one left only stale.
Now every branch must show its map before we race—
No petals lost, the union keeps each trace. 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: requiring all OR branches to be index-optimizable.
Description check ✅ Passed The description comprehensively covers the bug fix, implementation details, and includes both checklist items marked complete.
Linked Issues check ✅ Passed All coding requirements from issue #1502 are met: the fix ensures all OR branches must be optimizable before returning canOptimize:true, preventing silent row omission.
Out of Scope Changes check ✅ Passed All changes directly address the scope of issue #1502: the index-optimization fix, matching test coverage, and the required changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/db/src/utils/index-optimization.ts (1)

480-503: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Align OR capability check with the new all-branches requirement.

After Line [480], OR optimization requires every branch to be optimizable, but Line [502] still returns true when only some branches are optimizable. This makes canOptimizeExpression inconsistent with optimizeExpressionWithIndexes.

Suggested fix
 function canOptimizeOrExpression<
   T extends object,
   TKey extends string | number,
 >(expression: BasicExpression, collection: CollectionLike<T, TKey>): boolean {
   if (expression.type !== `func` || expression.args.length < 2) {
     return false
   }

-  // If any argument can be optimized, we can gain some speedup
-  return expression.args.some((arg) => canOptimizeExpression(arg, collection))
+  // OR is index-optimizable only when every branch is index-optimizable
+  return expression.args.every((arg) => canOptimizeExpression(arg, collection))
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/src/utils/index-optimization.ts` around lines 480 - 503, The OR
capability check is too permissive: update canOptimizeOrExpression so it only
returns true when every branch is optimizable (use a check like every on
expression.args calling canOptimizeExpression) instead of returning true when
some branches are optimizable; adjust only the boolean logic inside
canOptimizeOrExpression (function name) so it matches the requirement used by
optimizeExpressionWithIndexes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/tests/query/or-partial-optimization.test.ts`:
- Around line 39-45: The test uses explicit any in the query callbacks which
weakens type checking; remove the explicit any annotations from the callbacks
passed to query/.where and .select (e.g., the ({ item }: any) => ... parameters
used in the .from({ item: collection }), .where and .select calls) so TypeScript
can infer the correct types from the collection, or replace them with the
correct domain type if needed (e.g., ItemRow) — update the parameter signatures
for the .where and .select callbacks to omit ": any" and rely on inferred types
or the proper interface name.

---

Outside diff comments:
In `@packages/db/src/utils/index-optimization.ts`:
- Around line 480-503: The OR capability check is too permissive: update
canOptimizeOrExpression so it only returns true when every branch is optimizable
(use a check like every on expression.args calling canOptimizeExpression)
instead of returning true when some branches are optimizable; adjust only the
boolean logic inside canOptimizeOrExpression (function name) so it matches the
requirement used by optimizeExpressionWithIndexes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cc1d3b47-bfa5-42c4-b0a3-e7efd2469ff8

📥 Commits

Reviewing files that changed from the base of the PR and between be656be and 95ae26c.

📒 Files selected for processing (3)
  • .changeset/fix-or-partial-union.md
  • packages/db/src/utils/index-optimization.ts
  • packages/db/tests/query/or-partial-optimization.test.ts

Comment thread packages/db/tests/query/or-partial-optimization.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/db/tests/query/or-partial-optimization.test.ts (1)

21-58: 💤 Low value

Consider adding test cases for edge scenarios.

The current test validates the core fix (partial indexing fallback) well. To strengthen coverage per testing guidelines, consider adding cases for:

  • Both OR branches indexed (should use optimized union)
  • Neither branch indexed (full scan fallback)
  • Empty collection or single-item edge cases

These can be deferred to a follow-up if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/db/tests/query/or-partial-optimization.test.ts` around lines 21 -
58, Add additional test cases around the or() partial-index behavior: create
tests that use the same helpers (createCollection, createLiveQueryCollection,
createIndex, BasicIndex, stateWhenReady) to cover (1) both OR branches indexed
(expect optimized union behavior), (2) neither branch indexed (expect full-scan
fallback), and (3) edge collections (empty collection and single-item
collection) verifying liveQuery.toArray results; reuse the existing pattern
(query: q => q.from({ item: collection }).where(({ item }) =>
or(eq(item.category, 'A'), eq(item.tag, 'x'))).select(...), startSync: true) and
assert expected ids for each scenario.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/db/tests/query/or-partial-optimization.test.ts`:
- Around line 21-58: Add additional test cases around the or() partial-index
behavior: create tests that use the same helpers (createCollection,
createLiveQueryCollection, createIndex, BasicIndex, stateWhenReady) to cover (1)
both OR branches indexed (expect optimized union behavior), (2) neither branch
indexed (expect full-scan fallback), and (3) edge collections (empty collection
and single-item collection) verifying liveQuery.toArray results; reuse the
existing pattern (query: q => q.from({ item: collection }).where(({ item }) =>
or(eq(item.category, 'A'), eq(item.tag, 'x'))).select(...), startSync: true) and
assert expected ids for each scenario.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 40e74d91-0e66-4dcc-b367-4bfb4f9bc40c

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3d9eb and 3182b59.

📒 Files selected for processing (1)
  • packages/db/tests/query/or-partial-optimization.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

optimizeOrExpression silently drops items when one OR branch references an unindexed field

1 participant