@tanstack/db 0.0.14 → 0.0.15

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 (198) hide show
  1. package/dist/cjs/collection.cjs +117 -104
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +18 -21
  4. package/dist/cjs/index.cjs +35 -13
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/index.d.cts +0 -1
  7. package/dist/cjs/query/builder/functions.cjs +107 -0
  8. package/dist/cjs/query/builder/functions.cjs.map +1 -0
  9. package/dist/cjs/query/builder/functions.d.cts +38 -0
  10. package/dist/cjs/query/builder/index.cjs +499 -0
  11. package/dist/cjs/query/builder/index.cjs.map +1 -0
  12. package/dist/cjs/query/builder/index.d.cts +324 -0
  13. package/dist/cjs/query/builder/ref-proxy.cjs +96 -0
  14. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
  15. package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
  16. package/dist/cjs/query/builder/types.d.cts +80 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs +261 -0
  18. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
  19. package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
  20. package/dist/cjs/query/compiler/group-by.cjs +271 -0
  21. package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
  22. package/dist/cjs/query/compiler/group-by.d.cts +7 -0
  23. package/dist/cjs/query/compiler/index.cjs +181 -0
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -0
  25. package/dist/cjs/query/compiler/index.d.cts +15 -0
  26. package/dist/cjs/query/compiler/joins.cjs +116 -0
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -0
  28. package/dist/cjs/query/compiler/joins.d.cts +11 -0
  29. package/dist/cjs/query/compiler/order-by.cjs +89 -0
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
  31. package/dist/cjs/query/compiler/order-by.d.cts +9 -0
  32. package/dist/cjs/query/compiler/select.cjs +57 -0
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -0
  34. package/dist/cjs/query/compiler/select.d.cts +15 -0
  35. package/dist/cjs/query/index.d.cts +6 -5
  36. package/dist/cjs/query/ir.cjs +57 -0
  37. package/dist/cjs/query/ir.cjs.map +1 -0
  38. package/dist/cjs/query/ir.d.cts +81 -0
  39. package/dist/cjs/query/live-query-collection.cjs +224 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -0
  41. package/dist/cjs/query/live-query-collection.d.cts +124 -0
  42. package/dist/cjs/transactions.cjs +20 -13
  43. package/dist/cjs/transactions.cjs.map +1 -1
  44. package/dist/cjs/transactions.d.cts +10 -1
  45. package/dist/cjs/types.d.cts +13 -0
  46. package/dist/esm/collection.d.ts +18 -21
  47. package/dist/esm/collection.js +118 -105
  48. package/dist/esm/collection.js.map +1 -1
  49. package/dist/esm/index.d.ts +0 -1
  50. package/dist/esm/index.js +34 -12
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/query/builder/functions.d.ts +38 -0
  53. package/dist/esm/query/builder/functions.js +107 -0
  54. package/dist/esm/query/builder/functions.js.map +1 -0
  55. package/dist/esm/query/builder/index.d.ts +324 -0
  56. package/dist/esm/query/builder/index.js +499 -0
  57. package/dist/esm/query/builder/index.js.map +1 -0
  58. package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
  59. package/dist/esm/query/builder/ref-proxy.js +96 -0
  60. package/dist/esm/query/builder/ref-proxy.js.map +1 -0
  61. package/dist/esm/query/builder/types.d.ts +80 -0
  62. package/dist/esm/query/compiler/evaluators.d.ts +11 -0
  63. package/dist/esm/query/compiler/evaluators.js +261 -0
  64. package/dist/esm/query/compiler/evaluators.js.map +1 -0
  65. package/dist/esm/query/compiler/group-by.d.ts +7 -0
  66. package/dist/esm/query/compiler/group-by.js +271 -0
  67. package/dist/esm/query/compiler/group-by.js.map +1 -0
  68. package/dist/esm/query/compiler/index.d.ts +15 -0
  69. package/dist/esm/query/compiler/index.js +181 -0
  70. package/dist/esm/query/compiler/index.js.map +1 -0
  71. package/dist/esm/query/compiler/joins.d.ts +11 -0
  72. package/dist/esm/query/compiler/joins.js +116 -0
  73. package/dist/esm/query/compiler/joins.js.map +1 -0
  74. package/dist/esm/query/compiler/order-by.d.ts +9 -0
  75. package/dist/esm/query/compiler/order-by.js +89 -0
  76. package/dist/esm/query/compiler/order-by.js.map +1 -0
  77. package/dist/esm/query/compiler/select.d.ts +15 -0
  78. package/dist/esm/query/compiler/select.js +57 -0
  79. package/dist/esm/query/compiler/select.js.map +1 -0
  80. package/dist/esm/query/index.d.ts +6 -5
  81. package/dist/esm/query/ir.d.ts +81 -0
  82. package/dist/esm/query/ir.js +57 -0
  83. package/dist/esm/query/ir.js.map +1 -0
  84. package/dist/esm/query/live-query-collection.d.ts +124 -0
  85. package/dist/esm/query/live-query-collection.js +224 -0
  86. package/dist/esm/query/live-query-collection.js.map +1 -0
  87. package/dist/esm/transactions.d.ts +10 -1
  88. package/dist/esm/transactions.js +20 -13
  89. package/dist/esm/transactions.js.map +1 -1
  90. package/dist/esm/types.d.ts +13 -0
  91. package/package.json +3 -4
  92. package/src/collection.ts +152 -129
  93. package/src/index.ts +0 -1
  94. package/src/query/builder/functions.ts +267 -0
  95. package/src/query/builder/index.ts +648 -0
  96. package/src/query/builder/ref-proxy.ts +156 -0
  97. package/src/query/builder/types.ts +278 -0
  98. package/src/query/compiler/evaluators.ts +315 -0
  99. package/src/query/compiler/group-by.ts +428 -0
  100. package/src/query/compiler/index.ts +276 -0
  101. package/src/query/compiler/joins.ts +228 -0
  102. package/src/query/compiler/order-by.ts +139 -0
  103. package/src/query/compiler/select.ts +173 -0
  104. package/src/query/index.ts +64 -5
  105. package/src/query/ir.ts +128 -0
  106. package/src/query/live-query-collection.ts +509 -0
  107. package/src/transactions.ts +27 -16
  108. package/src/types.ts +15 -0
  109. package/dist/cjs/query/compiled-query.cjs +0 -160
  110. package/dist/cjs/query/compiled-query.cjs.map +0 -1
  111. package/dist/cjs/query/compiled-query.d.cts +0 -20
  112. package/dist/cjs/query/evaluators.cjs +0 -161
  113. package/dist/cjs/query/evaluators.cjs.map +0 -1
  114. package/dist/cjs/query/evaluators.d.cts +0 -14
  115. package/dist/cjs/query/extractors.cjs +0 -122
  116. package/dist/cjs/query/extractors.cjs.map +0 -1
  117. package/dist/cjs/query/extractors.d.cts +0 -22
  118. package/dist/cjs/query/functions.cjs +0 -152
  119. package/dist/cjs/query/functions.cjs.map +0 -1
  120. package/dist/cjs/query/functions.d.cts +0 -21
  121. package/dist/cjs/query/group-by.cjs +0 -88
  122. package/dist/cjs/query/group-by.cjs.map +0 -1
  123. package/dist/cjs/query/group-by.d.cts +0 -40
  124. package/dist/cjs/query/joins.cjs +0 -141
  125. package/dist/cjs/query/joins.cjs.map +0 -1
  126. package/dist/cjs/query/joins.d.cts +0 -14
  127. package/dist/cjs/query/order-by.cjs +0 -185
  128. package/dist/cjs/query/order-by.cjs.map +0 -1
  129. package/dist/cjs/query/order-by.d.cts +0 -3
  130. package/dist/cjs/query/pipeline-compiler.cjs +0 -89
  131. package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
  132. package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
  133. package/dist/cjs/query/query-builder.cjs +0 -307
  134. package/dist/cjs/query/query-builder.cjs.map +0 -1
  135. package/dist/cjs/query/query-builder.d.cts +0 -225
  136. package/dist/cjs/query/schema.d.cts +0 -100
  137. package/dist/cjs/query/select.cjs +0 -130
  138. package/dist/cjs/query/select.cjs.map +0 -1
  139. package/dist/cjs/query/select.d.cts +0 -3
  140. package/dist/cjs/query/types.d.cts +0 -189
  141. package/dist/cjs/query/utils.cjs +0 -154
  142. package/dist/cjs/query/utils.cjs.map +0 -1
  143. package/dist/cjs/query/utils.d.cts +0 -37
  144. package/dist/cjs/utils.cjs +0 -17
  145. package/dist/cjs/utils.cjs.map +0 -1
  146. package/dist/cjs/utils.d.cts +0 -3
  147. package/dist/esm/query/compiled-query.d.ts +0 -20
  148. package/dist/esm/query/compiled-query.js +0 -160
  149. package/dist/esm/query/compiled-query.js.map +0 -1
  150. package/dist/esm/query/evaluators.d.ts +0 -14
  151. package/dist/esm/query/evaluators.js +0 -161
  152. package/dist/esm/query/evaluators.js.map +0 -1
  153. package/dist/esm/query/extractors.d.ts +0 -22
  154. package/dist/esm/query/extractors.js +0 -122
  155. package/dist/esm/query/extractors.js.map +0 -1
  156. package/dist/esm/query/functions.d.ts +0 -21
  157. package/dist/esm/query/functions.js +0 -152
  158. package/dist/esm/query/functions.js.map +0 -1
  159. package/dist/esm/query/group-by.d.ts +0 -40
  160. package/dist/esm/query/group-by.js +0 -88
  161. package/dist/esm/query/group-by.js.map +0 -1
  162. package/dist/esm/query/joins.d.ts +0 -14
  163. package/dist/esm/query/joins.js +0 -141
  164. package/dist/esm/query/joins.js.map +0 -1
  165. package/dist/esm/query/order-by.d.ts +0 -3
  166. package/dist/esm/query/order-by.js +0 -185
  167. package/dist/esm/query/order-by.js.map +0 -1
  168. package/dist/esm/query/pipeline-compiler.d.ts +0 -10
  169. package/dist/esm/query/pipeline-compiler.js +0 -89
  170. package/dist/esm/query/pipeline-compiler.js.map +0 -1
  171. package/dist/esm/query/query-builder.d.ts +0 -225
  172. package/dist/esm/query/query-builder.js +0 -307
  173. package/dist/esm/query/query-builder.js.map +0 -1
  174. package/dist/esm/query/schema.d.ts +0 -100
  175. package/dist/esm/query/select.d.ts +0 -3
  176. package/dist/esm/query/select.js +0 -130
  177. package/dist/esm/query/select.js.map +0 -1
  178. package/dist/esm/query/types.d.ts +0 -189
  179. package/dist/esm/query/utils.d.ts +0 -37
  180. package/dist/esm/query/utils.js +0 -154
  181. package/dist/esm/query/utils.js.map +0 -1
  182. package/dist/esm/utils.d.ts +0 -3
  183. package/dist/esm/utils.js +0 -17
  184. package/dist/esm/utils.js.map +0 -1
  185. package/src/query/compiled-query.ts +0 -234
  186. package/src/query/evaluators.ts +0 -250
  187. package/src/query/extractors.ts +0 -214
  188. package/src/query/functions.ts +0 -297
  189. package/src/query/group-by.ts +0 -139
  190. package/src/query/joins.ts +0 -260
  191. package/src/query/order-by.ts +0 -264
  192. package/src/query/pipeline-compiler.ts +0 -149
  193. package/src/query/query-builder.ts +0 -902
  194. package/src/query/schema.ts +0 -268
  195. package/src/query/select.ts +0 -208
  196. package/src/query/types.ts +0 -418
  197. package/src/query/utils.ts +0 -245
  198. package/src/utils.ts +0 -15
@@ -0,0 +1,428 @@
1
+ import { filter, groupBy, groupByOperators, map } from "@electric-sql/d2mini"
2
+ import { Func, Ref } from "../ir.js"
3
+ import { compileExpression } from "./evaluators.js"
4
+ import type {
5
+ Aggregate,
6
+ BasicExpression,
7
+ GroupBy,
8
+ Having,
9
+ Select,
10
+ } from "../ir.js"
11
+ import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
12
+
13
+ const { sum, count, avg, min, max } = groupByOperators
14
+
15
+ /**
16
+ * Interface for caching the mapping between GROUP BY expressions and SELECT expressions
17
+ */
18
+ interface GroupBySelectMapping {
19
+ selectToGroupByIndex: Map<string, number> // Maps SELECT alias to GROUP BY expression index
20
+ groupByExpressions: Array<any> // The GROUP BY expressions for reference
21
+ }
22
+
23
+ /**
24
+ * Validates that all non-aggregate expressions in SELECT are present in GROUP BY
25
+ * and creates a cached mapping for efficient lookup during processing
26
+ */
27
+ function validateAndCreateMapping(
28
+ groupByClause: GroupBy,
29
+ selectClause?: Select
30
+ ): GroupBySelectMapping {
31
+ const selectToGroupByIndex = new Map<string, number>()
32
+ const groupByExpressions = [...groupByClause]
33
+
34
+ if (!selectClause) {
35
+ return { selectToGroupByIndex, groupByExpressions }
36
+ }
37
+
38
+ // Validate each SELECT expression
39
+ for (const [alias, expr] of Object.entries(selectClause)) {
40
+ if (expr.type === `agg`) {
41
+ // Aggregate expressions are allowed and don't need to be in GROUP BY
42
+ continue
43
+ }
44
+
45
+ // Non-aggregate expression must be in GROUP BY
46
+ const groupIndex = groupByExpressions.findIndex((groupExpr) =>
47
+ expressionsEqual(expr, groupExpr)
48
+ )
49
+
50
+ if (groupIndex === -1) {
51
+ throw new Error(
52
+ `Non-aggregate expression '${alias}' in SELECT must also appear in GROUP BY clause`
53
+ )
54
+ }
55
+
56
+ // Cache the mapping
57
+ selectToGroupByIndex.set(alias, groupIndex)
58
+ }
59
+
60
+ return { selectToGroupByIndex, groupByExpressions }
61
+ }
62
+
63
+ /**
64
+ * Processes the GROUP BY clause with optional HAVING and SELECT
65
+ * Works with the new __select_results structure from early SELECT processing
66
+ */
67
+ export function processGroupBy(
68
+ pipeline: NamespacedAndKeyedStream,
69
+ groupByClause: GroupBy,
70
+ havingClauses?: Array<Having>,
71
+ selectClause?: Select,
72
+ fnHavingClauses?: Array<(row: any) => any>
73
+ ): NamespacedAndKeyedStream {
74
+ // Handle empty GROUP BY (single-group aggregation)
75
+ if (groupByClause.length === 0) {
76
+ // For single-group aggregation, create a single group with all data
77
+ const aggregates: Record<string, any> = {}
78
+
79
+ if (selectClause) {
80
+ // Scan the SELECT clause for aggregate functions
81
+ for (const [alias, expr] of Object.entries(selectClause)) {
82
+ if (expr.type === `agg`) {
83
+ const aggExpr = expr
84
+ aggregates[alias] = getAggregateFunction(aggExpr)
85
+ }
86
+ }
87
+ }
88
+
89
+ // Use a constant key for single group
90
+ const keyExtractor = () => ({ __singleGroup: true })
91
+
92
+ // Apply the groupBy operator with single group
93
+ pipeline = pipeline.pipe(
94
+ groupBy(keyExtractor, aggregates)
95
+ ) as NamespacedAndKeyedStream
96
+
97
+ // Update __select_results to include aggregate values
98
+ pipeline = pipeline.pipe(
99
+ map(([, aggregatedRow]) => {
100
+ // Start with the existing __select_results from early SELECT processing
101
+ const selectResults = (aggregatedRow as any).__select_results || {}
102
+ const finalResults: Record<string, any> = { ...selectResults }
103
+
104
+ if (selectClause) {
105
+ // Update with aggregate results
106
+ for (const [alias, expr] of Object.entries(selectClause)) {
107
+ if (expr.type === `agg`) {
108
+ finalResults[alias] = aggregatedRow[alias]
109
+ }
110
+ // Non-aggregates keep their original values from early SELECT processing
111
+ }
112
+ }
113
+
114
+ // Use a single key for the result and update __select_results
115
+ return [
116
+ `single_group`,
117
+ {
118
+ ...aggregatedRow,
119
+ __select_results: finalResults,
120
+ },
121
+ ] as [unknown, Record<string, any>]
122
+ })
123
+ )
124
+
125
+ // Apply HAVING clauses if present
126
+ if (havingClauses && havingClauses.length > 0) {
127
+ for (const havingClause of havingClauses) {
128
+ const transformedHavingClause = transformHavingClause(
129
+ havingClause,
130
+ selectClause || {}
131
+ )
132
+ const compiledHaving = compileExpression(transformedHavingClause)
133
+
134
+ pipeline = pipeline.pipe(
135
+ filter(([, row]) => {
136
+ // Create a namespaced row structure for HAVING evaluation
137
+ const namespacedRow = { result: (row as any).__select_results }
138
+ return compiledHaving(namespacedRow)
139
+ })
140
+ )
141
+ }
142
+ }
143
+
144
+ // Apply functional HAVING clauses if present
145
+ if (fnHavingClauses && fnHavingClauses.length > 0) {
146
+ for (const fnHaving of fnHavingClauses) {
147
+ pipeline = pipeline.pipe(
148
+ filter(([, row]) => {
149
+ // Create a namespaced row structure for functional HAVING evaluation
150
+ const namespacedRow = { result: (row as any).__select_results }
151
+ return fnHaving(namespacedRow)
152
+ })
153
+ )
154
+ }
155
+ }
156
+
157
+ return pipeline
158
+ }
159
+
160
+ // Multi-group aggregation logic...
161
+ // Validate and create mapping for non-aggregate expressions in SELECT
162
+ const mapping = validateAndCreateMapping(groupByClause, selectClause)
163
+
164
+ // Pre-compile groupBy expressions
165
+ const compiledGroupByExpressions = groupByClause.map(compileExpression)
166
+
167
+ // Create a key extractor function using simple __key_X format
168
+ const keyExtractor = ([, row]: [
169
+ string,
170
+ NamespacedRow & { __select_results?: any },
171
+ ]) => {
172
+ // Use the original namespaced row for GROUP BY expressions, not __select_results
173
+ const namespacedRow = { ...row }
174
+ delete (namespacedRow as any).__select_results
175
+
176
+ const key: Record<string, unknown> = {}
177
+
178
+ // Use simple __key_X format for each groupBy expression
179
+ for (let i = 0; i < groupByClause.length; i++) {
180
+ const compiledExpr = compiledGroupByExpressions[i]!
181
+ const value = compiledExpr(namespacedRow)
182
+ key[`__key_${i}`] = value
183
+ }
184
+
185
+ return key
186
+ }
187
+
188
+ // Create aggregate functions for any aggregated columns in the SELECT clause
189
+ const aggregates: Record<string, any> = {}
190
+
191
+ if (selectClause) {
192
+ // Scan the SELECT clause for aggregate functions
193
+ for (const [alias, expr] of Object.entries(selectClause)) {
194
+ if (expr.type === `agg`) {
195
+ const aggExpr = expr
196
+ aggregates[alias] = getAggregateFunction(aggExpr)
197
+ }
198
+ }
199
+ }
200
+
201
+ // Apply the groupBy operator
202
+ pipeline = pipeline.pipe(groupBy(keyExtractor, aggregates))
203
+
204
+ // Update __select_results to handle GROUP BY results
205
+ pipeline = pipeline.pipe(
206
+ map(([, aggregatedRow]) => {
207
+ // Start with the existing __select_results from early SELECT processing
208
+ const selectResults = (aggregatedRow as any).__select_results || {}
209
+ const finalResults: Record<string, any> = {}
210
+
211
+ if (selectClause) {
212
+ // Process each SELECT expression
213
+ for (const [alias, expr] of Object.entries(selectClause)) {
214
+ if (expr.type !== `agg`) {
215
+ // Use cached mapping to get the corresponding __key_X for non-aggregates
216
+ const groupIndex = mapping.selectToGroupByIndex.get(alias)
217
+ if (groupIndex !== undefined) {
218
+ finalResults[alias] = aggregatedRow[`__key_${groupIndex}`]
219
+ } else {
220
+ // Fallback to original SELECT results
221
+ finalResults[alias] = selectResults[alias]
222
+ }
223
+ } else {
224
+ // Use aggregate results
225
+ finalResults[alias] = aggregatedRow[alias]
226
+ }
227
+ }
228
+ } else {
229
+ // No SELECT clause - just use the group keys
230
+ for (let i = 0; i < groupByClause.length; i++) {
231
+ finalResults[`__key_${i}`] = aggregatedRow[`__key_${i}`]
232
+ }
233
+ }
234
+
235
+ // Generate a simple key for the live collection using group values
236
+ let finalKey: unknown
237
+ if (groupByClause.length === 1) {
238
+ finalKey = aggregatedRow[`__key_0`]
239
+ } else {
240
+ const keyParts: Array<unknown> = []
241
+ for (let i = 0; i < groupByClause.length; i++) {
242
+ keyParts.push(aggregatedRow[`__key_${i}`])
243
+ }
244
+ finalKey = JSON.stringify(keyParts)
245
+ }
246
+
247
+ return [
248
+ finalKey,
249
+ {
250
+ ...aggregatedRow,
251
+ __select_results: finalResults,
252
+ },
253
+ ] as [unknown, Record<string, any>]
254
+ })
255
+ )
256
+
257
+ // Apply HAVING clauses if present
258
+ if (havingClauses && havingClauses.length > 0) {
259
+ for (const havingClause of havingClauses) {
260
+ const transformedHavingClause = transformHavingClause(
261
+ havingClause,
262
+ selectClause || {}
263
+ )
264
+ const compiledHaving = compileExpression(transformedHavingClause)
265
+
266
+ pipeline = pipeline.pipe(
267
+ filter(([, row]) => {
268
+ // Create a namespaced row structure for HAVING evaluation
269
+ const namespacedRow = { result: (row as any).__select_results }
270
+ return compiledHaving(namespacedRow)
271
+ })
272
+ )
273
+ }
274
+ }
275
+
276
+ // Apply functional HAVING clauses if present
277
+ if (fnHavingClauses && fnHavingClauses.length > 0) {
278
+ for (const fnHaving of fnHavingClauses) {
279
+ pipeline = pipeline.pipe(
280
+ filter(([, row]) => {
281
+ // Create a namespaced row structure for functional HAVING evaluation
282
+ const namespacedRow = { result: (row as any).__select_results }
283
+ return fnHaving(namespacedRow)
284
+ })
285
+ )
286
+ }
287
+ }
288
+
289
+ return pipeline
290
+ }
291
+
292
+ /**
293
+ * Helper function to check if two expressions are equal
294
+ */
295
+ function expressionsEqual(expr1: any, expr2: any): boolean {
296
+ if (!expr1 || !expr2) return false
297
+ if (expr1.type !== expr2.type) return false
298
+
299
+ switch (expr1.type) {
300
+ case `ref`:
301
+ // Compare paths as arrays
302
+ if (!expr1.path || !expr2.path) return false
303
+ if (expr1.path.length !== expr2.path.length) return false
304
+ return expr1.path.every(
305
+ (segment: string, i: number) => segment === expr2.path[i]
306
+ )
307
+ case `val`:
308
+ return expr1.value === expr2.value
309
+ case `func`:
310
+ return (
311
+ expr1.name === expr2.name &&
312
+ expr1.args?.length === expr2.args?.length &&
313
+ (expr1.args || []).every((arg: any, i: number) =>
314
+ expressionsEqual(arg, expr2.args[i])
315
+ )
316
+ )
317
+ case `agg`:
318
+ return (
319
+ expr1.name === expr2.name &&
320
+ expr1.args?.length === expr2.args?.length &&
321
+ (expr1.args || []).every((arg: any, i: number) =>
322
+ expressionsEqual(arg, expr2.args[i])
323
+ )
324
+ )
325
+ default:
326
+ return false
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Helper function to get an aggregate function based on the Agg expression
332
+ */
333
+ function getAggregateFunction(aggExpr: Aggregate) {
334
+ // Pre-compile the value extractor expression
335
+ const compiledExpr = compileExpression(aggExpr.args[0]!)
336
+
337
+ // Create a value extractor function for the expression to aggregate
338
+ const valueExtractor = ([, namespacedRow]: [string, NamespacedRow]) => {
339
+ const value = compiledExpr(namespacedRow)
340
+ // Ensure we return a number for numeric aggregate functions
341
+ return typeof value === `number` ? value : value != null ? Number(value) : 0
342
+ }
343
+
344
+ // Return the appropriate aggregate function
345
+ switch (aggExpr.name.toLowerCase()) {
346
+ case `sum`:
347
+ return sum(valueExtractor)
348
+ case `count`:
349
+ return count() // count() doesn't need a value extractor
350
+ case `avg`:
351
+ return avg(valueExtractor)
352
+ case `min`:
353
+ return min(valueExtractor)
354
+ case `max`:
355
+ return max(valueExtractor)
356
+ default:
357
+ throw new Error(`Unsupported aggregate function: ${aggExpr.name}`)
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Transforms a HAVING clause to replace Agg expressions with references to computed values
363
+ */
364
+ function transformHavingClause(
365
+ havingExpr: BasicExpression | Aggregate,
366
+ selectClause: Select
367
+ ): BasicExpression {
368
+ switch (havingExpr.type) {
369
+ case `agg`: {
370
+ const aggExpr = havingExpr
371
+ // Find matching aggregate in SELECT clause
372
+ for (const [alias, selectExpr] of Object.entries(selectClause)) {
373
+ if (selectExpr.type === `agg` && aggregatesEqual(aggExpr, selectExpr)) {
374
+ // Replace with a reference to the computed aggregate
375
+ return new Ref([`result`, alias])
376
+ }
377
+ }
378
+ // If no matching aggregate found in SELECT, throw error
379
+ throw new Error(
380
+ `Aggregate function in HAVING clause must also be in SELECT clause: ${aggExpr.name}`
381
+ )
382
+ }
383
+
384
+ case `func`: {
385
+ const funcExpr = havingExpr
386
+ // Transform function arguments recursively
387
+ const transformedArgs = funcExpr.args.map(
388
+ (arg: BasicExpression | Aggregate) =>
389
+ transformHavingClause(arg, selectClause)
390
+ )
391
+ return new Func(funcExpr.name, transformedArgs)
392
+ }
393
+
394
+ case `ref`: {
395
+ const refExpr = havingExpr
396
+ // Check if this is a direct reference to a SELECT alias
397
+ if (refExpr.path.length === 1) {
398
+ const alias = refExpr.path[0]!
399
+ if (selectClause[alias]) {
400
+ // This is a reference to a SELECT alias, convert to result.alias
401
+ return new Ref([`result`, alias])
402
+ }
403
+ }
404
+ // Return as-is for other refs
405
+ return havingExpr as BasicExpression
406
+ }
407
+
408
+ case `val`:
409
+ // Return as-is
410
+ return havingExpr as BasicExpression
411
+
412
+ default:
413
+ throw new Error(
414
+ `Unknown expression type in HAVING clause: ${(havingExpr as any).type}`
415
+ )
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Checks if two aggregate expressions are equal
421
+ */
422
+ function aggregatesEqual(agg1: Aggregate, agg2: Aggregate): boolean {
423
+ return (
424
+ agg1.name === agg2.name &&
425
+ agg1.args.length === agg2.args.length &&
426
+ agg1.args.every((arg, i) => expressionsEqual(arg, agg2.args[i]))
427
+ )
428
+ }
@@ -0,0 +1,276 @@
1
+ import { filter, map } from "@electric-sql/d2mini"
2
+ import { compileExpression } from "./evaluators.js"
3
+ import { processJoins } from "./joins.js"
4
+ import { processGroupBy } from "./group-by.js"
5
+ import { processOrderBy } from "./order-by.js"
6
+ import { processSelectToResults } from "./select.js"
7
+ import type { CollectionRef, QueryIR, QueryRef } from "../ir.js"
8
+ import type {
9
+ KeyedStream,
10
+ NamespacedAndKeyedStream,
11
+ ResultStream,
12
+ } from "../../types.js"
13
+
14
+ /**
15
+ * Cache for compiled subqueries to avoid duplicate compilation
16
+ */
17
+ type QueryCache = WeakMap<QueryIR, ResultStream>
18
+
19
+ /**
20
+ * Compiles a query2 IR into a D2 pipeline
21
+ * @param query The query IR to compile
22
+ * @param inputs Mapping of collection names to input streams
23
+ * @param cache Optional cache for compiled subqueries (used internally for recursion)
24
+ * @returns A stream builder representing the compiled query
25
+ */
26
+ export function compileQuery(
27
+ query: QueryIR,
28
+ inputs: Record<string, KeyedStream>,
29
+ cache: QueryCache = new WeakMap()
30
+ ): ResultStream {
31
+ // Check if this query has already been compiled
32
+ const cachedResult = cache.get(query)
33
+ if (cachedResult) {
34
+ return cachedResult
35
+ }
36
+
37
+ // Create a copy of the inputs map to avoid modifying the original
38
+ const allInputs = { ...inputs }
39
+
40
+ // Create a map of table aliases to inputs
41
+ const tables: Record<string, KeyedStream> = {}
42
+
43
+ // Process the FROM clause to get the main table
44
+ const { alias: mainTableAlias, input: mainInput } = processFrom(
45
+ query.from,
46
+ allInputs,
47
+ cache
48
+ )
49
+ tables[mainTableAlias] = mainInput
50
+
51
+ // Prepare the initial pipeline with the main table wrapped in its alias
52
+ let pipeline: NamespacedAndKeyedStream = mainInput.pipe(
53
+ map(([key, row]) => {
54
+ // Initialize the record with a nested structure
55
+ const ret = [key, { [mainTableAlias]: row }] as [
56
+ string,
57
+ Record<string, typeof row>,
58
+ ]
59
+ return ret
60
+ })
61
+ )
62
+
63
+ // Process JOIN clauses if they exist
64
+ if (query.join && query.join.length > 0) {
65
+ pipeline = processJoins(
66
+ pipeline,
67
+ query.join,
68
+ tables,
69
+ mainTableAlias,
70
+ allInputs,
71
+ cache
72
+ )
73
+ }
74
+
75
+ // Process the WHERE clause if it exists
76
+ if (query.where && query.where.length > 0) {
77
+ // Compile all WHERE expressions
78
+ const compiledWheres = query.where.map((where) => compileExpression(where))
79
+
80
+ // Apply each WHERE condition as a filter (they are ANDed together)
81
+ for (const compiledWhere of compiledWheres) {
82
+ pipeline = pipeline.pipe(
83
+ filter(([_key, namespacedRow]) => {
84
+ return compiledWhere(namespacedRow)
85
+ })
86
+ )
87
+ }
88
+ }
89
+
90
+ // Process functional WHERE clauses if they exist
91
+ if (query.fnWhere && query.fnWhere.length > 0) {
92
+ for (const fnWhere of query.fnWhere) {
93
+ pipeline = pipeline.pipe(
94
+ filter(([_key, namespacedRow]) => {
95
+ return fnWhere(namespacedRow)
96
+ })
97
+ )
98
+ }
99
+ }
100
+
101
+ // Process the SELECT clause early - always create __select_results
102
+ // This eliminates duplication and allows for future DISTINCT implementation
103
+ if (query.fnSelect) {
104
+ // Handle functional select - apply the function to transform the row
105
+ pipeline = pipeline.pipe(
106
+ map(([key, namespacedRow]) => {
107
+ const selectResults = query.fnSelect!(namespacedRow)
108
+ return [
109
+ key,
110
+ {
111
+ ...namespacedRow,
112
+ __select_results: selectResults,
113
+ },
114
+ ] as [string, typeof namespacedRow & { __select_results: any }]
115
+ })
116
+ )
117
+ } else if (query.select) {
118
+ pipeline = processSelectToResults(pipeline, query.select, allInputs)
119
+ } else {
120
+ // If no SELECT clause, create __select_results with the main table data
121
+ pipeline = pipeline.pipe(
122
+ map(([key, namespacedRow]) => {
123
+ const selectResults =
124
+ !query.join && !query.groupBy
125
+ ? namespacedRow[mainTableAlias]
126
+ : namespacedRow
127
+
128
+ return [
129
+ key,
130
+ {
131
+ ...namespacedRow,
132
+ __select_results: selectResults,
133
+ },
134
+ ] as [string, typeof namespacedRow & { __select_results: any }]
135
+ })
136
+ )
137
+ }
138
+
139
+ // Process the GROUP BY clause if it exists
140
+ if (query.groupBy && query.groupBy.length > 0) {
141
+ pipeline = processGroupBy(
142
+ pipeline,
143
+ query.groupBy,
144
+ query.having,
145
+ query.select,
146
+ query.fnHaving
147
+ )
148
+ } else if (query.select) {
149
+ // Check if SELECT contains aggregates but no GROUP BY (implicit single-group aggregation)
150
+ const hasAggregates = Object.values(query.select).some(
151
+ (expr) => expr.type === `agg`
152
+ )
153
+ if (hasAggregates) {
154
+ // Handle implicit single-group aggregation
155
+ pipeline = processGroupBy(
156
+ pipeline,
157
+ [], // Empty group by means single group
158
+ query.having,
159
+ query.select,
160
+ query.fnHaving
161
+ )
162
+ }
163
+ }
164
+
165
+ // Process the HAVING clause if it exists (only applies after GROUP BY)
166
+ if (query.having && (!query.groupBy || query.groupBy.length === 0)) {
167
+ // Check if we have aggregates in SELECT that would trigger implicit grouping
168
+ const hasAggregates = query.select
169
+ ? Object.values(query.select).some((expr) => expr.type === `agg`)
170
+ : false
171
+
172
+ if (!hasAggregates) {
173
+ throw new Error(`HAVING clause requires GROUP BY clause`)
174
+ }
175
+ }
176
+
177
+ // Process functional HAVING clauses outside of GROUP BY (treat as additional WHERE filters)
178
+ if (
179
+ query.fnHaving &&
180
+ query.fnHaving.length > 0 &&
181
+ (!query.groupBy || query.groupBy.length === 0)
182
+ ) {
183
+ // If there's no GROUP BY but there are fnHaving clauses, apply them as filters
184
+ for (const fnHaving of query.fnHaving) {
185
+ pipeline = pipeline.pipe(
186
+ filter(([_key, namespacedRow]) => {
187
+ return fnHaving(namespacedRow)
188
+ })
189
+ )
190
+ }
191
+ }
192
+
193
+ // Process orderBy parameter if it exists
194
+ if (query.orderBy && query.orderBy.length > 0) {
195
+ const orderedPipeline = processOrderBy(
196
+ pipeline,
197
+ query.orderBy,
198
+ query.limit,
199
+ query.offset
200
+ )
201
+
202
+ // Final step: extract the __select_results and include orderBy index
203
+ const resultPipeline = orderedPipeline.pipe(
204
+ map(([key, [row, orderByIndex]]) => {
205
+ // Extract the final results from __select_results and include orderBy index
206
+ const finalResults = (row as any).__select_results
207
+ return [key, [finalResults, orderByIndex]] as [unknown, [any, string]]
208
+ })
209
+ )
210
+
211
+ const result = resultPipeline
212
+ // Cache the result before returning
213
+ cache.set(query, result)
214
+ return result
215
+ } else if (query.limit !== undefined || query.offset !== undefined) {
216
+ // If there's a limit or offset without orderBy, throw an error
217
+ throw new Error(
218
+ `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`
219
+ )
220
+ }
221
+
222
+ // Final step: extract the __select_results and return tuple format (no orderBy)
223
+ const resultPipeline: ResultStream = pipeline.pipe(
224
+ map(([key, row]) => {
225
+ // Extract the final results from __select_results and return [key, [results, undefined]]
226
+ const finalResults = (row as any).__select_results
227
+ return [key, [finalResults, undefined]] as [
228
+ unknown,
229
+ [any, string | undefined],
230
+ ]
231
+ })
232
+ )
233
+
234
+ const result = resultPipeline
235
+ // Cache the result before returning
236
+ cache.set(query, result)
237
+ return result
238
+ }
239
+
240
+ /**
241
+ * Processes the FROM clause to extract the main table alias and input stream
242
+ */
243
+ function processFrom(
244
+ from: CollectionRef | QueryRef,
245
+ allInputs: Record<string, KeyedStream>,
246
+ cache: QueryCache
247
+ ): { alias: string; input: KeyedStream } {
248
+ switch (from.type) {
249
+ case `collectionRef`: {
250
+ const input = allInputs[from.collection.id]
251
+ if (!input) {
252
+ throw new Error(
253
+ `Input for collection "${from.collection.id}" not found in inputs map`
254
+ )
255
+ }
256
+ return { alias: from.alias, input }
257
+ }
258
+ case `queryRef`: {
259
+ // Recursively compile the sub-query with cache
260
+ const subQueryInput = compileQuery(from.query, allInputs, cache)
261
+
262
+ // Subqueries may return [key, [value, orderByIndex]] (with ORDER BY) or [key, value] (without ORDER BY)
263
+ // We need to extract just the value for use in parent queries
264
+ const extractedInput = subQueryInput.pipe(
265
+ map((data: any) => {
266
+ const [key, [value, _orderByIndex]] = data
267
+ return [key, value] as [unknown, any]
268
+ })
269
+ )
270
+
271
+ return { alias: from.alias, input: extractedInput }
272
+ }
273
+ default:
274
+ throw new Error(`Unsupported FROM type: ${(from as any).type}`)
275
+ }
276
+ }