Describe the bug
In packages/db/src/query/compiler/joins.ts, the lazy-join path emits:
[TanStack DB] [<collectionId>] Join requires an index on "<fieldPath>" for efficient loading. Falling back to loading all data. ...
When the joined side is a subquery, the collectionId in the warning comes from followRefCollection.id — the ultimate source of the join expression traced through the subquery's select. But the subscription that actually failed to optimize is looked up via aliasRemapping[lazyAlias], which resolves to the subquery's innermost .from alias.
When these resolve to different collections (because the subquery's select field comes from a joined side rather than the .from side), the warning names a collection whose advice is typically already satisfied, and hides the real failing subscription.
Data still flows via the fallback full-load, so this is a warning-quality bug, not a correctness bug.
To Reproduce
// packages/db/tests/lazy-join-wrong-collection-id.test.ts
import { beforeEach, describe, expect, test, vi } from 'vitest'
import { createLiveQueryCollection, eq } from '../src/query/index.js'
import { BTreeIndex, createCollection } from '../src/collection/index.js'
import { flushPromises, mockSyncCollectionOptions } from './utils.js'
type A = { id: string }
type B = { id: string; aId: string }
describe('lazy-join warning misattributed when followRef crosses a subquery join', () => {
let a: ReturnType<typeof createCollection<A>>
let b: ReturnType<typeof createCollection<B>>
let warnSpy: ReturnType<typeof vi.spyOn>
beforeEach(() => {
a = createCollection(mockSyncCollectionOptions<A>({
id: 'a', getKey: (r) => r.id,
autoIndex: 'eager', defaultIndexType: BTreeIndex,
initialData: [{ id: 'a1' }],
}))
b = createCollection(mockSyncCollectionOptions<B>({
id: 'b', getKey: (r) => r.id,
initialData: [{ id: 'b1', aId: 'a1' }],
}))
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
})
test('warning blames followRefCollection instead of the failing subscription', async () => {
const live = createLiveQueryCollection({
id: 'outer', startSync: true,
query: (q) => {
const sub = q
.from({ innerB: b })
.leftJoin({ innerA: a }, ({ innerA, innerB }) => eq(innerA.id, innerB.aId))
.select(({ innerA }) => ({ aId: innerA?.id }))
return q
.from({ outerB: b })
.leftJoin({ sub }, ({ outerB, sub }) => eq(sub.aId, outerB.aId))
.select(({ outerB }) => ({ id: outerB.id }))
},
})
await flushPromises()
const warnings = warnSpy.mock.calls.map((c) => String(c[0]))
.filter((m) => m.includes('Join requires an index'))
expect(warnings).toHaveLength(1)
// Warning names `[a]`. But `a` has an auto-indexed `id`; the advice
// "create an index on a.id" is already satisfied. The actually failing
// subscription is on `b` (aliasRemapping['sub'] = 'innerB').
expect(warnings[0]).toContain('[a]')
expect(warnings[0]).toContain('"id"')
})
})
Passes against @tanstack/db@0.6.5.
Expected behavior
Either:
- No warning — the lazy-loader should reach the correct collection's subscription (whichever one can actually satisfy the index lookup).
- Or a warning whose collection ID names the subscription that actually failed — so the "consider creating an index" advice is actionable.
Additional context
Trace through the compiler for the outer .leftJoin({ sub }, eq(sub.aId, outerB.aId)):
followRef(rawQuery, sub.aId, lazySource) walks sub → subquery → select.aId = innerA.id → innerA → collection A. Returns { collection: a, path: ['id'] }. So followRefCollection = a.
subQueryResult.collectionId = b (subquery's innermost .from), so aliasRemapping['sub'] = 'innerB', and subscriptions['innerB'] is the b subscription.
- The tap runs
bSubscription.requestSnapshot({ where: inArray(PropRef(['id']), joinKeys), optimizedOnly: true }). b has an id field but no index → findIndexForField(b, ['id']) returns undefined → canOptimize: false → currentStateAsChanges returns undefined → loaded = false → warning with collectionId = followRefCollection.id = 'a'.
I instrumented the warning site to log directly and confirmed the mismatch:
[VIBL-VERIFY] subscription.collection.id=b resolvedAlias=innerB followRefCollection.id=a fieldPath=id
Workaround: swap the subquery's .from so the innermost alias maps to followRefCollection. E.g., q.from({ innerA: a }).leftJoin({ innerB: b }, ...).select(({ innerA }) => ({ aId: innerA.id })). Tested — no warning in that shape.
@tanstack/db@0.6.5Describe the bug
In
packages/db/src/query/compiler/joins.ts, the lazy-join path emits:When the joined side is a subquery, the
collectionIdin the warning comes fromfollowRefCollection.id— the ultimate source of the join expression traced through the subquery'sselect. But the subscription that actually failed to optimize is looked up viaaliasRemapping[lazyAlias], which resolves to the subquery's innermost.fromalias.When these resolve to different collections (because the subquery's select field comes from a joined side rather than the
.fromside), the warning names a collection whose advice is typically already satisfied, and hides the real failing subscription.Data still flows via the fallback full-load, so this is a warning-quality bug, not a correctness bug.
To Reproduce
Passes against
@tanstack/db@0.6.5.Expected behavior
Either:
Additional context
Trace through the compiler for the outer
.leftJoin({ sub }, eq(sub.aId, outerB.aId)):followRef(rawQuery, sub.aId, lazySource)walkssub→ subquery →select.aId = innerA.id→innerA→ collection A. Returns{ collection: a, path: ['id'] }. SofollowRefCollection = a.subQueryResult.collectionId = b(subquery's innermost.from), soaliasRemapping['sub'] = 'innerB', andsubscriptions['innerB']is thebsubscription.bSubscription.requestSnapshot({ where: inArray(PropRef(['id']), joinKeys), optimizedOnly: true }).bhas anidfield but no index →findIndexForField(b, ['id'])returnsundefined→canOptimize: false→currentStateAsChangesreturnsundefined→loaded = false→ warning withcollectionId = followRefCollection.id = 'a'.I instrumented the warning site to log directly and confirmed the mismatch:
Workaround: swap the subquery's
.fromso the innermost alias maps tofollowRefCollection. E.g.,q.from({ innerA: a }).leftJoin({ innerB: b }, ...).select(({ innerA }) => ({ aId: innerA.id })). Tested — no warning in that shape.