Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .changeset/fix-live-query-default-getkey.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/db': patch
---

fix: default getKey on live query collections fails when used as a source in chained collections

The default WeakMap-based getKey breaks when enriched change values (with virtual props like $synced, $origin, $key) are passed through chained live query collections. The enriched objects are new references not found in the WeakMap, causing all items to resolve to key `undefined` and collapse into a single item. Falls back to `item.$key` when the WeakMap lookup misses.
3 changes: 2 additions & 1 deletion packages/db/src/query/live/collection-config-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@ export class CollectionConfigBuilder<
id: this.id,
getKey:
this.config.getKey ||
((item) => this.resultKeys.get(item) as string | number),
((item: any) =>
(this.resultKeys.get(item) ?? item.$key) as string | number),
sync: this.getSyncConfig(),
compare: this.compare,
defaultStringCollation: this.compareOptions,
Expand Down
117 changes: 117 additions & 0 deletions packages/db/tests/query/live-query-collection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2585,4 +2585,121 @@ describe(`createLiveQueryCollection`, () => {
await preloadPromise
})
})

describe(`chained live query collections without custom getKey`, () => {
it(`should return all items when a live query collection without getKey is used as a source`, async () => {
// Create a live query collection with the default (internal) getKey
const filteredUsers = createLiveQueryCollection({
id: `filtered-users`,
query: (q) =>
q
.from({ user: usersCollection })
.where(({ user }) => eq(user.active, true))
.select(({ user }) => ({
id: user.id,
name: user.name,
})),
})

// Use the live query collection as a source in another live query collection
const derived = createLiveQueryCollection({
id: `derived-from-live-query`,
query: (q) => q.from({ u: filteredUsers }),
})

await derived.preload()

// Should contain all active users (Alice and Bob), not just 1
expect(derived.size).toBe(2)
})

it(`should return all items when a live query collection with a join and no getKey is used as a source`, async () => {
type Team = {
id: number
name: string
lead_id: number
}

const teamsCollection = createCollection(
mockSyncCollectionOptions<Team>({
id: `test-teams`,
getKey: (team) => team.id,
initialData: [
{ id: 1, name: `Alpha`, lead_id: 1 },
{ id: 2, name: `Beta`, lead_id: 2 },
{ id: 3, name: `Gamma`, lead_id: 1 },
],
}),
)

// Join teams with users — no custom getKey
const teamsWithLeads = createLiveQueryCollection({
id: `teams-with-leads`,
query: (q) =>
q
.from({ team: teamsCollection })
.join({ user: usersCollection }, ({ team, user }) =>
eq(team.lead_id, user.id),
)
.select(({ team, user }) => ({
teamName: team.name,
leadName: user.name,
})),
})

// Use the joined live query collection as a source
const derived = createLiveQueryCollection({
id: `derived-from-join`,
query: (q) => q.from({ t: teamsWithLeads }),
})

await derived.preload()

// Should contain all 3 joined rows, not just 1
expect(derived.size).toBe(3)
expect(derived.toArray.map((row) => stripVirtualProps(row))).toEqual(
expect.arrayContaining([
expect.objectContaining({ teamName: `Alpha`, leadName: `Alice` }),
expect.objectContaining({ teamName: `Beta`, leadName: `Bob` }),
expect.objectContaining({ teamName: `Gamma`, leadName: `Alice` }),
]),
)
})

it(`should propagate updates through chained live query collections without custom getKey`, async () => {
// Intermediate live query collection — no custom getKey
const intermediate = createLiveQueryCollection({
id: `update-intermediate`,
query: (q) =>
q.from({ user: usersCollection }).select(({ user }) => ({
id: user.id,
name: user.name,
})),
})

// Derived from the intermediate
const derived = createLiveQueryCollection({
id: `update-derived`,
query: (q) => q.from({ u: intermediate }),
})

await derived.preload()

// Should have all 3 users from sampleUsers, not just 1
expect(derived.size).toBe(3)

// Sync a new user into the source collection
usersCollection.utils.begin()
usersCollection.utils.write({
type: `insert`,
value: { id: 4, name: `Diana`, active: true },
})
usersCollection.utils.commit()

await flushPromises()

// The derived collection should see all 4 items
expect(derived.size).toBe(4)
})
})
})
Loading