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
5 changes: 5 additions & 0 deletions .changeset/thin-rings-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

Fixes a bug where optimized joins would miss data
5 changes: 1 addition & 4 deletions packages/db/src/query/compiler/joins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,13 +208,10 @@ function processJoin(
? (joinedExpr as PropRef)
: (mainExpr as PropRef)

const activeColl =
activeCollection === `main` ? collections[mainTableId]! : lazyCollection

const followRefResult = followRef(
rawQuery,
lazyCollectionJoinExpr,
activeColl
lazyCollection
)!
const followRefCollection = followRefResult.collection

Expand Down
119 changes: 119 additions & 0 deletions packages/db/tests/collection-auto-index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,125 @@ describe(`Collection Auto-Indexing`, () => {
tracker.restore()
})

it(`should create auto-indexes for join key on lazy collection when joining subquery`, async () => {
const leftCollection = createCollection<TestItem, string>({
getKey: (item) => item.id,
autoIndex: `eager`,
startSync: true,
sync: {
sync: ({ begin, write, commit, markReady }) => {
begin()
for (const item of testData) {
write({
type: `insert`,
value: item,
})
}
commit()
markReady()
},
},
onInsert: async (_) => {},
})

const rightCollection = createCollection<TestItem2, string>({
getKey: (item) => item.id2,
autoIndex: `eager`,
startSync: true,
sync: {
sync: ({ begin, write, commit, markReady }) => {
begin()
write({
type: `insert`,
value: {
id2: `1`,
name: `Other Active Item`,
age: 40,
status: `active`,
createdAt: new Date(),
},
})
write({
type: `insert`,
value: {
id2: `other2`,
name: `Other Inactive Item`,
age: 35,
status: `inactive`,
createdAt: new Date(),
},
})
commit()
markReady()
},
},
})

await rightCollection.stateWhenReady()

const liveQuery = createLiveQueryCollection({
query: (q: any) =>
q
.from({ item: leftCollection })
.join(
{
other: q
.from({ other: rightCollection })
.select(({ other }: any) => ({
id2: other.id2,
name: other.name,
})),
},
({ item, other }: any) => eq(item.id, other.id2),
`left`
)
.select(({ item, other }: any) => ({
id: item.id,
name: item.name,
otherName: other.name,
})),
startSync: true,
})

await liveQuery.stateWhenReady()

expect(liveQuery.size).toBe(testData.length)

expect(rightCollection.indexes.size).toBe(1)

const index = rightCollection.indexes.values().next().value!
expect(index.expression).toEqual({
type: `ref`,
path: [`id2`],
})

const tracker = createIndexUsageTracker(rightCollection)

// Now send another item through the left collection
// and check that it used the index to join it to items of the right collection

leftCollection.insert({
id: `other2`,
name: `New Item`,
age: 25,
status: `active`,
createdAt: new Date(),
})

expect(tracker.stats.queriesExecuted).toEqual([
{
type: `index`,
operation: `eq`,
field: `id2`,
value: `other2`,
},
])

expect(liveQuery.size).toBe(testData.length + 1)

tracker.restore()
})

it(`should not create auto-indexes for unsupported operations`, async () => {
const autoIndexCollection = createCollection<TestItem, string>({
getKey: (item) => item.id,
Expand Down
74 changes: 74 additions & 0 deletions packages/db/tests/query/join-subquery.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,20 @@ const sampleUsers: Array<User> = [
},
]

const sampleProducts = [
{ id: 1, a: `8` },
{ id: 2, a: `6` },
{ id: 3, a: `0` },
{ id: 4, a: `5` },
]

const sampleTrials = [
{ id: 1, productId: 1, userId: 1, createdAt: new Date() },
{ id: 2, productId: 2, userId: 1, createdAt: new Date() },
{ id: 2, productId: 4, userId: 1, createdAt: null },
{ id: 3, productId: 3, userId: 2, createdAt: new Date() },
]

function createIssuesCollection(autoIndex: `off` | `eager` = `eager`) {
return createCollection(
mockSyncCollectionOptions<Issue>({
Expand All @@ -124,6 +138,28 @@ function createUsersCollection(autoIndex: `off` | `eager` = `eager`) {
)
}

function createProductsCollection(autoIndex: `off` | `eager` = `eager`) {
return createCollection(
mockSyncCollectionOptions({
id: `join-subquery-test-products`,
getKey: (product) => product.id,
initialData: sampleProducts,
autoIndex,
})
)
}

function createTrialsCollection(autoIndex: `off` | `eager` = `eager`) {
return createCollection(
mockSyncCollectionOptions({
id: `join-subquery-test-trials`,
getKey: (item) => `${item.productId}-${item.userId}`,
initialData: sampleTrials,
autoIndex,
})
)
}

function createJoinSubqueryTests(autoIndex: `off` | `eager`): void {
describe(`with autoIndex ${autoIndex}`, () => {
describe(`subqueries in FROM clause with joins`, () => {
Expand Down Expand Up @@ -272,10 +308,14 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void {
describe(`subqueries in JOIN clause`, () => {
let issuesCollection: ReturnType<typeof createIssuesCollection>
let usersCollection: ReturnType<typeof createUsersCollection>
let productsCollection: ReturnType<typeof createProductsCollection>
let trialsCollection: ReturnType<typeof createTrialsCollection>

beforeEach(() => {
issuesCollection = createIssuesCollection(autoIndex)
usersCollection = createUsersCollection(autoIndex)
productsCollection = createProductsCollection(autoIndex)
trialsCollection = createTrialsCollection(autoIndex)
})

test(`should use subquery in JOIN clause - inner join`, () => {
Expand Down Expand Up @@ -358,6 +398,40 @@ function createJoinSubqueryTests(autoIndex: `off` | `eager`): void {
})
})

test(`should use subquery in JOIN clause - left join for trials`, () => {
const joinSubquery = createLiveQueryCollection({
query: (q) => {
return q
.from({ product: productsCollection })
.join(
{
tried: q
.from({ tried: trialsCollection })
.where(({ tried }) => eq(tried.userId, 1)),
},
({ tried, product }) => eq(tried.productId, product.id),
`left`
)
.where(({ product }) => eq(product.id, 1))
.select(({ product, tried }) => ({
product,
tried,
}))
},
startSync: true,
})

const results = joinSubquery.toArray
expect(results).toHaveLength(1)
expect(results[0]!.product.id).toBe(1)
expect(results[0]!.tried).toBeDefined()
expect(results[0]!.tried!.userId).toBe(1)
expect(results[0]).toEqual({
product: { id: 1, a: `8` },
tried: sampleTrials[0],
})
})

test(`should handle subqueries with SELECT clauses in both FROM and JOIN`, () => {
const joinQuery = createLiveQueryCollection({
startSync: true,
Expand Down
Loading