@tanstack/db 0.0.13 → 0.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/dist/cjs/collection.cjs +117 -104
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +19 -22
  4. package/dist/cjs/index.cjs +35 -13
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/index.d.cts +0 -1
  7. package/dist/cjs/query/builder/functions.cjs +107 -0
  8. package/dist/cjs/query/builder/functions.cjs.map +1 -0
  9. package/dist/cjs/query/builder/functions.d.cts +38 -0
  10. package/dist/cjs/query/builder/index.cjs +499 -0
  11. package/dist/cjs/query/builder/index.cjs.map +1 -0
  12. package/dist/cjs/query/builder/index.d.cts +324 -0
  13. package/dist/cjs/query/builder/ref-proxy.cjs +96 -0
  14. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
  15. package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
  16. package/dist/cjs/query/builder/types.d.cts +80 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs +261 -0
  18. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
  19. package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
  20. package/dist/cjs/query/compiler/group-by.cjs +271 -0
  21. package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
  22. package/dist/cjs/query/compiler/group-by.d.cts +7 -0
  23. package/dist/cjs/query/compiler/index.cjs +181 -0
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -0
  25. package/dist/cjs/query/compiler/index.d.cts +15 -0
  26. package/dist/cjs/query/compiler/joins.cjs +116 -0
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -0
  28. package/dist/cjs/query/compiler/joins.d.cts +11 -0
  29. package/dist/cjs/query/compiler/order-by.cjs +89 -0
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
  31. package/dist/cjs/query/compiler/order-by.d.cts +9 -0
  32. package/dist/cjs/query/compiler/select.cjs +57 -0
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -0
  34. package/dist/cjs/query/compiler/select.d.cts +15 -0
  35. package/dist/cjs/query/index.d.cts +6 -5
  36. package/dist/cjs/query/ir.cjs +57 -0
  37. package/dist/cjs/query/ir.cjs.map +1 -0
  38. package/dist/cjs/query/ir.d.cts +81 -0
  39. package/dist/cjs/query/live-query-collection.cjs +224 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -0
  41. package/dist/cjs/query/live-query-collection.d.cts +124 -0
  42. package/dist/cjs/transactions.cjs +20 -13
  43. package/dist/cjs/transactions.cjs.map +1 -1
  44. package/dist/cjs/transactions.d.cts +13 -4
  45. package/dist/cjs/types.d.cts +14 -1
  46. package/dist/esm/collection.d.ts +19 -22
  47. package/dist/esm/collection.js +118 -105
  48. package/dist/esm/collection.js.map +1 -1
  49. package/dist/esm/index.d.ts +0 -1
  50. package/dist/esm/index.js +34 -12
  51. package/dist/esm/index.js.map +1 -1
  52. package/dist/esm/query/builder/functions.d.ts +38 -0
  53. package/dist/esm/query/builder/functions.js +107 -0
  54. package/dist/esm/query/builder/functions.js.map +1 -0
  55. package/dist/esm/query/builder/index.d.ts +324 -0
  56. package/dist/esm/query/builder/index.js +499 -0
  57. package/dist/esm/query/builder/index.js.map +1 -0
  58. package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
  59. package/dist/esm/query/builder/ref-proxy.js +96 -0
  60. package/dist/esm/query/builder/ref-proxy.js.map +1 -0
  61. package/dist/esm/query/builder/types.d.ts +80 -0
  62. package/dist/esm/query/compiler/evaluators.d.ts +11 -0
  63. package/dist/esm/query/compiler/evaluators.js +261 -0
  64. package/dist/esm/query/compiler/evaluators.js.map +1 -0
  65. package/dist/esm/query/compiler/group-by.d.ts +7 -0
  66. package/dist/esm/query/compiler/group-by.js +271 -0
  67. package/dist/esm/query/compiler/group-by.js.map +1 -0
  68. package/dist/esm/query/compiler/index.d.ts +15 -0
  69. package/dist/esm/query/compiler/index.js +181 -0
  70. package/dist/esm/query/compiler/index.js.map +1 -0
  71. package/dist/esm/query/compiler/joins.d.ts +11 -0
  72. package/dist/esm/query/compiler/joins.js +116 -0
  73. package/dist/esm/query/compiler/joins.js.map +1 -0
  74. package/dist/esm/query/compiler/order-by.d.ts +9 -0
  75. package/dist/esm/query/compiler/order-by.js +89 -0
  76. package/dist/esm/query/compiler/order-by.js.map +1 -0
  77. package/dist/esm/query/compiler/select.d.ts +15 -0
  78. package/dist/esm/query/compiler/select.js +57 -0
  79. package/dist/esm/query/compiler/select.js.map +1 -0
  80. package/dist/esm/query/index.d.ts +6 -5
  81. package/dist/esm/query/ir.d.ts +81 -0
  82. package/dist/esm/query/ir.js +57 -0
  83. package/dist/esm/query/ir.js.map +1 -0
  84. package/dist/esm/query/live-query-collection.d.ts +124 -0
  85. package/dist/esm/query/live-query-collection.js +224 -0
  86. package/dist/esm/query/live-query-collection.js.map +1 -0
  87. package/dist/esm/transactions.d.ts +13 -4
  88. package/dist/esm/transactions.js +20 -13
  89. package/dist/esm/transactions.js.map +1 -1
  90. package/dist/esm/types.d.ts +14 -1
  91. package/package.json +3 -4
  92. package/src/collection.ts +152 -129
  93. package/src/index.ts +0 -1
  94. package/src/query/builder/functions.ts +267 -0
  95. package/src/query/builder/index.ts +648 -0
  96. package/src/query/builder/ref-proxy.ts +156 -0
  97. package/src/query/builder/types.ts +278 -0
  98. package/src/query/compiler/evaluators.ts +315 -0
  99. package/src/query/compiler/group-by.ts +428 -0
  100. package/src/query/compiler/index.ts +276 -0
  101. package/src/query/compiler/joins.ts +228 -0
  102. package/src/query/compiler/order-by.ts +139 -0
  103. package/src/query/compiler/select.ts +173 -0
  104. package/src/query/index.ts +64 -5
  105. package/src/query/ir.ts +128 -0
  106. package/src/query/live-query-collection.ts +509 -0
  107. package/src/transactions.ts +34 -19
  108. package/src/types.ts +16 -1
  109. package/dist/cjs/query/compiled-query.cjs +0 -160
  110. package/dist/cjs/query/compiled-query.cjs.map +0 -1
  111. package/dist/cjs/query/compiled-query.d.cts +0 -20
  112. package/dist/cjs/query/evaluators.cjs +0 -161
  113. package/dist/cjs/query/evaluators.cjs.map +0 -1
  114. package/dist/cjs/query/evaluators.d.cts +0 -14
  115. package/dist/cjs/query/extractors.cjs +0 -122
  116. package/dist/cjs/query/extractors.cjs.map +0 -1
  117. package/dist/cjs/query/extractors.d.cts +0 -22
  118. package/dist/cjs/query/functions.cjs +0 -152
  119. package/dist/cjs/query/functions.cjs.map +0 -1
  120. package/dist/cjs/query/functions.d.cts +0 -21
  121. package/dist/cjs/query/group-by.cjs +0 -88
  122. package/dist/cjs/query/group-by.cjs.map +0 -1
  123. package/dist/cjs/query/group-by.d.cts +0 -40
  124. package/dist/cjs/query/joins.cjs +0 -141
  125. package/dist/cjs/query/joins.cjs.map +0 -1
  126. package/dist/cjs/query/joins.d.cts +0 -14
  127. package/dist/cjs/query/order-by.cjs +0 -185
  128. package/dist/cjs/query/order-by.cjs.map +0 -1
  129. package/dist/cjs/query/order-by.d.cts +0 -3
  130. package/dist/cjs/query/pipeline-compiler.cjs +0 -89
  131. package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
  132. package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
  133. package/dist/cjs/query/query-builder.cjs +0 -307
  134. package/dist/cjs/query/query-builder.cjs.map +0 -1
  135. package/dist/cjs/query/query-builder.d.cts +0 -225
  136. package/dist/cjs/query/schema.d.cts +0 -100
  137. package/dist/cjs/query/select.cjs +0 -130
  138. package/dist/cjs/query/select.cjs.map +0 -1
  139. package/dist/cjs/query/select.d.cts +0 -3
  140. package/dist/cjs/query/types.d.cts +0 -189
  141. package/dist/cjs/query/utils.cjs +0 -154
  142. package/dist/cjs/query/utils.cjs.map +0 -1
  143. package/dist/cjs/query/utils.d.cts +0 -37
  144. package/dist/cjs/utils.cjs +0 -17
  145. package/dist/cjs/utils.cjs.map +0 -1
  146. package/dist/cjs/utils.d.cts +0 -3
  147. package/dist/esm/query/compiled-query.d.ts +0 -20
  148. package/dist/esm/query/compiled-query.js +0 -160
  149. package/dist/esm/query/compiled-query.js.map +0 -1
  150. package/dist/esm/query/evaluators.d.ts +0 -14
  151. package/dist/esm/query/evaluators.js +0 -161
  152. package/dist/esm/query/evaluators.js.map +0 -1
  153. package/dist/esm/query/extractors.d.ts +0 -22
  154. package/dist/esm/query/extractors.js +0 -122
  155. package/dist/esm/query/extractors.js.map +0 -1
  156. package/dist/esm/query/functions.d.ts +0 -21
  157. package/dist/esm/query/functions.js +0 -152
  158. package/dist/esm/query/functions.js.map +0 -1
  159. package/dist/esm/query/group-by.d.ts +0 -40
  160. package/dist/esm/query/group-by.js +0 -88
  161. package/dist/esm/query/group-by.js.map +0 -1
  162. package/dist/esm/query/joins.d.ts +0 -14
  163. package/dist/esm/query/joins.js +0 -141
  164. package/dist/esm/query/joins.js.map +0 -1
  165. package/dist/esm/query/order-by.d.ts +0 -3
  166. package/dist/esm/query/order-by.js +0 -185
  167. package/dist/esm/query/order-by.js.map +0 -1
  168. package/dist/esm/query/pipeline-compiler.d.ts +0 -10
  169. package/dist/esm/query/pipeline-compiler.js +0 -89
  170. package/dist/esm/query/pipeline-compiler.js.map +0 -1
  171. package/dist/esm/query/query-builder.d.ts +0 -225
  172. package/dist/esm/query/query-builder.js +0 -307
  173. package/dist/esm/query/query-builder.js.map +0 -1
  174. package/dist/esm/query/schema.d.ts +0 -100
  175. package/dist/esm/query/select.d.ts +0 -3
  176. package/dist/esm/query/select.js +0 -130
  177. package/dist/esm/query/select.js.map +0 -1
  178. package/dist/esm/query/types.d.ts +0 -189
  179. package/dist/esm/query/utils.d.ts +0 -37
  180. package/dist/esm/query/utils.js +0 -154
  181. package/dist/esm/query/utils.js.map +0 -1
  182. package/dist/esm/utils.d.ts +0 -3
  183. package/dist/esm/utils.js +0 -17
  184. package/dist/esm/utils.js.map +0 -1
  185. package/src/query/compiled-query.ts +0 -234
  186. package/src/query/evaluators.ts +0 -250
  187. package/src/query/extractors.ts +0 -214
  188. package/src/query/functions.ts +0 -297
  189. package/src/query/group-by.ts +0 -139
  190. package/src/query/joins.ts +0 -260
  191. package/src/query/order-by.ts +0 -264
  192. package/src/query/pipeline-compiler.ts +0 -149
  193. package/src/query/query-builder.ts +0 -902
  194. package/src/query/schema.ts +0 -268
  195. package/src/query/select.ts +0 -208
  196. package/src/query/types.ts +0 -418
  197. package/src/query/utils.ts +0 -245
  198. package/src/utils.ts +0 -15
@@ -1,260 +0,0 @@
1
- import {
2
- consolidate,
3
- filter,
4
- join as joinOperator,
5
- map,
6
- } from "@electric-sql/d2mini"
7
- import { evaluateConditionOnNamespacedRow } from "./evaluators.js"
8
- import { extractJoinKey } from "./extractors.js"
9
- import type { Query } from "./index.js"
10
- import type { IStreamBuilder, JoinType } from "@electric-sql/d2mini"
11
- import type {
12
- KeyedStream,
13
- NamespacedAndKeyedStream,
14
- NamespacedRow,
15
- } from "../types.js"
16
-
17
- /**
18
- * Creates a processing pipeline for join clauses
19
- */
20
- export function processJoinClause(
21
- pipeline: NamespacedAndKeyedStream,
22
- query: Query,
23
- tables: Record<string, KeyedStream>,
24
- mainTableAlias: string,
25
- allInputs: Record<string, KeyedStream>
26
- ) {
27
- if (!query.join) return pipeline
28
- const input = allInputs[query.from]
29
-
30
- for (const joinClause of query.join) {
31
- // Create a stream for the joined table
32
- const joinedTableAlias = joinClause.as || joinClause.from
33
-
34
- // Get the right join type for the operator
35
- const joinType: JoinType =
36
- joinClause.type === `cross` ? `inner` : joinClause.type
37
-
38
- // The `in` is formatted as ['@mainKeyRef', '=', '@joinedKeyRef']
39
- // Destructure the main key reference and the joined key references
40
- const [mainKeyRef, , joinedKeyRefs] = joinClause.on
41
-
42
- // We need to prepare the main pipeline and the joined pipeline
43
- // to have the correct key format for joining
44
- const mainPipeline = pipeline.pipe(
45
- map(([currentKey, namespacedRow]) => {
46
- // Extract the key from the ON condition left side for the main table
47
- const mainRow = namespacedRow[mainTableAlias]!
48
-
49
- // Extract the join key from the main row
50
- const key = extractJoinKey(mainRow, mainKeyRef, mainTableAlias)
51
-
52
- // Return [key, namespacedRow] as a KeyValue type
53
- return [key, [currentKey, namespacedRow]] as [
54
- unknown,
55
- [string, typeof namespacedRow],
56
- ]
57
- })
58
- )
59
-
60
- // Get the joined table input from the inputs map
61
- let joinedTableInput: KeyedStream
62
-
63
- if (allInputs[joinClause.from]) {
64
- // Use the provided input if available
65
- joinedTableInput = allInputs[joinClause.from]!
66
- } else {
67
- // Create a new input if not provided
68
- joinedTableInput =
69
- input!.graph.newInput<[string, Record<string, unknown>]>()
70
- }
71
-
72
- tables[joinedTableAlias] = joinedTableInput
73
-
74
- // Create a pipeline for the joined table
75
- const joinedPipeline = joinedTableInput.pipe(
76
- map(([currentKey, row]) => {
77
- // Wrap the row in an object with the table alias as the key
78
- const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
79
-
80
- // Extract the key from the ON condition right side for the joined table
81
- const key = extractJoinKey(row, joinedKeyRefs, joinedTableAlias)
82
-
83
- // Return [key, namespacedRow] as a KeyValue type
84
- return [key, [currentKey, namespacedRow]] as [
85
- string,
86
- [string, typeof namespacedRow],
87
- ]
88
- })
89
- )
90
-
91
- // Apply join with appropriate typings based on join type
92
- switch (joinType) {
93
- case `inner`:
94
- pipeline = mainPipeline.pipe(
95
- joinOperator(joinedPipeline, `inner`),
96
- consolidate(),
97
- processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
98
- )
99
- break
100
- case `left`:
101
- pipeline = mainPipeline.pipe(
102
- joinOperator(joinedPipeline, `left`),
103
- consolidate(),
104
- processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
105
- )
106
- break
107
- case `right`:
108
- pipeline = mainPipeline.pipe(
109
- joinOperator(joinedPipeline, `right`),
110
- consolidate(),
111
- processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
112
- )
113
- break
114
- case `full`:
115
- pipeline = mainPipeline.pipe(
116
- joinOperator(joinedPipeline, `full`),
117
- consolidate(),
118
- processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
119
- )
120
- break
121
- default:
122
- pipeline = mainPipeline.pipe(
123
- joinOperator(joinedPipeline, `inner`),
124
- consolidate(),
125
- processJoinResults(mainTableAlias, joinedTableAlias, joinClause)
126
- )
127
- }
128
- }
129
- return pipeline
130
- }
131
-
132
- /**
133
- * Creates a processing pipeline for join results
134
- */
135
- export function processJoinResults(
136
- mainTableAlias: string,
137
- joinedTableAlias: string,
138
- joinClause: { on: any; type: string }
139
- ) {
140
- return function (
141
- pipeline: IStreamBuilder<
142
- [
143
- key: string,
144
- [
145
- [string, NamespacedRow] | undefined,
146
- [string, NamespacedRow] | undefined,
147
- ],
148
- ]
149
- >
150
- ): NamespacedAndKeyedStream {
151
- return pipeline.pipe(
152
- // Process the join result and handle nulls in the same step
153
- map((result) => {
154
- const [_key, [main, joined]] = result
155
- const mainKey = main?.[0]
156
- const mainNamespacedRow = main?.[1]
157
- const joinedKey = joined?.[0]
158
- const joinedNamespacedRow = joined?.[1]
159
-
160
- // For inner joins, both sides should be non-null
161
- if (joinClause.type === `inner` || joinClause.type === `cross`) {
162
- if (!mainNamespacedRow || !joinedNamespacedRow) {
163
- return undefined // Will be filtered out
164
- }
165
- }
166
-
167
- // For left joins, the main row must be non-null
168
- if (joinClause.type === `left` && !mainNamespacedRow) {
169
- return undefined // Will be filtered out
170
- }
171
-
172
- // For right joins, the joined row must be non-null
173
- if (joinClause.type === `right` && !joinedNamespacedRow) {
174
- return undefined // Will be filtered out
175
- }
176
-
177
- // Merge the nested rows
178
- const mergedNamespacedRow: NamespacedRow = {}
179
-
180
- // Add main row data if it exists
181
- if (mainNamespacedRow) {
182
- Object.entries(mainNamespacedRow).forEach(
183
- ([tableAlias, tableData]) => {
184
- mergedNamespacedRow[tableAlias] = tableData
185
- }
186
- )
187
- }
188
-
189
- // If we have a joined row, add it to the merged result
190
- if (joinedNamespacedRow) {
191
- Object.entries(joinedNamespacedRow).forEach(
192
- ([tableAlias, tableData]) => {
193
- mergedNamespacedRow[tableAlias] = tableData
194
- }
195
- )
196
- } else if (joinClause.type === `left` || joinClause.type === `full`) {
197
- // For left or full joins, add the joined table with undefined data if missing
198
- // mergedNamespacedRow[joinedTableAlias] = undefined
199
- }
200
-
201
- // For right or full joins, add the main table with undefined data if missing
202
- if (
203
- !mainNamespacedRow &&
204
- (joinClause.type === `right` || joinClause.type === `full`)
205
- ) {
206
- // mergedNamespacedRow[mainTableAlias] = undefined
207
- }
208
-
209
- // New key
210
- const newKey = `[${mainKey},${joinedKey}]`
211
-
212
- return [newKey, mergedNamespacedRow] as [
213
- string,
214
- typeof mergedNamespacedRow,
215
- ]
216
- }),
217
- // Filter out undefined results
218
- filter((value) => value !== undefined),
219
- // Process the ON condition
220
- filter(([_key, namespacedRow]: [string, NamespacedRow]) => {
221
- // If there's no ON condition, or it's a cross join, always return true
222
- if (!joinClause.on || joinClause.type === `cross`) {
223
- return true
224
- }
225
-
226
- // For LEFT JOIN, if the right side is null, we should include the row
227
- if (
228
- joinClause.type === `left` &&
229
- namespacedRow[joinedTableAlias] === undefined
230
- ) {
231
- return true
232
- }
233
-
234
- // For RIGHT JOIN, if the left side is null, we should include the row
235
- if (
236
- joinClause.type === `right` &&
237
- namespacedRow[mainTableAlias] === undefined
238
- ) {
239
- return true
240
- }
241
-
242
- // For FULL JOIN, if either side is null, we should include the row
243
- if (
244
- joinClause.type === `full` &&
245
- (namespacedRow[mainTableAlias] === undefined ||
246
- namespacedRow[joinedTableAlias] === undefined)
247
- ) {
248
- return true
249
- }
250
-
251
- return evaluateConditionOnNamespacedRow(
252
- namespacedRow,
253
- joinClause.on,
254
- mainTableAlias,
255
- joinedTableAlias
256
- )
257
- })
258
- )
259
- }
260
- }
@@ -1,264 +0,0 @@
1
- import {
2
- map,
3
- orderBy,
4
- orderByWithFractionalIndex,
5
- orderByWithIndex,
6
- } from "@electric-sql/d2mini"
7
- import { evaluateOperandOnNamespacedRow } from "./extractors"
8
- import { isOrderIndexFunctionCall } from "./utils"
9
- import type { ConditionOperand, Query } from "./schema"
10
- import type {
11
- KeyedNamespacedRow,
12
- NamespacedAndKeyedStream,
13
- NamespacedRow,
14
- } from "../types"
15
-
16
- type OrderByItem = {
17
- operand: ConditionOperand
18
- direction: `asc` | `desc`
19
- }
20
-
21
- type OrderByItems = Array<OrderByItem>
22
-
23
- export function processOrderBy(
24
- resultPipeline: NamespacedAndKeyedStream,
25
- query: Query,
26
- mainTableAlias: string
27
- ) {
28
- // Check if any column in the SELECT clause is an ORDER_INDEX function call
29
- let hasOrderIndexColumn = false
30
- let orderIndexType: `numeric` | `fractional` = `numeric`
31
- let orderIndexAlias = ``
32
-
33
- // Scan the SELECT clause for ORDER_INDEX functions
34
- // TODO: Select is going to be optional in future - we will automatically add an
35
- // attribute for the index column
36
- for (const item of query.select!) {
37
- if (typeof item === `object`) {
38
- for (const [alias, expr] of Object.entries(item)) {
39
- if (typeof expr === `object` && isOrderIndexFunctionCall(expr)) {
40
- hasOrderIndexColumn = true
41
- orderIndexAlias = alias
42
- orderIndexType = getOrderIndexType(expr)
43
- break
44
- }
45
- }
46
- }
47
- if (hasOrderIndexColumn) break
48
- }
49
-
50
- // Normalize orderBy to an array of objects
51
- const orderByItems: OrderByItems = []
52
-
53
- if (typeof query.orderBy === `string`) {
54
- // Handle string format: '@column'
55
- orderByItems.push({
56
- operand: query.orderBy,
57
- direction: `asc`,
58
- })
59
- } else if (Array.isArray(query.orderBy)) {
60
- // Handle array format: ['@column1', { '@column2': 'desc' }]
61
- for (const item of query.orderBy) {
62
- if (typeof item === `string`) {
63
- orderByItems.push({
64
- operand: item,
65
- direction: `asc`,
66
- })
67
- } else if (typeof item === `object`) {
68
- for (const [column, direction] of Object.entries(item)) {
69
- orderByItems.push({
70
- operand: column,
71
- direction: direction as `asc` | `desc`,
72
- })
73
- }
74
- }
75
- }
76
- } else if (typeof query.orderBy === `object`) {
77
- // Handle object format: { '@column': 'desc' }
78
- for (const [column, direction] of Object.entries(query.orderBy)) {
79
- orderByItems.push({
80
- operand: column,
81
- direction: direction as `asc` | `desc`,
82
- })
83
- }
84
- }
85
-
86
- // Create a value extractor function for the orderBy operator
87
- // const valueExtractor = ([key, namespacedRow]: [
88
- const valueExtractor = (namespacedRow: NamespacedRow) => {
89
- // For multiple orderBy columns, create a composite key
90
- if (orderByItems.length > 1) {
91
- return orderByItems.map((item) =>
92
- evaluateOperandOnNamespacedRow(
93
- namespacedRow,
94
- item.operand,
95
- mainTableAlias
96
- )
97
- )
98
- } else if (orderByItems.length === 1) {
99
- // For a single orderBy column, use the value directly
100
- const item = orderByItems[0]
101
- const val = evaluateOperandOnNamespacedRow(
102
- namespacedRow,
103
- item!.operand,
104
- mainTableAlias
105
- )
106
- return val
107
- }
108
-
109
- // Default case - no ordering
110
- return null
111
- }
112
-
113
- const ascComparator = (a: any, b: any): number => {
114
- // if a and b are both strings, compare them based on locale
115
- if (typeof a === `string` && typeof b === `string`) {
116
- return a.localeCompare(b)
117
- }
118
-
119
- // if a and b are both arrays, compare them element by element
120
- if (Array.isArray(a) && Array.isArray(b)) {
121
- for (let i = 0; i < Math.min(a.length, b.length); i++) {
122
- // Compare the values
123
- const result = ascComparator(a[i], b[i])
124
-
125
- if (result !== 0) {
126
- return result
127
- }
128
- }
129
- // All elements are equal up to the minimum length
130
- return a.length - b.length
131
- }
132
-
133
- // If at least one of the values is an object then we don't really know how to meaningfully compare them
134
- // therefore we turn them into strings and compare those
135
- // There are 2 exceptions:
136
- // 1) if both objects are dates then we can compare them
137
- // 2) if either object is nullish then we can't call toString on it
138
- const bothObjects = typeof a === `object` && typeof b === `object`
139
- const bothDates = a instanceof Date && b instanceof Date
140
- const notNull = a !== null && b !== null
141
- if (bothObjects && !bothDates && notNull) {
142
- // Every object should support `toString`
143
- return a.toString().localeCompare(b.toString())
144
- }
145
-
146
- if (a < b) return -1
147
- if (a > b) return 1
148
- return 0
149
- }
150
-
151
- const descComparator = (a: unknown, b: unknown): number => {
152
- return ascComparator(b, a)
153
- }
154
-
155
- // Create a multi-property comparator that respects the order and direction of each property
156
- const makeComparator = (orderByProps: OrderByItems) => {
157
- return (a: unknown, b: unknown) => {
158
- // If we're comparing arrays (multiple properties), compare each property in order
159
- if (orderByProps.length > 1) {
160
- // `a` and `b` must be arrays since `orderByItems.length > 1`
161
- // hence the extracted values must be arrays
162
- const arrayA = a as Array<unknown>
163
- const arrayB = b as Array<unknown>
164
- for (let i = 0; i < orderByProps.length; i++) {
165
- const direction = orderByProps[i]!.direction
166
- const compareFn =
167
- direction === `desc` ? descComparator : ascComparator
168
- const result = compareFn(arrayA[i], arrayB[i])
169
- if (result !== 0) {
170
- return result
171
- }
172
- }
173
- // should normally always be 0 because
174
- // both values are extracted based on orderByItems
175
- return arrayA.length - arrayB.length
176
- }
177
-
178
- // Single property comparison
179
- if (orderByProps.length === 1) {
180
- const direction = orderByProps[0]!.direction
181
- return direction === `desc` ? descComparator(a, b) : ascComparator(a, b)
182
- }
183
-
184
- return ascComparator(a, b)
185
- }
186
- }
187
- const comparator = makeComparator(orderByItems)
188
-
189
- // Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested
190
- if (hasOrderIndexColumn) {
191
- if (orderIndexType === `numeric`) {
192
- // Use orderByWithIndex for numeric indices
193
- resultPipeline = resultPipeline.pipe(
194
- orderByWithIndex(valueExtractor, {
195
- limit: query.limit,
196
- offset: query.offset,
197
- comparator,
198
- }),
199
- map(([key, [value, index]]) => {
200
- // Add the index to the result
201
- // We add this to the main table alias for now
202
- // TODO: re are going to need to refactor the whole order by pipeline
203
- const result = {
204
- ...(value as Record<string, unknown>),
205
- [mainTableAlias]: {
206
- ...value[mainTableAlias],
207
- [orderIndexAlias]: index,
208
- },
209
- }
210
- return [key, result] as KeyedNamespacedRow
211
- })
212
- )
213
- } else {
214
- // Use orderByWithFractionalIndex for fractional indices
215
- resultPipeline = resultPipeline.pipe(
216
- orderByWithFractionalIndex(valueExtractor, {
217
- limit: query.limit,
218
- offset: query.offset,
219
- comparator,
220
- }),
221
- map(([key, [value, index]]) => {
222
- // Add the index to the result
223
- // We add this to the main table alias for now
224
- // TODO: re are going to need to refactor the whole order by pipeline
225
- const result = {
226
- ...(value as Record<string, unknown>),
227
- [mainTableAlias]: {
228
- ...value[mainTableAlias],
229
- [orderIndexAlias]: index,
230
- },
231
- }
232
- return [key, result] as KeyedNamespacedRow
233
- })
234
- )
235
- }
236
- } else {
237
- // Use regular orderBy if no index column is requested
238
- resultPipeline = resultPipeline.pipe(
239
- orderBy(valueExtractor, {
240
- limit: query.limit,
241
- offset: query.offset,
242
- comparator,
243
- })
244
- )
245
- }
246
-
247
- return resultPipeline
248
- }
249
-
250
- // Helper function to extract the ORDER_INDEX type from a function call
251
- function getOrderIndexType(obj: any): `numeric` | `fractional` {
252
- if (!isOrderIndexFunctionCall(obj)) {
253
- throw new Error(`Not an ORDER_INDEX function call`)
254
- }
255
-
256
- const arg = obj[`ORDER_INDEX`]
257
- if (arg === `numeric` || arg === true || arg === `default`) {
258
- return `numeric`
259
- } else if (arg === `fractional`) {
260
- return `fractional`
261
- } else {
262
- throw new Error(`Invalid ORDER_INDEX type: ` + arg)
263
- }
264
- }
@@ -1,149 +0,0 @@
1
- import { filter, map } from "@electric-sql/d2mini"
2
- import { evaluateWhereOnNamespacedRow } 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 { processSelect } from "./select.js"
7
- import type { Query } from "./schema.js"
8
- import type { IStreamBuilder } from "@electric-sql/d2mini"
9
- import type {
10
- InputRow,
11
- KeyedStream,
12
- NamespacedAndKeyedStream,
13
- } from "../types.js"
14
-
15
- /**
16
- * Compiles a query into a D2 pipeline
17
- * @param query The query to compile
18
- * @param inputs Mapping of table names to input streams
19
- * @returns A stream builder representing the compiled query
20
- */
21
- export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
22
- query: Query,
23
- inputs: Record<string, KeyedStream>
24
- ): T {
25
- // Create a copy of the inputs map to avoid modifying the original
26
- const allInputs = { ...inputs }
27
-
28
- // Process WITH queries if they exist
29
- if (query.with && query.with.length > 0) {
30
- // Process each WITH query in order
31
- for (const withQuery of query.with) {
32
- // Ensure the WITH query has an alias
33
- if (!withQuery.as) {
34
- throw new Error(`WITH query must have an "as" property`)
35
- }
36
-
37
- // Check if this CTE name already exists in the inputs
38
- if (allInputs[withQuery.as]) {
39
- throw new Error(`CTE with name "${withQuery.as}" already exists`)
40
- }
41
-
42
- // Create a new query without the 'with' property to avoid circular references
43
- const withQueryWithoutWith = { ...withQuery, with: undefined }
44
-
45
- // Compile the WITH query using the current set of inputs
46
- // (which includes previously compiled WITH queries)
47
- const compiledWithQuery = compileQueryPipeline(
48
- withQueryWithoutWith,
49
- allInputs
50
- )
51
-
52
- // Add the compiled query to the inputs map using its alias
53
- allInputs[withQuery.as] = compiledWithQuery as KeyedStream
54
- }
55
- }
56
-
57
- // Create a map of table aliases to inputs
58
- const tables: Record<string, KeyedStream> = {}
59
-
60
- // The main table is the one in the FROM clause
61
- const mainTableAlias = query.as || query.from
62
-
63
- // Get the main input from the inputs map (now including CTEs)
64
- const input = allInputs[query.from]
65
- if (!input) {
66
- throw new Error(`Input for table "${query.from}" not found in inputs map`)
67
- }
68
-
69
- tables[mainTableAlias] = input
70
-
71
- // Prepare the initial pipeline with the main table wrapped in its alias
72
- let pipeline: NamespacedAndKeyedStream = input.pipe(
73
- map(([key, row]) => {
74
- // Initialize the record with a nested structure
75
- const ret = [key, { [mainTableAlias]: row }] as [
76
- string,
77
- Record<string, typeof row>,
78
- ]
79
- return ret
80
- })
81
- )
82
-
83
- // Process JOIN clauses if they exist
84
- if (query.join) {
85
- pipeline = processJoinClause(
86
- pipeline,
87
- query,
88
- tables,
89
- mainTableAlias,
90
- allInputs
91
- )
92
- }
93
-
94
- // Process the WHERE clause if it exists
95
- if (query.where) {
96
- pipeline = pipeline.pipe(
97
- filter(([_key, row]) => {
98
- const result = evaluateWhereOnNamespacedRow(
99
- row,
100
- query.where!,
101
- mainTableAlias
102
- )
103
- return result
104
- })
105
- )
106
- }
107
-
108
- // Process the GROUP BY clause if it exists
109
- if (query.groupBy) {
110
- pipeline = processGroupBy(pipeline, query, mainTableAlias)
111
- }
112
-
113
- // Process the HAVING clause if it exists
114
- // This works similarly to WHERE but is applied after any aggregations
115
- if (query.having) {
116
- pipeline = pipeline.pipe(
117
- filter(([_key, row]) => {
118
- // For HAVING, we're working with the flattened row that contains both
119
- // the group by keys and the aggregate results directly
120
- const result = evaluateWhereOnNamespacedRow(
121
- row,
122
- query.having!,
123
- mainTableAlias
124
- )
125
- return result
126
- })
127
- )
128
- }
129
-
130
- // Process orderBy parameter if it exists
131
- if (query.orderBy) {
132
- pipeline = processOrderBy(pipeline, query, mainTableAlias)
133
- } else if (query.limit !== undefined || query.offset !== undefined) {
134
- // If there's a limit or offset without orderBy, throw an error
135
- throw new Error(
136
- `LIMIT and OFFSET require an ORDER BY clause to ensure deterministic results`
137
- )
138
- }
139
-
140
- // Process the SELECT clause - this is where we flatten the structure
141
- const resultPipeline: KeyedStream | NamespacedAndKeyedStream = query.select
142
- ? processSelect(pipeline, query, mainTableAlias, allInputs)
143
- : !query.join && !query.groupBy
144
- ? pipeline.pipe(
145
- map(([key, row]) => [key, row[mainTableAlias]] as InputRow)
146
- )
147
- : pipeline
148
- return resultPipeline as T
149
- }