@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,61 @@
1
+ import { keyBy } from "@electric-sql/d2ts"
2
+ import type { IStreamBuilder } from "@electric-sql/d2ts"
3
+ import type { Query } from "./schema"
4
+
5
+ export function processKeyBy(
6
+ resultPipeline: IStreamBuilder<
7
+ Record<string, unknown> | [string | number, Record<string, unknown>]
8
+ >,
9
+ query: Query
10
+ ) {
11
+ if (!query.keyBy) {
12
+ return resultPipeline
13
+ }
14
+ const keyByParam = query.keyBy
15
+
16
+ resultPipeline = resultPipeline.pipe(
17
+ keyBy((row: Record<string, unknown>) => {
18
+ if (Array.isArray(keyByParam)) {
19
+ // Multiple columns - extract values and JSON stringify
20
+ const keyValues: Record<string, unknown> = {}
21
+ for (const keyColumn of keyByParam) {
22
+ // Remove @ prefix if present
23
+ const columnName = (keyColumn as string).startsWith(`@`)
24
+ ? (keyColumn as string).substring(1)
25
+ : (keyColumn as string)
26
+
27
+ if (columnName in row) {
28
+ keyValues[columnName] = row[columnName]
29
+ } else {
30
+ throw new Error(
31
+ `Key column "${columnName}" not found in result set. Make sure it's included in the select clause.`
32
+ )
33
+ }
34
+ }
35
+ return JSON.stringify(keyValues)
36
+ } else {
37
+ // Single column
38
+ // Remove @ prefix if present
39
+ const columnName = (keyByParam as string).startsWith(`@`)
40
+ ? (keyByParam as string).substring(1)
41
+ : (keyByParam as string)
42
+
43
+ if (!(columnName in row)) {
44
+ throw new Error(
45
+ `Key column "${columnName}" not found in result set. Make sure it's included in the select clause.`
46
+ )
47
+ }
48
+
49
+ const keyValue = row[columnName]
50
+ // Use the value directly if it's a string or number, otherwise JSON stringify
51
+ if (typeof keyValue === `string` || typeof keyValue === `number`) {
52
+ return keyValue
53
+ } else {
54
+ return JSON.stringify(keyValue)
55
+ }
56
+ }
57
+ })
58
+ )
59
+
60
+ return resultPipeline
61
+ }
@@ -0,0 +1,312 @@
1
+ import {
2
+ map,
3
+ orderBy,
4
+ orderByWithFractionalIndex,
5
+ orderByWithIndex,
6
+ topK,
7
+ topKWithFractionalIndex,
8
+ topKWithIndex,
9
+ } from "@electric-sql/d2ts"
10
+ import { evaluateOperandOnNestedRow } from "./extractors"
11
+ import { isOrderIndexFunctionCall } from "./utils"
12
+ import type { ConditionOperand, Query } from "./schema"
13
+ import type { IStreamBuilder } from "@electric-sql/d2ts"
14
+
15
+ export function processOrderBy(
16
+ resultPipeline: IStreamBuilder<
17
+ Record<string, unknown> | [string | number, Record<string, unknown>]
18
+ >,
19
+ query: Query,
20
+ mainTableAlias: string
21
+ ) {
22
+ // Check if any column in the SELECT clause is an ORDER_INDEX function call
23
+ let hasOrderIndexColumn = false
24
+ let orderIndexType: `numeric` | `fractional` = `numeric`
25
+ let orderIndexAlias = ``
26
+
27
+ // Scan the SELECT clause for ORDER_INDEX functions
28
+ for (const item of query.select) {
29
+ if (typeof item === `object`) {
30
+ for (const [alias, expr] of Object.entries(item)) {
31
+ if (typeof expr === `object` && isOrderIndexFunctionCall(expr)) {
32
+ hasOrderIndexColumn = true
33
+ orderIndexAlias = alias
34
+ orderIndexType = getOrderIndexType(expr)
35
+ break
36
+ }
37
+ }
38
+ }
39
+ if (hasOrderIndexColumn) break
40
+ }
41
+
42
+ // Normalize orderBy to an array of objects
43
+ const orderByItems: Array<{
44
+ operand: ConditionOperand
45
+ direction: `asc` | `desc`
46
+ }> = []
47
+
48
+ if (typeof query.orderBy === `string`) {
49
+ // Handle string format: '@column'
50
+ orderByItems.push({
51
+ operand: query.orderBy,
52
+ direction: `asc`,
53
+ })
54
+ } else if (Array.isArray(query.orderBy)) {
55
+ // Handle array format: ['@column1', { '@column2': 'desc' }]
56
+ for (const item of query.orderBy) {
57
+ if (typeof item === `string`) {
58
+ orderByItems.push({
59
+ operand: item,
60
+ direction: `asc`,
61
+ })
62
+ } else if (typeof item === `object`) {
63
+ for (const [column, direction] of Object.entries(item)) {
64
+ orderByItems.push({
65
+ operand: column,
66
+ direction: direction as `asc` | `desc`,
67
+ })
68
+ }
69
+ }
70
+ }
71
+ } else if (typeof query.orderBy === `object`) {
72
+ // Handle object format: { '@column': 'desc' }
73
+ for (const [column, direction] of Object.entries(query.orderBy)) {
74
+ orderByItems.push({
75
+ operand: column,
76
+ direction: direction as `asc` | `desc`,
77
+ })
78
+ }
79
+ }
80
+
81
+ // Create a value extractor function for the orderBy operator
82
+ const valueExtractor = (value: unknown) => {
83
+ const row = value as Record<string, unknown>
84
+
85
+ // Create a nested row structure for evaluateOperandOnNestedRow
86
+ const nestedRow: Record<string, unknown> = { [mainTableAlias]: row }
87
+
88
+ // For multiple orderBy columns, create a composite key
89
+ if (orderByItems.length > 1) {
90
+ return orderByItems.map((item) => {
91
+ const val = evaluateOperandOnNestedRow(
92
+ nestedRow,
93
+ item.operand,
94
+ mainTableAlias
95
+ )
96
+
97
+ // Reverse the value for 'desc' ordering
98
+ return item.direction === `desc` && typeof val === `number`
99
+ ? -val
100
+ : item.direction === `desc` && typeof val === `string`
101
+ ? String.fromCharCode(
102
+ ...[...val].map((c) => 0xffff - c.charCodeAt(0))
103
+ )
104
+ : val
105
+ })
106
+ } else if (orderByItems.length === 1) {
107
+ // For a single orderBy column, use the value directly
108
+ const item = orderByItems[0]
109
+ const val = evaluateOperandOnNestedRow(
110
+ nestedRow,
111
+ item!.operand,
112
+ mainTableAlias
113
+ )
114
+
115
+ // Reverse the value for 'desc' ordering
116
+ return item!.direction === `desc` && typeof val === `number`
117
+ ? -val
118
+ : item!.direction === `desc` && typeof val === `string`
119
+ ? String.fromCharCode(
120
+ ...[...val].map((c) => 0xffff - c.charCodeAt(0))
121
+ )
122
+ : val
123
+ }
124
+
125
+ // Default case - no ordering
126
+ return null
127
+ }
128
+
129
+ const comparator = (a: unknown, b: unknown): number => {
130
+ // if a and b are both numbers compare them directly
131
+ if (typeof a === `number` && typeof b === `number`) {
132
+ return a - b
133
+ }
134
+ // if a and b are both strings, compare them lexicographically
135
+ if (typeof a === `string` && typeof b === `string`) {
136
+ return a.localeCompare(b)
137
+ }
138
+ // if a and b are both booleans, compare them
139
+ if (typeof a === `boolean` && typeof b === `boolean`) {
140
+ return a === b ? 0 : a ? 1 : -1
141
+ }
142
+ // if a and b are both dates, compare them
143
+ if (a instanceof Date && b instanceof Date) {
144
+ return a.getTime() - b.getTime()
145
+ }
146
+ // if a and b are both null, return 0
147
+ if (a === null || b === null) {
148
+ return 0
149
+ }
150
+
151
+ // if a and b are both arrays, compare them element by element
152
+ if (Array.isArray(a) && Array.isArray(b)) {
153
+ for (let i = 0; i < Math.min(a.length, b.length); i++) {
154
+ // Get the values from the array
155
+ const aVal = a[i]
156
+ const bVal = b[i]
157
+
158
+ // Compare the values
159
+ let result: number
160
+
161
+ if (typeof aVal === `boolean` && typeof bVal === `boolean`) {
162
+ // Special handling for booleans - false comes before true
163
+ result = aVal === bVal ? 0 : aVal ? 1 : -1
164
+ } else if (typeof aVal === `number` && typeof bVal === `number`) {
165
+ // Numeric comparison
166
+ result = aVal - bVal
167
+ } else if (typeof aVal === `string` && typeof bVal === `string`) {
168
+ // String comparison
169
+ result = aVal.localeCompare(bVal)
170
+ } else {
171
+ // Default comparison using the general comparator
172
+ result = comparator(aVal, bVal)
173
+ }
174
+
175
+ if (result !== 0) {
176
+ return result
177
+ }
178
+ }
179
+ // All elements are equal up to the minimum length
180
+ return a.length - b.length
181
+ }
182
+ // if a and b are both null/undefined, return 0
183
+ if (a == null && b == null) {
184
+ return 0
185
+ }
186
+ // Fallback to string comparison for all other cases
187
+ return (a as any).toString().localeCompare((b as any).toString())
188
+ }
189
+
190
+ let topKComparator: (a: unknown, b: unknown) => number
191
+ if (!query.keyBy) {
192
+ topKComparator = (a, b) => {
193
+ const aValue = valueExtractor(a)
194
+ const bValue = valueExtractor(b)
195
+ return comparator(aValue, bValue)
196
+ }
197
+ }
198
+
199
+ // Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested
200
+ if (hasOrderIndexColumn) {
201
+ if (orderIndexType === `numeric`) {
202
+ if (query.keyBy) {
203
+ // Use orderByWithIndex for numeric indices
204
+ resultPipeline = resultPipeline.pipe(
205
+ orderByWithIndex(valueExtractor, {
206
+ limit: query.limit,
207
+ offset: query.offset,
208
+ comparator,
209
+ }),
210
+ map(([key, [value, index]]) => {
211
+ // Add the index to the result
212
+ const result = {
213
+ ...(value as Record<string, unknown>),
214
+ [orderIndexAlias]: index,
215
+ }
216
+ return [key, result]
217
+ })
218
+ )
219
+ } else {
220
+ // Use topKWithIndex for numeric indices
221
+ resultPipeline = resultPipeline.pipe(
222
+ map((value) => [null, value]),
223
+ topKWithIndex(topKComparator!, {
224
+ limit: query.limit,
225
+ offset: query.offset,
226
+ }),
227
+ map(([_, [value, index]]) => {
228
+ // Add the index to the result
229
+ return {
230
+ ...(value as Record<string, unknown>),
231
+ [orderIndexAlias]: index,
232
+ }
233
+ })
234
+ )
235
+ }
236
+ } else {
237
+ if (query.keyBy) {
238
+ // Use orderByWithFractionalIndex for fractional indices
239
+ resultPipeline = resultPipeline.pipe(
240
+ orderByWithFractionalIndex(valueExtractor, {
241
+ limit: query.limit,
242
+ offset: query.offset,
243
+ comparator,
244
+ }),
245
+ map(([key, [value, index]]) => {
246
+ // Add the index to the result
247
+ const result = {
248
+ ...(value as Record<string, unknown>),
249
+ [orderIndexAlias]: index,
250
+ }
251
+ return [key, result]
252
+ })
253
+ )
254
+ } else {
255
+ // Use topKWithFractionalIndex for fractional indices
256
+ resultPipeline = resultPipeline.pipe(
257
+ map((value) => [null, value]),
258
+ topKWithFractionalIndex(topKComparator!, {
259
+ limit: query.limit,
260
+ offset: query.offset,
261
+ }),
262
+ map(([_, [value, index]]) => {
263
+ // Add the index to the result
264
+ return {
265
+ ...(value as Record<string, unknown>),
266
+ [orderIndexAlias]: index,
267
+ }
268
+ })
269
+ )
270
+ }
271
+ }
272
+ } else {
273
+ if (query.keyBy) {
274
+ // Use regular orderBy if no index column is requested and but a keyBy is requested
275
+ resultPipeline = resultPipeline.pipe(
276
+ orderBy(valueExtractor, {
277
+ limit: query.limit,
278
+ offset: query.offset,
279
+ comparator,
280
+ })
281
+ )
282
+ } else {
283
+ // Use topK if no index column is requested and no keyBy is requested
284
+ resultPipeline = resultPipeline.pipe(
285
+ map((value) => [null, value]),
286
+ topK(topKComparator!, {
287
+ limit: query.limit,
288
+ offset: query.offset,
289
+ }),
290
+ map(([_, value]) => value as Record<string, unknown>)
291
+ )
292
+ }
293
+ }
294
+
295
+ return resultPipeline
296
+ }
297
+
298
+ // Helper function to extract the ORDER_INDEX type from a function call
299
+ function getOrderIndexType(obj: any): `numeric` | `fractional` {
300
+ if (!isOrderIndexFunctionCall(obj)) {
301
+ throw new Error(`Not an ORDER_INDEX function call`)
302
+ }
303
+
304
+ const arg = obj[`ORDER_INDEX`]
305
+ if (arg === `numeric` || arg === true || arg === `default`) {
306
+ return `numeric`
307
+ } else if (arg === `fractional`) {
308
+ return `fractional`
309
+ } else {
310
+ throw new Error(`Invalid ORDER_INDEX type: ` + arg)
311
+ }
312
+ }
@@ -0,0 +1,152 @@
1
+ import { filter, map } from "@electric-sql/d2ts"
2
+ import { evaluateConditionOnNestedRow } from "./evaluators.js"
3
+ import { processJoinClause } from "./joins.js"
4
+ import { processGroupBy } from "./group-by.js"
5
+ import { processOrderBy } from "./order-by.js"
6
+ import { processKeyBy } from "./key-by.js"
7
+ import { processSelect } from "./select.js"
8
+ import type { Condition, Query } from "./schema.js"
9
+ import type { IStreamBuilder } from "@electric-sql/d2ts"
10
+
11
+ /**
12
+ * Compiles a query into a D2 pipeline
13
+ * @param query The query to compile
14
+ * @param inputs Mapping of table names to input streams
15
+ * @returns A stream builder representing the compiled query
16
+ */
17
+ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
18
+ query: Query,
19
+ inputs: Record<string, IStreamBuilder<Record<string, unknown>>>
20
+ ): T {
21
+ // Create a copy of the inputs map to avoid modifying the original
22
+ const allInputs = { ...inputs }
23
+
24
+ // Process WITH queries if they exist
25
+ if (query.with && query.with.length > 0) {
26
+ // Process each WITH query in order
27
+ for (const withQuery of query.with) {
28
+ // Ensure the WITH query has an alias
29
+ if (!withQuery.as) {
30
+ throw new Error(`WITH query must have an "as" property`)
31
+ }
32
+
33
+ // Ensure the WITH query is not keyed
34
+ if ((withQuery as Query).keyBy !== undefined) {
35
+ throw new Error(`WITH query cannot have a "keyBy" property`)
36
+ }
37
+
38
+ // Check if this CTE name already exists in the inputs
39
+ if (allInputs[withQuery.as]) {
40
+ throw new Error(`CTE with name "${withQuery.as}" already exists`)
41
+ }
42
+
43
+ // Create a new query without the 'with' property to avoid circular references
44
+ const withQueryWithoutWith = { ...withQuery, with: undefined }
45
+
46
+ // Compile the WITH query using the current set of inputs
47
+ // (which includes previously compiled WITH queries)
48
+ const compiledWithQuery = compileQueryPipeline(
49
+ withQueryWithoutWith,
50
+ allInputs
51
+ )
52
+
53
+ // Add the compiled query to the inputs map using its alias
54
+ allInputs[withQuery.as] = compiledWithQuery as IStreamBuilder<
55
+ Record<string, unknown>
56
+ >
57
+ }
58
+ }
59
+
60
+ // Create a map of table aliases to inputs
61
+ const tables: Record<string, IStreamBuilder<Record<string, unknown>>> = {}
62
+
63
+ // The main table is the one in the FROM clause
64
+ const mainTableAlias = query.as || query.from
65
+
66
+ // Get the main input from the inputs map (now including CTEs)
67
+ const input = allInputs[query.from]
68
+ if (!input) {
69
+ throw new Error(`Input for table "${query.from}" not found in inputs map`)
70
+ }
71
+
72
+ tables[mainTableAlias] = input
73
+
74
+ // Prepare the initial pipeline with the main table wrapped in its alias
75
+ let pipeline = input.pipe(
76
+ map((row: unknown) => {
77
+ // Initialize the record with a nested structure
78
+ return { [mainTableAlias]: row } as Record<string, unknown>
79
+ })
80
+ )
81
+
82
+ // Process JOIN clauses if they exist
83
+ if (query.join) {
84
+ pipeline = processJoinClause(
85
+ pipeline,
86
+ query,
87
+ tables,
88
+ mainTableAlias,
89
+ allInputs
90
+ )
91
+ }
92
+
93
+ // Process the WHERE clause if it exists
94
+ if (query.where) {
95
+ pipeline = pipeline.pipe(
96
+ filter((nestedRow) => {
97
+ const result = evaluateConditionOnNestedRow(
98
+ nestedRow,
99
+ query.where as Condition,
100
+ mainTableAlias
101
+ )
102
+ return result
103
+ })
104
+ )
105
+ }
106
+
107
+ // Process the GROUP BY clause if it exists
108
+ if (query.groupBy) {
109
+ pipeline = processGroupBy(pipeline, query, mainTableAlias)
110
+ }
111
+
112
+ // Process the HAVING clause if it exists
113
+ // This works similarly to WHERE but is applied after any aggregations
114
+ if (query.having) {
115
+ pipeline = pipeline.pipe(
116
+ filter((row) => {
117
+ // For HAVING, we're working with the flattened row that contains both
118
+ // the group by keys and the aggregate results directly
119
+ const result = evaluateConditionOnNestedRow(
120
+ { [mainTableAlias]: row, ...row } as Record<string, unknown>,
121
+ query.having as Condition,
122
+ mainTableAlias
123
+ )
124
+ return result
125
+ })
126
+ )
127
+ }
128
+
129
+ // Process the SELECT clause - this is where we flatten the structure
130
+ pipeline = processSelect(pipeline, query, mainTableAlias, allInputs)
131
+
132
+ let resultPipeline: IStreamBuilder<
133
+ Record<string, unknown> | [string | number, Record<string, unknown>]
134
+ > = pipeline
135
+
136
+ // Process keyBy parameter if it exists
137
+ if (query.keyBy) {
138
+ resultPipeline = processKeyBy(resultPipeline, query)
139
+ }
140
+
141
+ // Process orderBy parameter if it exists
142
+ if (query.orderBy) {
143
+ resultPipeline = processOrderBy(resultPipeline, query, mainTableAlias)
144
+ } else if (query.limit !== undefined || query.offset !== undefined) {
145
+ // If there's a limit or offset without orderBy, throw an error
146
+ throw new Error(
147
+ `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`
148
+ )
149
+ }
150
+
151
+ return resultPipeline as T
152
+ }