fix(db): left join where filter on right-side field incorrectly includes unmatched rows#1254
fix(db): left join where filter on right-side field incorrectly includes unmatched rows#1254
Conversation
Verifies that `.where(({ r }) => isUndefined(r?.payload))` after a
leftJoin correctly filters out rows where the right side matched but
the field has a defined value, rather than stripping the right side
to undefined and keeping all rows.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 3134019 The changes in this PR will be included in the next version bump. This PR includes changesets to release 12 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
More templates
@tanstack/angular-db
@tanstack/db
@tanstack/db-ivm
@tanstack/electric-db-collection
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: +60 B (+0.07%) Total Size: 92.1 kB
ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 3.7 kB ℹ️ View Unchanged
|
The query optimizer was pushing single-source WHERE clauses into subqueries and sourceWhereClauses for the nullable side of outer joins (right side of LEFT, left side of RIGHT, both for FULL). This pre-filtered the data before the join, converting rows that should have been excluded by the WHERE into unmatched outer-join rows that then incorrectly survived the residual filter. The fix identifies nullable join sources and excludes them from predicate pushdown and index optimization extraction. Clauses for those sources are kept as post-join filters on the main pipeline. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root causeThe query optimizer has two mechanisms that push single-source WHERE clauses closer to the data:
Both are correct for inner joins, but break outer join semantics when applied to the nullable side (right side of LEFT, left side of RIGHT, both for FULL). Why it breaksConsider Without pushdown (correct): the join matches all right rows, then the WHERE filters the result:
With pushdown (broken): the right side is pre-filtered to only r3, so the join can't find r1/r2:
The optimizer keeps a residual WHERE for outer joins to handle exactly this, but it can't distinguish "genuinely unmatched" rows (l4) from "had a match but it was pre-filtered away" rows (l1, l2) — they look identical. When the WHERE condition happens to be true for unmatched rows (like FixA new This is conservative — it also blocks pushdown for cases where the residual would work correctly (e.g. |
|
🎉 This PR has been released! Thank you for your contribution! |
Summary
.where(({ r }) => isUndefined(r?.payload))after aleftJoinreturns all left rows with the right side stripped toundefined, instead of excluding rows whose right-side payload is defined.Fixes #685
Test plan
where with isUndefined on right-side field should filter entire joined rowspasses🤖 Generated with Claude Code