Skip to content

live query with joins emits delete events on ready #677

Description

@sunghwan2789
  • I've validated the bug against the latest version of DB packages

Describe the bug

When creating a live query with a three-collection chained LEFT JOIN (A -> B -> C), an issue occurs if the middle collection (B) is initially empty and is populated after the initial sync. The query subscription incorrectly emits "delete" and "insert" events, instead of "update" events, even though the data in the result set is only being updated, not removed or added.

A full reproduction is available in this test case:
https://github.com/sunghwan2789/db/blob/9e1f1d0bfad4e81634671671300467695e01eedc/packages/db/tests/query/join.test.ts#L1251-L1313

Test Setup:

// Test schema for three-collection join subscriptiontypePlayer={name: stringclub_id: stringposition: string}typeClient={name: stringplayer: string// references Player.nameemail: string}typeBalance={name: stringclient: string// references Client.nameamount: number}// Sample dataconstsamplePlayers: Array<Player>=[{name: `player1`,club_id: `club1`,position: `forward`},{name: `player2`,club_id: `club1`,position: `midfielder`},{name: `player3`,club_id: `club1`,position: `defender`},]constsampleClients: Array<Client>=[{name: `client1`,player: `player1`,email: `client1@example.com`},{name: `client2`,player: `player2`,email: `client2@example.com`},{name: `client3`,player: `player3`,email: `client3@example.com`},]constsampleBalances: Array<Balance>=[{name: `balance1`,client: `client1`,amount: 1000},{name: `balance2`,client: `client2`,amount: 2000},{name: `balance3`,client: `client3`,amount: 1500},]constplayersCollection=createCollection(mockSyncCollectionOptions<Player>({id: `test-players-subscription-${autoIndex}`,getKey: (player)=>player.name,initialData: samplePlayers,
autoIndex,}))constclientsCollection=createCollection(mockSyncCollectionOptionsNoInitialState<Client>({id: `test-clients-subscription-${autoIndex}`,getKey: (client)=>client.name,
autoIndex,}))constbalancesCollection=createCollection(mockSyncCollectionOptions<Balance>({id: `test-balances-subscription-${autoIndex}`,getKey: (balance)=>balance.name,initialData: sampleBalances,
autoIndex,}))// Create chained join query with three collections using left joinsconstchainedJoinQuery=createLiveQueryCollection({startSync: true,query: (q)=>q.from({player: playersCollection}).join({client: clientsCollection},({ client, player })=>eq(client.player,player.name),`left`).join({balance: balancesCollection},({ balance, client })=>eq(balance.client,client?.name),`left`).select(({ player, client, balance })=>({player_name: player.name,client_name: client?.name,balance_amount: balance?.amount,})),})// Track all change eventsconstchangeEvents: Array<any>=[]constsubscription=chainedJoinQuery.subscribeChanges((changes)=>{changeEvents.push(...changes)})// Initial state should have 3 results (left join includes all players even without clients)expect(chainedJoinQuery.toArray).toHaveLength(3)// Verify players are present but clients are nullconstinitialResults=chainedJoinQuery.toArrayexpect(initialResults.every((r)=>r.client_name===undefined)).toBe(true)expect(initialResults.every((r)=>r.balance_amount===undefined)).toBe(true)// Clear any initial events from subscription setupchangeEvents.length=0// Insert client data after eager sync completion and first assertionclientsCollection.utils.begin()sampleClients.forEach((client)=>{clientsCollection.utils.write({type: `insert`,value: client})})clientsCollection.utils.commit()clientsCollection.utils.markReady()// Should still have 3 results, but now with client dataexpect(chainedJoinQuery.toArray).toHaveLength(3)// Verify clients are now populatedconstresultsAfterInsert=chainedJoinQuery.toArrayexpect(resultsAfterInsert.every((r)=>r.client_name!==undefined)).toBe(true)// Verify change events are emittedexpect({delete: changeEvents.filter((c)=>c.type===`delete`).length,update: changeEvents.filter((c)=>c.type===`update`).length,insert: changeEvents.filter((c)=>c.type===`insert`).length,}).toEqual({delete: 0,update: 3,insert: 0})// fail with actual: { delete 3, update:0, insert: 6 }

Expected Behavior

When the clientsCollection is populated, the existing items in the chainedJoinQuery result set should be updated. Since the change events are keyed to the player, we expect to receive 3 update events.

Actual Behavior

The subscription incorrectly reports 3 delete events and 6 insert events, instead of 3 update events.

Additional Context

When attempting to provide a custom getKey option to the createLiveQueryCollection to work around this, the query fails with a DuplicateKeySyncError.

diff --git a/packages/db/tests/query/join.test.ts b/packages/db/tests/query/join.test.ts
index a1812de..5a0004c 100644
--- a/packages/db/tests/query/join.test.ts+++ b/packages/db/tests/query/join.test.ts@@ -1250,6 +1250,7 @@ function createJoinTests(autoIndex: `off` | `eager`): void {
// Create chained join query with three collections using left joins
const chainedJoinQuery = createLiveQueryCollection({
startSync: true,
+ getKey: (r) => r.player_name,
query: (q) =>
q
.from({ player: playersCollection })
Uncaught CollectionOperationError: Cannot insert document with key "player1" from sync because it already exists in the collection "live-query-56"

I think this is a regression of #658 ?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions