Skip to content

Commit 232f228

Browse files
kevin-dpautofix-ci[bot]claude
authored
fix: lazy load includes child collections in on-demand sync mode (#1471)
* Unit tests to check that includes are loaded lazily on-demand * Lazy load included rows in on-demand mode when index is available * ci: apply automated fixes * Fix type issue * changeset * fix: pass child where clauses to loadSubset in includes (#1472) * Unit tests to check that where clauses of included collections are passed * Unit tests to reproduce the problem with where clauses on included collections not being passed * Pass child where clauses to loadSubset * ci: apply automated fixes * changeset for child where clauses fix Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent dd16655 commit 232f228

File tree

4 files changed

+764
-0
lines changed

4 files changed

+764
-0
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/db': patch
3+
---
4+
5+
fix: pass child where clauses to loadSubset in includes
6+
7+
Pure-child WHERE clauses on includes subqueries (e.g., `.where(({ item }) => eq(item.status, 'active'))`) are now passed through to the child collection's `loadSubset`/`queryFn`, enabling server-side filtering. Previously only the correlation filter reached the sync layer; additional child filters were applied client-side only.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@tanstack/db': patch
3+
---
4+
5+
fix: lazy load includes child collections in on-demand sync mode
6+
7+
Includes child collections now use the same lazy loading mechanism as regular joins. When a query uses includes with a correlation WHERE clause (e.g., `.where(({ item }) => eq(item.rootId, r.id))`), only matching child rows are loaded on-demand via `requestSnapshot({ where: inArray(field, keys) })` instead of loading all data upfront. This ensures the sync layer's `queryFn` receives the correlation filter in `loadSubsetOptions`, enabling efficient server-side filtering.

packages/db/src/query/compiler/index.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
join as joinOperator,
55
map,
66
reduce,
7+
tap,
78
} from '@tanstack/db-ivm'
89
import { optimizeQuery } from '../optimizer.js'
910
import {
@@ -22,6 +23,8 @@ import {
2223
Value as ValClass,
2324
getWhereExpression,
2425
} from '../ir.js'
26+
import { ensureIndexForField } from '../../indexes/auto-index.js'
27+
import { inArray } from '../builder/functions.js'
2528
import { compileExpression, toBooleanPredicate } from './evaluators.js'
2629
import { processJoins } from './joins.js'
2730
import { containsAggregate, processGroupBy } from './group-by.js'
@@ -379,6 +382,72 @@ export function compileQuery(
379382
),
380383
)
381384

385+
// --- Includes lazy loading (mirrors join lazy loading in joins.ts) ---
386+
// Resolve the child correlation field to its underlying collection + field path
387+
// so we can set up an index and targeted requestSnapshot calls.
388+
const childCorrelationAlias = subquery.childCorrelationField.path[0]!
389+
const childFromCollection =
390+
subquery.query.from.type === `collectionRef`
391+
? subquery.query.from.collection
392+
: (null as unknown as Collection)
393+
const followRefResult = followRef(
394+
subquery.query,
395+
subquery.childCorrelationField,
396+
childFromCollection,
397+
)
398+
399+
if (followRefResult) {
400+
const followRefCollection = followRefResult.collection
401+
const fieldPath = followRefResult.path
402+
const fieldName = fieldPath[0]
403+
404+
// 1. Mark child source as lazy so CollectionSubscriber skips initial full load
405+
lazySources.add(childCorrelationAlias)
406+
407+
// 2. Ensure an index on the correlation field for efficient lookups
408+
if (fieldName) {
409+
ensureIndexForField(fieldName, fieldPath, followRefCollection)
410+
}
411+
412+
// 3. Tap parent keys to intercept correlation values and request
413+
// matching child rows on-demand via the child's subscription
414+
parentKeys = parentKeys.pipe(
415+
tap((data: any) => {
416+
const resolvedAlias =
417+
aliasRemapping[childCorrelationAlias] || childCorrelationAlias
418+
const lazySourceSubscription = subscriptions[resolvedAlias]
419+
420+
if (!lazySourceSubscription) {
421+
return
422+
}
423+
424+
if (lazySourceSubscription.hasLoadedInitialState()) {
425+
return
426+
}
427+
428+
const joinKeys = [
429+
...new Set(
430+
data
431+
.getInner()
432+
.map(
433+
([[correlationValue]]: any) => correlationValue as unknown,
434+
)
435+
.filter((key: unknown) => key != null),
436+
),
437+
]
438+
439+
if (joinKeys.length === 0) {
440+
return
441+
}
442+
443+
const lazyJoinRef = new PropRef(fieldPath)
444+
lazySourceSubscription.requestSnapshot({
445+
where: inArray(lazyJoinRef, joinKeys),
446+
})
447+
}),
448+
)
449+
}
450+
382451
// If parent filters exist, append them to the child query's WHERE
383452
const childQuery =
384453
subquery.parentFilters && subquery.parentFilters.length > 0
@@ -410,6 +479,9 @@ export function compileQuery(
410479
// Merge child's alias metadata into parent's
411480
Object.assign(aliasToCollectionId, childResult.aliasToCollectionId)
412481
Object.assign(aliasRemapping, childResult.aliasRemapping)
482+
for (const [alias, whereClause] of childResult.sourceWhereClauses) {
483+
sourceWhereClauses.set(alias, whereClause)
484+
}
413485

414486
includesResults.push({
415487
pipeline: childResult.pipeline,

0 commit comments

Comments
 (0)