Skip to content

Collections in outer joins with WHERE that checks for emptiness get filtered before join #574

@duailibe

Description

@duailibe

Problem

Given this query:

q.from({ user: usersCollection })
  .leftJoin(({ special: specialUsersCollection}) => eq(users.id, special.id))
  .where(({ special }) => isUndefined(special))

The result is all rows from usersCollection. What happens here is the isUndefined is pushed to a subquery on specialUsersCollection, which evaluates as false to all rows, so the resulting query is simply the original usersCollection

Investigation

Tried debugging and it seems the problem is in

// Optimize JOIN clauses and track what was optimized
const optimizedJoins = query.join
? query.join.map((joinClause) => ({
...joinClause,
from: optimizeFromWithTracking(
joinClause.from,
groupedClauses.singleSource,
actuallyOptimized
),
}))
: undefined

There's a special case that the join is an outer join + the where clause is filtering on the "emptiness" of the joined collection.

Funny enough there are tests that assert this doesn't happen

describe(`JOIN semantics preservation`, () => {
test(`should preserve WHERE clause semantics when pushing down to LEFT JOIN`, () => {
// This test reproduces the bug where pushing WHERE clauses into LEFT JOIN subqueries
// changes the semantics by filtering out null values that should remain

But that's not what I see in practice.

Reproduction

I added this test to packages/db/tests/query/join.test.ts:

test('left join without other', () => {
  const usersCollection = createUsersCollection()
  const specialUsersCollection = createCollection(
    mockSyncCollectionOptions({
      id: `special-users`,
      getKey: (user) => user.id,
      initialData: [{ id: 1, special: true }],
    })
  )

  const joinQuery = createLiveQueryCollection({
    startSync: true,
    query: (q) =>
      q
        .from({ user: usersCollection })
        .leftJoin(
          { special: specialUsersCollection },
          ({ user, special }) => eq(user.id, special.id),
        )
        .where(({ special }) => isUndefined(special)),
  })

  for (const row of joinQuery.toArray) {
    expect(row.special).toBeUndefined()
  }

  expect(joinQuery.size).toBe(3)
});

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