@tanstack/db 0.0.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.
- package/README.md +37 -0
- package/dist/cjs/SortedMap.cjs +140 -0
- package/dist/cjs/SortedMap.cjs.map +1 -0
- package/dist/cjs/SortedMap.d.cts +91 -0
- package/dist/cjs/collection.cjs +597 -0
- package/dist/cjs/collection.cjs.map +1 -0
- package/dist/cjs/collection.d.cts +176 -0
- package/dist/cjs/deferred.cjs +25 -0
- package/dist/cjs/deferred.cjs.map +1 -0
- package/dist/cjs/deferred.d.cts +20 -0
- package/dist/cjs/errors.cjs +10 -0
- package/dist/cjs/errors.cjs.map +1 -0
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +33 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/cjs/index.d.cts +9 -0
- package/dist/cjs/proxy.cjs +654 -0
- package/dist/cjs/proxy.cjs.map +1 -0
- package/dist/cjs/proxy.d.cts +59 -0
- package/dist/cjs/query/compiled-query.cjs +162 -0
- package/dist/cjs/query/compiled-query.cjs.map +1 -0
- package/dist/cjs/query/compiled-query.d.cts +22 -0
- package/dist/cjs/query/evaluators.cjs +146 -0
- package/dist/cjs/query/evaluators.cjs.map +1 -0
- package/dist/cjs/query/evaluators.d.cts +9 -0
- package/dist/cjs/query/extractors.cjs +122 -0
- package/dist/cjs/query/extractors.cjs.map +1 -0
- package/dist/cjs/query/extractors.d.cts +22 -0
- package/dist/cjs/query/functions.cjs +152 -0
- package/dist/cjs/query/functions.cjs.map +1 -0
- package/dist/cjs/query/functions.d.cts +21 -0
- package/dist/cjs/query/group-by.cjs +91 -0
- package/dist/cjs/query/group-by.cjs.map +1 -0
- package/dist/cjs/query/group-by.d.cts +40 -0
- package/dist/cjs/query/index.d.cts +5 -0
- package/dist/cjs/query/joins.cjs +155 -0
- package/dist/cjs/query/joins.cjs.map +1 -0
- package/dist/cjs/query/joins.d.cts +14 -0
- package/dist/cjs/query/key-by.cjs +43 -0
- package/dist/cjs/query/key-by.cjs.map +1 -0
- package/dist/cjs/query/key-by.d.cts +3 -0
- package/dist/cjs/query/order-by.cjs +229 -0
- package/dist/cjs/query/order-by.cjs.map +1 -0
- package/dist/cjs/query/order-by.d.cts +3 -0
- package/dist/cjs/query/pipeline-compiler.cjs +94 -0
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -0
- package/dist/cjs/query/pipeline-compiler.d.cts +9 -0
- package/dist/cjs/query/query-builder.cjs +314 -0
- package/dist/cjs/query/query-builder.cjs.map +1 -0
- package/dist/cjs/query/query-builder.d.cts +219 -0
- package/dist/cjs/query/schema.d.cts +98 -0
- package/dist/cjs/query/select.cjs +107 -0
- package/dist/cjs/query/select.cjs.map +1 -0
- package/dist/cjs/query/select.d.cts +3 -0
- package/dist/cjs/query/types.d.cts +188 -0
- package/dist/cjs/query/utils.cjs +154 -0
- package/dist/cjs/query/utils.cjs.map +1 -0
- package/dist/cjs/query/utils.d.cts +37 -0
- package/dist/cjs/transactions.cjs +137 -0
- package/dist/cjs/transactions.cjs.map +1 -0
- package/dist/cjs/transactions.d.cts +27 -0
- package/dist/cjs/types.d.cts +94 -0
- package/dist/cjs/utils.cjs +17 -0
- package/dist/cjs/utils.cjs.map +1 -0
- package/dist/cjs/utils.d.cts +3 -0
- package/dist/esm/SortedMap.d.ts +91 -0
- package/dist/esm/SortedMap.js +140 -0
- package/dist/esm/SortedMap.js.map +1 -0
- package/dist/esm/collection.d.ts +176 -0
- package/dist/esm/collection.js +597 -0
- package/dist/esm/collection.js.map +1 -0
- package/dist/esm/deferred.d.ts +20 -0
- package/dist/esm/deferred.js +25 -0
- package/dist/esm/deferred.js.map +1 -0
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +10 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/index.d.ts +9 -0
- package/dist/esm/index.js +33 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/proxy.d.ts +59 -0
- package/dist/esm/proxy.js +654 -0
- package/dist/esm/proxy.js.map +1 -0
- package/dist/esm/query/compiled-query.d.ts +22 -0
- package/dist/esm/query/compiled-query.js +162 -0
- package/dist/esm/query/compiled-query.js.map +1 -0
- package/dist/esm/query/evaluators.d.ts +9 -0
- package/dist/esm/query/evaluators.js +146 -0
- package/dist/esm/query/evaluators.js.map +1 -0
- package/dist/esm/query/extractors.d.ts +22 -0
- package/dist/esm/query/extractors.js +122 -0
- package/dist/esm/query/extractors.js.map +1 -0
- package/dist/esm/query/functions.d.ts +21 -0
- package/dist/esm/query/functions.js +152 -0
- package/dist/esm/query/functions.js.map +1 -0
- package/dist/esm/query/group-by.d.ts +40 -0
- package/dist/esm/query/group-by.js +91 -0
- package/dist/esm/query/group-by.js.map +1 -0
- package/dist/esm/query/index.d.ts +5 -0
- package/dist/esm/query/joins.d.ts +14 -0
- package/dist/esm/query/joins.js +155 -0
- package/dist/esm/query/joins.js.map +1 -0
- package/dist/esm/query/key-by.d.ts +3 -0
- package/dist/esm/query/key-by.js +43 -0
- package/dist/esm/query/key-by.js.map +1 -0
- package/dist/esm/query/order-by.d.ts +3 -0
- package/dist/esm/query/order-by.js +229 -0
- package/dist/esm/query/order-by.js.map +1 -0
- package/dist/esm/query/pipeline-compiler.d.ts +9 -0
- package/dist/esm/query/pipeline-compiler.js +94 -0
- package/dist/esm/query/pipeline-compiler.js.map +1 -0
- package/dist/esm/query/query-builder.d.ts +219 -0
- package/dist/esm/query/query-builder.js +314 -0
- package/dist/esm/query/query-builder.js.map +1 -0
- package/dist/esm/query/schema.d.ts +98 -0
- package/dist/esm/query/select.d.ts +3 -0
- package/dist/esm/query/select.js +107 -0
- package/dist/esm/query/select.js.map +1 -0
- package/dist/esm/query/types.d.ts +188 -0
- package/dist/esm/query/utils.d.ts +37 -0
- package/dist/esm/query/utils.js +154 -0
- package/dist/esm/query/utils.js.map +1 -0
- package/dist/esm/transactions.d.ts +27 -0
- package/dist/esm/transactions.js +137 -0
- package/dist/esm/transactions.js.map +1 -0
- package/dist/esm/types.d.ts +94 -0
- package/dist/esm/utils.d.ts +3 -0
- package/dist/esm/utils.js +17 -0
- package/dist/esm/utils.js.map +1 -0
- package/package.json +57 -0
- package/src/SortedMap.ts +163 -0
- package/src/collection.ts +919 -0
- package/src/deferred.ts +47 -0
- package/src/errors.ts +6 -0
- package/src/index.ts +12 -0
- package/src/proxy.ts +1104 -0
- package/src/query/compiled-query.ts +193 -0
- package/src/query/evaluators.ts +222 -0
- package/src/query/extractors.ts +211 -0
- package/src/query/functions.ts +297 -0
- package/src/query/group-by.ts +137 -0
- package/src/query/index.ts +5 -0
- package/src/query/joins.ts +247 -0
- package/src/query/key-by.ts +61 -0
- package/src/query/order-by.ts +312 -0
- package/src/query/pipeline-compiler.ts +152 -0
- package/src/query/query-builder.ts +898 -0
- package/src/query/schema.ts +255 -0
- package/src/query/select.ts +173 -0
- package/src/query/types.ts +417 -0
- package/src/query/utils.ts +245 -0
- package/src/transactions.ts +198 -0
- package/src/types.ts +125 -0
- package/src/utils.ts +15 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import type { AllowedFunctionName } from "./schema.js"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type for function implementations
|
|
5
|
+
*/
|
|
6
|
+
type FunctionImplementation = (arg: unknown) => unknown
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Converts a string to uppercase
|
|
10
|
+
*/
|
|
11
|
+
function upperFunction(arg: unknown): string {
|
|
12
|
+
if (typeof arg !== `string`) {
|
|
13
|
+
throw new Error(`UPPER function expects a string argument`)
|
|
14
|
+
}
|
|
15
|
+
return arg.toUpperCase()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Converts a string to lowercase
|
|
20
|
+
*/
|
|
21
|
+
function lowerFunction(arg: unknown): string {
|
|
22
|
+
if (typeof arg !== `string`) {
|
|
23
|
+
throw new Error(`LOWER function expects a string argument`)
|
|
24
|
+
}
|
|
25
|
+
return arg.toLowerCase()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Returns the length of a string or array
|
|
30
|
+
*/
|
|
31
|
+
function lengthFunction(arg: unknown): number {
|
|
32
|
+
if (typeof arg === `string` || Array.isArray(arg)) {
|
|
33
|
+
return arg.length
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
throw new Error(`LENGTH function expects a string or array argument`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Concatenates multiple strings
|
|
41
|
+
*/
|
|
42
|
+
function concatFunction(arg: unknown): string {
|
|
43
|
+
if (!Array.isArray(arg)) {
|
|
44
|
+
throw new Error(`CONCAT function expects an array of string arguments`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (arg.length === 0) {
|
|
48
|
+
return ``
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check that all arguments are strings
|
|
52
|
+
for (let i = 0; i < arg.length; i++) {
|
|
53
|
+
if (arg[i] !== null && arg[i] !== undefined && typeof arg[i] !== `string`) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`CONCAT function expects all arguments to be strings, but argument at position ${i} is ${typeof arg[i]}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Concatenate strings, treating null and undefined as empty strings
|
|
61
|
+
return arg
|
|
62
|
+
.map((str) => (str === null || str === undefined ? `` : str))
|
|
63
|
+
.join(``)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns the first non-null, non-undefined value from an array
|
|
68
|
+
*/
|
|
69
|
+
function coalesceFunction(arg: unknown): unknown {
|
|
70
|
+
if (!Array.isArray(arg)) {
|
|
71
|
+
throw new Error(`COALESCE function expects an array of arguments`)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (arg.length === 0) {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Return the first non-null, non-undefined value
|
|
79
|
+
for (const value of arg) {
|
|
80
|
+
if (value !== null && value !== undefined) {
|
|
81
|
+
return value
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// If all values were null or undefined, return null
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates or converts a value to a Date object
|
|
91
|
+
*/
|
|
92
|
+
function dateFunction(arg: unknown): Date | null {
|
|
93
|
+
// If the argument is already a Date, return it
|
|
94
|
+
if (arg instanceof Date) {
|
|
95
|
+
return arg
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If the argument is null or undefined, return null
|
|
99
|
+
if (arg === null || arg === undefined) {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Handle string and number conversions
|
|
104
|
+
if (typeof arg === `string` || typeof arg === `number`) {
|
|
105
|
+
const date = new Date(arg)
|
|
106
|
+
|
|
107
|
+
// Check if the date is valid
|
|
108
|
+
if (isNaN(date.getTime())) {
|
|
109
|
+
throw new Error(`DATE function could not parse "${arg}" as a valid date`)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return date
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
throw new Error(`DATE function expects a string, number, or Date argument`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Extracts a value from a JSON string or object using a path.
|
|
120
|
+
* Similar to PostgreSQL's json_extract_path function.
|
|
121
|
+
*
|
|
122
|
+
* Usage: JSON_EXTRACT([jsonInput, 'path', 'to', 'property'])
|
|
123
|
+
* Example: JSON_EXTRACT(['{"user": {"name": "John"}}', 'user', 'name']) returns "John"
|
|
124
|
+
*/
|
|
125
|
+
function jsonExtractFunction(arg: unknown): unknown {
|
|
126
|
+
if (!Array.isArray(arg) || arg.length < 1) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`JSON_EXTRACT function expects an array with at least one element [jsonInput, ...pathElements]`
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const [jsonInput, ...pathElements] = arg
|
|
133
|
+
|
|
134
|
+
// Handle null or undefined input
|
|
135
|
+
if (jsonInput === null || jsonInput === undefined) {
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Parse JSON if it's a string
|
|
140
|
+
let jsonData: any
|
|
141
|
+
|
|
142
|
+
if (typeof jsonInput === `string`) {
|
|
143
|
+
try {
|
|
144
|
+
jsonData = JSON.parse(jsonInput)
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`JSON_EXTRACT function could not parse JSON string: ${error instanceof Error ? error.message : String(error)}`
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
} else if (typeof jsonInput === `object`) {
|
|
151
|
+
// If already an object, use it directly
|
|
152
|
+
jsonData = jsonInput
|
|
153
|
+
} else {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`JSON_EXTRACT function expects a JSON string or object as the first argument`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// If no path elements, return the parsed JSON
|
|
160
|
+
if (pathElements.length === 0) {
|
|
161
|
+
return jsonData
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Navigate through the path elements
|
|
165
|
+
let current = jsonData
|
|
166
|
+
|
|
167
|
+
for (let i = 0; i < pathElements.length; i++) {
|
|
168
|
+
const pathElement = pathElements[i]
|
|
169
|
+
|
|
170
|
+
// Path elements should be strings
|
|
171
|
+
if (typeof pathElement !== `string`) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`JSON_EXTRACT function expects path elements to be strings, but element at position ${i + 1} is ${typeof pathElement}`
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// If current node is null or undefined, or not an object, we can't navigate further
|
|
178
|
+
if (
|
|
179
|
+
current === null ||
|
|
180
|
+
current === undefined ||
|
|
181
|
+
typeof current !== `object`
|
|
182
|
+
) {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Access property
|
|
187
|
+
current = current[pathElement]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Return null instead of undefined for consistency
|
|
191
|
+
return current === undefined ? null : current
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Placeholder function for ORDER_INDEX
|
|
196
|
+
* This function doesn't do anything when called directly, as the actual index
|
|
197
|
+
* is provided by the orderBy operator during query execution.
|
|
198
|
+
* The argument can be 'numeric', 'fractional', or any truthy value (defaults to 'numeric')
|
|
199
|
+
*/
|
|
200
|
+
function orderIndexFunction(arg: unknown): null {
|
|
201
|
+
// This is just a placeholder - the actual index is provided by the orderBy operator
|
|
202
|
+
// The function validates that the argument is one of the expected values
|
|
203
|
+
if (
|
|
204
|
+
arg !== `numeric` &&
|
|
205
|
+
arg !== `fractional` &&
|
|
206
|
+
arg !== true &&
|
|
207
|
+
arg !== `default`
|
|
208
|
+
) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`ORDER_INDEX function expects "numeric", "fractional", "default", or true as argument`
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Map of function names to their implementations
|
|
218
|
+
*/
|
|
219
|
+
const functionImplementations: Record<
|
|
220
|
+
AllowedFunctionName,
|
|
221
|
+
FunctionImplementation
|
|
222
|
+
> = {
|
|
223
|
+
// Map function names to their implementation functions
|
|
224
|
+
DATE: dateFunction,
|
|
225
|
+
JSON_EXTRACT: jsonExtractFunction,
|
|
226
|
+
JSON_EXTRACT_PATH: jsonExtractFunction, // Alias for JSON_EXTRACT
|
|
227
|
+
UPPER: upperFunction,
|
|
228
|
+
LOWER: lowerFunction,
|
|
229
|
+
COALESCE: coalesceFunction,
|
|
230
|
+
CONCAT: concatFunction,
|
|
231
|
+
LENGTH: lengthFunction,
|
|
232
|
+
ORDER_INDEX: orderIndexFunction,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Evaluates a function call with the given name and arguments
|
|
237
|
+
* @param functionName The name of the function to evaluate
|
|
238
|
+
* @param arg The arguments to pass to the function
|
|
239
|
+
* @returns The result of the function call
|
|
240
|
+
*/
|
|
241
|
+
export function evaluateFunction(
|
|
242
|
+
functionName: AllowedFunctionName,
|
|
243
|
+
arg: unknown
|
|
244
|
+
): unknown {
|
|
245
|
+
const implementation = functionImplementations[functionName] as
|
|
246
|
+
| FunctionImplementation
|
|
247
|
+
| undefined // Double check that the implementation is defined
|
|
248
|
+
|
|
249
|
+
if (!implementation) {
|
|
250
|
+
throw new Error(`Unknown function: ${functionName}`)
|
|
251
|
+
}
|
|
252
|
+
return implementation(arg)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Determines if an object is a function call
|
|
257
|
+
* @param obj The object to check
|
|
258
|
+
* @returns True if the object is a function call, false otherwise
|
|
259
|
+
*/
|
|
260
|
+
export function isFunctionCall(obj: unknown): boolean {
|
|
261
|
+
if (!obj || typeof obj !== `object`) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const keys = Object.keys(obj)
|
|
266
|
+
if (keys.length !== 1) {
|
|
267
|
+
return false
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const functionName = keys[0] as string
|
|
271
|
+
|
|
272
|
+
// Check if the key is one of the allowed function names
|
|
273
|
+
return Object.keys(functionImplementations).includes(functionName)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Extracts the function name and argument from a function call object.
|
|
278
|
+
*/
|
|
279
|
+
export function extractFunctionCall(obj: Record<string, unknown>): {
|
|
280
|
+
functionName: AllowedFunctionName
|
|
281
|
+
argument: unknown
|
|
282
|
+
} {
|
|
283
|
+
const keys = Object.keys(obj)
|
|
284
|
+
if (keys.length !== 1) {
|
|
285
|
+
throw new Error(`Invalid function call: object must have exactly one key`)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const functionName = keys[0] as AllowedFunctionName
|
|
289
|
+
if (!Object.keys(functionImplementations).includes(functionName)) {
|
|
290
|
+
throw new Error(`Invalid function name: ${functionName}`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
functionName,
|
|
295
|
+
argument: obj[functionName],
|
|
296
|
+
}
|
|
297
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { groupBy, groupByOperators, map } from "@electric-sql/d2ts"
|
|
2
|
+
import {
|
|
3
|
+
evaluateOperandOnNestedRow,
|
|
4
|
+
extractValueFromNestedRow,
|
|
5
|
+
} from "./extractors"
|
|
6
|
+
import { isAggregateFunctionCall } from "./utils"
|
|
7
|
+
import type { ConditionOperand, FunctionCall, Query } from "./schema"
|
|
8
|
+
import type { IStreamBuilder } from "@electric-sql/d2ts"
|
|
9
|
+
|
|
10
|
+
const { sum, count, avg, min, max, median, mode } = groupByOperators
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Process the groupBy clause in a D2QL query
|
|
14
|
+
*/
|
|
15
|
+
export function processGroupBy(
|
|
16
|
+
pipeline: IStreamBuilder<Record<string, unknown>>,
|
|
17
|
+
query: Query,
|
|
18
|
+
mainTableAlias: string
|
|
19
|
+
) {
|
|
20
|
+
// Normalize groupBy to an array of column references
|
|
21
|
+
const groupByColumns = Array.isArray(query.groupBy)
|
|
22
|
+
? query.groupBy
|
|
23
|
+
: [query.groupBy]
|
|
24
|
+
|
|
25
|
+
// Create a key extractor function for the groupBy operator
|
|
26
|
+
const keyExtractor = (nestedRow: Record<string, unknown>) => {
|
|
27
|
+
const key: Record<string, unknown> = {}
|
|
28
|
+
|
|
29
|
+
// Extract each groupBy column value
|
|
30
|
+
for (const column of groupByColumns) {
|
|
31
|
+
if (typeof column === `string` && (column as string).startsWith(`@`)) {
|
|
32
|
+
const columnRef = (column as string).substring(1)
|
|
33
|
+
const columnName = columnRef.includes(`.`)
|
|
34
|
+
? columnRef.split(`.`)[1]
|
|
35
|
+
: columnRef
|
|
36
|
+
|
|
37
|
+
key[columnName!] = extractValueFromNestedRow(
|
|
38
|
+
nestedRow,
|
|
39
|
+
columnRef,
|
|
40
|
+
mainTableAlias
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return key
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Create aggregate functions for any aggregated columns in the SELECT clause
|
|
49
|
+
const aggregates: Record<string, any> = {}
|
|
50
|
+
|
|
51
|
+
// Scan the SELECT clause for aggregate functions
|
|
52
|
+
for (const item of query.select) {
|
|
53
|
+
if (typeof item === `object`) {
|
|
54
|
+
for (const [alias, expr] of Object.entries(item)) {
|
|
55
|
+
if (typeof expr === `object` && isAggregateFunctionCall(expr)) {
|
|
56
|
+
// Get the function name (the only key in the object)
|
|
57
|
+
const functionName = Object.keys(expr)[0]
|
|
58
|
+
// Get the column reference or expression to aggregate
|
|
59
|
+
const columnRef = (expr as FunctionCall)[
|
|
60
|
+
functionName as keyof FunctionCall
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
// Add the aggregate function to our aggregates object
|
|
64
|
+
aggregates[alias] = getAggregateFunction(
|
|
65
|
+
functionName!,
|
|
66
|
+
columnRef,
|
|
67
|
+
mainTableAlias
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Apply the groupBy operator if we have any aggregates
|
|
75
|
+
if (Object.keys(aggregates).length > 0) {
|
|
76
|
+
pipeline = pipeline.pipe(
|
|
77
|
+
groupBy(keyExtractor, aggregates),
|
|
78
|
+
// Convert KeyValue<string, ResultType> to Record<string, unknown>
|
|
79
|
+
map(([_key, value]) => {
|
|
80
|
+
// After groupBy, the value already contains both the key fields and aggregate results
|
|
81
|
+
// We need to return it as is, not wrapped in a nested structure
|
|
82
|
+
return value as Record<string, unknown>
|
|
83
|
+
})
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return pipeline
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Helper function to get an aggregate function based on the function name
|
|
92
|
+
*/
|
|
93
|
+
export function getAggregateFunction(
|
|
94
|
+
functionName: string,
|
|
95
|
+
columnRef: string | ConditionOperand,
|
|
96
|
+
mainTableAlias: string
|
|
97
|
+
) {
|
|
98
|
+
// Create a value extractor function for the column to aggregate
|
|
99
|
+
const valueExtractor = (nestedRow: Record<string, unknown>) => {
|
|
100
|
+
let value: unknown
|
|
101
|
+
if (typeof columnRef === `string` && columnRef.startsWith(`@`)) {
|
|
102
|
+
value = extractValueFromNestedRow(
|
|
103
|
+
nestedRow,
|
|
104
|
+
columnRef.substring(1),
|
|
105
|
+
mainTableAlias
|
|
106
|
+
)
|
|
107
|
+
} else {
|
|
108
|
+
value = evaluateOperandOnNestedRow(
|
|
109
|
+
nestedRow,
|
|
110
|
+
columnRef as ConditionOperand,
|
|
111
|
+
mainTableAlias
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
// Ensure we return a number for aggregate functions
|
|
115
|
+
return typeof value === `number` ? value : 0
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Return the appropriate aggregate function
|
|
119
|
+
switch (functionName.toUpperCase()) {
|
|
120
|
+
case `SUM`:
|
|
121
|
+
return sum(valueExtractor)
|
|
122
|
+
case `COUNT`:
|
|
123
|
+
return count() // count() doesn't need a value extractor
|
|
124
|
+
case `AVG`:
|
|
125
|
+
return avg(valueExtractor)
|
|
126
|
+
case `MIN`:
|
|
127
|
+
return min(valueExtractor)
|
|
128
|
+
case `MAX`:
|
|
129
|
+
return max(valueExtractor)
|
|
130
|
+
case `MEDIAN`:
|
|
131
|
+
return median(valueExtractor)
|
|
132
|
+
case `MODE`:
|
|
133
|
+
return mode(valueExtractor)
|
|
134
|
+
default:
|
|
135
|
+
throw new Error(`Unsupported aggregate function: ${functionName}`)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import {
|
|
2
|
+
consolidate,
|
|
3
|
+
filter,
|
|
4
|
+
join as joinOperator,
|
|
5
|
+
map,
|
|
6
|
+
} from "@electric-sql/d2ts"
|
|
7
|
+
import { evaluateConditionOnNestedRow } from "./evaluators.js"
|
|
8
|
+
import { extractJoinKey } from "./extractors.js"
|
|
9
|
+
import type { Query } from "./index.js"
|
|
10
|
+
import type { IStreamBuilder, JoinType } from "@electric-sql/d2ts"
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a processing pipeline for join clauses
|
|
14
|
+
*/
|
|
15
|
+
export function processJoinClause(
|
|
16
|
+
pipeline: IStreamBuilder<Record<string, unknown>>,
|
|
17
|
+
query: Query,
|
|
18
|
+
tables: Record<string, IStreamBuilder<Record<string, unknown>>>,
|
|
19
|
+
mainTableAlias: string,
|
|
20
|
+
allInputs: Record<string, IStreamBuilder<Record<string, unknown>>>
|
|
21
|
+
) {
|
|
22
|
+
if (!query.join) return pipeline
|
|
23
|
+
const input = allInputs[query.from]
|
|
24
|
+
|
|
25
|
+
for (const joinClause of query.join) {
|
|
26
|
+
// Create a stream for the joined table
|
|
27
|
+
const joinedTableAlias = joinClause.as || joinClause.from
|
|
28
|
+
|
|
29
|
+
// Get the right join type for the operator
|
|
30
|
+
const joinType: JoinType =
|
|
31
|
+
joinClause.type === `cross` ? `inner` : joinClause.type
|
|
32
|
+
|
|
33
|
+
// We need to prepare the main pipeline and the joined pipeline
|
|
34
|
+
// to have the correct key format for joining
|
|
35
|
+
const mainPipeline = pipeline.pipe(
|
|
36
|
+
map((nestedRow: Record<string, unknown>) => {
|
|
37
|
+
// Extract the key from the ON condition left side for the main table
|
|
38
|
+
const mainRow = nestedRow[mainTableAlias] as Record<string, unknown>
|
|
39
|
+
|
|
40
|
+
// Extract the join key from the main row
|
|
41
|
+
const keyValue = extractJoinKey(
|
|
42
|
+
mainRow,
|
|
43
|
+
joinClause.on[0],
|
|
44
|
+
mainTableAlias
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
// Return [key, nestedRow] as a KeyValue type
|
|
48
|
+
return [keyValue, nestedRow] as [unknown, Record<string, unknown>]
|
|
49
|
+
})
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
// Get the joined table input from the inputs map
|
|
53
|
+
let joinedTableInput: IStreamBuilder<Record<string, unknown>>
|
|
54
|
+
|
|
55
|
+
if (allInputs[joinClause.from]) {
|
|
56
|
+
// Use the provided input if available
|
|
57
|
+
joinedTableInput = allInputs[joinClause.from]!
|
|
58
|
+
} else {
|
|
59
|
+
// Create a new input if not provided
|
|
60
|
+
joinedTableInput = input!.graph.newInput<Record<string, unknown>>()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
tables[joinedTableAlias] = joinedTableInput
|
|
64
|
+
|
|
65
|
+
// Create a pipeline for the joined table
|
|
66
|
+
const joinedPipeline = joinedTableInput.pipe(
|
|
67
|
+
map((row: Record<string, unknown>) => {
|
|
68
|
+
// Wrap the row in an object with the table alias as the key
|
|
69
|
+
const nestedRow = { [joinedTableAlias]: row }
|
|
70
|
+
|
|
71
|
+
// Extract the key from the ON condition right side for the joined table
|
|
72
|
+
const keyValue = extractJoinKey(row, joinClause.on[2], joinedTableAlias)
|
|
73
|
+
|
|
74
|
+
// Return [key, nestedRow] as a KeyValue type
|
|
75
|
+
return [keyValue, nestedRow] as [unknown, Record<string, unknown>]
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Apply join with appropriate typings based on join type
|
|
80
|
+
switch (joinType) {
|
|
81
|
+
case `inner`:
|
|
82
|
+
pipeline = mainPipeline.pipe(
|
|
83
|
+
joinOperator(joinedPipeline, `inner`),
|
|
84
|
+
consolidate(),
|
|
85
|
+
processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
|
|
86
|
+
)
|
|
87
|
+
break
|
|
88
|
+
case `left`:
|
|
89
|
+
pipeline = mainPipeline.pipe(
|
|
90
|
+
joinOperator(joinedPipeline, `left`),
|
|
91
|
+
consolidate(),
|
|
92
|
+
processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
|
|
93
|
+
)
|
|
94
|
+
break
|
|
95
|
+
case `right`:
|
|
96
|
+
pipeline = mainPipeline.pipe(
|
|
97
|
+
joinOperator(joinedPipeline, `right`),
|
|
98
|
+
consolidate(),
|
|
99
|
+
processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
|
|
100
|
+
)
|
|
101
|
+
break
|
|
102
|
+
case `full`:
|
|
103
|
+
pipeline = mainPipeline.pipe(
|
|
104
|
+
joinOperator(joinedPipeline, `full`),
|
|
105
|
+
consolidate(),
|
|
106
|
+
processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
|
|
107
|
+
)
|
|
108
|
+
break
|
|
109
|
+
default:
|
|
110
|
+
pipeline = mainPipeline.pipe(
|
|
111
|
+
joinOperator(joinedPipeline, `inner`),
|
|
112
|
+
consolidate(),
|
|
113
|
+
processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return pipeline
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Creates a processing pipeline for join results
|
|
122
|
+
*/
|
|
123
|
+
export function processJoinResults(
|
|
124
|
+
mainTableAlias: string,
|
|
125
|
+
joinedTableAlias: string,
|
|
126
|
+
joinClause: { on: any; where?: any; type: string }
|
|
127
|
+
) {
|
|
128
|
+
return function (
|
|
129
|
+
pipeline: IStreamBuilder<unknown>
|
|
130
|
+
): IStreamBuilder<Record<string, unknown>> {
|
|
131
|
+
return pipeline.pipe(
|
|
132
|
+
// Process the join result and handle nulls in the same step
|
|
133
|
+
map((result: unknown) => {
|
|
134
|
+
const [_key, [mainNestedRow, joinedNestedRow]] = result as [
|
|
135
|
+
unknown,
|
|
136
|
+
[
|
|
137
|
+
Record<string, unknown> | undefined,
|
|
138
|
+
Record<string, unknown> | undefined,
|
|
139
|
+
],
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
// For inner joins, both sides should be non-null
|
|
143
|
+
if (joinClause.type === `inner` || joinClause.type === `cross`) {
|
|
144
|
+
if (!mainNestedRow || !joinedNestedRow) {
|
|
145
|
+
return undefined // Will be filtered out
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// For left joins, the main row must be non-null
|
|
150
|
+
if (joinClause.type === `left` && !mainNestedRow) {
|
|
151
|
+
return undefined // Will be filtered out
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// For right joins, the joined row must be non-null
|
|
155
|
+
if (joinClause.type === `right` && !joinedNestedRow) {
|
|
156
|
+
return undefined // Will be filtered out
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Merge the nested rows
|
|
160
|
+
const mergedNestedRow: Record<string, unknown> = {}
|
|
161
|
+
|
|
162
|
+
// Add main row data if it exists
|
|
163
|
+
if (mainNestedRow) {
|
|
164
|
+
Object.entries(mainNestedRow).forEach(([tableAlias, tableData]) => {
|
|
165
|
+
mergedNestedRow[tableAlias] = tableData
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// If we have a joined row, add it to the merged result
|
|
170
|
+
if (joinedNestedRow) {
|
|
171
|
+
Object.entries(joinedNestedRow).forEach(([tableAlias, tableData]) => {
|
|
172
|
+
mergedNestedRow[tableAlias] = tableData
|
|
173
|
+
})
|
|
174
|
+
} else if (joinClause.type === `left` || joinClause.type === `full`) {
|
|
175
|
+
// For left or full joins, add the joined table with null data if missing
|
|
176
|
+
mergedNestedRow[joinedTableAlias] = null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// For right or full joins, add the main table with null data if missing
|
|
180
|
+
if (
|
|
181
|
+
!mainNestedRow &&
|
|
182
|
+
(joinClause.type === `right` || joinClause.type === `full`)
|
|
183
|
+
) {
|
|
184
|
+
mergedNestedRow[mainTableAlias] = null
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return mergedNestedRow
|
|
188
|
+
}),
|
|
189
|
+
// Filter out undefined results
|
|
190
|
+
filter(
|
|
191
|
+
(value: unknown): value is Record<string, unknown> =>
|
|
192
|
+
value !== undefined
|
|
193
|
+
),
|
|
194
|
+
// Process the ON condition
|
|
195
|
+
filter((nestedRow: Record<string, unknown>) => {
|
|
196
|
+
// If there's no ON condition, or it's a cross join, always return true
|
|
197
|
+
if (!joinClause.on || joinClause.type === `cross`) {
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// For LEFT JOIN, if the right side is null, we should include the row
|
|
202
|
+
if (
|
|
203
|
+
joinClause.type === `left` &&
|
|
204
|
+
nestedRow[joinedTableAlias] === null
|
|
205
|
+
) {
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// For RIGHT JOIN, if the left side is null, we should include the row
|
|
210
|
+
if (joinClause.type === `right` && nestedRow[mainTableAlias] === null) {
|
|
211
|
+
return true
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// For FULL JOIN, if either side is null, we should include the row
|
|
215
|
+
if (
|
|
216
|
+
joinClause.type === `full` &&
|
|
217
|
+
(nestedRow[mainTableAlias] === null ||
|
|
218
|
+
nestedRow[joinedTableAlias] === null)
|
|
219
|
+
) {
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const result = evaluateConditionOnNestedRow(
|
|
224
|
+
nestedRow,
|
|
225
|
+
joinClause.on,
|
|
226
|
+
mainTableAlias,
|
|
227
|
+
joinedTableAlias
|
|
228
|
+
)
|
|
229
|
+
return result
|
|
230
|
+
}),
|
|
231
|
+
// Process the WHERE clause for the join if it exists
|
|
232
|
+
filter((nestedRow: Record<string, unknown>) => {
|
|
233
|
+
if (!joinClause.where) {
|
|
234
|
+
return true
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result = evaluateConditionOnNestedRow(
|
|
238
|
+
nestedRow,
|
|
239
|
+
joinClause.where,
|
|
240
|
+
mainTableAlias,
|
|
241
|
+
joinedTableAlias
|
|
242
|
+
)
|
|
243
|
+
return result
|
|
244
|
+
})
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
}
|