@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.
Files changed (154) hide show
  1. package/README.md +37 -0
  2. package/dist/cjs/SortedMap.cjs +140 -0
  3. package/dist/cjs/SortedMap.cjs.map +1 -0
  4. package/dist/cjs/SortedMap.d.cts +91 -0
  5. package/dist/cjs/collection.cjs +597 -0
  6. package/dist/cjs/collection.cjs.map +1 -0
  7. package/dist/cjs/collection.d.cts +176 -0
  8. package/dist/cjs/deferred.cjs +25 -0
  9. package/dist/cjs/deferred.cjs.map +1 -0
  10. package/dist/cjs/deferred.d.cts +20 -0
  11. package/dist/cjs/errors.cjs +10 -0
  12. package/dist/cjs/errors.cjs.map +1 -0
  13. package/dist/cjs/errors.d.cts +3 -0
  14. package/dist/cjs/index.cjs +33 -0
  15. package/dist/cjs/index.cjs.map +1 -0
  16. package/dist/cjs/index.d.cts +9 -0
  17. package/dist/cjs/proxy.cjs +654 -0
  18. package/dist/cjs/proxy.cjs.map +1 -0
  19. package/dist/cjs/proxy.d.cts +59 -0
  20. package/dist/cjs/query/compiled-query.cjs +162 -0
  21. package/dist/cjs/query/compiled-query.cjs.map +1 -0
  22. package/dist/cjs/query/compiled-query.d.cts +22 -0
  23. package/dist/cjs/query/evaluators.cjs +146 -0
  24. package/dist/cjs/query/evaluators.cjs.map +1 -0
  25. package/dist/cjs/query/evaluators.d.cts +9 -0
  26. package/dist/cjs/query/extractors.cjs +122 -0
  27. package/dist/cjs/query/extractors.cjs.map +1 -0
  28. package/dist/cjs/query/extractors.d.cts +22 -0
  29. package/dist/cjs/query/functions.cjs +152 -0
  30. package/dist/cjs/query/functions.cjs.map +1 -0
  31. package/dist/cjs/query/functions.d.cts +21 -0
  32. package/dist/cjs/query/group-by.cjs +91 -0
  33. package/dist/cjs/query/group-by.cjs.map +1 -0
  34. package/dist/cjs/query/group-by.d.cts +40 -0
  35. package/dist/cjs/query/index.d.cts +5 -0
  36. package/dist/cjs/query/joins.cjs +155 -0
  37. package/dist/cjs/query/joins.cjs.map +1 -0
  38. package/dist/cjs/query/joins.d.cts +14 -0
  39. package/dist/cjs/query/key-by.cjs +43 -0
  40. package/dist/cjs/query/key-by.cjs.map +1 -0
  41. package/dist/cjs/query/key-by.d.cts +3 -0
  42. package/dist/cjs/query/order-by.cjs +229 -0
  43. package/dist/cjs/query/order-by.cjs.map +1 -0
  44. package/dist/cjs/query/order-by.d.cts +3 -0
  45. package/dist/cjs/query/pipeline-compiler.cjs +94 -0
  46. package/dist/cjs/query/pipeline-compiler.cjs.map +1 -0
  47. package/dist/cjs/query/pipeline-compiler.d.cts +9 -0
  48. package/dist/cjs/query/query-builder.cjs +314 -0
  49. package/dist/cjs/query/query-builder.cjs.map +1 -0
  50. package/dist/cjs/query/query-builder.d.cts +219 -0
  51. package/dist/cjs/query/schema.d.cts +98 -0
  52. package/dist/cjs/query/select.cjs +107 -0
  53. package/dist/cjs/query/select.cjs.map +1 -0
  54. package/dist/cjs/query/select.d.cts +3 -0
  55. package/dist/cjs/query/types.d.cts +188 -0
  56. package/dist/cjs/query/utils.cjs +154 -0
  57. package/dist/cjs/query/utils.cjs.map +1 -0
  58. package/dist/cjs/query/utils.d.cts +37 -0
  59. package/dist/cjs/transactions.cjs +137 -0
  60. package/dist/cjs/transactions.cjs.map +1 -0
  61. package/dist/cjs/transactions.d.cts +27 -0
  62. package/dist/cjs/types.d.cts +94 -0
  63. package/dist/cjs/utils.cjs +17 -0
  64. package/dist/cjs/utils.cjs.map +1 -0
  65. package/dist/cjs/utils.d.cts +3 -0
  66. package/dist/esm/SortedMap.d.ts +91 -0
  67. package/dist/esm/SortedMap.js +140 -0
  68. package/dist/esm/SortedMap.js.map +1 -0
  69. package/dist/esm/collection.d.ts +176 -0
  70. package/dist/esm/collection.js +597 -0
  71. package/dist/esm/collection.js.map +1 -0
  72. package/dist/esm/deferred.d.ts +20 -0
  73. package/dist/esm/deferred.js +25 -0
  74. package/dist/esm/deferred.js.map +1 -0
  75. package/dist/esm/errors.d.ts +3 -0
  76. package/dist/esm/errors.js +10 -0
  77. package/dist/esm/errors.js.map +1 -0
  78. package/dist/esm/index.d.ts +9 -0
  79. package/dist/esm/index.js +33 -0
  80. package/dist/esm/index.js.map +1 -0
  81. package/dist/esm/proxy.d.ts +59 -0
  82. package/dist/esm/proxy.js +654 -0
  83. package/dist/esm/proxy.js.map +1 -0
  84. package/dist/esm/query/compiled-query.d.ts +22 -0
  85. package/dist/esm/query/compiled-query.js +162 -0
  86. package/dist/esm/query/compiled-query.js.map +1 -0
  87. package/dist/esm/query/evaluators.d.ts +9 -0
  88. package/dist/esm/query/evaluators.js +146 -0
  89. package/dist/esm/query/evaluators.js.map +1 -0
  90. package/dist/esm/query/extractors.d.ts +22 -0
  91. package/dist/esm/query/extractors.js +122 -0
  92. package/dist/esm/query/extractors.js.map +1 -0
  93. package/dist/esm/query/functions.d.ts +21 -0
  94. package/dist/esm/query/functions.js +152 -0
  95. package/dist/esm/query/functions.js.map +1 -0
  96. package/dist/esm/query/group-by.d.ts +40 -0
  97. package/dist/esm/query/group-by.js +91 -0
  98. package/dist/esm/query/group-by.js.map +1 -0
  99. package/dist/esm/query/index.d.ts +5 -0
  100. package/dist/esm/query/joins.d.ts +14 -0
  101. package/dist/esm/query/joins.js +155 -0
  102. package/dist/esm/query/joins.js.map +1 -0
  103. package/dist/esm/query/key-by.d.ts +3 -0
  104. package/dist/esm/query/key-by.js +43 -0
  105. package/dist/esm/query/key-by.js.map +1 -0
  106. package/dist/esm/query/order-by.d.ts +3 -0
  107. package/dist/esm/query/order-by.js +229 -0
  108. package/dist/esm/query/order-by.js.map +1 -0
  109. package/dist/esm/query/pipeline-compiler.d.ts +9 -0
  110. package/dist/esm/query/pipeline-compiler.js +94 -0
  111. package/dist/esm/query/pipeline-compiler.js.map +1 -0
  112. package/dist/esm/query/query-builder.d.ts +219 -0
  113. package/dist/esm/query/query-builder.js +314 -0
  114. package/dist/esm/query/query-builder.js.map +1 -0
  115. package/dist/esm/query/schema.d.ts +98 -0
  116. package/dist/esm/query/select.d.ts +3 -0
  117. package/dist/esm/query/select.js +107 -0
  118. package/dist/esm/query/select.js.map +1 -0
  119. package/dist/esm/query/types.d.ts +188 -0
  120. package/dist/esm/query/utils.d.ts +37 -0
  121. package/dist/esm/query/utils.js +154 -0
  122. package/dist/esm/query/utils.js.map +1 -0
  123. package/dist/esm/transactions.d.ts +27 -0
  124. package/dist/esm/transactions.js +137 -0
  125. package/dist/esm/transactions.js.map +1 -0
  126. package/dist/esm/types.d.ts +94 -0
  127. package/dist/esm/utils.d.ts +3 -0
  128. package/dist/esm/utils.js +17 -0
  129. package/dist/esm/utils.js.map +1 -0
  130. package/package.json +57 -0
  131. package/src/SortedMap.ts +163 -0
  132. package/src/collection.ts +919 -0
  133. package/src/deferred.ts +47 -0
  134. package/src/errors.ts +6 -0
  135. package/src/index.ts +12 -0
  136. package/src/proxy.ts +1104 -0
  137. package/src/query/compiled-query.ts +193 -0
  138. package/src/query/evaluators.ts +222 -0
  139. package/src/query/extractors.ts +211 -0
  140. package/src/query/functions.ts +297 -0
  141. package/src/query/group-by.ts +137 -0
  142. package/src/query/index.ts +5 -0
  143. package/src/query/joins.ts +247 -0
  144. package/src/query/key-by.ts +61 -0
  145. package/src/query/order-by.ts +312 -0
  146. package/src/query/pipeline-compiler.ts +152 -0
  147. package/src/query/query-builder.ts +898 -0
  148. package/src/query/schema.ts +255 -0
  149. package/src/query/select.ts +173 -0
  150. package/src/query/types.ts +417 -0
  151. package/src/query/utils.ts +245 -0
  152. package/src/transactions.ts +198 -0
  153. package/src/types.ts +125 -0
  154. 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,5 @@
1
+ export * from "./query-builder.js"
2
+ export * from "./compiled-query.js"
3
+ export * from "./pipeline-compiler.js"
4
+ export * from "./schema.js"
5
+ export * from "./types.js"
@@ -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
+ }