-
Notifications
You must be signed in to change notification settings - Fork 171
Description
- I've validated the bug against the latest version of DB packages
Describe the bug
When using leftJoin, the select() callback receives proxy objects for unmatched (right-side) rows instead of null or undefined. These proxy objects are truthy, so conditional checks like right ? { id: right.id } : null always take the truthy branch, even when there was no matching row on the right side of the join.
This means there's no way to distinguish matched from unmatched rows inside select(). Accessing fields on these proxy objects (e.g. right.name) also returns truthy empty objects {} rather than undefined, so even right?.id ? ... : null doesn't work.
To Reproduce
import { createCollection, localOnlyCollectionOptions, createLiveQueryCollection, eq, Query } from '@tanstack/db';
import { z } from 'zod';
const users = createCollection(
localOnlyCollectionOptions({
id: 'users',
getKey: (item: { id: string }) => item.id,
schema: z.object({ id: z.string(), name: z.string() }),
initialData: [
{ id: 'u1', name: 'Alice' },
{ id: 'u2', name: 'Bob' },
],
}),
);
const profiles = createCollection(
localOnlyCollectionOptions({
id: 'profiles',
getKey: (item: { id: string }) => item.id,
schema: z.object({ id: z.string(), userId: z.string(), bio: z.string() }),
initialData: [
{ id: 'p1', userId: 'u1', bio: 'Hello world' },
// No profile for u2
],
}),
);
const lq = createLiveQueryCollection({
startSync: true,
query: (q) =>
q
.from({ user: users })
.leftJoin({ profile: profiles }, ({ user, profile }) => eq(user.id, profile.userId))
.select(({ user, profile }) => ({
name: user.name,
// This conditional NEVER takes the null branch because `profile` is always a truthy proxy object
bio: profile ? profile.bio : null,
})),
});
const data = lq.toArray;
console.log(data);
// Actual: [{ name: 'Alice', bio: 'Hello world' }, { name: 'Bob', bio: {} }]
// Expected: [{ name: 'Alice', bio: 'Hello world' }, { name: 'Bob', bio: null }]The key observation: console.log(profile) inside the select for the unmatched row logs {} (a truthy empty object), and console.log(profile?.bio) also logs {}.
Expected behavior
For unmatched rows in a LEFT JOIN, the right-side table reference in the select() callback should be null or undefined (matching the TypeScript types, which correctly type it as potentially undefined). This would allow standard null checks to work:
.select(({ user, profile }) => ({
name: user.name,
bio: profile ? profile.bio : null, // should produce null for unmatched rows
}))Desktop:
- OS: macOS 15
- Runtime: Vite dev server / Chrome
- TanStack DB version: latest
Additional context
This is related to but distinct from #685, which covers LEFT JOIN nullability issues in where() clauses. This bug is specifically about the select() callback receiving proxy objects that don't behave like the types suggest.
The TypeScript types are correct (they mark LEFT JOIN references as potentially undefined), but the runtime behavior doesn't match — the references are always truthy proxy objects regardless of whether a match exists.