Skip to content
Closed
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/heavy-pandas-tell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@tanstack/db": patch
---

Materialize includes into parent rows before they commit so emitted change events already carry their included values. The includes flush ran only after the parent live query collection committed, patching stored rows in place without emitting; any consumer that copies rows at emit time — most commonly another live query layered on the collection, the shape framework adapters produce for component-level queries — captured the pre-patch row with its included fields unset. A parent-only update (no child changes) never triggered the follow-up re-emit, so those consumers kept empty includes until an unrelated child change recomputed the row. Fixes #1635.
36 changes: 36 additions & 0 deletions packages/db/src/query/live/collection-config-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,7 @@ export class CollectionConfigBuilder<

// 1. Flush parent changes
if (hasParentChanges) {
patchParentChangesFromIncludes(includesState, changesToApply)
begin()
changesToApply.forEach(this.applyChanges.bind(this, config))
commit()
Expand Down Expand Up @@ -1238,6 +1239,41 @@ function materializesInline(state: IncludesOutputState): boolean {
return state.materialization !== `collection`
}

/**
* Materializes the current includes state into freshly computed parent
* change values, so the change events emitted on commit already carry
* their included values.
*/
function patchParentChangesFromIncludes<TResult extends object>(
includesState: Array<IncludesOutputState>,
parentChanges: Map<unknown, Changes<TResult>>,
): void {
for (const state of includesState) {
if (!materializesInline(state)) continue
for (const [, changes] of parentChanges) {
if (changes.inserts > 0) {
const parentResult = changes.value as Record<string | symbol, any>
const routing = parentResult[INCLUDES_ROUTING]?.[state.fieldName]
const correlationKey = routing?.correlationKey
if (correlationKey != null) {
const routingKey = computeRoutingKey(
correlationKey,
routing?.parentContext ?? null,
)
setIncludedValue(
parentResult,
state.resultPath,
materializeIncludedValue(
state,
state.childRegistry.get(routingKey),
),
)
}
}
}
}
}

function materializeIncludedValue(
state: IncludesOutputState,
entry: ChildCollectionEntry | undefined,
Expand Down
47 changes: 47 additions & 0 deletions packages/db/tests/query/includes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,53 @@ describe(`includes subqueries`, () => {
})
})

describe(`parent-only updates`, () => {
it(`keeps materialized includes on rows observed through a layered live query`, async () => {
const collection = createLiveQueryCollection((q) =>
q.from({ p: projects }).select(({ p }) => ({
id: p.id,
name: p.name,
issues: materialize(
q
.from({ i: issues })
.where(({ i }) => eq(i.projectId, p.id))
.select(({ i }) => ({
id: i.id,
title: i.title,
})),
),
})),
)
await collection.preload()

const layered = createLiveQueryCollection((q) =>
q.from({ row: collection }),
)
await layered.preload()

expect(sortedPlainRows(layered.get(1)!.issues)).toEqual([
{ id: 10, title: `Bug in Alpha` },
{ id: 11, title: `Feature for Alpha` },
])

projects.utils.begin()
projects.utils.write({
type: `update`,
value: { id: 1, name: `Alpha renamed` },
})
projects.utils.commit()

await vi.waitFor(() => {
expect(layered.get(1)?.name).toBe(`Alpha renamed`)
})

expect(sortedPlainRows(layered.get(1)!.issues)).toEqual([
{ id: 10, title: `Bug in Alpha` },
{ id: 11, title: `Feature for Alpha` },
])
})
})

describe(`inner join filtering`, () => {
it(`only shows children for parents matching a WHERE clause`, async () => {
const collection = createLiveQueryCollection((q) =>
Expand Down