@tanstack/db 0.0.4 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection.cjs +113 -94
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +38 -11
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/proxy.cjs +87 -248
- package/dist/cjs/proxy.cjs.map +1 -1
- package/dist/cjs/proxy.d.cts +5 -5
- package/dist/cjs/query/compiled-query.cjs +23 -14
- package/dist/cjs/query/compiled-query.cjs.map +1 -1
- package/dist/cjs/query/compiled-query.d.cts +3 -1
- package/dist/cjs/query/evaluators.cjs +20 -20
- package/dist/cjs/query/evaluators.cjs.map +1 -1
- package/dist/cjs/query/evaluators.d.cts +3 -2
- package/dist/cjs/query/extractors.cjs +20 -20
- package/dist/cjs/query/extractors.cjs.map +1 -1
- package/dist/cjs/query/extractors.d.cts +3 -3
- package/dist/cjs/query/group-by.cjs +12 -15
- package/dist/cjs/query/group-by.cjs.map +1 -1
- package/dist/cjs/query/group-by.d.cts +7 -7
- package/dist/cjs/query/joins.cjs +41 -55
- package/dist/cjs/query/joins.cjs.map +1 -1
- package/dist/cjs/query/joins.d.cts +3 -3
- package/dist/cjs/query/order-by.cjs +37 -84
- package/dist/cjs/query/order-by.cjs.map +1 -1
- package/dist/cjs/query/order-by.d.cts +2 -2
- package/dist/cjs/query/pipeline-compiler.cjs +13 -18
- package/dist/cjs/query/pipeline-compiler.cjs.map +1 -1
- package/dist/cjs/query/pipeline-compiler.d.cts +2 -1
- package/dist/cjs/query/query-builder.cjs +0 -12
- package/dist/cjs/query/query-builder.cjs.map +1 -1
- package/dist/cjs/query/query-builder.d.cts +4 -8
- package/dist/cjs/query/schema.d.cts +1 -6
- package/dist/cjs/query/select.cjs +35 -24
- package/dist/cjs/query/select.cjs.map +1 -1
- package/dist/cjs/query/select.d.cts +2 -2
- package/dist/cjs/query/types.d.cts +1 -0
- package/dist/cjs/transactions.cjs +17 -8
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/types.d.cts +41 -7
- package/dist/esm/collection.d.ts +38 -11
- package/dist/esm/collection.js +113 -94
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/proxy.d.ts +5 -5
- package/dist/esm/proxy.js +87 -248
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query/compiled-query.d.ts +3 -1
- package/dist/esm/query/compiled-query.js +23 -14
- package/dist/esm/query/compiled-query.js.map +1 -1
- package/dist/esm/query/evaluators.d.ts +3 -2
- package/dist/esm/query/evaluators.js +21 -21
- package/dist/esm/query/evaluators.js.map +1 -1
- package/dist/esm/query/extractors.d.ts +3 -3
- package/dist/esm/query/extractors.js +20 -20
- package/dist/esm/query/extractors.js.map +1 -1
- package/dist/esm/query/group-by.d.ts +7 -7
- package/dist/esm/query/group-by.js +14 -17
- package/dist/esm/query/group-by.js.map +1 -1
- package/dist/esm/query/joins.d.ts +3 -3
- package/dist/esm/query/joins.js +42 -56
- package/dist/esm/query/joins.js.map +1 -1
- package/dist/esm/query/order-by.d.ts +2 -2
- package/dist/esm/query/order-by.js +39 -86
- package/dist/esm/query/order-by.js.map +1 -1
- package/dist/esm/query/pipeline-compiler.d.ts +2 -1
- package/dist/esm/query/pipeline-compiler.js +14 -19
- package/dist/esm/query/pipeline-compiler.js.map +1 -1
- package/dist/esm/query/query-builder.d.ts +4 -8
- package/dist/esm/query/query-builder.js +0 -12
- package/dist/esm/query/query-builder.js.map +1 -1
- package/dist/esm/query/schema.d.ts +1 -6
- package/dist/esm/query/select.d.ts +2 -2
- package/dist/esm/query/select.js +36 -25
- package/dist/esm/query/select.js.map +1 -1
- package/dist/esm/query/types.d.ts +1 -0
- package/dist/esm/transactions.js +17 -8
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +41 -7
- package/package.json +2 -2
- package/src/collection.ts +174 -121
- package/src/proxy.ts +141 -358
- package/src/query/compiled-query.ts +30 -15
- package/src/query/evaluators.ts +22 -21
- package/src/query/extractors.ts +24 -21
- package/src/query/group-by.ts +24 -22
- package/src/query/joins.ts +88 -75
- package/src/query/order-by.ts +56 -106
- package/src/query/pipeline-compiler.ts +34 -37
- package/src/query/query-builder.ts +9 -23
- package/src/query/schema.ts +1 -10
- package/src/query/select.ts +44 -32
- package/src/query/types.ts +1 -0
- package/src/transactions.ts +22 -13
- package/src/types.ts +48 -7
- package/dist/cjs/query/key-by.cjs +0 -43
- package/dist/cjs/query/key-by.cjs.map +0 -1
- package/dist/cjs/query/key-by.d.cts +0 -3
- package/dist/esm/query/key-by.d.ts +0 -3
- package/dist/esm/query/key-by.js +0 -43
- package/dist/esm/query/key-by.js.map +0 -1
- package/src/query/key-by.ts +0 -61
package/src/query/joins.ts
CHANGED
|
@@ -4,20 +4,25 @@ import {
|
|
|
4
4
|
join as joinOperator,
|
|
5
5
|
map,
|
|
6
6
|
} from "@electric-sql/d2ts"
|
|
7
|
-
import {
|
|
7
|
+
import { evaluateConditionOnNamespacedRow } from "./evaluators.js"
|
|
8
8
|
import { extractJoinKey } from "./extractors.js"
|
|
9
9
|
import type { Query } from "./index.js"
|
|
10
10
|
import type { IStreamBuilder, JoinType } from "@electric-sql/d2ts"
|
|
11
|
+
import type {
|
|
12
|
+
KeyedStream,
|
|
13
|
+
NamespacedAndKeyedStream,
|
|
14
|
+
NamespacedRow,
|
|
15
|
+
} from "../types.js"
|
|
11
16
|
|
|
12
17
|
/**
|
|
13
18
|
* Creates a processing pipeline for join clauses
|
|
14
19
|
*/
|
|
15
20
|
export function processJoinClause(
|
|
16
|
-
pipeline:
|
|
21
|
+
pipeline: NamespacedAndKeyedStream,
|
|
17
22
|
query: Query,
|
|
18
|
-
tables: Record<string,
|
|
23
|
+
tables: Record<string, KeyedStream>,
|
|
19
24
|
mainTableAlias: string,
|
|
20
|
-
allInputs: Record<string,
|
|
25
|
+
allInputs: Record<string, KeyedStream>
|
|
21
26
|
) {
|
|
22
27
|
if (!query.join) return pipeline
|
|
23
28
|
const input = allInputs[query.from]
|
|
@@ -30,49 +35,56 @@ export function processJoinClause(
|
|
|
30
35
|
const joinType: JoinType =
|
|
31
36
|
joinClause.type === `cross` ? `inner` : joinClause.type
|
|
32
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
|
+
|
|
33
42
|
// We need to prepare the main pipeline and the joined pipeline
|
|
34
43
|
// to have the correct key format for joining
|
|
35
44
|
const mainPipeline = pipeline.pipe(
|
|
36
|
-
map((
|
|
45
|
+
map(([currentKey, namespacedRow]) => {
|
|
37
46
|
// Extract the key from the ON condition left side for the main table
|
|
38
|
-
const mainRow =
|
|
47
|
+
const mainRow = namespacedRow[mainTableAlias]!
|
|
39
48
|
|
|
40
49
|
// Extract the join key from the main row
|
|
41
|
-
const
|
|
42
|
-
mainRow,
|
|
43
|
-
joinClause.on[0],
|
|
44
|
-
mainTableAlias
|
|
45
|
-
)
|
|
50
|
+
const key = extractJoinKey(mainRow, mainKeyRef, mainTableAlias)
|
|
46
51
|
|
|
47
|
-
// Return [key,
|
|
48
|
-
return [
|
|
52
|
+
// Return [key, namespacedRow] as a KeyValue type
|
|
53
|
+
return [key, [currentKey, namespacedRow]] as [
|
|
54
|
+
unknown,
|
|
55
|
+
[string, typeof namespacedRow],
|
|
56
|
+
]
|
|
49
57
|
})
|
|
50
58
|
)
|
|
51
59
|
|
|
52
60
|
// Get the joined table input from the inputs map
|
|
53
|
-
let joinedTableInput:
|
|
61
|
+
let joinedTableInput: KeyedStream
|
|
54
62
|
|
|
55
63
|
if (allInputs[joinClause.from]) {
|
|
56
64
|
// Use the provided input if available
|
|
57
65
|
joinedTableInput = allInputs[joinClause.from]!
|
|
58
66
|
} else {
|
|
59
67
|
// Create a new input if not provided
|
|
60
|
-
joinedTableInput =
|
|
68
|
+
joinedTableInput =
|
|
69
|
+
input!.graph.newInput<[string, Record<string, unknown>]>()
|
|
61
70
|
}
|
|
62
71
|
|
|
63
72
|
tables[joinedTableAlias] = joinedTableInput
|
|
64
73
|
|
|
65
74
|
// Create a pipeline for the joined table
|
|
66
75
|
const joinedPipeline = joinedTableInput.pipe(
|
|
67
|
-
map((
|
|
76
|
+
map(([currentKey, row]) => {
|
|
68
77
|
// Wrap the row in an object with the table alias as the key
|
|
69
|
-
const
|
|
78
|
+
const namespacedRow: NamespacedRow = { [joinedTableAlias]: row }
|
|
70
79
|
|
|
71
80
|
// Extract the key from the ON condition right side for the joined table
|
|
72
|
-
const
|
|
81
|
+
const key = extractJoinKey(row, joinedKeyRefs, joinedTableAlias)
|
|
73
82
|
|
|
74
|
-
// Return [key,
|
|
75
|
-
return [
|
|
83
|
+
// Return [key, namespacedRow] as a KeyValue type
|
|
84
|
+
return [key, [currentKey, namespacedRow]] as [
|
|
85
|
+
string,
|
|
86
|
+
[string, typeof namespacedRow],
|
|
87
|
+
]
|
|
76
88
|
})
|
|
77
89
|
)
|
|
78
90
|
|
|
@@ -123,76 +135,89 @@ export function processJoinClause(
|
|
|
123
135
|
export function processJoinResults(
|
|
124
136
|
mainTableAlias: string,
|
|
125
137
|
joinedTableAlias: string,
|
|
126
|
-
joinClause: { on: any;
|
|
138
|
+
joinClause: { on: any; type: string }
|
|
127
139
|
) {
|
|
128
140
|
return function (
|
|
129
|
-
pipeline: IStreamBuilder<
|
|
130
|
-
|
|
141
|
+
pipeline: IStreamBuilder<
|
|
142
|
+
[
|
|
143
|
+
key: string,
|
|
144
|
+
[
|
|
145
|
+
[string, NamespacedRow] | undefined,
|
|
146
|
+
[string, NamespacedRow] | undefined,
|
|
147
|
+
],
|
|
148
|
+
]
|
|
149
|
+
>
|
|
150
|
+
): NamespacedAndKeyedStream {
|
|
131
151
|
return pipeline.pipe(
|
|
132
152
|
// Process the join result and handle nulls in the same step
|
|
133
|
-
map((result
|
|
134
|
-
const [_key, [
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
],
|
|
140
|
-
]
|
|
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]
|
|
141
159
|
|
|
142
160
|
// For inner joins, both sides should be non-null
|
|
143
161
|
if (joinClause.type === `inner` || joinClause.type === `cross`) {
|
|
144
|
-
if (!
|
|
162
|
+
if (!mainNamespacedRow || !joinedNamespacedRow) {
|
|
145
163
|
return undefined // Will be filtered out
|
|
146
164
|
}
|
|
147
165
|
}
|
|
148
166
|
|
|
149
167
|
// For left joins, the main row must be non-null
|
|
150
|
-
if (joinClause.type === `left` && !
|
|
168
|
+
if (joinClause.type === `left` && !mainNamespacedRow) {
|
|
151
169
|
return undefined // Will be filtered out
|
|
152
170
|
}
|
|
153
171
|
|
|
154
172
|
// For right joins, the joined row must be non-null
|
|
155
|
-
if (joinClause.type === `right` && !
|
|
173
|
+
if (joinClause.type === `right` && !joinedNamespacedRow) {
|
|
156
174
|
return undefined // Will be filtered out
|
|
157
175
|
}
|
|
158
176
|
|
|
159
177
|
// Merge the nested rows
|
|
160
|
-
const
|
|
178
|
+
const mergedNamespacedRow: NamespacedRow = {}
|
|
161
179
|
|
|
162
180
|
// Add main row data if it exists
|
|
163
|
-
if (
|
|
164
|
-
Object.entries(
|
|
165
|
-
|
|
166
|
-
|
|
181
|
+
if (mainNamespacedRow) {
|
|
182
|
+
Object.entries(mainNamespacedRow).forEach(
|
|
183
|
+
([tableAlias, tableData]) => {
|
|
184
|
+
mergedNamespacedRow[tableAlias] = tableData
|
|
185
|
+
}
|
|
186
|
+
)
|
|
167
187
|
}
|
|
168
188
|
|
|
169
189
|
// If we have a joined row, add it to the merged result
|
|
170
|
-
if (
|
|
171
|
-
Object.entries(
|
|
172
|
-
|
|
173
|
-
|
|
190
|
+
if (joinedNamespacedRow) {
|
|
191
|
+
Object.entries(joinedNamespacedRow).forEach(
|
|
192
|
+
([tableAlias, tableData]) => {
|
|
193
|
+
mergedNamespacedRow[tableAlias] = tableData
|
|
194
|
+
}
|
|
195
|
+
)
|
|
174
196
|
} else if (joinClause.type === `left` || joinClause.type === `full`) {
|
|
175
|
-
// For left or full joins, add the joined table with
|
|
176
|
-
|
|
197
|
+
// For left or full joins, add the joined table with undefined data if missing
|
|
198
|
+
// mergedNamespacedRow[joinedTableAlias] = undefined
|
|
177
199
|
}
|
|
178
200
|
|
|
179
|
-
// For right or full joins, add the main table with
|
|
201
|
+
// For right or full joins, add the main table with undefined data if missing
|
|
180
202
|
if (
|
|
181
|
-
!
|
|
203
|
+
!mainNamespacedRow &&
|
|
182
204
|
(joinClause.type === `right` || joinClause.type === `full`)
|
|
183
205
|
) {
|
|
184
|
-
|
|
206
|
+
// mergedNamespacedRow[mainTableAlias] = undefined
|
|
185
207
|
}
|
|
186
208
|
|
|
187
|
-
|
|
209
|
+
// New key
|
|
210
|
+
const newKey = `[${mainKey},${joinedKey}]`
|
|
211
|
+
|
|
212
|
+
return [newKey, mergedNamespacedRow] as [
|
|
213
|
+
string,
|
|
214
|
+
typeof mergedNamespacedRow,
|
|
215
|
+
]
|
|
188
216
|
}),
|
|
189
217
|
// Filter out undefined results
|
|
190
|
-
filter(
|
|
191
|
-
(value: unknown): value is Record<string, unknown> =>
|
|
192
|
-
value !== undefined
|
|
193
|
-
),
|
|
218
|
+
filter((value) => value !== undefined),
|
|
194
219
|
// Process the ON condition
|
|
195
|
-
filter((
|
|
220
|
+
filter(([_key, namespacedRow]: [string, NamespacedRow]) => {
|
|
196
221
|
// If there's no ON condition, or it's a cross join, always return true
|
|
197
222
|
if (!joinClause.on || joinClause.type === `cross`) {
|
|
198
223
|
return true
|
|
@@ -201,46 +226,34 @@ export function processJoinResults(
|
|
|
201
226
|
// For LEFT JOIN, if the right side is null, we should include the row
|
|
202
227
|
if (
|
|
203
228
|
joinClause.type === `left` &&
|
|
204
|
-
|
|
229
|
+
namespacedRow[joinedTableAlias] === undefined
|
|
205
230
|
) {
|
|
206
231
|
return true
|
|
207
232
|
}
|
|
208
233
|
|
|
209
234
|
// For RIGHT JOIN, if the left side is null, we should include the row
|
|
210
|
-
if (
|
|
235
|
+
if (
|
|
236
|
+
joinClause.type === `right` &&
|
|
237
|
+
namespacedRow[mainTableAlias] === undefined
|
|
238
|
+
) {
|
|
211
239
|
return true
|
|
212
240
|
}
|
|
213
241
|
|
|
214
242
|
// For FULL JOIN, if either side is null, we should include the row
|
|
215
243
|
if (
|
|
216
244
|
joinClause.type === `full` &&
|
|
217
|
-
(
|
|
218
|
-
|
|
245
|
+
(namespacedRow[mainTableAlias] === undefined ||
|
|
246
|
+
namespacedRow[joinedTableAlias] === undefined)
|
|
219
247
|
) {
|
|
220
248
|
return true
|
|
221
249
|
}
|
|
222
250
|
|
|
223
|
-
|
|
224
|
-
|
|
251
|
+
return evaluateConditionOnNamespacedRow(
|
|
252
|
+
namespacedRow,
|
|
225
253
|
joinClause.on,
|
|
226
254
|
mainTableAlias,
|
|
227
255
|
joinedTableAlias
|
|
228
256
|
)
|
|
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
257
|
})
|
|
245
258
|
)
|
|
246
259
|
}
|
package/src/query/order-by.ts
CHANGED
|
@@ -3,19 +3,18 @@ import {
|
|
|
3
3
|
orderBy,
|
|
4
4
|
orderByWithFractionalIndex,
|
|
5
5
|
orderByWithIndex,
|
|
6
|
-
topK,
|
|
7
|
-
topKWithFractionalIndex,
|
|
8
|
-
topKWithIndex,
|
|
9
6
|
} from "@electric-sql/d2ts"
|
|
10
|
-
import {
|
|
7
|
+
import { evaluateOperandOnNamespacedRow } from "./extractors"
|
|
11
8
|
import { isOrderIndexFunctionCall } from "./utils"
|
|
12
9
|
import type { ConditionOperand, Query } from "./schema"
|
|
13
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
KeyedNamespacedRow,
|
|
12
|
+
NamespacedAndKeyedStream,
|
|
13
|
+
NamespacedRow,
|
|
14
|
+
} from "../types"
|
|
14
15
|
|
|
15
16
|
export function processOrderBy(
|
|
16
|
-
resultPipeline:
|
|
17
|
-
Record<string, unknown> | [string | number, Record<string, unknown>]
|
|
18
|
-
>,
|
|
17
|
+
resultPipeline: NamespacedAndKeyedStream,
|
|
19
18
|
query: Query,
|
|
20
19
|
mainTableAlias: string
|
|
21
20
|
) {
|
|
@@ -25,7 +24,9 @@ export function processOrderBy(
|
|
|
25
24
|
let orderIndexAlias = ``
|
|
26
25
|
|
|
27
26
|
// Scan the SELECT clause for ORDER_INDEX functions
|
|
28
|
-
|
|
27
|
+
// TODO: Select is going to be optional in future - we will automatically add an
|
|
28
|
+
// attribute for the index column
|
|
29
|
+
for (const item of query.select!) {
|
|
29
30
|
if (typeof item === `object`) {
|
|
30
31
|
for (const [alias, expr] of Object.entries(item)) {
|
|
31
32
|
if (typeof expr === `object` && isOrderIndexFunctionCall(expr)) {
|
|
@@ -79,17 +80,13 @@ export function processOrderBy(
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
// Create a value extractor function for the orderBy operator
|
|
82
|
-
const valueExtractor = (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Create a nested row structure for evaluateOperandOnNestedRow
|
|
86
|
-
const nestedRow: Record<string, unknown> = { [mainTableAlias]: row }
|
|
87
|
-
|
|
83
|
+
// const valueExtractor = ([key, namespacedRow]: [
|
|
84
|
+
const valueExtractor = (namespacedRow: NamespacedRow) => {
|
|
88
85
|
// For multiple orderBy columns, create a composite key
|
|
89
86
|
if (orderByItems.length > 1) {
|
|
90
87
|
return orderByItems.map((item) => {
|
|
91
|
-
const val =
|
|
92
|
-
|
|
88
|
+
const val = evaluateOperandOnNamespacedRow(
|
|
89
|
+
namespacedRow,
|
|
93
90
|
item.operand,
|
|
94
91
|
mainTableAlias
|
|
95
92
|
)
|
|
@@ -106,8 +103,8 @@ export function processOrderBy(
|
|
|
106
103
|
} else if (orderByItems.length === 1) {
|
|
107
104
|
// For a single orderBy column, use the value directly
|
|
108
105
|
const item = orderByItems[0]
|
|
109
|
-
const val =
|
|
110
|
-
|
|
106
|
+
const val = evaluateOperandOnNamespacedRow(
|
|
107
|
+
namespacedRow,
|
|
111
108
|
item!.operand,
|
|
112
109
|
mainTableAlias
|
|
113
110
|
)
|
|
@@ -187,109 +184,62 @@ export function processOrderBy(
|
|
|
187
184
|
return (a as any).toString().localeCompare((b as any).toString())
|
|
188
185
|
}
|
|
189
186
|
|
|
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
187
|
// Apply the appropriate orderBy operator based on whether an ORDER_INDEX column is requested
|
|
200
188
|
if (hasOrderIndexColumn) {
|
|
201
189
|
if (orderIndexType === `numeric`) {
|
|
202
|
-
|
|
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
|
|
190
|
+
// Use orderByWithIndex for numeric indices
|
|
275
191
|
resultPipeline = resultPipeline.pipe(
|
|
276
|
-
|
|
192
|
+
orderByWithIndex(valueExtractor, {
|
|
277
193
|
limit: query.limit,
|
|
278
194
|
offset: query.offset,
|
|
279
195
|
comparator,
|
|
196
|
+
}),
|
|
197
|
+
map(([key, [value, index]]) => {
|
|
198
|
+
// Add the index to the result
|
|
199
|
+
// We add this to the main table alias for now
|
|
200
|
+
// TODO: re are going to need to refactor the whole order by pipeline
|
|
201
|
+
const result = {
|
|
202
|
+
...(value as Record<string, unknown>),
|
|
203
|
+
[mainTableAlias]: {
|
|
204
|
+
...value[mainTableAlias],
|
|
205
|
+
[orderIndexAlias]: index,
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
return [key, result] as KeyedNamespacedRow
|
|
280
209
|
})
|
|
281
210
|
)
|
|
282
211
|
} else {
|
|
283
|
-
// Use
|
|
212
|
+
// Use orderByWithFractionalIndex for fractional indices
|
|
284
213
|
resultPipeline = resultPipeline.pipe(
|
|
285
|
-
|
|
286
|
-
topK(topKComparator!, {
|
|
214
|
+
orderByWithFractionalIndex(valueExtractor, {
|
|
287
215
|
limit: query.limit,
|
|
288
216
|
offset: query.offset,
|
|
217
|
+
comparator,
|
|
289
218
|
}),
|
|
290
|
-
map(([
|
|
219
|
+
map(([key, [value, index]]) => {
|
|
220
|
+
// Add the index to the result
|
|
221
|
+
// We add this to the main table alias for now
|
|
222
|
+
// TODO: re are going to need to refactor the whole order by pipeline
|
|
223
|
+
const result = {
|
|
224
|
+
...(value as Record<string, unknown>),
|
|
225
|
+
[mainTableAlias]: {
|
|
226
|
+
...value[mainTableAlias],
|
|
227
|
+
[orderIndexAlias]: index,
|
|
228
|
+
},
|
|
229
|
+
}
|
|
230
|
+
return [key, result] as KeyedNamespacedRow
|
|
231
|
+
})
|
|
291
232
|
)
|
|
292
233
|
}
|
|
234
|
+
} else {
|
|
235
|
+
// Use regular orderBy if no index column is requested
|
|
236
|
+
resultPipeline = resultPipeline.pipe(
|
|
237
|
+
orderBy(valueExtractor, {
|
|
238
|
+
limit: query.limit,
|
|
239
|
+
offset: query.offset,
|
|
240
|
+
comparator,
|
|
241
|
+
})
|
|
242
|
+
)
|
|
293
243
|
}
|
|
294
244
|
|
|
295
245
|
return resultPipeline
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { filter, map } from "@electric-sql/d2ts"
|
|
2
|
-
import {
|
|
2
|
+
import { evaluateConditionOnNamespacedRow } from "./evaluators.js"
|
|
3
3
|
import { processJoinClause } from "./joins.js"
|
|
4
4
|
import { processGroupBy } from "./group-by.js"
|
|
5
5
|
import { processOrderBy } from "./order-by.js"
|
|
6
|
-
import { processKeyBy } from "./key-by.js"
|
|
7
6
|
import { processSelect } from "./select.js"
|
|
8
|
-
import type {
|
|
7
|
+
import type { Query } from "./schema.js"
|
|
9
8
|
import type { IStreamBuilder } from "@electric-sql/d2ts"
|
|
9
|
+
import type {
|
|
10
|
+
InputRow,
|
|
11
|
+
KeyedStream,
|
|
12
|
+
NamespacedAndKeyedStream,
|
|
13
|
+
} from "../types.js"
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* Compiles a query into a D2 pipeline
|
|
@@ -16,7 +20,7 @@ import type { IStreamBuilder } from "@electric-sql/d2ts"
|
|
|
16
20
|
*/
|
|
17
21
|
export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
18
22
|
query: Query,
|
|
19
|
-
inputs: Record<string,
|
|
23
|
+
inputs: Record<string, KeyedStream>
|
|
20
24
|
): T {
|
|
21
25
|
// Create a copy of the inputs map to avoid modifying the original
|
|
22
26
|
const allInputs = { ...inputs }
|
|
@@ -30,11 +34,6 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
30
34
|
throw new Error(`WITH query must have an "as" property`)
|
|
31
35
|
}
|
|
32
36
|
|
|
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
37
|
// Check if this CTE name already exists in the inputs
|
|
39
38
|
if (allInputs[withQuery.as]) {
|
|
40
39
|
throw new Error(`CTE with name "${withQuery.as}" already exists`)
|
|
@@ -51,14 +50,12 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
51
50
|
)
|
|
52
51
|
|
|
53
52
|
// Add the compiled query to the inputs map using its alias
|
|
54
|
-
allInputs[withQuery.as] = compiledWithQuery as
|
|
55
|
-
Record<string, unknown>
|
|
56
|
-
>
|
|
53
|
+
allInputs[withQuery.as] = compiledWithQuery as KeyedStream
|
|
57
54
|
}
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
// Create a map of table aliases to inputs
|
|
61
|
-
const tables: Record<string,
|
|
58
|
+
const tables: Record<string, KeyedStream> = {}
|
|
62
59
|
|
|
63
60
|
// The main table is the one in the FROM clause
|
|
64
61
|
const mainTableAlias = query.as || query.from
|
|
@@ -72,10 +69,14 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
72
69
|
tables[mainTableAlias] = input
|
|
73
70
|
|
|
74
71
|
// Prepare the initial pipeline with the main table wrapped in its alias
|
|
75
|
-
let pipeline = input.pipe(
|
|
76
|
-
map((row
|
|
72
|
+
let pipeline: NamespacedAndKeyedStream = input.pipe(
|
|
73
|
+
map(([key, row]) => {
|
|
77
74
|
// Initialize the record with a nested structure
|
|
78
|
-
|
|
75
|
+
const ret = [key, { [mainTableAlias]: row }] as [
|
|
76
|
+
string,
|
|
77
|
+
Record<string, typeof row>,
|
|
78
|
+
]
|
|
79
|
+
return ret
|
|
79
80
|
})
|
|
80
81
|
)
|
|
81
82
|
|
|
@@ -93,10 +94,10 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
93
94
|
// Process the WHERE clause if it exists
|
|
94
95
|
if (query.where) {
|
|
95
96
|
pipeline = pipeline.pipe(
|
|
96
|
-
filter((
|
|
97
|
-
const result =
|
|
98
|
-
|
|
99
|
-
query.where
|
|
97
|
+
filter(([_key, row]) => {
|
|
98
|
+
const result = evaluateConditionOnNamespacedRow(
|
|
99
|
+
row,
|
|
100
|
+
query.where!,
|
|
100
101
|
mainTableAlias
|
|
101
102
|
)
|
|
102
103
|
return result
|
|
@@ -113,12 +114,12 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
113
114
|
// This works similarly to WHERE but is applied after any aggregations
|
|
114
115
|
if (query.having) {
|
|
115
116
|
pipeline = pipeline.pipe(
|
|
116
|
-
filter((row) => {
|
|
117
|
+
filter(([_key, row]) => {
|
|
117
118
|
// For HAVING, we're working with the flattened row that contains both
|
|
118
119
|
// the group by keys and the aggregate results directly
|
|
119
|
-
const result =
|
|
120
|
-
|
|
121
|
-
query.having
|
|
120
|
+
const result = evaluateConditionOnNamespacedRow(
|
|
121
|
+
row,
|
|
122
|
+
query.having!,
|
|
122
123
|
mainTableAlias
|
|
123
124
|
)
|
|
124
125
|
return result
|
|
@@ -126,21 +127,9 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
126
127
|
)
|
|
127
128
|
}
|
|
128
129
|
|
|
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
130
|
// Process orderBy parameter if it exists
|
|
142
131
|
if (query.orderBy) {
|
|
143
|
-
|
|
132
|
+
pipeline = processOrderBy(pipeline, query, mainTableAlias)
|
|
144
133
|
} else if (query.limit !== undefined || query.offset !== undefined) {
|
|
145
134
|
// If there's a limit or offset without orderBy, throw an error
|
|
146
135
|
throw new Error(
|
|
@@ -148,5 +137,13 @@ export function compileQueryPipeline<T extends IStreamBuilder<unknown>>(
|
|
|
148
137
|
)
|
|
149
138
|
}
|
|
150
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
|
|
151
148
|
return resultPipeline as T
|
|
152
149
|
}
|