You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* 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>
Add includes (hierarchical data) documentation to all framework SKILL.md files and fix inaccurate toArray scalar select constraint in db-core/live-queries skill.
Copy file name to clipboardExpand all lines: docs/guides/live-queries.md
+220-3Lines changed: 220 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -50,6 +50,7 @@ query outputs automatically and should not be persisted back to storage.
50
50
-[Select Projections](#select)
51
51
-[Joins](#joins)
52
52
-[Subqueries](#subqueries)
53
+
-[Includes](#includes)
53
54
-[groupBy and Aggregations](#groupby-and-aggregations)
54
55
-[findOne](#findone)
55
56
-[Distinct](#distinct)
@@ -760,9 +761,8 @@ A `join` without a `select` will return row objects that are namespaced with the
760
761
761
762
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.
762
763
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.
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.
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()`:
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
+
<likey={project.id}>
1245
+
{project.name}
1246
+
{/* Pass the child collection to a subcomponent */}
1247
+
<IssueListissuesCollection={project.issues} />
1248
+
</li>
1249
+
))}
1250
+
</ul>
1251
+
)
1252
+
}
1253
+
1254
+
function IssueList({ issuesCollection }) {
1255
+
// Subscribe to the child collection for reactive updates
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
+
1056
1273
## groupBy and Aggregations
1057
1274
1058
1275
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.
<li*ngFor="let todo of query.data(); trackBy: trackById">{{ todo.text }}</li>
183
183
```
184
184
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) {
- 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.
290
291
- Collection includes (bare subquery) require an **object**`select`.
291
292
- Includes subqueries are compiled into the same incremental pipeline as the parent query -- they are not separate live queries.
0 commit comments