@tanstack/db 0.5.33 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. package/dist/cjs/collection/change-events.cjs.map +1 -1
  2. package/dist/cjs/collection/change-events.d.cts +3 -2
  3. package/dist/cjs/collection/changes.cjs +13 -4
  4. package/dist/cjs/collection/changes.cjs.map +1 -1
  5. package/dist/cjs/collection/changes.d.cts +10 -1
  6. package/dist/cjs/collection/cleanup-queue.cjs +89 -0
  7. package/dist/cjs/collection/cleanup-queue.cjs.map +1 -0
  8. package/dist/cjs/collection/cleanup-queue.d.cts +30 -0
  9. package/dist/cjs/collection/events.cjs +14 -0
  10. package/dist/cjs/collection/events.cjs.map +1 -1
  11. package/dist/cjs/collection/events.d.cts +39 -1
  12. package/dist/cjs/collection/index.cjs +66 -28
  13. package/dist/cjs/collection/index.cjs.map +1 -1
  14. package/dist/cjs/collection/index.d.cts +49 -36
  15. package/dist/cjs/collection/indexes.cjs +211 -62
  16. package/dist/cjs/collection/indexes.cjs.map +1 -1
  17. package/dist/cjs/collection/indexes.d.cts +27 -17
  18. package/dist/cjs/collection/lifecycle.cjs +5 -22
  19. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  20. package/dist/cjs/collection/lifecycle.d.cts +0 -1
  21. package/dist/cjs/collection/mutations.cjs +18 -0
  22. package/dist/cjs/collection/mutations.cjs.map +1 -1
  23. package/dist/cjs/collection/mutations.d.cts +1 -0
  24. package/dist/cjs/collection/state.cjs +381 -53
  25. package/dist/cjs/collection/state.cjs.map +1 -1
  26. package/dist/cjs/collection/state.d.cts +65 -1
  27. package/dist/cjs/collection/subscription.cjs +6 -0
  28. package/dist/cjs/collection/subscription.cjs.map +1 -1
  29. package/dist/cjs/collection/subscription.d.cts +4 -0
  30. package/dist/cjs/collection/sync.cjs +108 -1
  31. package/dist/cjs/collection/sync.cjs.map +1 -1
  32. package/dist/cjs/collection/sync.d.cts +2 -0
  33. package/dist/cjs/collection/transaction-metadata.cjs +5 -0
  34. package/dist/cjs/collection/transaction-metadata.cjs.map +1 -0
  35. package/dist/cjs/collection/transaction-metadata.d.cts +1 -0
  36. package/dist/cjs/errors.cjs +8 -0
  37. package/dist/cjs/errors.cjs.map +1 -1
  38. package/dist/cjs/errors.d.cts +3 -0
  39. package/dist/cjs/index.cjs +22 -4
  40. package/dist/cjs/index.cjs.map +1 -1
  41. package/dist/cjs/index.d.cts +11 -3
  42. package/dist/cjs/indexes/auto-index.cjs +13 -6
  43. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  44. package/dist/cjs/indexes/base-index.cjs +0 -3
  45. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  46. package/dist/cjs/indexes/base-index.d.cts +2 -6
  47. package/dist/cjs/indexes/basic-index.cjs +361 -0
  48. package/dist/cjs/indexes/basic-index.cjs.map +1 -0
  49. package/dist/cjs/indexes/basic-index.d.cts +102 -0
  50. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  51. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  52. package/dist/cjs/indexes/index-options.d.cts +8 -9
  53. package/dist/cjs/indexes/index-registry.cjs +89 -0
  54. package/dist/cjs/indexes/index-registry.cjs.map +1 -0
  55. package/dist/cjs/indexes/index-registry.d.cts +61 -0
  56. package/dist/cjs/local-only.cjs +5 -0
  57. package/dist/cjs/local-only.cjs.map +1 -1
  58. package/dist/cjs/query/builder/functions.cjs +27 -11
  59. package/dist/cjs/query/builder/functions.cjs.map +1 -1
  60. package/dist/cjs/query/builder/functions.d.cts +25 -3
  61. package/dist/cjs/query/builder/index.cjs +200 -39
  62. package/dist/cjs/query/builder/index.cjs.map +1 -1
  63. package/dist/cjs/query/builder/index.d.cts +4 -3
  64. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  65. package/dist/cjs/query/builder/ref-proxy.d.cts +14 -3
  66. package/dist/cjs/query/builder/types.d.cts +84 -19
  67. package/dist/cjs/query/compiler/evaluators.cjs +51 -0
  68. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  69. package/dist/cjs/query/compiler/group-by.cjs +100 -28
  70. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  71. package/dist/cjs/query/compiler/group-by.d.cts +4 -2
  72. package/dist/cjs/query/compiler/index.cjs +283 -11
  73. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  74. package/dist/cjs/query/compiler/index.d.cts +30 -2
  75. package/dist/cjs/query/compiler/order-by.cjs +29 -10
  76. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  77. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  78. package/dist/cjs/query/compiler/select.cjs +8 -0
  79. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  80. package/dist/cjs/query/index.d.cts +2 -1
  81. package/dist/cjs/query/ir.cjs +18 -1
  82. package/dist/cjs/query/ir.cjs.map +1 -1
  83. package/dist/cjs/query/ir.d.cts +21 -1
  84. package/dist/cjs/query/live/collection-config-builder.cjs +501 -5
  85. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  86. package/dist/cjs/query/live/collection-config-builder.d.cts +7 -0
  87. package/dist/cjs/query/live/types.d.cts +3 -3
  88. package/dist/cjs/query/live/utils.cjs +43 -3
  89. package/dist/cjs/query/live/utils.cjs.map +1 -1
  90. package/dist/cjs/query/live/utils.d.cts +1 -0
  91. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  92. package/dist/cjs/query/live-query-collection.d.cts +9 -6
  93. package/dist/cjs/query/query-once.cjs.map +1 -1
  94. package/dist/cjs/query/query-once.d.cts +7 -5
  95. package/dist/cjs/query/subset-dedupe.cjs +9 -3
  96. package/dist/cjs/query/subset-dedupe.cjs.map +1 -1
  97. package/dist/cjs/types.d.cts +42 -8
  98. package/dist/cjs/utils/array-utils.cjs +27 -0
  99. package/dist/cjs/utils/array-utils.cjs.map +1 -0
  100. package/dist/cjs/utils/array-utils.d.cts +16 -0
  101. package/dist/cjs/utils/comparison.cjs +11 -0
  102. package/dist/cjs/utils/comparison.cjs.map +1 -1
  103. package/dist/cjs/utils/index-optimization.cjs +4 -0
  104. package/dist/cjs/utils/index-optimization.cjs.map +1 -1
  105. package/dist/cjs/utils.cjs +7 -9
  106. package/dist/cjs/utils.cjs.map +1 -1
  107. package/dist/cjs/utils.d.cts +6 -1
  108. package/dist/cjs/virtual-props.cjs +33 -0
  109. package/dist/cjs/virtual-props.cjs.map +1 -0
  110. package/dist/cjs/virtual-props.d.cts +196 -0
  111. package/dist/esm/collection/change-events.d.ts +3 -2
  112. package/dist/esm/collection/change-events.js.map +1 -1
  113. package/dist/esm/collection/changes.d.ts +10 -1
  114. package/dist/esm/collection/changes.js +13 -4
  115. package/dist/esm/collection/changes.js.map +1 -1
  116. package/dist/esm/collection/cleanup-queue.d.ts +30 -0
  117. package/dist/esm/collection/cleanup-queue.js +89 -0
  118. package/dist/esm/collection/cleanup-queue.js.map +1 -0
  119. package/dist/esm/collection/events.d.ts +39 -1
  120. package/dist/esm/collection/events.js +14 -0
  121. package/dist/esm/collection/events.js.map +1 -1
  122. package/dist/esm/collection/index.d.ts +49 -36
  123. package/dist/esm/collection/index.js +67 -29
  124. package/dist/esm/collection/index.js.map +1 -1
  125. package/dist/esm/collection/indexes.d.ts +27 -17
  126. package/dist/esm/collection/indexes.js +211 -62
  127. package/dist/esm/collection/indexes.js.map +1 -1
  128. package/dist/esm/collection/lifecycle.d.ts +0 -1
  129. package/dist/esm/collection/lifecycle.js +5 -22
  130. package/dist/esm/collection/lifecycle.js.map +1 -1
  131. package/dist/esm/collection/mutations.d.ts +1 -0
  132. package/dist/esm/collection/mutations.js +18 -0
  133. package/dist/esm/collection/mutations.js.map +1 -1
  134. package/dist/esm/collection/state.d.ts +65 -1
  135. package/dist/esm/collection/state.js +381 -53
  136. package/dist/esm/collection/state.js.map +1 -1
  137. package/dist/esm/collection/subscription.d.ts +4 -0
  138. package/dist/esm/collection/subscription.js +6 -0
  139. package/dist/esm/collection/subscription.js.map +1 -1
  140. package/dist/esm/collection/sync.d.ts +2 -0
  141. package/dist/esm/collection/sync.js +108 -1
  142. package/dist/esm/collection/sync.js.map +1 -1
  143. package/dist/esm/collection/transaction-metadata.d.ts +1 -0
  144. package/dist/esm/collection/transaction-metadata.js +5 -0
  145. package/dist/esm/collection/transaction-metadata.js.map +1 -0
  146. package/dist/esm/errors.d.ts +3 -0
  147. package/dist/esm/errors.js +8 -0
  148. package/dist/esm/errors.js.map +1 -1
  149. package/dist/esm/index.d.ts +11 -3
  150. package/dist/esm/index.js +25 -7
  151. package/dist/esm/index.js.map +1 -1
  152. package/dist/esm/indexes/auto-index.js +13 -6
  153. package/dist/esm/indexes/auto-index.js.map +1 -1
  154. package/dist/esm/indexes/base-index.d.ts +2 -6
  155. package/dist/esm/indexes/base-index.js +1 -4
  156. package/dist/esm/indexes/base-index.js.map +1 -1
  157. package/dist/esm/indexes/basic-index.d.ts +102 -0
  158. package/dist/esm/indexes/basic-index.js +361 -0
  159. package/dist/esm/indexes/basic-index.js.map +1 -0
  160. package/dist/esm/indexes/btree-index.d.ts +1 -1
  161. package/dist/esm/indexes/btree-index.js.map +1 -1
  162. package/dist/esm/indexes/index-options.d.ts +8 -9
  163. package/dist/esm/indexes/index-registry.d.ts +61 -0
  164. package/dist/esm/indexes/index-registry.js +89 -0
  165. package/dist/esm/indexes/index-registry.js.map +1 -0
  166. package/dist/esm/local-only.js +5 -0
  167. package/dist/esm/local-only.js.map +1 -1
  168. package/dist/esm/query/builder/functions.d.ts +25 -3
  169. package/dist/esm/query/builder/functions.js +27 -11
  170. package/dist/esm/query/builder/functions.js.map +1 -1
  171. package/dist/esm/query/builder/index.d.ts +4 -3
  172. package/dist/esm/query/builder/index.js +201 -40
  173. package/dist/esm/query/builder/index.js.map +1 -1
  174. package/dist/esm/query/builder/ref-proxy.d.ts +14 -3
  175. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  176. package/dist/esm/query/builder/types.d.ts +84 -19
  177. package/dist/esm/query/compiler/evaluators.js +51 -0
  178. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  179. package/dist/esm/query/compiler/group-by.d.ts +4 -2
  180. package/dist/esm/query/compiler/group-by.js +101 -29
  181. package/dist/esm/query/compiler/group-by.js.map +1 -1
  182. package/dist/esm/query/compiler/index.d.ts +30 -2
  183. package/dist/esm/query/compiler/index.js +285 -13
  184. package/dist/esm/query/compiler/index.js.map +1 -1
  185. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  186. package/dist/esm/query/compiler/order-by.js +30 -11
  187. package/dist/esm/query/compiler/order-by.js.map +1 -1
  188. package/dist/esm/query/compiler/select.js +8 -0
  189. package/dist/esm/query/compiler/select.js.map +1 -1
  190. package/dist/esm/query/index.d.ts +2 -1
  191. package/dist/esm/query/ir.d.ts +21 -1
  192. package/dist/esm/query/ir.js +18 -1
  193. package/dist/esm/query/ir.js.map +1 -1
  194. package/dist/esm/query/live/collection-config-builder.d.ts +7 -0
  195. package/dist/esm/query/live/collection-config-builder.js +503 -7
  196. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  197. package/dist/esm/query/live/types.d.ts +3 -3
  198. package/dist/esm/query/live/utils.d.ts +1 -0
  199. package/dist/esm/query/live/utils.js +43 -3
  200. package/dist/esm/query/live/utils.js.map +1 -1
  201. package/dist/esm/query/live-query-collection.d.ts +9 -6
  202. package/dist/esm/query/live-query-collection.js.map +1 -1
  203. package/dist/esm/query/query-once.d.ts +7 -5
  204. package/dist/esm/query/query-once.js.map +1 -1
  205. package/dist/esm/query/subset-dedupe.js +9 -3
  206. package/dist/esm/query/subset-dedupe.js.map +1 -1
  207. package/dist/esm/types.d.ts +42 -8
  208. package/dist/esm/utils/array-utils.d.ts +16 -0
  209. package/dist/esm/utils/array-utils.js +27 -0
  210. package/dist/esm/utils/array-utils.js.map +1 -0
  211. package/dist/esm/utils/comparison.js +11 -0
  212. package/dist/esm/utils/comparison.js.map +1 -1
  213. package/dist/esm/utils/index-optimization.js +4 -0
  214. package/dist/esm/utils/index-optimization.js.map +1 -1
  215. package/dist/esm/utils.d.ts +6 -1
  216. package/dist/esm/utils.js +7 -9
  217. package/dist/esm/utils.js.map +1 -1
  218. package/dist/esm/virtual-props.d.ts +196 -0
  219. package/dist/esm/virtual-props.js +33 -0
  220. package/dist/esm/virtual-props.js.map +1 -0
  221. package/package.json +4 -3
  222. package/skills/db-core/SKILL.md +4 -2
  223. package/skills/db-core/collection-setup/SKILL.md +30 -11
  224. package/skills/db-core/collection-setup/references/electric-adapter.md +1 -1
  225. package/skills/db-core/collection-setup/references/powersync-adapter.md +4 -0
  226. package/skills/db-core/collection-setup/references/query-adapter.md +32 -0
  227. package/skills/db-core/custom-adapter/SKILL.md +58 -9
  228. package/skills/db-core/live-queries/SKILL.md +162 -2
  229. package/skills/db-core/mutations-optimistic/SKILL.md +1 -1
  230. package/skills/db-core/persistence/SKILL.md +241 -0
  231. package/skills/meta-framework/SKILL.md +1 -1
  232. package/src/collection/change-events.ts +13 -9
  233. package/src/collection/changes.ts +30 -7
  234. package/src/collection/cleanup-queue.ts +105 -0
  235. package/src/collection/events.ts +65 -0
  236. package/src/collection/index.ts +110 -45
  237. package/src/collection/indexes.ts +283 -76
  238. package/src/collection/lifecycle.ts +5 -26
  239. package/src/collection/mutations.ts +21 -0
  240. package/src/collection/state.ts +545 -71
  241. package/src/collection/subscription.ts +7 -0
  242. package/src/collection/sync.ts +137 -0
  243. package/src/collection/transaction-metadata.ts +1 -0
  244. package/src/errors.ts +9 -0
  245. package/src/index.ts +46 -3
  246. package/src/indexes/auto-index.ts +18 -8
  247. package/src/indexes/base-index.ts +2 -10
  248. package/src/indexes/basic-index.ts +507 -0
  249. package/src/indexes/btree-index.ts +1 -1
  250. package/src/indexes/index-options.ts +17 -37
  251. package/src/indexes/index-registry.ts +174 -0
  252. package/src/local-only.ts +7 -0
  253. package/src/query/builder/functions.ts +84 -7
  254. package/src/query/builder/index.ts +329 -9
  255. package/src/query/builder/ref-proxy.ts +22 -4
  256. package/src/query/builder/types.ts +257 -62
  257. package/src/query/compiler/evaluators.ts +57 -0
  258. package/src/query/compiler/group-by.ts +156 -35
  259. package/src/query/compiler/index.ts +445 -15
  260. package/src/query/compiler/order-by.ts +51 -12
  261. package/src/query/compiler/select.ts +9 -0
  262. package/src/query/index.ts +7 -0
  263. package/src/query/ir.ts +23 -2
  264. package/src/query/live/collection-config-builder.ts +809 -9
  265. package/src/query/live/types.ts +10 -4
  266. package/src/query/live/utils.ts +64 -3
  267. package/src/query/live-query-collection.ts +43 -18
  268. package/src/query/query-once.ts +31 -12
  269. package/src/query/subset-dedupe.ts +11 -7
  270. package/src/types.ts +49 -9
  271. package/src/utils/array-utils.ts +49 -0
  272. package/src/utils/comparison.ts +14 -0
  273. package/src/utils/index-optimization.ts +4 -0
  274. package/src/utils.ts +12 -9
  275. package/src/virtual-props.ts +282 -0
  276. package/dist/cjs/indexes/lazy-index.cjs +0 -190
  277. package/dist/cjs/indexes/lazy-index.cjs.map +0 -1
  278. package/dist/cjs/indexes/lazy-index.d.cts +0 -96
  279. package/dist/esm/indexes/lazy-index.d.ts +0 -96
  280. package/dist/esm/indexes/lazy-index.js +0 -190
  281. package/dist/esm/indexes/lazy-index.js.map +0 -1
  282. package/src/indexes/lazy-index.ts +0 -251
@@ -21,6 +21,43 @@ import type {
21
21
  Select,
22
22
  } from '../ir.js'
23
23
  import type { NamespacedAndKeyedStream, NamespacedRow } from '../../types.js'
24
+ import type { VirtualOrigin } from '../../virtual-props.js'
25
+
26
+ const VIRTUAL_SYNCED_KEY = `__virtual_synced__`
27
+ const VIRTUAL_HAS_LOCAL_KEY = `__virtual_has_local__`
28
+
29
+ type RowVirtualMetadata = {
30
+ synced: boolean
31
+ hasLocal: boolean
32
+ }
33
+
34
+ function getRowVirtualMetadata(row: NamespacedRow): RowVirtualMetadata {
35
+ let found = false
36
+ let allSynced = true
37
+ let hasLocal = false
38
+
39
+ for (const [alias, value] of Object.entries(row)) {
40
+ if (alias === `$selected`) continue
41
+ const asRecord = value
42
+ const hasSyncedProp = `$synced` in asRecord
43
+ const hasOriginProp = `$origin` in asRecord
44
+ if (!hasSyncedProp && !hasOriginProp) {
45
+ continue
46
+ }
47
+ found = true
48
+ if (asRecord.$synced === false) {
49
+ allSynced = false
50
+ }
51
+ if (asRecord.$origin === `local`) {
52
+ hasLocal = true
53
+ }
54
+ }
55
+
56
+ return {
57
+ synced: found ? allSynced : true,
58
+ hasLocal,
59
+ }
60
+ }
24
61
 
25
62
  const { sum, count, avg, min, max } = groupByOperators
26
63
 
@@ -80,11 +117,40 @@ export function processGroupBy(
80
117
  havingClauses?: Array<Having>,
81
118
  selectClause?: Select,
82
119
  fnHavingClauses?: Array<(row: any) => any>,
120
+ aggregateCollectionId?: string,
121
+ mainSource?: string,
83
122
  ): NamespacedAndKeyedStream {
123
+ const virtualAggregates: Record<string, any> = {
124
+ [VIRTUAL_SYNCED_KEY]: {
125
+ preMap: ([, row]: [string, NamespacedRow]) =>
126
+ getRowVirtualMetadata(row).synced,
127
+ reduce: (values: Array<[boolean, number]>) => {
128
+ for (const [isSynced, multiplicity] of values) {
129
+ if (!isSynced && multiplicity > 0) {
130
+ return false
131
+ }
132
+ }
133
+ return true
134
+ },
135
+ },
136
+ [VIRTUAL_HAS_LOCAL_KEY]: {
137
+ preMap: ([, row]: [string, NamespacedRow]) =>
138
+ getRowVirtualMetadata(row).hasLocal,
139
+ reduce: (values: Array<[boolean, number]>) => {
140
+ for (const [isLocal, multiplicity] of values) {
141
+ if (isLocal && multiplicity > 0) {
142
+ return true
143
+ }
144
+ }
145
+ return false
146
+ },
147
+ },
148
+ }
149
+
84
150
  // Handle empty GROUP BY (single-group aggregation)
85
151
  if (groupByClause.length === 0) {
86
152
  // For single-group aggregation, create a single group with all data
87
- const aggregates: Record<string, any> = {}
153
+ const aggregates: Record<string, any> = virtualAggregates
88
154
 
89
155
  // Expressions that wrap aggregates (e.g. coalesce(count(...), 0)).
90
156
  // Keys are the original SELECT aliases; values are pre-compiled evaluators
@@ -110,8 +176,15 @@ export function processGroupBy(
110
176
  }
111
177
  }
112
178
 
113
- // Use a constant key for single group
114
- const keyExtractor = () => ({ __singleGroup: true })
179
+ // Use a constant key for single group.
180
+ // When mainSource is set (includes mode), include __correlationKey so that
181
+ // rows from different parents aggregate separately.
182
+ const keyExtractor = mainSource
183
+ ? ([, row]: [string, NamespacedRow]) => ({
184
+ __singleGroup: true,
185
+ __correlationKey: (row as any)?.[mainSource]?.__correlationKey,
186
+ })
187
+ : () => ({ __singleGroup: true })
115
188
 
116
189
  // Apply the groupBy operator with single group
117
190
  pipeline = pipeline.pipe(
@@ -139,14 +212,37 @@ export function processGroupBy(
139
212
  )
140
213
  }
141
214
 
142
- // Use a single key for the result and update $selected
143
- return [
144
- `single_group`,
145
- {
146
- ...aggregatedRow,
147
- $selected: finalResults,
148
- },
149
- ] as [unknown, Record<string, any>]
215
+ // Use a single key for the result and update $selected.
216
+ // When in includes mode, restore the namespaced source structure with
217
+ // __correlationKey so output extraction can route results per-parent.
218
+ const correlationKey = mainSource
219
+ ? (aggregatedRow as any).__correlationKey
220
+ : undefined
221
+ const resultKey =
222
+ correlationKey !== undefined
223
+ ? `single_group_${serializeValue(correlationKey)}`
224
+ : `single_group`
225
+ const resultRow: Record<string, any> = {
226
+ ...(aggregatedRow as Record<string, any>),
227
+ $selected: finalResults,
228
+ }
229
+ const groupSynced = (aggregatedRow as Record<string, any>)[
230
+ VIRTUAL_SYNCED_KEY
231
+ ]
232
+ const groupHasLocal = (aggregatedRow as Record<string, any>)[
233
+ VIRTUAL_HAS_LOCAL_KEY
234
+ ]
235
+ resultRow.$synced = groupSynced ?? true
236
+ resultRow.$origin = (
237
+ groupHasLocal ? `local` : `remote`
238
+ ) satisfies VirtualOrigin
239
+ resultRow.$key = resultKey
240
+ resultRow.$collectionId =
241
+ aggregateCollectionId ?? resultRow.$collectionId
242
+ if (mainSource && correlationKey !== undefined) {
243
+ resultRow[mainSource] = { __correlationKey: correlationKey }
244
+ }
245
+ return [resultKey, resultRow] as [unknown, Record<string, any>]
150
246
  }),
151
247
  )
152
248
 
@@ -196,7 +292,9 @@ export function processGroupBy(
196
292
  compileExpression(e),
197
293
  )
198
294
 
199
- // Create a key extractor function using simple __key_X format
295
+ // Create a key extractor function using simple __key_X format.
296
+ // When mainSource is set (includes mode), include __correlationKey so that
297
+ // rows from different parents with the same group key aggregate separately.
200
298
  const keyExtractor = ([, row]: [
201
299
  string,
202
300
  NamespacedRow & { $selected?: any },
@@ -214,11 +312,15 @@ export function processGroupBy(
214
312
  key[`__key_${i}`] = value
215
313
  }
216
314
 
315
+ if (mainSource) {
316
+ key.__correlationKey = (row as any)?.[mainSource]?.__correlationKey
317
+ }
318
+
217
319
  return key
218
320
  }
219
321
 
220
322
  // Create aggregate functions for any aggregated columns in the SELECT clause
221
- const aggregates: Record<string, any> = {}
323
+ const aggregates: Record<string, any> = virtualAggregates
222
324
  const wrappedAggExprs: Record<string, (data: any) => any> = {}
223
325
  const aggCounter = { value: 0 }
224
326
 
@@ -278,25 +380,44 @@ export function processGroupBy(
278
380
  }
279
381
  }
280
382
 
281
- // Generate a simple key for the live collection using group values
282
- let finalKey: unknown
283
- if (groupByClause.length === 1) {
284
- finalKey = aggregatedRow[`__key_0`]
285
- } else {
286
- const keyParts: Array<unknown> = []
287
- for (let i = 0; i < groupByClause.length; i++) {
288
- keyParts.push(aggregatedRow[`__key_${i}`])
289
- }
290
- finalKey = serializeValue(keyParts)
383
+ // Generate a simple key for the live collection using group values.
384
+ // When in includes mode, include the correlation key so that groups
385
+ // from different parents don't collide.
386
+ const correlationKey = mainSource
387
+ ? (aggregatedRow as any).__correlationKey
388
+ : undefined
389
+ const keyParts: Array<unknown> = []
390
+ for (let i = 0; i < groupByClause.length; i++) {
391
+ keyParts.push(aggregatedRow[`__key_${i}`])
291
392
  }
292
-
293
- return [
294
- finalKey,
295
- {
296
- ...aggregatedRow,
297
- $selected: finalResults,
298
- },
299
- ] as [unknown, Record<string, any>]
393
+ if (correlationKey !== undefined) {
394
+ keyParts.push(correlationKey)
395
+ }
396
+ const finalKey =
397
+ keyParts.length === 1 ? keyParts[0] : serializeValue(keyParts)
398
+
399
+ // When in includes mode, restore the namespaced source structure with
400
+ // __correlationKey so output extraction can route results per-parent.
401
+ const resultRow: Record<string, any> = {
402
+ ...(aggregatedRow as Record<string, any>),
403
+ $selected: finalResults,
404
+ }
405
+ const groupSynced = (aggregatedRow as Record<string, any>)[
406
+ VIRTUAL_SYNCED_KEY
407
+ ]
408
+ const groupHasLocal = (aggregatedRow as Record<string, any>)[
409
+ VIRTUAL_HAS_LOCAL_KEY
410
+ ]
411
+ resultRow.$synced = groupSynced ?? true
412
+ resultRow.$origin = (
413
+ groupHasLocal ? `local` : `remote`
414
+ ) satisfies VirtualOrigin
415
+ resultRow.$key = finalKey
416
+ resultRow.$collectionId = aggregateCollectionId ?? resultRow.$collectionId
417
+ if (mainSource && correlationKey !== undefined) {
418
+ resultRow[mainSource] = { __correlationKey: correlationKey }
419
+ }
420
+ return [finalKey, resultRow] as [unknown, Record<string, any>]
300
421
  }),
301
422
  )
302
423
 
@@ -519,7 +640,7 @@ function evaluateWrappedAggregates(
519
640
  * contain an Aggregate. Safely returns false for nested Select objects.
520
641
  */
521
642
  export function containsAggregate(
522
- expr: BasicExpression | Aggregate | Select,
643
+ expr: BasicExpression | Aggregate | Select | { type: string },
523
644
  ): boolean {
524
645
  if (!isExpressionLike(expr)) {
525
646
  return false
@@ -527,9 +648,9 @@ export function containsAggregate(
527
648
  if (expr.type === `agg`) {
528
649
  return true
529
650
  }
530
- if (expr.type === `func`) {
531
- return expr.args.some((arg: BasicExpression | Aggregate) =>
532
- containsAggregate(arg),
651
+ if (expr.type === `func` && `args` in expr) {
652
+ return (expr.args as Array<BasicExpression | Aggregate>).some(
653
+ (arg: BasicExpression | Aggregate) => containsAggregate(arg),
533
654
  )
534
655
  }
535
656
  return false