Skip to content

Commit 1e69dd6

Browse files
kevin-dpclaudeKyleAMathewsautofix-ci[bot]
authored
Docs for includes (#1361)
* Docs for includes (#1317) * Document includes subqueries in live queries guide Add an Includes section to docs/guides/live-queries.md covering: - Basic includes with correlation conditions - Additional filters including parent-referencing WHERE clauses - Ordering and limiting per parent - toArray() for plain array results - Aggregates per parent - Nested includes Also add packages/db/INCLUDES.md with architectural documentation and update the V2 roadmap to reflect implemented features. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Improve includes docs: use concrete examples instead of generic "parent/child" terminology Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Document how to use includes with React via subcomponents with useLiveQuery Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add React test for includes: child collection subscription via useLiveQuery Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * docs: add includes sections to framework SKILL.md files Add hierarchical data (includes) documentation to all framework skills (React, Solid, Vue, Svelte, Angular) and fix inaccurate toArray scalar select constraint in db-core/live-queries skill. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * ci: apply automated fixes * chore: add changeset for includes SKILL.md updates Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Kyle Mathews <mathews.kyle@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent c336aae commit 1e69dd6

File tree

9 files changed

+670
-4
lines changed

9 files changed

+670
-4
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@tanstack/db': patch
3+
'@tanstack/react-db': patch
4+
'@tanstack/solid-db': patch
5+
'@tanstack/vue-db': patch
6+
'@tanstack/svelte-db': patch
7+
'@tanstack/angular-db': patch
8+
---
9+
10+
Add includes (hierarchical data) documentation to all framework SKILL.md files and fix inaccurate toArray scalar select constraint in db-core/live-queries skill.

docs/guides/live-queries.md

Lines changed: 220 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ query outputs automatically and should not be persisted back to storage.
5050
- [Select Projections](#select)
5151
- [Joins](#joins)
5252
- [Subqueries](#subqueries)
53+
- [Includes](#includes)
5354
- [groupBy and Aggregations](#groupby-and-aggregations)
5455
- [findOne](#findone)
5556
- [Distinct](#distinct)
@@ -760,9 +761,8 @@ A `join` without a `select` will return row objects that are namespaced with the
760761

761762
The result type of a join will take into account the join type, with the optionality of the joined fields being determined by the join type.
762763

763-
> [!NOTE]
764-
> We are working on an `include` system that will enable joins that project to a hierarchical object. For example an `issue` row could have a `comments` property that is an array of `comment` rows.
765-
> See [this issue](https://github.com/TanStack/db/issues/288) for more details.
764+
> [!TIP]
765+
> If you need hierarchical results instead of flat joined rows (e.g., each project with its nested issues), see [Includes](#includes) below.
766766
767767
### Method Signature
768768

@@ -1053,6 +1053,223 @@ const topUsers = createCollection(liveQueryCollectionOptions({
10531053
}))
10541054
```
10551055

1056+
## Includes
1057+
1058+
Includes let you nest subqueries inside `.select()` to produce hierarchical results. Instead of joins that flatten 1:N relationships into repeated rows, each parent row gets a nested collection of its related items.
1059+
1060+
```ts
1061+
import { createLiveQueryCollection, eq } from '@tanstack/db'
1062+
1063+
const projectsWithIssues = createLiveQueryCollection((q) =>
1064+
q.from({ p: projectsCollection }).select(({ p }) => ({
1065+
id: p.id,
1066+
name: p.name,
1067+
issues: q
1068+
.from({ i: issuesCollection })
1069+
.where(({ i }) => eq(i.projectId, p.id))
1070+
.select(({ i }) => ({
1071+
id: i.id,
1072+
title: i.title,
1073+
})),
1074+
})),
1075+
)
1076+
```
1077+
1078+
Each project's `issues` field is a live `Collection` that updates incrementally as the underlying data changes.
1079+
1080+
### Correlation Condition
1081+
1082+
The child query's `.where()` must contain an `eq()` that links a child field to a parent field — this is the **correlation condition**. It tells the system how children relate to parents.
1083+
1084+
```ts
1085+
// The correlation condition: links issues to their parent project
1086+
.where(({ i }) => eq(i.projectId, p.id))
1087+
```
1088+
1089+
The correlation condition can appear as a standalone `.where()`, or inside an `and()`:
1090+
1091+
```ts
1092+
// Also valid — correlation is extracted from inside and()
1093+
.where(({ i }) => and(eq(i.projectId, p.id), eq(i.status, 'open')))
1094+
```
1095+
1096+
The correlation field does not need to be included in the parent's `.select()`.
1097+
1098+
### Additional Filters
1099+
1100+
Child queries support additional `.where()` clauses beyond the correlation condition, including filters that reference parent fields:
1101+
1102+
```ts
1103+
q.from({ p: projectsCollection }).select(({ p }) => ({
1104+
id: p.id,
1105+
name: p.name,
1106+
issues: q
1107+
.from({ i: issuesCollection })
1108+
.where(({ i }) => eq(i.projectId, p.id)) // correlation
1109+
.where(({ i }) => eq(i.createdBy, p.createdBy)) // parent-referencing filter
1110+
.where(({ i }) => eq(i.status, 'open')) // pure child filter
1111+
.select(({ i }) => ({
1112+
id: i.id,
1113+
title: i.title,
1114+
})),
1115+
}))
1116+
```
1117+
1118+
Parent-referencing filters are fully reactive — if a parent's field changes, the child results update automatically.
1119+
1120+
### Ordering and Limiting
1121+
1122+
Child queries support `.orderBy()` and `.limit()`, applied per parent:
1123+
1124+
```ts
1125+
q.from({ p: projectsCollection }).select(({ p }) => ({
1126+
id: p.id,
1127+
name: p.name,
1128+
issues: q
1129+
.from({ i: issuesCollection })
1130+
.where(({ i }) => eq(i.projectId, p.id))
1131+
.orderBy(({ i }) => i.createdAt, 'desc')
1132+
.limit(5)
1133+
.select(({ i }) => ({
1134+
id: i.id,
1135+
title: i.title,
1136+
})),
1137+
}))
1138+
```
1139+
1140+
Each project gets its own top-5 issues, not 5 issues shared across all projects.
1141+
1142+
### toArray
1143+
1144+
By default, each child result is a live `Collection`. If you want a plain array instead, wrap the child query with `toArray()`:
1145+
1146+
```ts
1147+
import { createLiveQueryCollection, eq, toArray } from '@tanstack/db'
1148+
1149+
const projectsWithIssues = createLiveQueryCollection((q) =>
1150+
q.from({ p: projectsCollection }).select(({ p }) => ({
1151+
id: p.id,
1152+
name: p.name,
1153+
issues: toArray(
1154+
q
1155+
.from({ i: issuesCollection })
1156+
.where(({ i }) => eq(i.projectId, p.id))
1157+
.select(({ i }) => ({
1158+
id: i.id,
1159+
title: i.title,
1160+
})),
1161+
),
1162+
})),
1163+
)
1164+
```
1165+
1166+
With `toArray()`, the project row is re-emitted whenever its issues change. Without it, the child `Collection` updates independently.
1167+
1168+
### Aggregates
1169+
1170+
You can use aggregate functions in child queries. Aggregates are computed per parent:
1171+
1172+
```ts
1173+
import { createLiveQueryCollection, eq, count } from '@tanstack/db'
1174+
1175+
const projectsWithCounts = createLiveQueryCollection((q) =>
1176+
q.from({ p: projectsCollection }).select(({ p }) => ({
1177+
id: p.id,
1178+
name: p.name,
1179+
issueCount: q
1180+
.from({ i: issuesCollection })
1181+
.where(({ i }) => eq(i.projectId, p.id))
1182+
.select(({ i }) => ({ total: count(i.id) })),
1183+
})),
1184+
)
1185+
```
1186+
1187+
Each project gets its own count. The count updates reactively as issues are added or removed.
1188+
1189+
### Nested Includes
1190+
1191+
Includes nest arbitrarily. For example, projects can include issues, which include comments:
1192+
1193+
```ts
1194+
const tree = createLiveQueryCollection((q) =>
1195+
q.from({ p: projectsCollection }).select(({ p }) => ({
1196+
id: p.id,
1197+
name: p.name,
1198+
issues: q
1199+
.from({ i: issuesCollection })
1200+
.where(({ i }) => eq(i.projectId, p.id))
1201+
.select(({ i }) => ({
1202+
id: i.id,
1203+
title: i.title,
1204+
comments: q
1205+
.from({ c: commentsCollection })
1206+
.where(({ c }) => eq(c.issueId, i.id))
1207+
.select(({ c }) => ({
1208+
id: c.id,
1209+
body: c.body,
1210+
})),
1211+
})),
1212+
})),
1213+
)
1214+
```
1215+
1216+
Each level updates independently and incrementally — adding a comment to an issue does not re-process other issues or projects.
1217+
1218+
### Using Includes with React
1219+
1220+
When using includes with React, each child `Collection` needs its own `useLiveQuery` subscription to receive reactive updates. Pass the child collection to a subcomponent that calls `useLiveQuery(childCollection)`:
1221+
1222+
```tsx
1223+
import { useLiveQuery } from '@tanstack/react-db'
1224+
import { eq } from '@tanstack/db'
1225+
1226+
function ProjectList() {
1227+
const { data: projects } = useLiveQuery((q) =>
1228+
q.from({ p: projectsCollection }).select(({ p }) => ({
1229+
id: p.id,
1230+
name: p.name,
1231+
issues: q
1232+
.from({ i: issuesCollection })
1233+
.where(({ i }) => eq(i.projectId, p.id))
1234+
.select(({ i }) => ({
1235+
id: i.id,
1236+
title: i.title,
1237+
})),
1238+
})),
1239+
)
1240+
1241+
return (
1242+
<ul>
1243+
{projects.map((project) => (
1244+
<li key={project.id}>
1245+
{project.name}
1246+
{/* Pass the child collection to a subcomponent */}
1247+
<IssueList issuesCollection={project.issues} />
1248+
</li>
1249+
))}
1250+
</ul>
1251+
)
1252+
}
1253+
1254+
function IssueList({ issuesCollection }) {
1255+
// Subscribe to the child collection for reactive updates
1256+
const { data: issues } = useLiveQuery(issuesCollection)
1257+
1258+
return (
1259+
<ul>
1260+
{issues.map((issue) => (
1261+
<li key={issue.id}>{issue.title}</li>
1262+
))}
1263+
</ul>
1264+
)
1265+
}
1266+
```
1267+
1268+
Each `IssueList` component independently subscribes to its project's issues. When an issue is added or removed, only the affected `IssueList` re-renders — the parent `ProjectList` does not.
1269+
1270+
> [!NOTE]
1271+
> You must pass the child collection to a subcomponent and subscribe with `useLiveQuery`. Reading `project.issues` directly in the parent without subscribing will give you the collection object, but the component won't re-render when the child data changes.
1272+
10561273
## groupBy and Aggregations
10571274

10581275
Use `groupBy` to group your data and apply aggregate functions. When you use aggregates in `select` without `groupBy`, the entire result set is treated as a single group.

packages/angular-db/skills/angular-db/SKILL.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,76 @@ Angular 16 structural directives:
182182
<li *ngFor="let todo of query.data(); trackBy: trackById">{{ todo.text }}</li>
183183
```
184184

185+
## Includes (Hierarchical Data)
186+
187+
When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `injectLiveQuery` in a child component:
188+
189+
```typescript
190+
@Component({
191+
selector: 'app-project-list',
192+
standalone: true,
193+
imports: [IssueListComponent],
194+
template: `
195+
@for (project of query.data(); track project.id) {
196+
<div>
197+
{{ project.name }}
198+
<app-issue-list [issuesCollection]="project.issues" />
199+
</div>
200+
}
201+
`,
202+
})
203+
export class ProjectListComponent {
204+
query = injectLiveQuery((q) =>
205+
q.from({ p: projectsCollection }).select(({ p }) => ({
206+
id: p.id,
207+
name: p.name,
208+
issues: q
209+
.from({ i: issuesCollection })
210+
.where(({ i }) => eq(i.projectId, p.id))
211+
.select(({ i }) => ({ id: i.id, title: i.title })),
212+
})),
213+
)
214+
}
215+
216+
// Child component subscribes to the child Collection
217+
@Component({
218+
selector: 'app-issue-list',
219+
standalone: true,
220+
template: `
221+
@for (issue of query.data(); track issue.id) {
222+
<li>{{ issue.title }}</li>
223+
}
224+
`,
225+
})
226+
export class IssueListComponent {
227+
issuesCollection = input.required<Collection>()
228+
229+
query = injectLiveQuery(this.issuesCollection())
230+
}
231+
```
232+
233+
With `toArray()`, child results are plain arrays and the parent re-emits on child changes:
234+
235+
```typescript
236+
import { toArray, eq } from '@tanstack/angular-db'
237+
238+
query = injectLiveQuery((q) =>
239+
q.from({ p: projectsCollection }).select(({ p }) => ({
240+
id: p.id,
241+
name: p.name,
242+
issues: toArray(
243+
q
244+
.from({ i: issuesCollection })
245+
.where(({ i }) => eq(i.projectId, p.id))
246+
.select(({ i }) => ({ id: i.id, title: i.title })),
247+
),
248+
})),
249+
)
250+
// project.issues is a plain array — no child component subscription needed
251+
```
252+
253+
See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).
254+
185255
## Common Mistakes
186256

187257
### CRITICAL Using injectLiveQuery outside injection context

packages/db/skills/db-core/live-queries/SKILL.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -286,7 +286,8 @@ const messagesWithContent = createLiveQueryCollection((q) =>
286286
### Includes rules
287287

288288
- The subquery **must** have a `where` clause with an `eq()` correlating a parent alias with a child alias. The library extracts this automatically as the join condition.
289-
- `toArray()` and `concat(toArray())` require the subquery to use a **scalar** `select` (e.g., `select(({ c }) => c.text)`), not an object select.
289+
- `toArray()` works with both scalar selects (e.g., `select(({ c }) => c.text)``string[]`) and object selects (e.g., `select(({ c }) => ({ id: c.id, title: c.title }))``Array<{id, title}>`).
290+
- `concat(toArray())` requires a **scalar** `select` to concatenate into a string.
290291
- Collection includes (bare subquery) require an **object** `select`.
291292
- Includes subqueries are compiled into the same incremental pipeline as the parent query -- they are not separate live queries.
292293

0 commit comments

Comments
 (0)