@tanstack/db 0.0.14 → 0.0.16
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.
- package/dist/cjs/collection.cjs +117 -104
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +18 -21
- package/dist/cjs/index.cjs +31 -13
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +0 -1
- package/dist/cjs/query/builder/functions.cjs +107 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -0
- package/dist/cjs/query/builder/functions.d.cts +38 -0
- package/dist/cjs/query/builder/index.cjs +499 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -0
- package/dist/cjs/query/builder/index.d.cts +324 -0
- package/dist/cjs/query/builder/ref-proxy.cjs +92 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
- package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
- package/dist/cjs/query/builder/types.d.cts +81 -0
- package/dist/cjs/query/compiler/evaluators.cjs +261 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
- package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
- package/dist/cjs/query/compiler/group-by.cjs +271 -0
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/group-by.d.cts +7 -0
- package/dist/cjs/query/compiler/index.cjs +181 -0
- package/dist/cjs/query/compiler/index.cjs.map +1 -0
- package/dist/cjs/query/compiler/index.d.cts +15 -0
- package/dist/cjs/query/compiler/joins.cjs +116 -0
- package/dist/cjs/query/compiler/joins.cjs.map +1 -0
- package/dist/cjs/query/compiler/joins.d.cts +11 -0
- package/dist/cjs/query/compiler/order-by.cjs +89 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/order-by.d.cts +9 -0
- package/dist/cjs/query/compiler/select.cjs +57 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -0
- package/dist/cjs/query/compiler/select.d.cts +15 -0
- package/dist/cjs/query/index.d.cts +5 -5
- package/dist/cjs/query/ir.cjs +57 -0
- package/dist/cjs/query/ir.cjs.map +1 -0
- package/dist/cjs/query/ir.d.cts +81 -0
- package/dist/cjs/query/live-query-collection.cjs +224 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -0
- package/dist/cjs/query/live-query-collection.d.cts +124 -0
- package/dist/cjs/transactions.cjs +20 -13
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +10 -1
- package/dist/cjs/types.d.cts +13 -0
- package/dist/esm/collection.d.ts +18 -21
- package/dist/esm/collection.js +118 -105
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +30 -12
- package/dist/esm/query/builder/functions.d.ts +38 -0
- package/dist/esm/query/builder/functions.js +107 -0
- package/dist/esm/query/builder/functions.js.map +1 -0
- package/dist/esm/query/builder/index.d.ts +324 -0
- package/dist/esm/query/builder/index.js +499 -0
- package/dist/esm/query/builder/index.js.map +1 -0
- package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
- package/dist/esm/query/builder/ref-proxy.js +92 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -0
- package/dist/esm/query/builder/types.d.ts +81 -0
- package/dist/esm/query/compiler/evaluators.d.ts +11 -0
- package/dist/esm/query/compiler/evaluators.js +261 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -0
- package/dist/esm/query/compiler/group-by.d.ts +7 -0
- package/dist/esm/query/compiler/group-by.js +271 -0
- package/dist/esm/query/compiler/group-by.js.map +1 -0
- package/dist/esm/query/compiler/index.d.ts +15 -0
- package/dist/esm/query/compiler/index.js +181 -0
- package/dist/esm/query/compiler/index.js.map +1 -0
- package/dist/esm/query/compiler/joins.d.ts +11 -0
- package/dist/esm/query/compiler/joins.js +116 -0
- package/dist/esm/query/compiler/joins.js.map +1 -0
- package/dist/esm/query/compiler/order-by.d.ts +9 -0
- package/dist/esm/query/compiler/order-by.js +89 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -0
- package/dist/esm/query/compiler/select.d.ts +15 -0
- package/dist/esm/query/compiler/select.js +57 -0
- package/dist/esm/query/compiler/select.js.map +1 -0
- package/dist/esm/query/index.d.ts +5 -5
- package/dist/esm/query/ir.d.ts +81 -0
- package/dist/esm/query/ir.js +57 -0
- package/dist/esm/query/ir.js.map +1 -0
- package/dist/esm/query/live-query-collection.d.ts +124 -0
- package/dist/esm/query/live-query-collection.js +224 -0
- package/dist/esm/query/live-query-collection.js.map +1 -0
- package/dist/esm/transactions.d.ts +10 -1
- package/dist/esm/transactions.js +20 -13
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +13 -0
- package/package.json +3 -4
- package/src/collection.ts +152 -129
- package/src/index.ts +0 -1
- package/src/query/builder/functions.ts +267 -0
- package/src/query/builder/index.ts +648 -0
- package/src/query/builder/ref-proxy.ts +156 -0
- package/src/query/builder/types.ts +282 -0
- package/src/query/compiler/evaluators.ts +315 -0
- package/src/query/compiler/group-by.ts +428 -0
- package/src/query/compiler/index.ts +276 -0
- package/src/query/compiler/joins.ts +228 -0
- package/src/query/compiler/order-by.ts +139 -0
- package/src/query/compiler/select.ts +173 -0
- package/src/query/index.ts +54 -5
- package/src/query/ir.ts +128 -0
- package/src/query/live-query-collection.ts +512 -0
- package/src/transactions.ts +27 -16
- package/src/types.ts +15 -0
- package/dist/cjs/query/compiled-query.cjs +0 -160
- package/dist/cjs/query/compiled-query.cjs.map +0 -1
- package/dist/cjs/query/compiled-query.d.cts +0 -20
- package/dist/cjs/query/evaluators.cjs +0 -161
- package/dist/cjs/query/evaluators.cjs.map +0 -1
- package/dist/cjs/query/evaluators.d.cts +0 -14
- package/dist/cjs/query/extractors.cjs +0 -122
- package/dist/cjs/query/extractors.cjs.map +0 -1
- package/dist/cjs/query/extractors.d.cts +0 -22
- package/dist/cjs/query/functions.cjs +0 -152
- package/dist/cjs/query/functions.cjs.map +0 -1
- package/dist/cjs/query/functions.d.cts +0 -21
- package/dist/cjs/query/group-by.cjs +0 -88
- package/dist/cjs/query/group-by.cjs.map +0 -1
- package/dist/cjs/query/group-by.d.cts +0 -40
- package/dist/cjs/query/joins.cjs +0 -141
- package/dist/cjs/query/joins.cjs.map +0 -1
- package/dist/cjs/query/joins.d.cts +0 -14
- package/dist/cjs/query/order-by.cjs +0 -185
- package/dist/cjs/query/order-by.cjs.map +0 -1
- package/dist/cjs/query/order-by.d.cts +0 -3
- package/dist/cjs/query/pipeline-compiler.cjs +0 -89
- package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
- package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
- package/dist/cjs/query/query-builder.cjs +0 -307
- package/dist/cjs/query/query-builder.cjs.map +0 -1
- package/dist/cjs/query/query-builder.d.cts +0 -225
- package/dist/cjs/query/schema.d.cts +0 -100
- package/dist/cjs/query/select.cjs +0 -130
- package/dist/cjs/query/select.cjs.map +0 -1
- package/dist/cjs/query/select.d.cts +0 -3
- package/dist/cjs/query/types.d.cts +0 -189
- package/dist/cjs/query/utils.cjs +0 -154
- package/dist/cjs/query/utils.cjs.map +0 -1
- package/dist/cjs/query/utils.d.cts +0 -37
- package/dist/cjs/utils.cjs +0 -17
- package/dist/cjs/utils.cjs.map +0 -1
- package/dist/cjs/utils.d.cts +0 -3
- package/dist/esm/query/compiled-query.d.ts +0 -20
- package/dist/esm/query/compiled-query.js +0 -160
- package/dist/esm/query/compiled-query.js.map +0 -1
- package/dist/esm/query/evaluators.d.ts +0 -14
- package/dist/esm/query/evaluators.js +0 -161
- package/dist/esm/query/evaluators.js.map +0 -1
- package/dist/esm/query/extractors.d.ts +0 -22
- package/dist/esm/query/extractors.js +0 -122
- package/dist/esm/query/extractors.js.map +0 -1
- package/dist/esm/query/functions.d.ts +0 -21
- package/dist/esm/query/functions.js +0 -152
- package/dist/esm/query/functions.js.map +0 -1
- package/dist/esm/query/group-by.d.ts +0 -40
- package/dist/esm/query/group-by.js +0 -88
- package/dist/esm/query/group-by.js.map +0 -1
- package/dist/esm/query/joins.d.ts +0 -14
- package/dist/esm/query/joins.js +0 -141
- package/dist/esm/query/joins.js.map +0 -1
- package/dist/esm/query/order-by.d.ts +0 -3
- package/dist/esm/query/order-by.js +0 -185
- package/dist/esm/query/order-by.js.map +0 -1
- package/dist/esm/query/pipeline-compiler.d.ts +0 -10
- package/dist/esm/query/pipeline-compiler.js +0 -89
- package/dist/esm/query/pipeline-compiler.js.map +0 -1
- package/dist/esm/query/query-builder.d.ts +0 -225
- package/dist/esm/query/query-builder.js +0 -307
- package/dist/esm/query/query-builder.js.map +0 -1
- package/dist/esm/query/schema.d.ts +0 -100
- package/dist/esm/query/select.d.ts +0 -3
- package/dist/esm/query/select.js +0 -130
- package/dist/esm/query/select.js.map +0 -1
- package/dist/esm/query/types.d.ts +0 -189
- package/dist/esm/query/utils.d.ts +0 -37
- package/dist/esm/query/utils.js +0 -154
- package/dist/esm/query/utils.js.map +0 -1
- package/dist/esm/utils.d.ts +0 -3
- package/dist/esm/utils.js +0 -17
- package/dist/esm/utils.js.map +0 -1
- package/src/query/compiled-query.ts +0 -234
- package/src/query/evaluators.ts +0 -250
- package/src/query/extractors.ts +0 -214
- package/src/query/functions.ts +0 -297
- package/src/query/group-by.ts +0 -139
- package/src/query/joins.ts +0 -260
- package/src/query/order-by.ts +0 -264
- package/src/query/pipeline-compiler.ts +0 -149
- package/src/query/query-builder.ts +0 -902
- package/src/query/schema.ts +0 -268
- package/src/query/select.ts +0 -208
- package/src/query/types.ts +0 -418
- package/src/query/utils.ts +0 -245
- package/src/utils.ts +0 -15
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
import { filter, groupBy, groupByOperators, map } from "@electric-sql/d2mini"
|
|
2
|
+
import { Func, PropRef } 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 PropRef([`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 PropRef([`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
|
+
}
|