@tanstack/db-ivm 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/package.json +63 -0
- package/src/d2.ts +150 -0
- package/src/graph.ts +131 -0
- package/src/index.ts +4 -0
- package/src/indexes.ts +117 -0
- package/src/multiset.ts +215 -0
- package/src/operators/concat.ts +45 -0
- package/src/operators/consolidate.ts +50 -0
- package/src/operators/count.ts +52 -0
- package/src/operators/debug.ts +58 -0
- package/src/operators/distinct.ts +98 -0
- package/src/operators/filter.ts +48 -0
- package/src/operators/filterBy.ts +31 -0
- package/src/operators/groupBy.ts +354 -0
- package/src/operators/index.ts +18 -0
- package/src/operators/join.ts +265 -0
- package/src/operators/keying.ts +34 -0
- package/src/operators/map.ts +48 -0
- package/src/operators/negate.ts +33 -0
- package/src/operators/orderBy.ts +213 -0
- package/src/operators/orderByBTree.ts +20 -0
- package/src/operators/output.ts +53 -0
- package/src/operators/pipe.ts +55 -0
- package/src/operators/reduce.ts +124 -0
- package/src/operators/topK.ts +92 -0
- package/src/operators/topKWithFractionalIndex.ts +347 -0
- package/src/operators/topKWithFractionalIndexBTree.ts +306 -0
- package/src/types.ts +376 -0
- package/src/utils.ts +163 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { map } from "./map.js"
|
|
2
|
+
import { reduce } from "./reduce.js"
|
|
3
|
+
import type { IStreamBuilder, KeyValue } from "../types.js"
|
|
4
|
+
|
|
5
|
+
type GroupKey = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
type BasicAggregateFunction<T, R, V = unknown> = {
|
|
8
|
+
preMap: (data: T) => V
|
|
9
|
+
reduce: (values: Array<[V, number]>) => V
|
|
10
|
+
postMap?: (result: V) => R
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PipedAggregateFunction<T, R> = {
|
|
14
|
+
pipe: (stream: IStreamBuilder<T>) => IStreamBuilder<KeyValue<string, R>>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type AggregateFunction<T, R, V = unknown> =
|
|
18
|
+
| BasicAggregateFunction<T, R, V>
|
|
19
|
+
| PipedAggregateFunction<T, R>
|
|
20
|
+
|
|
21
|
+
type ExtractAggregateReturnType<T, A> =
|
|
22
|
+
A extends AggregateFunction<T, infer R, any> ? R : never
|
|
23
|
+
|
|
24
|
+
type AggregatesReturnType<T, A> = {
|
|
25
|
+
[K in keyof A]: ExtractAggregateReturnType<T, A[K]>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function isPipedAggregateFunction<T, R>(
|
|
29
|
+
aggregate: AggregateFunction<T, R>
|
|
30
|
+
): aggregate is PipedAggregateFunction<T, R> {
|
|
31
|
+
return `pipe` in aggregate
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Groups data by key and applies multiple aggregate operations
|
|
36
|
+
* @param keyExtractor Function to extract grouping key from data
|
|
37
|
+
* @param aggregates Object mapping aggregate names to aggregate functions
|
|
38
|
+
*/
|
|
39
|
+
export function groupBy<
|
|
40
|
+
T,
|
|
41
|
+
K extends GroupKey,
|
|
42
|
+
A extends Record<string, AggregateFunction<T, any, any>>,
|
|
43
|
+
>(keyExtractor: (data: T) => K, aggregates: A = {} as A) {
|
|
44
|
+
type ResultType = K & AggregatesReturnType<T, A>
|
|
45
|
+
|
|
46
|
+
const basicAggregates = Object.fromEntries(
|
|
47
|
+
Object.entries(aggregates).filter(
|
|
48
|
+
([_, aggregate]) => !isPipedAggregateFunction(aggregate)
|
|
49
|
+
)
|
|
50
|
+
) as Record<string, BasicAggregateFunction<T, any, any>>
|
|
51
|
+
|
|
52
|
+
// @ts-expect-error - TODO: we don't use this yet, but we will
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
54
|
+
const pipedAggregates = Object.fromEntries(
|
|
55
|
+
Object.entries(aggregates).filter(([_, aggregate]) =>
|
|
56
|
+
isPipedAggregateFunction(aggregate)
|
|
57
|
+
)
|
|
58
|
+
) as Record<string, PipedAggregateFunction<T, any>>
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
stream: IStreamBuilder<T>
|
|
62
|
+
): IStreamBuilder<KeyValue<string, ResultType>> => {
|
|
63
|
+
// Special key to store the original key object
|
|
64
|
+
const KEY_SENTINEL = `__original_key__`
|
|
65
|
+
|
|
66
|
+
// First map to extract keys and pre-aggregate values
|
|
67
|
+
const withKeysAndValues = stream.pipe(
|
|
68
|
+
map((data) => {
|
|
69
|
+
const key = keyExtractor(data)
|
|
70
|
+
const keyString = JSON.stringify(key)
|
|
71
|
+
|
|
72
|
+
// Create values object with pre-aggregated values
|
|
73
|
+
const values: Record<string, unknown> = {}
|
|
74
|
+
|
|
75
|
+
// Store the original key object
|
|
76
|
+
values[KEY_SENTINEL] = key
|
|
77
|
+
|
|
78
|
+
// Add pre-aggregated values
|
|
79
|
+
for (const [name, aggregate] of Object.entries(basicAggregates)) {
|
|
80
|
+
values[name] = aggregate.preMap(data)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [keyString, values] as KeyValue<string, Record<string, unknown>>
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Then reduce to compute aggregates
|
|
88
|
+
const reduced = withKeysAndValues.pipe(
|
|
89
|
+
reduce((values) => {
|
|
90
|
+
// Calculate total multiplicity to check if the group should exist
|
|
91
|
+
let totalMultiplicity = 0
|
|
92
|
+
for (const [_, multiplicity] of values) {
|
|
93
|
+
totalMultiplicity += multiplicity
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If total multiplicity is 0 or negative, the group should be removed completely
|
|
97
|
+
if (totalMultiplicity <= 0) {
|
|
98
|
+
return []
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result: Record<string, unknown> = {}
|
|
102
|
+
|
|
103
|
+
// Get the original key from first value in group
|
|
104
|
+
const originalKey = values[0]?.[0]?.[KEY_SENTINEL]
|
|
105
|
+
result[KEY_SENTINEL] = originalKey
|
|
106
|
+
|
|
107
|
+
// Apply each aggregate function
|
|
108
|
+
for (const [name, aggregate] of Object.entries(basicAggregates)) {
|
|
109
|
+
const preValues = values.map(
|
|
110
|
+
([v, m]) => [v[name], m] as [any, number]
|
|
111
|
+
)
|
|
112
|
+
result[name] = aggregate.reduce(preValues)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return [[result, 1]]
|
|
116
|
+
})
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// Finally map to extract the key and include all values
|
|
120
|
+
return reduced.pipe(
|
|
121
|
+
map(([keyString, values]) => {
|
|
122
|
+
// Extract the original key
|
|
123
|
+
const key = values[KEY_SENTINEL] as K
|
|
124
|
+
|
|
125
|
+
// Create intermediate result with key values and aggregate results
|
|
126
|
+
const result: Record<string, unknown> = {}
|
|
127
|
+
|
|
128
|
+
// Add key properties to result
|
|
129
|
+
Object.assign(result, key)
|
|
130
|
+
|
|
131
|
+
// Apply postMap if provided
|
|
132
|
+
for (const [name, aggregate] of Object.entries(basicAggregates)) {
|
|
133
|
+
if (aggregate.postMap) {
|
|
134
|
+
result[name] = aggregate.postMap(values[name])
|
|
135
|
+
} else {
|
|
136
|
+
result[name] = values[name]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Return with the string key instead of the object
|
|
141
|
+
return [keyString, result] as KeyValue<string, ResultType>
|
|
142
|
+
})
|
|
143
|
+
)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Creates a sum aggregate function
|
|
149
|
+
*/
|
|
150
|
+
export function sum<T>(
|
|
151
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
152
|
+
): AggregateFunction<T, number, number> {
|
|
153
|
+
return {
|
|
154
|
+
preMap: (data: T) => valueExtractor(data),
|
|
155
|
+
reduce: (values: Array<[number, number]>) => {
|
|
156
|
+
let total = 0
|
|
157
|
+
for (const [value, multiplicity] of values) {
|
|
158
|
+
total += value * multiplicity
|
|
159
|
+
}
|
|
160
|
+
return total
|
|
161
|
+
},
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Creates a count aggregate function
|
|
167
|
+
*/
|
|
168
|
+
export function count<T>(): AggregateFunction<T, number, number> {
|
|
169
|
+
return {
|
|
170
|
+
preMap: () => 1,
|
|
171
|
+
reduce: (values: Array<[number, number]>) => {
|
|
172
|
+
let totalCount = 0
|
|
173
|
+
for (const [_, multiplicity] of values) {
|
|
174
|
+
totalCount += multiplicity
|
|
175
|
+
}
|
|
176
|
+
return totalCount
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Creates an average aggregate function
|
|
183
|
+
*/
|
|
184
|
+
export function avg<T>(
|
|
185
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
186
|
+
): AggregateFunction<T, number, { sum: number; count: number }> {
|
|
187
|
+
return {
|
|
188
|
+
preMap: (data: T) => ({
|
|
189
|
+
sum: valueExtractor(data),
|
|
190
|
+
count: 0,
|
|
191
|
+
}),
|
|
192
|
+
reduce: (values: Array<[{ sum: number; count: number }, number]>) => {
|
|
193
|
+
let totalSum = 0
|
|
194
|
+
let totalCount = 0
|
|
195
|
+
for (const [value, multiplicity] of values) {
|
|
196
|
+
totalSum += value.sum * multiplicity
|
|
197
|
+
totalCount += multiplicity
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
sum: totalSum,
|
|
201
|
+
count: totalCount,
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
postMap: (result: { sum: number; count: number }) => {
|
|
205
|
+
return result.sum / result.count
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Creates a min aggregate function that computes the minimum value in a group
|
|
212
|
+
* @param valueExtractor Function to extract a numeric value from each data entry
|
|
213
|
+
*/
|
|
214
|
+
export function min<T>(
|
|
215
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
216
|
+
): AggregateFunction<T, number, number> {
|
|
217
|
+
return {
|
|
218
|
+
preMap: (data: T) => valueExtractor(data),
|
|
219
|
+
reduce: (values: Array<[number, number]>) => {
|
|
220
|
+
let minValue = Number.POSITIVE_INFINITY
|
|
221
|
+
for (const [value, _multiplicity] of values) {
|
|
222
|
+
if (value < minValue) {
|
|
223
|
+
minValue = value
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return minValue === Number.POSITIVE_INFINITY ? 0 : minValue
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Creates a max aggregate function that computes the maximum value in a group
|
|
233
|
+
* @param valueExtractor Function to extract a numeric value from each data entry
|
|
234
|
+
*/
|
|
235
|
+
export function max<T>(
|
|
236
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
237
|
+
): AggregateFunction<T, number, number> {
|
|
238
|
+
return {
|
|
239
|
+
preMap: (data: T) => valueExtractor(data),
|
|
240
|
+
reduce: (values: Array<[number, number]>) => {
|
|
241
|
+
let maxValue = Number.NEGATIVE_INFINITY
|
|
242
|
+
for (const [value, _multiplicity] of values) {
|
|
243
|
+
if (value > maxValue) {
|
|
244
|
+
maxValue = value
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return maxValue === Number.NEGATIVE_INFINITY ? 0 : maxValue
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Creates a median aggregate function that computes the middle value in a sorted group
|
|
254
|
+
* If there's an even number of values, returns the average of the two middle values
|
|
255
|
+
* @param valueExtractor Function to extract a numeric value from each data entry
|
|
256
|
+
*/
|
|
257
|
+
export function median<T>(
|
|
258
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
259
|
+
): AggregateFunction<T, number, Array<number>> {
|
|
260
|
+
return {
|
|
261
|
+
preMap: (data: T) => [valueExtractor(data)],
|
|
262
|
+
reduce: (values: Array<[Array<number>, number]>) => {
|
|
263
|
+
// Flatten all values, taking multiplicity into account
|
|
264
|
+
const allValues: Array<number> = []
|
|
265
|
+
for (const [valueArray, multiplicity] of values) {
|
|
266
|
+
for (const value of valueArray) {
|
|
267
|
+
// Add each value multiple times based on multiplicity
|
|
268
|
+
for (let i = 0; i < multiplicity; i++) {
|
|
269
|
+
allValues.push(value)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Return empty array if no values
|
|
275
|
+
if (allValues.length === 0) {
|
|
276
|
+
return []
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Sort values
|
|
280
|
+
allValues.sort((a, b) => a - b)
|
|
281
|
+
|
|
282
|
+
return allValues
|
|
283
|
+
},
|
|
284
|
+
postMap: (result: Array<number>) => {
|
|
285
|
+
if (result.length === 0) return 0
|
|
286
|
+
|
|
287
|
+
const mid = Math.floor(result.length / 2)
|
|
288
|
+
|
|
289
|
+
// If even number of values, average the two middle values
|
|
290
|
+
if (result.length % 2 === 0) {
|
|
291
|
+
return (result[mid - 1]! + result[mid]!) / 2
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// If odd number of values, return the middle value
|
|
295
|
+
return result[mid]!
|
|
296
|
+
},
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Creates a mode aggregate function that computes the most frequent value in a group
|
|
302
|
+
* If multiple values have the same highest frequency, returns the first one encountered
|
|
303
|
+
* @param valueExtractor Function to extract a value from each data entry
|
|
304
|
+
*/
|
|
305
|
+
export function mode<T>(
|
|
306
|
+
valueExtractor: (value: T) => number = (v) => v as unknown as number
|
|
307
|
+
): AggregateFunction<T, number, Map<number, number>> {
|
|
308
|
+
return {
|
|
309
|
+
preMap: (data: T) => {
|
|
310
|
+
const value = valueExtractor(data)
|
|
311
|
+
const frequencyMap = new Map<number, number>()
|
|
312
|
+
frequencyMap.set(value, 1)
|
|
313
|
+
return frequencyMap
|
|
314
|
+
},
|
|
315
|
+
reduce: (values: Array<[Map<number, number>, number]>) => {
|
|
316
|
+
// Combine all frequency maps
|
|
317
|
+
const combinedMap = new Map<number, number>()
|
|
318
|
+
|
|
319
|
+
for (const [frequencyMap, multiplicity] of values) {
|
|
320
|
+
for (const [value, frequencyCount] of frequencyMap.entries()) {
|
|
321
|
+
const currentCount = combinedMap.get(value) || 0
|
|
322
|
+
combinedMap.set(value, currentCount + frequencyCount * multiplicity)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return combinedMap
|
|
327
|
+
},
|
|
328
|
+
postMap: (result: Map<number, number>) => {
|
|
329
|
+
if (result.size === 0) return 0
|
|
330
|
+
|
|
331
|
+
let modeValue = 0
|
|
332
|
+
let maxFrequency = 0
|
|
333
|
+
|
|
334
|
+
for (const [value, frequency] of result.entries()) {
|
|
335
|
+
if (frequency > maxFrequency) {
|
|
336
|
+
maxFrequency = frequency
|
|
337
|
+
modeValue = value
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return modeValue
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const groupByOperators = {
|
|
347
|
+
sum,
|
|
348
|
+
count,
|
|
349
|
+
avg,
|
|
350
|
+
min,
|
|
351
|
+
max,
|
|
352
|
+
median,
|
|
353
|
+
mode,
|
|
354
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from "./pipe.js"
|
|
2
|
+
export * from "./map.js"
|
|
3
|
+
export * from "./filter.js"
|
|
4
|
+
export * from "./negate.js"
|
|
5
|
+
export * from "./concat.js"
|
|
6
|
+
export * from "./debug.js"
|
|
7
|
+
export * from "./output.js"
|
|
8
|
+
export * from "./consolidate.js"
|
|
9
|
+
export * from "./join.js"
|
|
10
|
+
export * from "./reduce.js"
|
|
11
|
+
export * from "./count.js"
|
|
12
|
+
export * from "./distinct.js"
|
|
13
|
+
export * from "./keying.js"
|
|
14
|
+
export * from "./topK.js"
|
|
15
|
+
export * from "./topKWithFractionalIndex.js"
|
|
16
|
+
export * from "./orderBy.js"
|
|
17
|
+
export * from "./filterBy.js"
|
|
18
|
+
export { groupBy, groupByOperators } from "./groupBy.js"
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { BinaryOperator, DifferenceStreamWriter } from "../graph.js"
|
|
2
|
+
import { StreamBuilder } from "../d2.js"
|
|
3
|
+
import { MultiSet } from "../multiset.js"
|
|
4
|
+
import { Index } from "../indexes.js"
|
|
5
|
+
import { negate } from "./negate.js"
|
|
6
|
+
import { map } from "./map.js"
|
|
7
|
+
import { concat } from "./concat.js"
|
|
8
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
9
|
+
import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Type of join to perform
|
|
13
|
+
*/
|
|
14
|
+
export type JoinType = `inner` | `left` | `right` | `full` | `anti`
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Operator that joins two input streams
|
|
18
|
+
*/
|
|
19
|
+
export class JoinOperator<K, V1, V2> extends BinaryOperator<
|
|
20
|
+
[K, V1] | [K, V2] | [K, [V1, V2]]
|
|
21
|
+
> {
|
|
22
|
+
#indexA = new Index<K, V1>()
|
|
23
|
+
#indexB = new Index<K, V2>()
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
id: number,
|
|
27
|
+
inputA: DifferenceStreamReader<[K, V1]>,
|
|
28
|
+
inputB: DifferenceStreamReader<[K, V2]>,
|
|
29
|
+
output: DifferenceStreamWriter<[K, [V1, V2]]>
|
|
30
|
+
) {
|
|
31
|
+
super(id, inputA, inputB, output)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
run(): void {
|
|
35
|
+
const deltaA = new Index<K, V1>()
|
|
36
|
+
const deltaB = new Index<K, V2>()
|
|
37
|
+
|
|
38
|
+
// Process input A - process ALL messages, not just the first one
|
|
39
|
+
const messagesA = this.inputAMessages()
|
|
40
|
+
for (const message of messagesA) {
|
|
41
|
+
const multiSetMessage = message as unknown as MultiSet<[K, V1]>
|
|
42
|
+
for (const [item, multiplicity] of multiSetMessage.getInner()) {
|
|
43
|
+
const [key, value] = item
|
|
44
|
+
deltaA.addValue(key, [value, multiplicity])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Process input B - process ALL messages, not just the first one
|
|
49
|
+
const messagesB = this.inputBMessages()
|
|
50
|
+
for (const message of messagesB) {
|
|
51
|
+
const multiSetMessage = message as unknown as MultiSet<[K, V2]>
|
|
52
|
+
for (const [item, multiplicity] of multiSetMessage.getInner()) {
|
|
53
|
+
const [key, value] = item
|
|
54
|
+
deltaB.addValue(key, [value, multiplicity])
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Process results
|
|
59
|
+
const results = new MultiSet<[K, [V1, V2]]>()
|
|
60
|
+
|
|
61
|
+
// Join deltaA with existing indexB
|
|
62
|
+
results.extend(deltaA.join(this.#indexB))
|
|
63
|
+
|
|
64
|
+
// Append deltaA to indexA
|
|
65
|
+
this.#indexA.append(deltaA)
|
|
66
|
+
|
|
67
|
+
// Join existing indexA with deltaB
|
|
68
|
+
results.extend(this.#indexA.join(deltaB))
|
|
69
|
+
|
|
70
|
+
// Send results
|
|
71
|
+
if (results.getInner().length > 0) {
|
|
72
|
+
this.output.sendData(results)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Append deltaB to indexB
|
|
76
|
+
this.#indexB.append(deltaB)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Joins two input streams
|
|
82
|
+
* @param other - The other stream to join with
|
|
83
|
+
* @param type - The type of join to perform
|
|
84
|
+
*/
|
|
85
|
+
export function join<
|
|
86
|
+
K,
|
|
87
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
88
|
+
V2,
|
|
89
|
+
T,
|
|
90
|
+
>(
|
|
91
|
+
other: IStreamBuilder<KeyValue<K, V2>>,
|
|
92
|
+
type: JoinType = `inner`
|
|
93
|
+
): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
|
|
94
|
+
switch (type) {
|
|
95
|
+
case `inner`:
|
|
96
|
+
return innerJoin(other) as unknown as PipedOperator<
|
|
97
|
+
T,
|
|
98
|
+
KeyValue<K, [V1, V2]>
|
|
99
|
+
>
|
|
100
|
+
case `anti`:
|
|
101
|
+
return antiJoin(other) as unknown as PipedOperator<
|
|
102
|
+
T,
|
|
103
|
+
KeyValue<K, [V1, null]>
|
|
104
|
+
>
|
|
105
|
+
case `left`:
|
|
106
|
+
return leftJoin(other) as unknown as PipedOperator<
|
|
107
|
+
T,
|
|
108
|
+
KeyValue<K, [V1, V2 | null]>
|
|
109
|
+
>
|
|
110
|
+
case `right`:
|
|
111
|
+
return rightJoin(other) as unknown as PipedOperator<
|
|
112
|
+
T,
|
|
113
|
+
KeyValue<K, [V1 | null, V2]>
|
|
114
|
+
>
|
|
115
|
+
case `full`:
|
|
116
|
+
return fullJoin(other) as unknown as PipedOperator<
|
|
117
|
+
T,
|
|
118
|
+
KeyValue<K, [V1 | null, V2 | null]>
|
|
119
|
+
>
|
|
120
|
+
default:
|
|
121
|
+
throw new Error(`Join type ${type} is invalid`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Joins two input streams
|
|
127
|
+
* @param other - The other stream to join with
|
|
128
|
+
*/
|
|
129
|
+
export function innerJoin<
|
|
130
|
+
K,
|
|
131
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
132
|
+
V2,
|
|
133
|
+
T,
|
|
134
|
+
>(
|
|
135
|
+
other: IStreamBuilder<KeyValue<K, V2>>
|
|
136
|
+
): PipedOperator<T, KeyValue<K, [V1, V2]>> {
|
|
137
|
+
return (stream: IStreamBuilder<T>): IStreamBuilder<KeyValue<K, [V1, V2]>> => {
|
|
138
|
+
if (stream.graph !== other.graph) {
|
|
139
|
+
throw new Error(`Cannot join streams from different graphs`)
|
|
140
|
+
}
|
|
141
|
+
const output = new StreamBuilder<KeyValue<K, [V1, V2]>>(
|
|
142
|
+
stream.graph,
|
|
143
|
+
new DifferenceStreamWriter<KeyValue<K, [V1, V2]>>()
|
|
144
|
+
)
|
|
145
|
+
const operator = new JoinOperator<K, V1, V2>(
|
|
146
|
+
stream.graph.getNextOperatorId(),
|
|
147
|
+
stream.connectReader() as DifferenceStreamReader<KeyValue<K, V1>>,
|
|
148
|
+
other.connectReader(),
|
|
149
|
+
output.writer
|
|
150
|
+
)
|
|
151
|
+
stream.graph.addOperator(operator)
|
|
152
|
+
stream.graph.addStream(output.connectReader())
|
|
153
|
+
return output
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Joins two input streams
|
|
159
|
+
* @param other - The other stream to join with
|
|
160
|
+
*/
|
|
161
|
+
export function antiJoin<
|
|
162
|
+
K,
|
|
163
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
164
|
+
V2,
|
|
165
|
+
T,
|
|
166
|
+
>(
|
|
167
|
+
other: IStreamBuilder<KeyValue<K, V2>>
|
|
168
|
+
): PipedOperator<T, KeyValue<K, [V1, null]>> {
|
|
169
|
+
return (
|
|
170
|
+
stream: IStreamBuilder<T>
|
|
171
|
+
): IStreamBuilder<KeyValue<K, [V1, null]>> => {
|
|
172
|
+
const matchedLeft = stream.pipe(
|
|
173
|
+
innerJoin(other),
|
|
174
|
+
map(([key, [valueLeft, _valueRight]]) => [key, valueLeft])
|
|
175
|
+
)
|
|
176
|
+
const anti = stream.pipe(
|
|
177
|
+
concat(matchedLeft.pipe(negate())),
|
|
178
|
+
// @ts-ignore TODO: fix this
|
|
179
|
+
map(([key, value]) => [key, [value, null]])
|
|
180
|
+
)
|
|
181
|
+
return anti as IStreamBuilder<KeyValue<K, [V1, null]>>
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Joins two input streams
|
|
187
|
+
* @param other - The other stream to join with
|
|
188
|
+
*/
|
|
189
|
+
export function leftJoin<
|
|
190
|
+
K,
|
|
191
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
192
|
+
V2,
|
|
193
|
+
T,
|
|
194
|
+
>(
|
|
195
|
+
other: IStreamBuilder<KeyValue<K, V2>>
|
|
196
|
+
): PipedOperator<T, KeyValue<K, [V1, V2 | null]>> {
|
|
197
|
+
return (
|
|
198
|
+
stream: IStreamBuilder<T>
|
|
199
|
+
): IStreamBuilder<KeyValue<K, [V1, V2 | null]>> => {
|
|
200
|
+
const left = stream
|
|
201
|
+
const right = other
|
|
202
|
+
const inner = left.pipe(innerJoin(right))
|
|
203
|
+
const anti = left.pipe(antiJoin(right))
|
|
204
|
+
return inner.pipe(concat(anti)) as IStreamBuilder<
|
|
205
|
+
KeyValue<K, [V1, V2 | null]>
|
|
206
|
+
>
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Joins two input streams
|
|
212
|
+
* @param other - The other stream to join with
|
|
213
|
+
*/
|
|
214
|
+
export function rightJoin<
|
|
215
|
+
K,
|
|
216
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
217
|
+
V2,
|
|
218
|
+
T,
|
|
219
|
+
>(
|
|
220
|
+
other: IStreamBuilder<KeyValue<K, V2>>
|
|
221
|
+
): PipedOperator<T, KeyValue<K, [V1 | null, V2]>> {
|
|
222
|
+
return (
|
|
223
|
+
stream: IStreamBuilder<T>
|
|
224
|
+
): IStreamBuilder<KeyValue<K, [V1 | null, V2]>> => {
|
|
225
|
+
const left = stream as IStreamBuilder<KeyValue<K, V1>>
|
|
226
|
+
const right = other
|
|
227
|
+
const inner = left.pipe(innerJoin(right))
|
|
228
|
+
const anti = right.pipe(
|
|
229
|
+
antiJoin(left),
|
|
230
|
+
map(([key, [a, b]]) => [key, [b, a]])
|
|
231
|
+
)
|
|
232
|
+
return inner.pipe(concat(anti)) as IStreamBuilder<
|
|
233
|
+
KeyValue<K, [V1 | null, V2]>
|
|
234
|
+
>
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Joins two input streams
|
|
240
|
+
* @param other - The other stream to join with
|
|
241
|
+
*/
|
|
242
|
+
export function fullJoin<
|
|
243
|
+
K,
|
|
244
|
+
V1 extends T extends KeyValue<infer _KT, infer VT> ? VT : never,
|
|
245
|
+
V2,
|
|
246
|
+
T,
|
|
247
|
+
>(
|
|
248
|
+
other: IStreamBuilder<KeyValue<K, V2>>
|
|
249
|
+
): PipedOperator<T, KeyValue<K, [V1 | null, V2 | null]>> {
|
|
250
|
+
return (
|
|
251
|
+
stream: IStreamBuilder<T>
|
|
252
|
+
): IStreamBuilder<KeyValue<K, [V1 | null, V2 | null]>> => {
|
|
253
|
+
const left = stream as IStreamBuilder<KeyValue<K, V1>>
|
|
254
|
+
const right = other
|
|
255
|
+
const inner = left.pipe(innerJoin(right))
|
|
256
|
+
const antiLeft = left.pipe(antiJoin(right))
|
|
257
|
+
const antiRight = right.pipe(
|
|
258
|
+
antiJoin(left),
|
|
259
|
+
map(([key, [a, b]]) => [key, [b, a]])
|
|
260
|
+
)
|
|
261
|
+
return inner.pipe(concat(antiLeft), concat(antiRight)) as IStreamBuilder<
|
|
262
|
+
KeyValue<K, [V1 | null, V2 | null]>
|
|
263
|
+
>
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { map } from "./map.js"
|
|
2
|
+
import type { PipedOperator } from "../types.js"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type for a keyed value
|
|
6
|
+
*/
|
|
7
|
+
export type Keyed<K, V> = [K, V]
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Takes an unkeyed input and returns a keyed result
|
|
11
|
+
* @param keyFn - Function to generate the key for each value
|
|
12
|
+
*/
|
|
13
|
+
export function keyBy<T, K>(
|
|
14
|
+
keyFn: (value: T) => K
|
|
15
|
+
): PipedOperator<T, Keyed<K, T>> {
|
|
16
|
+
return map((value: T): Keyed<K, T> => [keyFn(value), value])
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Removes the key from a keyed stream
|
|
21
|
+
*/
|
|
22
|
+
export function unkey<K, V>(): PipedOperator<Keyed<K, V>, V> {
|
|
23
|
+
return map(([_, value]: Keyed<K, V>): V => value)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Takes a keyed input and rekeys it with a new key
|
|
28
|
+
* @param keyFn - Function to generate the new key for each value
|
|
29
|
+
*/
|
|
30
|
+
export function rekey<K1, K2, V>(
|
|
31
|
+
keyFn: (value: V) => K2
|
|
32
|
+
): PipedOperator<Keyed<K1, V>, Keyed<K2, V>> {
|
|
33
|
+
return map(([_, value]: Keyed<K1, V>): Keyed<K2, V> => [keyFn(value), value])
|
|
34
|
+
}
|