@vaultgradient/pipequery-lang 1.12.3 → 1.13.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 +75 -3
- package/dist/builder/index.cjs +926 -0
- package/dist/builder/index.d.cts +458 -0
- package/dist/builder/index.d.ts +458 -0
- package/dist/builder/index.js +926 -0
- package/dist/chunk-KNVFXND5.js +2384 -0
- package/dist/chunk-LWM2ZXGD.cjs +2384 -0
- package/dist/engine/index.cjs +12 -2084
- package/dist/engine/index.d.cts +16 -140
- package/dist/engine/index.d.ts +16 -140
- package/dist/engine/index.js +19 -2091
- package/dist/react/index.cjs +18 -2
- package/dist/react/index.js +17 -1
- package/dist/types-CukH_v-n.d.cts +141 -0
- package/dist/types-CukH_v-n.d.ts +141 -0
- package/package.json +11 -1
package/dist/engine/index.js
CHANGED
|
@@ -1,2093 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"drawdown",
|
|
17
|
-
"pct",
|
|
18
|
-
"sharpe",
|
|
19
|
-
"calmar",
|
|
20
|
-
"sortino",
|
|
21
|
-
"info_ratio",
|
|
22
|
-
"distinct_count",
|
|
23
|
-
"sum_abs",
|
|
24
|
-
"abs_sum",
|
|
25
|
-
"first_value",
|
|
26
|
-
"last_value"
|
|
27
|
-
]);
|
|
28
|
-
var TWO_ARG_AGGREGATES = /* @__PURE__ */ new Set([
|
|
29
|
-
"percentile",
|
|
30
|
-
"vwap",
|
|
31
|
-
"wavg",
|
|
32
|
-
"info_ratio"
|
|
33
|
-
]);
|
|
34
|
-
var DataWeaveError = class extends Error {
|
|
35
|
-
position;
|
|
36
|
-
line;
|
|
37
|
-
column;
|
|
38
|
-
constructor(message, position, line, column) {
|
|
39
|
-
super(message);
|
|
40
|
-
this.name = "DataWeaveError";
|
|
41
|
-
this.position = position;
|
|
42
|
-
this.line = line;
|
|
43
|
-
this.column = column;
|
|
44
|
-
}
|
|
45
|
-
};
|
|
46
|
-
var LexerError = class extends DataWeaveError {
|
|
47
|
-
constructor(message, position, line, column) {
|
|
48
|
-
super(message, position, line, column);
|
|
49
|
-
this.name = "LexerError";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var ParseError = class extends DataWeaveError {
|
|
53
|
-
constructor(message, position, line, column) {
|
|
54
|
-
super(message, position, line, column);
|
|
55
|
-
this.name = "ParseError";
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var RuntimeError = class extends Error {
|
|
59
|
-
constructor(message) {
|
|
60
|
-
super(message);
|
|
61
|
-
this.name = "RuntimeError";
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// src/engine/lexer.ts
|
|
66
|
-
var KEYWORDS = {
|
|
67
|
-
as: "AS",
|
|
68
|
-
asc: "ASC",
|
|
69
|
-
desc: "DESC",
|
|
70
|
-
true: "BOOLEAN",
|
|
71
|
-
false: "BOOLEAN",
|
|
72
|
-
null: "NULL"
|
|
73
|
-
};
|
|
74
|
-
function tokenize(source) {
|
|
75
|
-
const tokens = [];
|
|
76
|
-
let pos = 0;
|
|
77
|
-
let line = 1;
|
|
78
|
-
let column = 1;
|
|
79
|
-
function advance() {
|
|
80
|
-
const ch = source[pos];
|
|
81
|
-
pos++;
|
|
82
|
-
if (ch === "\n") {
|
|
83
|
-
line++;
|
|
84
|
-
column = 1;
|
|
85
|
-
} else {
|
|
86
|
-
column++;
|
|
87
|
-
}
|
|
88
|
-
return ch;
|
|
89
|
-
}
|
|
90
|
-
function peek() {
|
|
91
|
-
return source[pos];
|
|
92
|
-
}
|
|
93
|
-
function peekNext() {
|
|
94
|
-
return source[pos + 1];
|
|
95
|
-
}
|
|
96
|
-
function addToken(type, value, startPos, startLine, startCol) {
|
|
97
|
-
tokens.push({ type, value, position: startPos, line: startLine, column: startCol });
|
|
98
|
-
}
|
|
99
|
-
function error(msg) {
|
|
100
|
-
throw new LexerError(msg, pos, line, column);
|
|
101
|
-
}
|
|
102
|
-
while (pos < source.length) {
|
|
103
|
-
const startPos = pos;
|
|
104
|
-
const startLine = line;
|
|
105
|
-
const startCol = column;
|
|
106
|
-
const ch = peek();
|
|
107
|
-
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
|
|
108
|
-
advance();
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (ch === "|") {
|
|
112
|
-
advance();
|
|
113
|
-
addToken("PIPE", "|", startPos, startLine, startCol);
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (ch === "(") {
|
|
117
|
-
advance();
|
|
118
|
-
addToken("LPAREN", "(", startPos, startLine, startCol);
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (ch === ")") {
|
|
122
|
-
advance();
|
|
123
|
-
addToken("RPAREN", ")", startPos, startLine, startCol);
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (ch === ",") {
|
|
127
|
-
advance();
|
|
128
|
-
addToken("COMMA", ",", startPos, startLine, startCol);
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (ch === ".") {
|
|
132
|
-
advance();
|
|
133
|
-
addToken("DOT", ".", startPos, startLine, startCol);
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
if (ch === "+") {
|
|
137
|
-
advance();
|
|
138
|
-
addToken("PLUS", "+", startPos, startLine, startCol);
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
if (ch === "-") {
|
|
142
|
-
advance();
|
|
143
|
-
addToken("MINUS", "-", startPos, startLine, startCol);
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (ch === "*") {
|
|
147
|
-
advance();
|
|
148
|
-
addToken("STAR", "*", startPos, startLine, startCol);
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
if (ch === "/") {
|
|
152
|
-
advance();
|
|
153
|
-
addToken("SLASH", "/", startPos, startLine, startCol);
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
if (ch === "%") {
|
|
157
|
-
advance();
|
|
158
|
-
addToken("MOD", "%", startPos, startLine, startCol);
|
|
159
|
-
continue;
|
|
160
|
-
}
|
|
161
|
-
if (ch === ">" && peekNext() === "=") {
|
|
162
|
-
advance();
|
|
163
|
-
advance();
|
|
164
|
-
addToken("GTE", ">=", startPos, startLine, startCol);
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
if (ch === ">") {
|
|
168
|
-
advance();
|
|
169
|
-
addToken("GT", ">", startPos, startLine, startCol);
|
|
170
|
-
continue;
|
|
171
|
-
}
|
|
172
|
-
if (ch === "<" && peekNext() === "=") {
|
|
173
|
-
advance();
|
|
174
|
-
advance();
|
|
175
|
-
addToken("LTE", "<=", startPos, startLine, startCol);
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
if (ch === "<") {
|
|
179
|
-
advance();
|
|
180
|
-
addToken("LT", "<", startPos, startLine, startCol);
|
|
181
|
-
continue;
|
|
182
|
-
}
|
|
183
|
-
if (ch === "=" && peekNext() === "=") {
|
|
184
|
-
advance();
|
|
185
|
-
advance();
|
|
186
|
-
addToken("EQ", "==", startPos, startLine, startCol);
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
if (ch === "!" && peekNext() === "=") {
|
|
190
|
-
advance();
|
|
191
|
-
advance();
|
|
192
|
-
addToken("NEQ", "!=", startPos, startLine, startCol);
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
if (ch === "!") {
|
|
196
|
-
advance();
|
|
197
|
-
addToken("NOT", "!", startPos, startLine, startCol);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (ch === "&" && peekNext() === "&") {
|
|
201
|
-
advance();
|
|
202
|
-
advance();
|
|
203
|
-
addToken("AND", "&&", startPos, startLine, startCol);
|
|
204
|
-
continue;
|
|
205
|
-
}
|
|
206
|
-
if (ch === "|" && peekNext() === "|") {
|
|
207
|
-
advance();
|
|
208
|
-
advance();
|
|
209
|
-
addToken("OR", "||", startPos, startLine, startCol);
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (ch >= "0" && ch <= "9") {
|
|
213
|
-
const start = pos;
|
|
214
|
-
while (pos < source.length && peek() >= "0" && peek() <= "9") advance();
|
|
215
|
-
if (pos < source.length && peek() === "." && pos + 1 < source.length && source[pos + 1] >= "0" && source[pos + 1] <= "9") {
|
|
216
|
-
advance();
|
|
217
|
-
while (pos < source.length && peek() >= "0" && peek() <= "9") advance();
|
|
218
|
-
}
|
|
219
|
-
addToken("NUMBER", source.slice(start, pos), startPos, startLine, startCol);
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (ch === '"' || ch === "'") {
|
|
223
|
-
const quote = ch;
|
|
224
|
-
advance();
|
|
225
|
-
const start = pos;
|
|
226
|
-
let value = "";
|
|
227
|
-
while (pos < source.length && peek() !== quote) {
|
|
228
|
-
if (peek() === "\\" && pos + 1 < source.length) {
|
|
229
|
-
advance();
|
|
230
|
-
const escaped = advance();
|
|
231
|
-
if (escaped === "n") value += "\n";
|
|
232
|
-
else if (escaped === "t") value += " ";
|
|
233
|
-
else value += escaped;
|
|
234
|
-
} else {
|
|
235
|
-
value += advance();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
if (pos >= source.length) {
|
|
239
|
-
error(`Unterminated string starting at line ${startLine}, column ${startCol}`);
|
|
240
|
-
}
|
|
241
|
-
advance();
|
|
242
|
-
void start;
|
|
243
|
-
addToken("STRING", value, startPos, startLine, startCol);
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_") {
|
|
247
|
-
const start = pos;
|
|
248
|
-
while (pos < source.length && (peek() >= "a" && peek() <= "z" || peek() >= "A" && peek() <= "Z" || peek() >= "0" && peek() <= "9" || peek() === "_")) {
|
|
249
|
-
advance();
|
|
250
|
-
}
|
|
251
|
-
const word = source.slice(start, pos);
|
|
252
|
-
const keywordType = KEYWORDS[word];
|
|
253
|
-
if (keywordType) {
|
|
254
|
-
addToken(keywordType, word, startPos, startLine, startCol);
|
|
255
|
-
} else {
|
|
256
|
-
addToken("IDENTIFIER", word, startPos, startLine, startCol);
|
|
257
|
-
}
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
error(`Unexpected character '${ch}'`);
|
|
261
|
-
}
|
|
262
|
-
addToken("EOF", "", pos, line, column);
|
|
263
|
-
return tokens;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// src/engine/parser.ts
|
|
267
|
-
function parse(tokens) {
|
|
268
|
-
let pos = 0;
|
|
269
|
-
function peek() {
|
|
270
|
-
return tokens[pos];
|
|
271
|
-
}
|
|
272
|
-
function advance() {
|
|
273
|
-
const token = tokens[pos];
|
|
274
|
-
pos++;
|
|
275
|
-
return token;
|
|
276
|
-
}
|
|
277
|
-
function expect(type, context) {
|
|
278
|
-
const token = peek();
|
|
279
|
-
if (token.type !== type) {
|
|
280
|
-
const ctx = context ? ` ${context}` : "";
|
|
281
|
-
throw new ParseError(
|
|
282
|
-
`Expected ${type} but found ${token.type} ('${token.value}')${ctx}`,
|
|
283
|
-
token.position,
|
|
284
|
-
token.line,
|
|
285
|
-
token.column
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
return advance();
|
|
289
|
-
}
|
|
290
|
-
function match(type) {
|
|
291
|
-
if (peek().type === type) {
|
|
292
|
-
advance();
|
|
293
|
-
return true;
|
|
294
|
-
}
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
function parseExpression() {
|
|
298
|
-
let expr = parseOrExpr();
|
|
299
|
-
if (peek().type === "AS") {
|
|
300
|
-
advance();
|
|
301
|
-
const nameToken = expect("IDENTIFIER", 'after "as"');
|
|
302
|
-
expr = { kind: "AliasExpr", expression: expr, alias: nameToken.value };
|
|
303
|
-
}
|
|
304
|
-
return expr;
|
|
305
|
-
}
|
|
306
|
-
function parseOrExpr() {
|
|
307
|
-
let left = parseAndExpr();
|
|
308
|
-
while (peek().type === "OR") {
|
|
309
|
-
advance();
|
|
310
|
-
const right = parseAndExpr();
|
|
311
|
-
left = { kind: "BinaryExpr", operator: "||", left, right };
|
|
312
|
-
}
|
|
313
|
-
return left;
|
|
314
|
-
}
|
|
315
|
-
function parseAndExpr() {
|
|
316
|
-
let left = parseEqualityExpr();
|
|
317
|
-
while (peek().type === "AND") {
|
|
318
|
-
advance();
|
|
319
|
-
const right = parseEqualityExpr();
|
|
320
|
-
left = { kind: "BinaryExpr", operator: "&&", left, right };
|
|
321
|
-
}
|
|
322
|
-
return left;
|
|
323
|
-
}
|
|
324
|
-
function parseEqualityExpr() {
|
|
325
|
-
let left = parseComparisonExpr();
|
|
326
|
-
while (peek().type === "EQ" || peek().type === "NEQ") {
|
|
327
|
-
const op = advance().value;
|
|
328
|
-
const right = parseComparisonExpr();
|
|
329
|
-
left = { kind: "BinaryExpr", operator: op, left, right };
|
|
330
|
-
}
|
|
331
|
-
return left;
|
|
332
|
-
}
|
|
333
|
-
function parseComparisonExpr() {
|
|
334
|
-
let left = parseAdditiveExpr();
|
|
335
|
-
while (peek().type === "GT" || peek().type === "GTE" || peek().type === "LT" || peek().type === "LTE") {
|
|
336
|
-
const op = advance().value;
|
|
337
|
-
const right = parseAdditiveExpr();
|
|
338
|
-
left = { kind: "BinaryExpr", operator: op, left, right };
|
|
339
|
-
}
|
|
340
|
-
return left;
|
|
341
|
-
}
|
|
342
|
-
function parseAdditiveExpr() {
|
|
343
|
-
let left = parseMultiplicativeExpr();
|
|
344
|
-
while (peek().type === "PLUS" || peek().type === "MINUS") {
|
|
345
|
-
const op = advance().value;
|
|
346
|
-
const right = parseMultiplicativeExpr();
|
|
347
|
-
left = { kind: "BinaryExpr", operator: op, left, right };
|
|
348
|
-
}
|
|
349
|
-
return left;
|
|
350
|
-
}
|
|
351
|
-
function parseMultiplicativeExpr() {
|
|
352
|
-
let left = parseUnaryExpr();
|
|
353
|
-
while (peek().type === "STAR" || peek().type === "SLASH" || peek().type === "MOD") {
|
|
354
|
-
const op = advance().value;
|
|
355
|
-
const right = parseUnaryExpr();
|
|
356
|
-
left = { kind: "BinaryExpr", operator: op, left, right };
|
|
357
|
-
}
|
|
358
|
-
return left;
|
|
359
|
-
}
|
|
360
|
-
function parseUnaryExpr() {
|
|
361
|
-
if (peek().type === "NOT") {
|
|
362
|
-
advance();
|
|
363
|
-
const operand = parseUnaryExpr();
|
|
364
|
-
return { kind: "UnaryExpr", operator: "!", operand };
|
|
365
|
-
}
|
|
366
|
-
if (peek().type === "MINUS") {
|
|
367
|
-
advance();
|
|
368
|
-
const operand = parseUnaryExpr();
|
|
369
|
-
return { kind: "UnaryExpr", operator: "-", operand };
|
|
370
|
-
}
|
|
371
|
-
return parsePrimaryExpr();
|
|
372
|
-
}
|
|
373
|
-
function parsePrimaryExpr() {
|
|
374
|
-
const token = peek();
|
|
375
|
-
if (token.type === "NUMBER") {
|
|
376
|
-
advance();
|
|
377
|
-
return { kind: "NumberLiteral", value: Number(token.value) };
|
|
378
|
-
}
|
|
379
|
-
if (token.type === "STRING") {
|
|
380
|
-
advance();
|
|
381
|
-
return { kind: "StringLiteral", value: token.value };
|
|
382
|
-
}
|
|
383
|
-
if (token.type === "BOOLEAN") {
|
|
384
|
-
advance();
|
|
385
|
-
return { kind: "BooleanLiteral", value: token.value === "true" };
|
|
386
|
-
}
|
|
387
|
-
if (token.type === "NULL") {
|
|
388
|
-
advance();
|
|
389
|
-
return { kind: "NullLiteral" };
|
|
390
|
-
}
|
|
391
|
-
if (token.type === "IDENTIFIER") {
|
|
392
|
-
advance();
|
|
393
|
-
const name = token.value;
|
|
394
|
-
if (peek().type === "LPAREN") {
|
|
395
|
-
advance();
|
|
396
|
-
const args = [];
|
|
397
|
-
if (peek().type !== "RPAREN") {
|
|
398
|
-
args.push(parseExpression());
|
|
399
|
-
while (match("COMMA")) {
|
|
400
|
-
args.push(parseExpression());
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
expect("RPAREN", "after function arguments");
|
|
404
|
-
return { kind: "FunctionCall", name, args };
|
|
405
|
-
}
|
|
406
|
-
const path = [name];
|
|
407
|
-
while (peek().type === "DOT") {
|
|
408
|
-
advance();
|
|
409
|
-
const next = expect("IDENTIFIER", 'after "."');
|
|
410
|
-
path.push(next.value);
|
|
411
|
-
}
|
|
412
|
-
return { kind: "FieldAccess", path };
|
|
413
|
-
}
|
|
414
|
-
if (token.type === "LPAREN") {
|
|
415
|
-
advance();
|
|
416
|
-
const expr = parseExpression();
|
|
417
|
-
expect("RPAREN", "after grouped expression");
|
|
418
|
-
return expr;
|
|
419
|
-
}
|
|
420
|
-
throw new ParseError(
|
|
421
|
-
`Unexpected token ${token.type} ('${token.value}')`,
|
|
422
|
-
token.position,
|
|
423
|
-
token.line,
|
|
424
|
-
token.column
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
function isAggregateExpression(expr) {
|
|
428
|
-
if (expr.kind === "AliasExpr") return isAggregateExpression(expr.expression);
|
|
429
|
-
if (expr.kind === "FunctionCall") return AGGREGATE_NAMES.has(expr.name);
|
|
430
|
-
return false;
|
|
431
|
-
}
|
|
432
|
-
function parseOperation() {
|
|
433
|
-
const nameToken = expect("IDENTIFIER", 'after "|"');
|
|
434
|
-
const name = nameToken.value;
|
|
435
|
-
expect("LPAREN", `after "${name}"`);
|
|
436
|
-
let op;
|
|
437
|
-
switch (name) {
|
|
438
|
-
case "where": {
|
|
439
|
-
const condition = parseExpression();
|
|
440
|
-
op = { kind: "WhereOp", condition };
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
443
|
-
case "select": {
|
|
444
|
-
const fields = [];
|
|
445
|
-
if (peek().type !== "RPAREN") {
|
|
446
|
-
fields.push(parseExpression());
|
|
447
|
-
while (match("COMMA")) {
|
|
448
|
-
fields.push(parseExpression());
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
op = { kind: "SelectOp", fields };
|
|
452
|
-
break;
|
|
453
|
-
}
|
|
454
|
-
case "sort": {
|
|
455
|
-
const criteria = [];
|
|
456
|
-
if (peek().type !== "RPAREN") {
|
|
457
|
-
const expr = parseOrExpr();
|
|
458
|
-
let direction = "asc";
|
|
459
|
-
if (peek().type === "ASC") {
|
|
460
|
-
advance();
|
|
461
|
-
direction = "asc";
|
|
462
|
-
} else if (peek().type === "DESC") {
|
|
463
|
-
advance();
|
|
464
|
-
direction = "desc";
|
|
465
|
-
}
|
|
466
|
-
criteria.push({ expression: expr, direction });
|
|
467
|
-
while (match("COMMA")) {
|
|
468
|
-
const e = parseOrExpr();
|
|
469
|
-
let d = "asc";
|
|
470
|
-
if (peek().type === "ASC") {
|
|
471
|
-
advance();
|
|
472
|
-
d = "asc";
|
|
473
|
-
} else if (peek().type === "DESC") {
|
|
474
|
-
advance();
|
|
475
|
-
d = "desc";
|
|
476
|
-
}
|
|
477
|
-
criteria.push({ expression: e, direction: d });
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
op = { kind: "SortOp", criteria };
|
|
481
|
-
break;
|
|
482
|
-
}
|
|
483
|
-
case "groupBy": {
|
|
484
|
-
const keys = [];
|
|
485
|
-
if (peek().type !== "RPAREN") {
|
|
486
|
-
keys.push(parseOrExpr());
|
|
487
|
-
while (match("COMMA")) {
|
|
488
|
-
keys.push(parseOrExpr());
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
op = { kind: "GroupByOp", keys };
|
|
492
|
-
break;
|
|
493
|
-
}
|
|
494
|
-
case "join": {
|
|
495
|
-
const rightToken = expect("IDENTIFIER", "for join source");
|
|
496
|
-
const right = rightToken.value;
|
|
497
|
-
expect("COMMA", "after join source");
|
|
498
|
-
const condition = parseExpression();
|
|
499
|
-
op = { kind: "JoinOp", right, condition };
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
case "first": {
|
|
503
|
-
const numToken = expect("NUMBER", "for first count");
|
|
504
|
-
op = { kind: "FirstOp", count: Number(numToken.value) };
|
|
505
|
-
break;
|
|
506
|
-
}
|
|
507
|
-
case "last": {
|
|
508
|
-
const numToken = expect("NUMBER", "for last count");
|
|
509
|
-
op = { kind: "LastOp", count: Number(numToken.value) };
|
|
510
|
-
break;
|
|
511
|
-
}
|
|
512
|
-
case "distinct": {
|
|
513
|
-
const fields = [];
|
|
514
|
-
if (peek().type !== "RPAREN") {
|
|
515
|
-
fields.push(parseOrExpr());
|
|
516
|
-
while (match("COMMA")) {
|
|
517
|
-
fields.push(parseOrExpr());
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
op = { kind: "DistinctOp", fields: fields.length > 0 ? fields : void 0 };
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
case "flatten": {
|
|
524
|
-
let field;
|
|
525
|
-
if (peek().type !== "RPAREN") {
|
|
526
|
-
field = parseOrExpr();
|
|
527
|
-
}
|
|
528
|
-
op = { kind: "FlattenOp", field };
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case "transpose": {
|
|
532
|
-
let headerField;
|
|
533
|
-
if (peek().type !== "RPAREN") {
|
|
534
|
-
headerField = parseOrExpr();
|
|
535
|
-
}
|
|
536
|
-
op = { kind: "TransposeOp", headerField };
|
|
537
|
-
break;
|
|
538
|
-
}
|
|
539
|
-
case "rollup": {
|
|
540
|
-
const keys = [];
|
|
541
|
-
const aggregates = [];
|
|
542
|
-
let seenAggregate = false;
|
|
543
|
-
if (peek().type !== "RPAREN") {
|
|
544
|
-
const expr = parseExpression();
|
|
545
|
-
if (isAggregateExpression(expr)) {
|
|
546
|
-
seenAggregate = true;
|
|
547
|
-
aggregates.push(expr);
|
|
548
|
-
} else {
|
|
549
|
-
keys.push(expr);
|
|
550
|
-
}
|
|
551
|
-
while (match("COMMA")) {
|
|
552
|
-
const e = parseExpression();
|
|
553
|
-
if (isAggregateExpression(e)) {
|
|
554
|
-
seenAggregate = true;
|
|
555
|
-
aggregates.push(e);
|
|
556
|
-
} else if (seenAggregate) {
|
|
557
|
-
const t = peek();
|
|
558
|
-
throw new ParseError(
|
|
559
|
-
"rollup: group keys must come before aggregate expressions",
|
|
560
|
-
t.position,
|
|
561
|
-
t.line,
|
|
562
|
-
t.column
|
|
563
|
-
);
|
|
564
|
-
} else {
|
|
565
|
-
keys.push(e);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
if (aggregates.length === 0) {
|
|
570
|
-
const t = peek();
|
|
571
|
-
throw new ParseError(
|
|
572
|
-
"rollup: at least one aggregate expression is required",
|
|
573
|
-
t.position,
|
|
574
|
-
t.line,
|
|
575
|
-
t.column
|
|
576
|
-
);
|
|
577
|
-
}
|
|
578
|
-
op = { kind: "RollupOp", keys, aggregates };
|
|
579
|
-
break;
|
|
580
|
-
}
|
|
581
|
-
case "pivot": {
|
|
582
|
-
const pivotField = parseOrExpr();
|
|
583
|
-
expect("COMMA", "after pivot field");
|
|
584
|
-
const aggregates = [];
|
|
585
|
-
aggregates.push(parseExpression());
|
|
586
|
-
while (match("COMMA")) {
|
|
587
|
-
aggregates.push(parseExpression());
|
|
588
|
-
}
|
|
589
|
-
op = { kind: "PivotOp", pivotField, aggregates };
|
|
590
|
-
break;
|
|
591
|
-
}
|
|
592
|
-
case "map": {
|
|
593
|
-
const fields = [];
|
|
594
|
-
if (peek().type !== "RPAREN") {
|
|
595
|
-
fields.push(parseExpression());
|
|
596
|
-
while (match("COMMA")) {
|
|
597
|
-
fields.push(parseExpression());
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
op = { kind: "MapOp", fields };
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
case "reduce": {
|
|
604
|
-
const initial = parseExpression();
|
|
605
|
-
expect("COMMA", "after reduce initial value");
|
|
606
|
-
const accumulator = parseExpression();
|
|
607
|
-
op = { kind: "ReduceOp", initial, accumulator };
|
|
608
|
-
break;
|
|
609
|
-
}
|
|
610
|
-
default: {
|
|
611
|
-
if (AGGREGATE_NAMES.has(name)) {
|
|
612
|
-
if (TWO_ARG_AGGREGATES.has(name)) {
|
|
613
|
-
const args = [];
|
|
614
|
-
args.push(parseOrExpr());
|
|
615
|
-
expect("COMMA", `after first argument of ${name}`);
|
|
616
|
-
args.push(parseOrExpr());
|
|
617
|
-
op = { kind: "AggregateOp", function: name, args };
|
|
618
|
-
} else {
|
|
619
|
-
let field;
|
|
620
|
-
if (peek().type !== "RPAREN") {
|
|
621
|
-
field = parseOrExpr();
|
|
622
|
-
}
|
|
623
|
-
op = { kind: "AggregateOp", function: name, field };
|
|
624
|
-
}
|
|
625
|
-
break;
|
|
626
|
-
}
|
|
627
|
-
throw new ParseError(
|
|
628
|
-
`Unknown operation "${name}"`,
|
|
629
|
-
nameToken.position,
|
|
630
|
-
nameToken.line,
|
|
631
|
-
nameToken.column
|
|
632
|
-
);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
expect("RPAREN", `after ${name} arguments`);
|
|
636
|
-
return op;
|
|
637
|
-
}
|
|
638
|
-
function parsePipeline() {
|
|
639
|
-
const sourceToken = expect("IDENTIFIER", "at start of pipeline");
|
|
640
|
-
const operations = [];
|
|
641
|
-
while (peek().type === "PIPE") {
|
|
642
|
-
advance();
|
|
643
|
-
operations.push(parseOperation());
|
|
644
|
-
}
|
|
645
|
-
if (peek().type !== "EOF") {
|
|
646
|
-
const t = peek();
|
|
647
|
-
throw new ParseError(
|
|
648
|
-
`Unexpected token ${t.type} ('${t.value}') after pipeline`,
|
|
649
|
-
t.position,
|
|
650
|
-
t.line,
|
|
651
|
-
t.column
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
return { kind: "Pipeline", source: sourceToken.value, operations };
|
|
655
|
-
}
|
|
656
|
-
return parsePipeline();
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
// src/engine/runtime.ts
|
|
660
|
-
var accessorCache = /* @__PURE__ */ new Map();
|
|
661
|
-
function getFieldAccessor(path) {
|
|
662
|
-
const key = path.join(".");
|
|
663
|
-
let fn = accessorCache.get(key);
|
|
664
|
-
if (fn) return fn;
|
|
665
|
-
if (path.length === 1) {
|
|
666
|
-
const field = path[0];
|
|
667
|
-
fn = (obj) => obj[field];
|
|
668
|
-
} else {
|
|
669
|
-
const segments = [...path];
|
|
670
|
-
fn = (obj) => {
|
|
671
|
-
let val = obj;
|
|
672
|
-
for (const segment of segments) {
|
|
673
|
-
if (val == null) return void 0;
|
|
674
|
-
val = val[segment];
|
|
675
|
-
}
|
|
676
|
-
return val;
|
|
677
|
-
};
|
|
678
|
-
}
|
|
679
|
-
accessorCache.set(key, fn);
|
|
680
|
-
return fn;
|
|
681
|
-
}
|
|
682
|
-
function compareValues(a, b) {
|
|
683
|
-
if (a === b) return 0;
|
|
684
|
-
if (a == null && b == null) return 0;
|
|
685
|
-
if (a == null) return -1;
|
|
686
|
-
if (b == null) return 1;
|
|
687
|
-
if (typeof a === "number" && typeof b === "number") return a - b;
|
|
688
|
-
return String(a).localeCompare(String(b));
|
|
689
|
-
}
|
|
690
|
-
function groupByFn(data, keyFns) {
|
|
691
|
-
const groups = /* @__PURE__ */ new Map();
|
|
692
|
-
if (keyFns.length === 1) {
|
|
693
|
-
const { fn, name } = keyFns[0];
|
|
694
|
-
for (const row of data) {
|
|
695
|
-
const val = fn(row);
|
|
696
|
-
const groupKey = String(val);
|
|
697
|
-
let group = groups.get(groupKey);
|
|
698
|
-
if (!group) {
|
|
699
|
-
group = { keys: { [name]: val }, rows: [] };
|
|
700
|
-
groups.set(groupKey, group);
|
|
701
|
-
}
|
|
702
|
-
group.rows.push(row);
|
|
703
|
-
}
|
|
704
|
-
} else {
|
|
705
|
-
for (const row of data) {
|
|
706
|
-
const keys = {};
|
|
707
|
-
let groupKey = "";
|
|
708
|
-
for (let i = 0; i < keyFns.length; i++) {
|
|
709
|
-
const { fn, name } = keyFns[i];
|
|
710
|
-
const val = fn(row);
|
|
711
|
-
keys[name] = val;
|
|
712
|
-
groupKey += (i > 0 ? "\0" : "") + String(val);
|
|
713
|
-
}
|
|
714
|
-
let group = groups.get(groupKey);
|
|
715
|
-
if (!group) {
|
|
716
|
-
group = { keys, rows: [] };
|
|
717
|
-
groups.set(groupKey, group);
|
|
718
|
-
}
|
|
719
|
-
group.rows.push(row);
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
return Array.from(groups.values());
|
|
723
|
-
}
|
|
724
|
-
function nestedLoopJoin(left, right, conditionFn) {
|
|
725
|
-
const result = [];
|
|
726
|
-
for (const l of left) {
|
|
727
|
-
for (const r of right) {
|
|
728
|
-
const combined = { ...l, ...r };
|
|
729
|
-
if (conditionFn(combined)) {
|
|
730
|
-
result.push(combined);
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
return result;
|
|
735
|
-
}
|
|
736
|
-
function hashJoin(left, right, leftKeyFn, rightKeyFn) {
|
|
737
|
-
const hashMap = /* @__PURE__ */ new Map();
|
|
738
|
-
for (const r of right) {
|
|
739
|
-
const key = rightKeyFn(r);
|
|
740
|
-
let bucket = hashMap.get(key);
|
|
741
|
-
if (!bucket) {
|
|
742
|
-
bucket = [];
|
|
743
|
-
hashMap.set(key, bucket);
|
|
744
|
-
}
|
|
745
|
-
bucket.push(r);
|
|
746
|
-
}
|
|
747
|
-
const result = [];
|
|
748
|
-
for (const l of left) {
|
|
749
|
-
const matches = hashMap.get(leftKeyFn(l));
|
|
750
|
-
if (matches) {
|
|
751
|
-
for (const r of matches) {
|
|
752
|
-
result.push({ ...l, ...r });
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
return result;
|
|
757
|
-
}
|
|
758
|
-
function distinctFn(data, keyFn) {
|
|
759
|
-
const seen = /* @__PURE__ */ new Set();
|
|
760
|
-
return data.filter((row) => {
|
|
761
|
-
const key = keyFn ? keyFn(row) : JSON.stringify(row);
|
|
762
|
-
if (seen.has(key)) return false;
|
|
763
|
-
seen.add(key);
|
|
764
|
-
return true;
|
|
765
|
-
});
|
|
766
|
-
}
|
|
767
|
-
function transposeFn(data, headerFn, headerFieldName) {
|
|
768
|
-
if (data.length === 0) return [];
|
|
769
|
-
const colNames = data.map(
|
|
770
|
-
(row, i) => headerFn ? String(headerFn(row)) : `col_${i}`
|
|
771
|
-
);
|
|
772
|
-
const allFields = /* @__PURE__ */ new Set();
|
|
773
|
-
for (const row of data) {
|
|
774
|
-
for (const key of Object.keys(row)) {
|
|
775
|
-
if (key !== "_group" && key !== headerFieldName) {
|
|
776
|
-
allFields.add(key);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
const results = [];
|
|
781
|
-
for (const field of allFields) {
|
|
782
|
-
const row = { _field: field };
|
|
783
|
-
for (let i = 0; i < data.length; i++) {
|
|
784
|
-
row[colNames[i]] = data[i][field] ?? null;
|
|
785
|
-
}
|
|
786
|
-
results.push(row);
|
|
787
|
-
}
|
|
788
|
-
return results;
|
|
789
|
-
}
|
|
790
|
-
function rollupFn(data, keyFns, aggFns) {
|
|
791
|
-
const results = [];
|
|
792
|
-
const numKeys = keyFns.length;
|
|
793
|
-
for (let level = 0; level <= numKeys; level++) {
|
|
794
|
-
const activeKeys = keyFns.slice(0, numKeys - level);
|
|
795
|
-
const groups = groupByFn(data, activeKeys);
|
|
796
|
-
for (const group of groups) {
|
|
797
|
-
const row = {};
|
|
798
|
-
for (const { name } of activeKeys) {
|
|
799
|
-
row[name] = group.keys[name];
|
|
800
|
-
}
|
|
801
|
-
for (let i = numKeys - level; i < numKeys; i++) {
|
|
802
|
-
row[keyFns[i].name] = null;
|
|
803
|
-
}
|
|
804
|
-
const groupedRow = { ...row, _group: group.rows };
|
|
805
|
-
for (const { fn, name } of aggFns) {
|
|
806
|
-
row[name] = fn(groupedRow);
|
|
807
|
-
}
|
|
808
|
-
row._rollupLevel = level;
|
|
809
|
-
results.push(row);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
return results;
|
|
813
|
-
}
|
|
814
|
-
function defaultForAggregate(name) {
|
|
815
|
-
if (name === "min" || name === "max") return null;
|
|
816
|
-
return 0;
|
|
817
|
-
}
|
|
818
|
-
function pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate) {
|
|
819
|
-
const pivotGroups = /* @__PURE__ */ new Map();
|
|
820
|
-
for (const row of data) {
|
|
821
|
-
const pivotVal = String(pivotFn(row));
|
|
822
|
-
if (!pivotGroups.has(pivotVal)) pivotGroups.set(pivotVal, []);
|
|
823
|
-
pivotGroups.get(pivotVal).push(row);
|
|
824
|
-
}
|
|
825
|
-
const result = {};
|
|
826
|
-
for (const [pivotVal, groupRows] of pivotGroups) {
|
|
827
|
-
const syntheticRow = { _group: groupRows };
|
|
828
|
-
const safeName = pivotVal.replace(/\s+/g, "_");
|
|
829
|
-
for (const { fn, name } of aggCompilers) {
|
|
830
|
-
const colName = singleAggregate ? safeName : `${name}_${safeName}`;
|
|
831
|
-
result[colName] = fn(syntheticRow);
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
return [result];
|
|
835
|
-
}
|
|
836
|
-
function pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate) {
|
|
837
|
-
const allPivotValues = /* @__PURE__ */ new Set();
|
|
838
|
-
for (const groupRow of data) {
|
|
839
|
-
const groupData = groupRow._group;
|
|
840
|
-
for (const row of groupData) {
|
|
841
|
-
allPivotValues.add(String(pivotFn(row)));
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
return data.map((groupRow) => {
|
|
845
|
-
const groupData = groupRow._group;
|
|
846
|
-
const result = {};
|
|
847
|
-
for (const key of Object.keys(groupRow)) {
|
|
848
|
-
if (key !== "_group") result[key] = groupRow[key];
|
|
849
|
-
}
|
|
850
|
-
const subGroups = /* @__PURE__ */ new Map();
|
|
851
|
-
for (const row of groupData) {
|
|
852
|
-
const pivotVal = String(pivotFn(row));
|
|
853
|
-
if (!subGroups.has(pivotVal)) subGroups.set(pivotVal, []);
|
|
854
|
-
subGroups.get(pivotVal).push(row);
|
|
855
|
-
}
|
|
856
|
-
for (const pivotVal of allPivotValues) {
|
|
857
|
-
const subGroupRows = subGroups.get(pivotVal) || [];
|
|
858
|
-
const syntheticRow = { _group: subGroupRows };
|
|
859
|
-
const safeName = pivotVal.replace(/\s+/g, "_");
|
|
860
|
-
for (const { fn, name } of aggCompilers) {
|
|
861
|
-
const colName = singleAggregate ? safeName : `${name}_${safeName}`;
|
|
862
|
-
result[colName] = subGroupRows.length > 0 ? fn(syntheticRow) : defaultForAggregate(name);
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
return result;
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
function computeWindowValues(data, windowDefs) {
|
|
869
|
-
const n = data.length;
|
|
870
|
-
const results = Array.from({ length: n }, () => ({}));
|
|
871
|
-
for (const def of windowDefs) {
|
|
872
|
-
switch (def.name) {
|
|
873
|
-
case "row_number":
|
|
874
|
-
case "running_count":
|
|
875
|
-
for (let i = 0; i < n; i++) results[i][def.id] = i + 1;
|
|
876
|
-
break;
|
|
877
|
-
case "running_sum": {
|
|
878
|
-
let acc = 0;
|
|
879
|
-
for (let i = 0; i < n; i++) {
|
|
880
|
-
acc += Number(def.fieldFn(data[i]));
|
|
881
|
-
results[i][def.id] = acc;
|
|
882
|
-
}
|
|
883
|
-
break;
|
|
884
|
-
}
|
|
885
|
-
case "running_avg": {
|
|
886
|
-
let acc = 0;
|
|
887
|
-
for (let i = 0; i < n; i++) {
|
|
888
|
-
acc += Number(def.fieldFn(data[i]));
|
|
889
|
-
results[i][def.id] = acc / (i + 1);
|
|
890
|
-
}
|
|
891
|
-
break;
|
|
892
|
-
}
|
|
893
|
-
case "running_min": {
|
|
894
|
-
let min = Infinity;
|
|
895
|
-
for (let i = 0; i < n; i++) {
|
|
896
|
-
const val = Number(def.fieldFn(data[i]));
|
|
897
|
-
if (val < min) min = val;
|
|
898
|
-
results[i][def.id] = min;
|
|
899
|
-
}
|
|
900
|
-
break;
|
|
901
|
-
}
|
|
902
|
-
case "running_max": {
|
|
903
|
-
let max = -Infinity;
|
|
904
|
-
for (let i = 0; i < n; i++) {
|
|
905
|
-
const val = Number(def.fieldFn(data[i]));
|
|
906
|
-
if (val > max) max = val;
|
|
907
|
-
results[i][def.id] = max;
|
|
908
|
-
}
|
|
909
|
-
break;
|
|
910
|
-
}
|
|
911
|
-
case "lag": {
|
|
912
|
-
const offset = def.offset;
|
|
913
|
-
for (let i = 0; i < n; i++) {
|
|
914
|
-
results[i][def.id] = i >= offset ? def.fieldFn(data[i - offset]) : null;
|
|
915
|
-
}
|
|
916
|
-
break;
|
|
917
|
-
}
|
|
918
|
-
case "lead": {
|
|
919
|
-
const offset = def.offset;
|
|
920
|
-
for (let i = 0; i < n; i++) {
|
|
921
|
-
results[i][def.id] = i + offset < n ? def.fieldFn(data[i + offset]) : null;
|
|
922
|
-
}
|
|
923
|
-
break;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
return results;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
// src/engine/compiler.ts
|
|
931
|
-
function computeMedian(values) {
|
|
932
|
-
if (values.length === 0) return 0;
|
|
933
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
934
|
-
const mid = Math.floor(sorted.length / 2);
|
|
935
|
-
return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
936
|
-
}
|
|
937
|
-
function computePercentile(values, p) {
|
|
938
|
-
if (values.length === 0) return 0;
|
|
939
|
-
const sorted = [...values].sort((a, b) => a - b);
|
|
940
|
-
const idx = p / 100 * (sorted.length - 1);
|
|
941
|
-
const lo = Math.floor(idx);
|
|
942
|
-
const hi = Math.ceil(idx);
|
|
943
|
-
if (lo === hi) return sorted[lo];
|
|
944
|
-
return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
|
|
945
|
-
}
|
|
946
|
-
function computeVariance(values) {
|
|
947
|
-
if (values.length === 0) return 0;
|
|
948
|
-
const mean = values.reduce((a, b) => a + b, 0) / values.length;
|
|
949
|
-
return values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / values.length;
|
|
950
|
-
}
|
|
951
|
-
function computeStddev(values) {
|
|
952
|
-
return Math.sqrt(computeVariance(values));
|
|
953
|
-
}
|
|
954
|
-
function computeSkew(values) {
|
|
955
|
-
const n = values.length;
|
|
956
|
-
if (n < 3) return 0;
|
|
957
|
-
const mean = values.reduce((a, b) => a + b, 0) / n;
|
|
958
|
-
const sd = computeStddev(values);
|
|
959
|
-
if (sd === 0) return 0;
|
|
960
|
-
return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 3, 0) / n;
|
|
961
|
-
}
|
|
962
|
-
function computeKurt(values) {
|
|
963
|
-
const n = values.length;
|
|
964
|
-
if (n < 4) return 0;
|
|
965
|
-
const mean = values.reduce((a, b) => a + b, 0) / n;
|
|
966
|
-
const sd = computeStddev(values);
|
|
967
|
-
if (sd === 0) return 0;
|
|
968
|
-
return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 4, 0) / n - 3;
|
|
969
|
-
}
|
|
970
|
-
function computeDrawdown(values) {
|
|
971
|
-
if (values.length === 0) return 0;
|
|
972
|
-
let peak = values[0];
|
|
973
|
-
let maxDd = 0;
|
|
974
|
-
for (const v of values) {
|
|
975
|
-
if (v > peak) peak = v;
|
|
976
|
-
const dd = peak === 0 ? 0 : (v - peak) / peak;
|
|
977
|
-
if (dd < maxDd) maxDd = dd;
|
|
978
|
-
}
|
|
979
|
-
return maxDd;
|
|
980
|
-
}
|
|
981
|
-
function computeDownsideDeviation(values, target = 0) {
|
|
982
|
-
if (values.length === 0) return 0;
|
|
983
|
-
const downside = values.filter((v) => v < target).map((v) => (v - target) ** 2);
|
|
984
|
-
if (downside.length === 0) return 0;
|
|
985
|
-
return Math.sqrt(downside.reduce((a, b) => a + b, 0) / values.length);
|
|
986
|
-
}
|
|
987
|
-
var WINDOW_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
988
|
-
"running_sum",
|
|
989
|
-
"running_avg",
|
|
990
|
-
"running_count",
|
|
991
|
-
"running_min",
|
|
992
|
-
"running_max",
|
|
993
|
-
"row_number",
|
|
994
|
-
"lag",
|
|
995
|
-
"lead"
|
|
996
|
-
]);
|
|
997
|
-
var windowIdCounter = 0;
|
|
998
|
-
function compileExpression(expr, grouped = false) {
|
|
999
|
-
switch (expr.kind) {
|
|
1000
|
-
case "NumberLiteral": {
|
|
1001
|
-
const v = expr.value;
|
|
1002
|
-
return () => v;
|
|
1003
|
-
}
|
|
1004
|
-
case "StringLiteral": {
|
|
1005
|
-
const v = expr.value;
|
|
1006
|
-
return () => v;
|
|
1007
|
-
}
|
|
1008
|
-
case "BooleanLiteral": {
|
|
1009
|
-
const v = expr.value;
|
|
1010
|
-
return () => v;
|
|
1011
|
-
}
|
|
1012
|
-
case "NullLiteral":
|
|
1013
|
-
return () => null;
|
|
1014
|
-
case "FieldAccess": {
|
|
1015
|
-
const accessor = getFieldAccessor(expr.path);
|
|
1016
|
-
return accessor;
|
|
1017
|
-
}
|
|
1018
|
-
case "BinaryExpr": {
|
|
1019
|
-
const leftFn = compileExpression(expr.left, grouped);
|
|
1020
|
-
const rightFn = compileExpression(expr.right, grouped);
|
|
1021
|
-
const op = expr.operator;
|
|
1022
|
-
switch (op) {
|
|
1023
|
-
case ">":
|
|
1024
|
-
return (row) => leftFn(row) > rightFn(row);
|
|
1025
|
-
case ">=":
|
|
1026
|
-
return (row) => leftFn(row) >= rightFn(row);
|
|
1027
|
-
case "<":
|
|
1028
|
-
return (row) => leftFn(row) < rightFn(row);
|
|
1029
|
-
case "<=":
|
|
1030
|
-
return (row) => leftFn(row) <= rightFn(row);
|
|
1031
|
-
case "==":
|
|
1032
|
-
return (row) => leftFn(row) === rightFn(row);
|
|
1033
|
-
case "!=":
|
|
1034
|
-
return (row) => leftFn(row) !== rightFn(row);
|
|
1035
|
-
case "&&":
|
|
1036
|
-
return (row) => leftFn(row) && rightFn(row);
|
|
1037
|
-
case "||":
|
|
1038
|
-
return (row) => leftFn(row) || rightFn(row);
|
|
1039
|
-
case "+":
|
|
1040
|
-
return (row) => leftFn(row) + rightFn(row);
|
|
1041
|
-
case "-":
|
|
1042
|
-
return (row) => leftFn(row) - rightFn(row);
|
|
1043
|
-
case "*":
|
|
1044
|
-
return (row) => leftFn(row) * rightFn(row);
|
|
1045
|
-
case "/":
|
|
1046
|
-
return (row) => leftFn(row) / rightFn(row);
|
|
1047
|
-
case "%":
|
|
1048
|
-
return (row) => leftFn(row) % rightFn(row);
|
|
1049
|
-
}
|
|
1050
|
-
break;
|
|
1051
|
-
}
|
|
1052
|
-
case "UnaryExpr": {
|
|
1053
|
-
const operandFn = compileExpression(expr.operand, grouped);
|
|
1054
|
-
if (expr.operator === "!") return (row) => !operandFn(row);
|
|
1055
|
-
return (row) => -operandFn(row);
|
|
1056
|
-
}
|
|
1057
|
-
case "FunctionCall":
|
|
1058
|
-
return compileFunctionCall(expr, grouped);
|
|
1059
|
-
case "AliasExpr":
|
|
1060
|
-
return compileExpression(expr.expression, grouped);
|
|
1061
|
-
}
|
|
1062
|
-
throw new RuntimeError(`Unknown expression kind: ${expr.kind}`);
|
|
1063
|
-
}
|
|
1064
|
-
function compileFunctionCall(expr, grouped) {
|
|
1065
|
-
const { name, args } = expr;
|
|
1066
|
-
if (grouped && AGGREGATE_NAMES.has(name)) {
|
|
1067
|
-
return compileGroupAggregate(name, args);
|
|
1068
|
-
}
|
|
1069
|
-
if (WINDOW_FUNCTION_NAMES.has(name)) {
|
|
1070
|
-
const id = `_w${windowIdCounter++}`;
|
|
1071
|
-
const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
|
|
1072
|
-
const offset = args.length > 1 ? args[1].value || 1 : 1;
|
|
1073
|
-
const fn = ((row) => {
|
|
1074
|
-
const ctx = row._windowCtx;
|
|
1075
|
-
return ctx?.[id];
|
|
1076
|
-
});
|
|
1077
|
-
fn._windowDef = { id, name, fieldFn, offset };
|
|
1078
|
-
return fn;
|
|
1079
|
-
}
|
|
1080
|
-
switch (name) {
|
|
1081
|
-
case "if": {
|
|
1082
|
-
if (args.length < 2 || args.length > 3) {
|
|
1083
|
-
throw new RuntimeError(`if() expects 2 or 3 arguments, got ${args.length}`);
|
|
1084
|
-
}
|
|
1085
|
-
const condFn = compileExpression(args[0], grouped);
|
|
1086
|
-
const thenFn = compileExpression(args[1], grouped);
|
|
1087
|
-
const elseFn = args.length === 3 ? compileExpression(args[2], grouped) : () => null;
|
|
1088
|
-
return (row) => condFn(row) ? thenFn(row) : elseFn(row);
|
|
1089
|
-
}
|
|
1090
|
-
case "coalesce": {
|
|
1091
|
-
const fns = args.map((a) => compileExpression(a, grouped));
|
|
1092
|
-
return (row) => {
|
|
1093
|
-
for (const fn of fns) {
|
|
1094
|
-
const val = fn(row);
|
|
1095
|
-
if (val != null) return val;
|
|
1096
|
-
}
|
|
1097
|
-
return null;
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
case "lower": {
|
|
1101
|
-
const fn = compileExpression(args[0], grouped);
|
|
1102
|
-
return (row) => String(fn(row)).toLowerCase();
|
|
1103
|
-
}
|
|
1104
|
-
case "upper": {
|
|
1105
|
-
const fn = compileExpression(args[0], grouped);
|
|
1106
|
-
return (row) => String(fn(row)).toUpperCase();
|
|
1107
|
-
}
|
|
1108
|
-
case "len": {
|
|
1109
|
-
const fn = compileExpression(args[0], grouped);
|
|
1110
|
-
return (row) => {
|
|
1111
|
-
const val = fn(row);
|
|
1112
|
-
if (Array.isArray(val)) return val.length;
|
|
1113
|
-
if (typeof val === "string") return val.length;
|
|
1114
|
-
return 0;
|
|
1115
|
-
};
|
|
1116
|
-
}
|
|
1117
|
-
case "abs": {
|
|
1118
|
-
const fn = compileExpression(args[0], grouped);
|
|
1119
|
-
return (row) => Math.abs(fn(row));
|
|
1120
|
-
}
|
|
1121
|
-
case "round": {
|
|
1122
|
-
const fn = compileExpression(args[0], grouped);
|
|
1123
|
-
const precFn = args.length > 1 ? compileExpression(args[1], grouped) : () => 0;
|
|
1124
|
-
return (row) => {
|
|
1125
|
-
const val = fn(row);
|
|
1126
|
-
const prec = precFn(row);
|
|
1127
|
-
const factor = Math.pow(10, prec);
|
|
1128
|
-
return Math.round(val * factor) / factor;
|
|
1129
|
-
};
|
|
1130
|
-
}
|
|
1131
|
-
case "concat": {
|
|
1132
|
-
const fns = args.map((a) => compileExpression(a, grouped));
|
|
1133
|
-
return (row) => fns.map((fn) => String(fn(row))).join("");
|
|
1134
|
-
}
|
|
1135
|
-
// ── String functions ────────────────────────────────────
|
|
1136
|
-
case "contains": {
|
|
1137
|
-
if (args.length !== 2) throw new RuntimeError(`contains() expects 2 arguments, got ${args.length}`);
|
|
1138
|
-
const strFn = compileExpression(args[0], grouped);
|
|
1139
|
-
const searchFn = compileExpression(args[1], grouped);
|
|
1140
|
-
return (row) => String(strFn(row)).includes(String(searchFn(row)));
|
|
1141
|
-
}
|
|
1142
|
-
case "startsWith": {
|
|
1143
|
-
if (args.length !== 2) throw new RuntimeError(`startsWith() expects 2 arguments, got ${args.length}`);
|
|
1144
|
-
const strFn = compileExpression(args[0], grouped);
|
|
1145
|
-
const prefixFn = compileExpression(args[1], grouped);
|
|
1146
|
-
return (row) => String(strFn(row)).startsWith(String(prefixFn(row)));
|
|
1147
|
-
}
|
|
1148
|
-
case "endsWith": {
|
|
1149
|
-
if (args.length !== 2) throw new RuntimeError(`endsWith() expects 2 arguments, got ${args.length}`);
|
|
1150
|
-
const strFn = compileExpression(args[0], grouped);
|
|
1151
|
-
const suffixFn = compileExpression(args[1], grouped);
|
|
1152
|
-
return (row) => String(strFn(row)).endsWith(String(suffixFn(row)));
|
|
1153
|
-
}
|
|
1154
|
-
case "trim": {
|
|
1155
|
-
if (args.length !== 1) throw new RuntimeError(`trim() expects 1 argument, got ${args.length}`);
|
|
1156
|
-
const fn = compileExpression(args[0], grouped);
|
|
1157
|
-
return (row) => String(fn(row)).trim();
|
|
1158
|
-
}
|
|
1159
|
-
case "substring": {
|
|
1160
|
-
if (args.length < 2 || args.length > 3) throw new RuntimeError(`substring() expects 2 or 3 arguments, got ${args.length}`);
|
|
1161
|
-
const strFn = compileExpression(args[0], grouped);
|
|
1162
|
-
const startFn = compileExpression(args[1], grouped);
|
|
1163
|
-
const lenFn = args.length === 3 ? compileExpression(args[2], grouped) : void 0;
|
|
1164
|
-
return (row) => {
|
|
1165
|
-
const s = String(strFn(row));
|
|
1166
|
-
const start = Number(startFn(row));
|
|
1167
|
-
if (lenFn) {
|
|
1168
|
-
return s.substring(start, start + Number(lenFn(row)));
|
|
1169
|
-
}
|
|
1170
|
-
return s.substring(start);
|
|
1171
|
-
};
|
|
1172
|
-
}
|
|
1173
|
-
case "replace": {
|
|
1174
|
-
if (args.length !== 3) throw new RuntimeError(`replace() expects 3 arguments, got ${args.length}`);
|
|
1175
|
-
const strFn = compileExpression(args[0], grouped);
|
|
1176
|
-
const searchFn = compileExpression(args[1], grouped);
|
|
1177
|
-
const replaceFn = compileExpression(args[2], grouped);
|
|
1178
|
-
return (row) => String(strFn(row)).replaceAll(String(searchFn(row)), String(replaceFn(row)));
|
|
1179
|
-
}
|
|
1180
|
-
// Non-grouped aggregates used as functions return NaN or undefined — they're
|
|
1181
|
-
// only meaningful as pipeline-level AggregateOps or in grouped selects
|
|
1182
|
-
default:
|
|
1183
|
-
throw new RuntimeError(`Unknown function: ${name}`);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
function compileGroupAggregate(name, args) {
|
|
1187
|
-
const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
|
|
1188
|
-
const secondFn = args.length > 1 ? compileExpression(args[1], false) : void 0;
|
|
1189
|
-
const extractValues = (row) => row._group.map((r) => Number(fieldFn(r)));
|
|
1190
|
-
switch (name) {
|
|
1191
|
-
// ── Core ───────────────────────────────────────────────
|
|
1192
|
-
case "sum":
|
|
1193
|
-
return (row) => {
|
|
1194
|
-
const group = row._group;
|
|
1195
|
-
return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
|
|
1196
|
-
};
|
|
1197
|
-
case "avg":
|
|
1198
|
-
return (row) => {
|
|
1199
|
-
const group = row._group;
|
|
1200
|
-
if (group.length === 0) return 0;
|
|
1201
|
-
const total = group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
|
|
1202
|
-
return total / group.length;
|
|
1203
|
-
};
|
|
1204
|
-
case "min":
|
|
1205
|
-
return (row) => {
|
|
1206
|
-
const group = row._group;
|
|
1207
|
-
if (group.length === 0) return null;
|
|
1208
|
-
return group.reduce((min, r) => {
|
|
1209
|
-
const val = fieldFn(r);
|
|
1210
|
-
return val < min ? val : min;
|
|
1211
|
-
}, fieldFn(group[0]));
|
|
1212
|
-
};
|
|
1213
|
-
case "max":
|
|
1214
|
-
return (row) => {
|
|
1215
|
-
const group = row._group;
|
|
1216
|
-
if (group.length === 0) return null;
|
|
1217
|
-
return group.reduce((max, r) => {
|
|
1218
|
-
const val = fieldFn(r);
|
|
1219
|
-
return val > max ? val : max;
|
|
1220
|
-
}, fieldFn(group[0]));
|
|
1221
|
-
};
|
|
1222
|
-
case "count":
|
|
1223
|
-
return (row) => {
|
|
1224
|
-
const group = row._group;
|
|
1225
|
-
return group.length;
|
|
1226
|
-
};
|
|
1227
|
-
// ── Statistical ───────────────────────────────────────
|
|
1228
|
-
case "median":
|
|
1229
|
-
return (row) => computeMedian(extractValues(row));
|
|
1230
|
-
case "stddev":
|
|
1231
|
-
return (row) => computeStddev(extractValues(row));
|
|
1232
|
-
case "var":
|
|
1233
|
-
return (row) => computeVariance(extractValues(row));
|
|
1234
|
-
case "percentile":
|
|
1235
|
-
return (row) => computePercentile(extractValues(row), Number(secondFn(row)));
|
|
1236
|
-
case "skew":
|
|
1237
|
-
return (row) => computeSkew(extractValues(row));
|
|
1238
|
-
case "kurt":
|
|
1239
|
-
return (row) => computeKurt(extractValues(row));
|
|
1240
|
-
// ── Finance ───────────────────────────────────────────
|
|
1241
|
-
case "vwap":
|
|
1242
|
-
return (row) => {
|
|
1243
|
-
const group = row._group;
|
|
1244
|
-
let sumPV = 0, sumV = 0;
|
|
1245
|
-
for (const r of group) {
|
|
1246
|
-
const p = Number(fieldFn(r));
|
|
1247
|
-
const v = Number(secondFn(r));
|
|
1248
|
-
sumPV += p * v;
|
|
1249
|
-
sumV += v;
|
|
1250
|
-
}
|
|
1251
|
-
return sumV === 0 ? 0 : sumPV / sumV;
|
|
1252
|
-
};
|
|
1253
|
-
case "wavg":
|
|
1254
|
-
return (row) => {
|
|
1255
|
-
const group = row._group;
|
|
1256
|
-
let sumWV = 0, sumW = 0;
|
|
1257
|
-
for (const r of group) {
|
|
1258
|
-
const val = Number(fieldFn(r));
|
|
1259
|
-
const w = Number(secondFn(r));
|
|
1260
|
-
sumWV += val * w;
|
|
1261
|
-
sumW += w;
|
|
1262
|
-
}
|
|
1263
|
-
return sumW === 0 ? 0 : sumWV / sumW;
|
|
1264
|
-
};
|
|
1265
|
-
case "drawdown":
|
|
1266
|
-
return (row) => computeDrawdown(extractValues(row));
|
|
1267
|
-
// ── Ratios ────────────────────────────────────────────
|
|
1268
|
-
case "pct":
|
|
1269
|
-
return (row) => {
|
|
1270
|
-
const group = row._group;
|
|
1271
|
-
return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
|
|
1272
|
-
};
|
|
1273
|
-
case "sharpe":
|
|
1274
|
-
return (row) => {
|
|
1275
|
-
const vals = extractValues(row);
|
|
1276
|
-
if (vals.length === 0) return 0;
|
|
1277
|
-
const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
1278
|
-
const sd = computeStddev(vals);
|
|
1279
|
-
return sd === 0 ? 0 : mean / sd;
|
|
1280
|
-
};
|
|
1281
|
-
case "calmar":
|
|
1282
|
-
return (row) => {
|
|
1283
|
-
const vals = extractValues(row);
|
|
1284
|
-
if (vals.length === 0) return 0;
|
|
1285
|
-
const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
1286
|
-
const dd = Math.abs(computeDrawdown(vals));
|
|
1287
|
-
return dd === 0 ? 0 : mean / dd;
|
|
1288
|
-
};
|
|
1289
|
-
case "sortino":
|
|
1290
|
-
return (row) => {
|
|
1291
|
-
const vals = extractValues(row);
|
|
1292
|
-
if (vals.length === 0) return 0;
|
|
1293
|
-
const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
|
|
1294
|
-
const dsd = computeDownsideDeviation(vals, 0);
|
|
1295
|
-
return dsd === 0 ? 0 : mean / dsd;
|
|
1296
|
-
};
|
|
1297
|
-
case "info_ratio":
|
|
1298
|
-
return (row) => {
|
|
1299
|
-
const group = row._group;
|
|
1300
|
-
const excess = group.map((r) => Number(fieldFn(r)) - Number(secondFn(r)));
|
|
1301
|
-
if (excess.length === 0) return 0;
|
|
1302
|
-
const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
|
|
1303
|
-
const sd = computeStddev(excess);
|
|
1304
|
-
return sd === 0 ? 0 : mean / sd;
|
|
1305
|
-
};
|
|
1306
|
-
// ── Counting ──────────────────────────────────────────
|
|
1307
|
-
case "distinct_count":
|
|
1308
|
-
return (row) => {
|
|
1309
|
-
const group = row._group;
|
|
1310
|
-
return new Set(group.map((r) => fieldFn(r))).size;
|
|
1311
|
-
};
|
|
1312
|
-
case "sum_abs":
|
|
1313
|
-
return (row) => {
|
|
1314
|
-
const group = row._group;
|
|
1315
|
-
return group.reduce((acc, r) => acc + Math.abs(Number(fieldFn(r))), 0);
|
|
1316
|
-
};
|
|
1317
|
-
case "abs_sum":
|
|
1318
|
-
return (row) => {
|
|
1319
|
-
const group = row._group;
|
|
1320
|
-
return Math.abs(group.reduce((acc, r) => acc + Number(fieldFn(r)), 0));
|
|
1321
|
-
};
|
|
1322
|
-
// ── Range ─────────────────────────────────────────────
|
|
1323
|
-
case "first_value":
|
|
1324
|
-
return (row) => {
|
|
1325
|
-
const group = row._group;
|
|
1326
|
-
return group.length > 0 ? fieldFn(group[0]) : null;
|
|
1327
|
-
};
|
|
1328
|
-
case "last_value":
|
|
1329
|
-
return (row) => {
|
|
1330
|
-
const group = row._group;
|
|
1331
|
-
return group.length > 0 ? fieldFn(group[group.length - 1]) : null;
|
|
1332
|
-
};
|
|
1333
|
-
default:
|
|
1334
|
-
throw new RuntimeError(`Unknown aggregate function: ${name}`);
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
function resolveFieldName(expr) {
|
|
1338
|
-
switch (expr.kind) {
|
|
1339
|
-
case "AliasExpr":
|
|
1340
|
-
return expr.alias;
|
|
1341
|
-
case "FieldAccess":
|
|
1342
|
-
return expr.path[expr.path.length - 1];
|
|
1343
|
-
case "FunctionCall":
|
|
1344
|
-
return expr.name;
|
|
1345
|
-
default:
|
|
1346
|
-
return "value";
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
function compileOperation(op, isGrouped) {
|
|
1350
|
-
switch (op.kind) {
|
|
1351
|
-
case "WhereOp": {
|
|
1352
|
-
const condFn = compileExpression(op.condition);
|
|
1353
|
-
return {
|
|
1354
|
-
fn: (data) => data.filter((row) => condFn(row)),
|
|
1355
|
-
producesGrouped: false
|
|
1356
|
-
};
|
|
1357
|
-
}
|
|
1358
|
-
case "SelectOp":
|
|
1359
|
-
return compileSelectOp(op, isGrouped);
|
|
1360
|
-
case "SortOp": {
|
|
1361
|
-
const criteria = op.criteria.map((c) => ({
|
|
1362
|
-
fn: compileExpression(c.expression),
|
|
1363
|
-
dir: c.direction === "desc" ? -1 : 1
|
|
1364
|
-
}));
|
|
1365
|
-
return {
|
|
1366
|
-
fn: (data) => {
|
|
1367
|
-
return [...data].sort((a, b) => {
|
|
1368
|
-
for (const { fn, dir } of criteria) {
|
|
1369
|
-
const cmp = compareValues(fn(a), fn(b));
|
|
1370
|
-
if (cmp !== 0) return cmp * dir;
|
|
1371
|
-
}
|
|
1372
|
-
return 0;
|
|
1373
|
-
});
|
|
1374
|
-
},
|
|
1375
|
-
producesGrouped: false
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1378
|
-
case "GroupByOp": {
|
|
1379
|
-
const keyFns = op.keys.map((k) => {
|
|
1380
|
-
const fn = compileExpression(k);
|
|
1381
|
-
const name = k.kind === "FieldAccess" ? k.path[k.path.length - 1] : "key";
|
|
1382
|
-
return { fn, name };
|
|
1383
|
-
});
|
|
1384
|
-
return {
|
|
1385
|
-
fn: (data) => {
|
|
1386
|
-
const groups = groupByFn(data, keyFns);
|
|
1387
|
-
return groups.map((g) => ({ ...g.keys, _group: g.rows }));
|
|
1388
|
-
},
|
|
1389
|
-
producesGrouped: true
|
|
1390
|
-
};
|
|
1391
|
-
}
|
|
1392
|
-
case "JoinOp": {
|
|
1393
|
-
const rightSource = op.right;
|
|
1394
|
-
if (op.condition.kind === "BinaryExpr" && op.condition.operator === "==" && op.condition.left.kind === "FieldAccess" && op.condition.right.kind === "FieldAccess") {
|
|
1395
|
-
const leftKeyFn = compileExpression(op.condition.left);
|
|
1396
|
-
const rightKeyFn = compileExpression(op.condition.right);
|
|
1397
|
-
return {
|
|
1398
|
-
fn: (data, context) => {
|
|
1399
|
-
const rightData = context[rightSource];
|
|
1400
|
-
if (!rightData) {
|
|
1401
|
-
throw new RuntimeError(`Join source "${rightSource}" not found in context`);
|
|
1402
|
-
}
|
|
1403
|
-
return hashJoin(data, rightData, leftKeyFn, rightKeyFn);
|
|
1404
|
-
},
|
|
1405
|
-
producesGrouped: false
|
|
1406
|
-
};
|
|
1407
|
-
}
|
|
1408
|
-
const condFn = compileExpression(op.condition);
|
|
1409
|
-
return {
|
|
1410
|
-
fn: (data, context) => {
|
|
1411
|
-
const rightData = context[rightSource];
|
|
1412
|
-
if (!rightData) {
|
|
1413
|
-
throw new RuntimeError(`Join source "${rightSource}" not found in context`);
|
|
1414
|
-
}
|
|
1415
|
-
return nestedLoopJoin(data, rightData, condFn);
|
|
1416
|
-
},
|
|
1417
|
-
producesGrouped: false
|
|
1418
|
-
};
|
|
1419
|
-
}
|
|
1420
|
-
case "FirstOp": {
|
|
1421
|
-
const count = op.count;
|
|
1422
|
-
return {
|
|
1423
|
-
fn: (data) => data.slice(0, count),
|
|
1424
|
-
producesGrouped: false
|
|
1425
|
-
};
|
|
1426
|
-
}
|
|
1427
|
-
case "LastOp": {
|
|
1428
|
-
const count = op.count;
|
|
1429
|
-
return {
|
|
1430
|
-
fn: (data) => data.slice(-count),
|
|
1431
|
-
producesGrouped: false
|
|
1432
|
-
};
|
|
1433
|
-
}
|
|
1434
|
-
case "DistinctOp": {
|
|
1435
|
-
const keyFn = op.fields ? (() => {
|
|
1436
|
-
const fns = op.fields.map((f) => compileExpression(f));
|
|
1437
|
-
return (row) => JSON.stringify(fns.map((fn) => fn(row)));
|
|
1438
|
-
})() : void 0;
|
|
1439
|
-
return {
|
|
1440
|
-
fn: (data) => distinctFn(data, keyFn),
|
|
1441
|
-
producesGrouped: false
|
|
1442
|
-
};
|
|
1443
|
-
}
|
|
1444
|
-
case "FlattenOp": {
|
|
1445
|
-
if (op.field) {
|
|
1446
|
-
const fieldFn = compileExpression(op.field);
|
|
1447
|
-
return {
|
|
1448
|
-
fn: (data) => data.flatMap((row) => {
|
|
1449
|
-
const val = fieldFn(row);
|
|
1450
|
-
return Array.isArray(val) ? val : [val];
|
|
1451
|
-
}),
|
|
1452
|
-
producesGrouped: false
|
|
1453
|
-
};
|
|
1454
|
-
}
|
|
1455
|
-
return {
|
|
1456
|
-
fn: (data) => data.flat(),
|
|
1457
|
-
producesGrouped: false
|
|
1458
|
-
};
|
|
1459
|
-
}
|
|
1460
|
-
case "TransposeOp": {
|
|
1461
|
-
const headerFn = op.headerField ? compileExpression(op.headerField) : void 0;
|
|
1462
|
-
const headerFieldName = op.headerField ? resolveFieldName(op.headerField) : void 0;
|
|
1463
|
-
return {
|
|
1464
|
-
fn: (data) => transposeFn(data, headerFn, headerFieldName),
|
|
1465
|
-
producesGrouped: false
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
case "RollupOp": {
|
|
1469
|
-
const keyFns = op.keys.map((k) => {
|
|
1470
|
-
const fn = compileExpression(k);
|
|
1471
|
-
const name = resolveFieldName(k);
|
|
1472
|
-
return { fn, name };
|
|
1473
|
-
});
|
|
1474
|
-
const aggFns = op.aggregates.map((a) => {
|
|
1475
|
-
const fn = compileExpression(a, true);
|
|
1476
|
-
const name = resolveFieldName(a);
|
|
1477
|
-
return { fn, name };
|
|
1478
|
-
});
|
|
1479
|
-
const pctCols = /* @__PURE__ */ new Set();
|
|
1480
|
-
for (const a of op.aggregates) {
|
|
1481
|
-
const expr = a.kind === "AliasExpr" ? a.expression : a;
|
|
1482
|
-
if (expr.kind === "FunctionCall" && expr.name === "pct") {
|
|
1483
|
-
pctCols.add(resolveFieldName(a));
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
return {
|
|
1487
|
-
fn: (data) => {
|
|
1488
|
-
const results = rollupFn(data, keyFns, aggFns);
|
|
1489
|
-
if (pctCols.size > 0) {
|
|
1490
|
-
const numKeys = keyFns.length;
|
|
1491
|
-
const grandRow = results.find((r) => r._rollupLevel === numKeys);
|
|
1492
|
-
for (const col of pctCols) {
|
|
1493
|
-
const total = Number(grandRow?.[col]) || 1;
|
|
1494
|
-
for (const row of results) {
|
|
1495
|
-
row[col] = Number(row[col]) / total * 100;
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
if (pctCols.size === aggFns.length) {
|
|
1499
|
-
return results.filter((r) => r._rollupLevel !== numKeys);
|
|
1500
|
-
}
|
|
1501
|
-
}
|
|
1502
|
-
return results;
|
|
1503
|
-
},
|
|
1504
|
-
producesGrouped: false
|
|
1505
|
-
};
|
|
1506
|
-
}
|
|
1507
|
-
case "PivotOp": {
|
|
1508
|
-
const pivotFn = compileExpression(op.pivotField);
|
|
1509
|
-
const aggCompilers = op.aggregates.map((a) => ({
|
|
1510
|
-
fn: compileExpression(a, true),
|
|
1511
|
-
// grouped = true
|
|
1512
|
-
name: resolveFieldName(a)
|
|
1513
|
-
}));
|
|
1514
|
-
const singleAggregate = aggCompilers.length === 1;
|
|
1515
|
-
return {
|
|
1516
|
-
fn: (data) => {
|
|
1517
|
-
if (isGrouped) {
|
|
1518
|
-
return pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate);
|
|
1519
|
-
}
|
|
1520
|
-
return pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate);
|
|
1521
|
-
},
|
|
1522
|
-
producesGrouped: false
|
|
1523
|
-
};
|
|
1524
|
-
}
|
|
1525
|
-
case "MapOp": {
|
|
1526
|
-
const fieldCompilers = op.fields.map((field) => {
|
|
1527
|
-
const name = resolveFieldName(field);
|
|
1528
|
-
const fn = compileExpression(field, isGrouped);
|
|
1529
|
-
return { name, fn };
|
|
1530
|
-
});
|
|
1531
|
-
return {
|
|
1532
|
-
fn: (data) => data.map((row) => {
|
|
1533
|
-
const result = { ...row };
|
|
1534
|
-
for (const { name, fn } of fieldCompilers) {
|
|
1535
|
-
result[name] = fn(row);
|
|
1536
|
-
}
|
|
1537
|
-
return result;
|
|
1538
|
-
}),
|
|
1539
|
-
producesGrouped: isGrouped
|
|
1540
|
-
};
|
|
1541
|
-
}
|
|
1542
|
-
case "ReduceOp": {
|
|
1543
|
-
const initialFn = compileExpression(op.initial);
|
|
1544
|
-
const accFn = compileExpression(op.accumulator);
|
|
1545
|
-
return {
|
|
1546
|
-
fn: (data) => {
|
|
1547
|
-
let acc = initialFn({});
|
|
1548
|
-
for (const row of data) {
|
|
1549
|
-
acc = accFn({ ...row, _acc: acc });
|
|
1550
|
-
}
|
|
1551
|
-
return acc;
|
|
1552
|
-
},
|
|
1553
|
-
producesGrouped: false
|
|
1554
|
-
};
|
|
1555
|
-
}
|
|
1556
|
-
case "AggregateOp": {
|
|
1557
|
-
const argFns = op.args?.map((a) => compileExpression(a));
|
|
1558
|
-
const primaryFn = argFns ? argFns[0] : op.field ? compileExpression(op.field) : void 0;
|
|
1559
|
-
const secondFn = argFns ? argFns[1] : void 0;
|
|
1560
|
-
const aggName = op.function;
|
|
1561
|
-
return {
|
|
1562
|
-
fn: (data) => {
|
|
1563
|
-
const vals = () => data.map((row) => Number(primaryFn(row)));
|
|
1564
|
-
switch (aggName) {
|
|
1565
|
-
// ── Core ──────────────────────────────────────
|
|
1566
|
-
case "count":
|
|
1567
|
-
return data.length;
|
|
1568
|
-
case "sum":
|
|
1569
|
-
return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
|
|
1570
|
-
case "avg": {
|
|
1571
|
-
if (data.length === 0) return 0;
|
|
1572
|
-
const total = data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
|
|
1573
|
-
return total / data.length;
|
|
1574
|
-
}
|
|
1575
|
-
case "min":
|
|
1576
|
-
return data.reduce((min, row) => {
|
|
1577
|
-
const val = primaryFn(row);
|
|
1578
|
-
return val < min ? val : min;
|
|
1579
|
-
}, primaryFn(data[0]));
|
|
1580
|
-
case "max":
|
|
1581
|
-
return data.reduce((max, row) => {
|
|
1582
|
-
const val = primaryFn(row);
|
|
1583
|
-
return val > max ? val : max;
|
|
1584
|
-
}, primaryFn(data[0]));
|
|
1585
|
-
// ── Statistical ───────────────────────────────
|
|
1586
|
-
case "median":
|
|
1587
|
-
return computeMedian(vals());
|
|
1588
|
-
case "stddev":
|
|
1589
|
-
return computeStddev(vals());
|
|
1590
|
-
case "var":
|
|
1591
|
-
return computeVariance(vals());
|
|
1592
|
-
case "percentile":
|
|
1593
|
-
return computePercentile(vals(), Number(secondFn(data[0])));
|
|
1594
|
-
case "skew":
|
|
1595
|
-
return computeSkew(vals());
|
|
1596
|
-
case "kurt":
|
|
1597
|
-
return computeKurt(vals());
|
|
1598
|
-
// ── Finance ───────────────────────────────────
|
|
1599
|
-
case "vwap": {
|
|
1600
|
-
let sumPV = 0, sumV = 0;
|
|
1601
|
-
for (const row of data) {
|
|
1602
|
-
const p = Number(primaryFn(row));
|
|
1603
|
-
const v = Number(secondFn(row));
|
|
1604
|
-
sumPV += p * v;
|
|
1605
|
-
sumV += v;
|
|
1606
|
-
}
|
|
1607
|
-
return sumV === 0 ? 0 : sumPV / sumV;
|
|
1608
|
-
}
|
|
1609
|
-
case "wavg": {
|
|
1610
|
-
let sumWV = 0, sumW = 0;
|
|
1611
|
-
for (const row of data) {
|
|
1612
|
-
const val = Number(primaryFn(row));
|
|
1613
|
-
const w = Number(secondFn(row));
|
|
1614
|
-
sumWV += val * w;
|
|
1615
|
-
sumW += w;
|
|
1616
|
-
}
|
|
1617
|
-
return sumW === 0 ? 0 : sumWV / sumW;
|
|
1618
|
-
}
|
|
1619
|
-
case "drawdown":
|
|
1620
|
-
return computeDrawdown(vals());
|
|
1621
|
-
// ── Ratios ────────────────────────────────────
|
|
1622
|
-
case "pct": {
|
|
1623
|
-
return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
|
|
1624
|
-
}
|
|
1625
|
-
case "sharpe": {
|
|
1626
|
-
const v = vals();
|
|
1627
|
-
if (v.length === 0) return 0;
|
|
1628
|
-
const mean = v.reduce((a, b) => a + b, 0) / v.length;
|
|
1629
|
-
const sd = computeStddev(v);
|
|
1630
|
-
return sd === 0 ? 0 : mean / sd;
|
|
1631
|
-
}
|
|
1632
|
-
case "calmar": {
|
|
1633
|
-
const v = vals();
|
|
1634
|
-
if (v.length === 0) return 0;
|
|
1635
|
-
const mean = v.reduce((a, b) => a + b, 0) / v.length;
|
|
1636
|
-
const dd = Math.abs(computeDrawdown(v));
|
|
1637
|
-
return dd === 0 ? 0 : mean / dd;
|
|
1638
|
-
}
|
|
1639
|
-
case "sortino": {
|
|
1640
|
-
const v = vals();
|
|
1641
|
-
if (v.length === 0) return 0;
|
|
1642
|
-
const mean = v.reduce((a, b) => a + b, 0) / v.length;
|
|
1643
|
-
const dsd = computeDownsideDeviation(v, 0);
|
|
1644
|
-
return dsd === 0 ? 0 : mean / dsd;
|
|
1645
|
-
}
|
|
1646
|
-
case "info_ratio": {
|
|
1647
|
-
const excess = data.map((row) => Number(primaryFn(row)) - Number(secondFn(row)));
|
|
1648
|
-
if (excess.length === 0) return 0;
|
|
1649
|
-
const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
|
|
1650
|
-
const sd = computeStddev(excess);
|
|
1651
|
-
return sd === 0 ? 0 : mean / sd;
|
|
1652
|
-
}
|
|
1653
|
-
// ── Counting ──────────────────────────────────
|
|
1654
|
-
case "distinct_count":
|
|
1655
|
-
return new Set(data.map((row) => primaryFn(row))).size;
|
|
1656
|
-
case "sum_abs":
|
|
1657
|
-
return data.reduce((acc, row) => acc + Math.abs(Number(primaryFn(row))), 0);
|
|
1658
|
-
case "abs_sum":
|
|
1659
|
-
return Math.abs(data.reduce((acc, row) => acc + Number(primaryFn(row)), 0));
|
|
1660
|
-
// ── Range ─────────────────────────────────────
|
|
1661
|
-
case "first_value":
|
|
1662
|
-
return data.length > 0 ? primaryFn(data[0]) : null;
|
|
1663
|
-
case "last_value":
|
|
1664
|
-
return data.length > 0 ? primaryFn(data[data.length - 1]) : null;
|
|
1665
|
-
}
|
|
1666
|
-
},
|
|
1667
|
-
producesGrouped: false
|
|
1668
|
-
};
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
}
|
|
1672
|
-
function compileSelectOp(op, isGrouped) {
|
|
1673
|
-
const fieldCompilers = op.fields.map((field) => {
|
|
1674
|
-
const name = resolveFieldName(field);
|
|
1675
|
-
const fn = compileExpression(field, isGrouped);
|
|
1676
|
-
return { name, fn };
|
|
1677
|
-
});
|
|
1678
|
-
const windowDefs = [];
|
|
1679
|
-
for (const { fn } of fieldCompilers) {
|
|
1680
|
-
const wDef = fn._windowDef;
|
|
1681
|
-
if (wDef) windowDefs.push(wDef);
|
|
1682
|
-
}
|
|
1683
|
-
if (windowDefs.length > 0) {
|
|
1684
|
-
return {
|
|
1685
|
-
fn: (data) => {
|
|
1686
|
-
const windowValues = computeWindowValues(data, windowDefs);
|
|
1687
|
-
return data.map((row, i) => {
|
|
1688
|
-
const enrichedRow = { ...row, _windowCtx: windowValues[i] };
|
|
1689
|
-
const result = {};
|
|
1690
|
-
for (const { name, fn } of fieldCompilers) {
|
|
1691
|
-
result[name] = fn(enrichedRow);
|
|
1692
|
-
}
|
|
1693
|
-
return result;
|
|
1694
|
-
});
|
|
1695
|
-
},
|
|
1696
|
-
producesGrouped: false
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
return {
|
|
1700
|
-
fn: (data) => {
|
|
1701
|
-
return data.map((row) => {
|
|
1702
|
-
const result = {};
|
|
1703
|
-
for (const { name, fn } of fieldCompilers) {
|
|
1704
|
-
result[name] = fn(row);
|
|
1705
|
-
}
|
|
1706
|
-
return result;
|
|
1707
|
-
});
|
|
1708
|
-
},
|
|
1709
|
-
producesGrouped: false
|
|
1710
|
-
};
|
|
1711
|
-
}
|
|
1712
|
-
function compile(ast, source) {
|
|
1713
|
-
const compiledOps = [];
|
|
1714
|
-
let isGrouped = false;
|
|
1715
|
-
for (const op of ast.operations) {
|
|
1716
|
-
const { fn, producesGrouped } = compileOperation(op, isGrouped);
|
|
1717
|
-
compiledOps.push(fn);
|
|
1718
|
-
isGrouped = producesGrouped;
|
|
1719
|
-
}
|
|
1720
|
-
const sourceName = ast.source;
|
|
1721
|
-
const queryFn = (context) => {
|
|
1722
|
-
const sourceData = context[sourceName];
|
|
1723
|
-
if (!sourceData) {
|
|
1724
|
-
throw new RuntimeError(`Source "${sourceName}" not found in context`);
|
|
1725
|
-
}
|
|
1726
|
-
if (!Array.isArray(sourceData)) {
|
|
1727
|
-
throw new RuntimeError(`Source "${sourceName}" is not an array`);
|
|
1728
|
-
}
|
|
1729
|
-
let data = sourceData;
|
|
1730
|
-
for (const opFn of compiledOps) {
|
|
1731
|
-
if (!Array.isArray(data)) {
|
|
1732
|
-
return data;
|
|
1733
|
-
}
|
|
1734
|
-
data = opFn(data, context);
|
|
1735
|
-
}
|
|
1736
|
-
return data;
|
|
1737
|
-
};
|
|
1738
|
-
const compiled = queryFn;
|
|
1739
|
-
Object.defineProperty(compiled, "source", { value: source, writable: false });
|
|
1740
|
-
Object.defineProperty(compiled, "ast", { value: ast, writable: false });
|
|
1741
|
-
return compiled;
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
// src/engine/cache.ts
|
|
1745
|
-
var QueryCache = class {
|
|
1746
|
-
cache = /* @__PURE__ */ new Map();
|
|
1747
|
-
maxSize;
|
|
1748
|
-
constructor(maxSize = 128) {
|
|
1749
|
-
this.maxSize = maxSize;
|
|
1750
|
-
}
|
|
1751
|
-
get(key) {
|
|
1752
|
-
const value = this.cache.get(key);
|
|
1753
|
-
if (value) {
|
|
1754
|
-
this.cache.delete(key);
|
|
1755
|
-
this.cache.set(key, value);
|
|
1756
|
-
}
|
|
1757
|
-
return value;
|
|
1758
|
-
}
|
|
1759
|
-
set(key, value) {
|
|
1760
|
-
if (this.cache.has(key)) {
|
|
1761
|
-
this.cache.delete(key);
|
|
1762
|
-
} else if (this.cache.size >= this.maxSize) {
|
|
1763
|
-
const firstKey = this.cache.keys().next().value;
|
|
1764
|
-
if (firstKey !== void 0) {
|
|
1765
|
-
this.cache.delete(firstKey);
|
|
1766
|
-
}
|
|
1767
|
-
}
|
|
1768
|
-
this.cache.set(key, value);
|
|
1769
|
-
}
|
|
1770
|
-
clear() {
|
|
1771
|
-
this.cache.clear();
|
|
1772
|
-
}
|
|
1773
|
-
get size() {
|
|
1774
|
-
return this.cache.size;
|
|
1775
|
-
}
|
|
1776
|
-
};
|
|
1777
|
-
|
|
1778
|
-
// src/engine/live.ts
|
|
1779
|
-
var OPERATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1780
|
-
"where",
|
|
1781
|
-
"select",
|
|
1782
|
-
"sort",
|
|
1783
|
-
"groupBy",
|
|
1784
|
-
"join",
|
|
1785
|
-
"first",
|
|
1786
|
-
"last",
|
|
1787
|
-
"distinct",
|
|
1788
|
-
"map",
|
|
1789
|
-
"reduce",
|
|
1790
|
-
"rollup",
|
|
1791
|
-
"pivot",
|
|
1792
|
-
"flatten",
|
|
1793
|
-
"transpose"
|
|
1794
|
-
]);
|
|
1795
|
-
function normalizeExpression(expression) {
|
|
1796
|
-
const trimmed = expression.trimStart();
|
|
1797
|
-
const match = trimmed.match(/^(\w+)\s*\(/);
|
|
1798
|
-
if (match && OPERATION_KEYWORDS.has(match[1])) {
|
|
1799
|
-
return `_data | ${trimmed}`;
|
|
1800
|
-
}
|
|
1801
|
-
return expression;
|
|
1802
|
-
}
|
|
1803
|
-
var LiveQuery = class {
|
|
1804
|
-
_index;
|
|
1805
|
-
_keyFields;
|
|
1806
|
-
_sourceName;
|
|
1807
|
-
_context;
|
|
1808
|
-
_compiled;
|
|
1809
|
-
_throttle;
|
|
1810
|
-
_pendingTimer;
|
|
1811
|
-
_executing;
|
|
1812
|
-
_dirtyDuringExec;
|
|
1813
|
-
_result;
|
|
1814
|
-
_stats;
|
|
1815
|
-
_subscribers;
|
|
1816
|
-
_batchDepth;
|
|
1817
|
-
_batchDirty;
|
|
1818
|
-
_disposed;
|
|
1819
|
-
constructor(data, expression, options) {
|
|
1820
|
-
this._keyFields = Array.isArray(options.key) ? options.key : [options.key];
|
|
1821
|
-
this._throttle = options.throttle ?? 0;
|
|
1822
|
-
this._pendingTimer = null;
|
|
1823
|
-
this._executing = false;
|
|
1824
|
-
this._dirtyDuringExec = false;
|
|
1825
|
-
this._subscribers = /* @__PURE__ */ new Set();
|
|
1826
|
-
this._batchDepth = 0;
|
|
1827
|
-
this._batchDirty = false;
|
|
1828
|
-
this._disposed = false;
|
|
1829
|
-
this._result = void 0;
|
|
1830
|
-
this._stats = {
|
|
1831
|
-
patchMs: 0,
|
|
1832
|
-
executionMs: 0,
|
|
1833
|
-
totalMs: 0,
|
|
1834
|
-
rowCount: 0,
|
|
1835
|
-
resultCount: 0,
|
|
1836
|
-
patchCount: 0,
|
|
1837
|
-
tick: 0
|
|
1838
|
-
};
|
|
1839
|
-
if (Array.isArray(data)) {
|
|
1840
|
-
this._sourceName = "_data";
|
|
1841
|
-
this._compiled = compile2(normalizeExpression(expression));
|
|
1842
|
-
this._context = { _data: [] };
|
|
1843
|
-
this._index = /* @__PURE__ */ new Map();
|
|
1844
|
-
this._buildIndex(data);
|
|
1845
|
-
} else {
|
|
1846
|
-
this._sourceName = options.source ?? Object.keys(data)[0];
|
|
1847
|
-
this._compiled = compile2(expression);
|
|
1848
|
-
this._context = { ...data };
|
|
1849
|
-
this._index = /* @__PURE__ */ new Map();
|
|
1850
|
-
const sourceData = data[this._sourceName];
|
|
1851
|
-
if (Array.isArray(sourceData)) {
|
|
1852
|
-
this._buildIndex(sourceData);
|
|
1853
|
-
}
|
|
1854
|
-
}
|
|
1855
|
-
this._executeSync();
|
|
1856
|
-
}
|
|
1857
|
-
// ── Public getters ─────────────────────────────────────────────
|
|
1858
|
-
get result() {
|
|
1859
|
-
return this._result;
|
|
1860
|
-
}
|
|
1861
|
-
get stats() {
|
|
1862
|
-
return { ...this._stats };
|
|
1863
|
-
}
|
|
1864
|
-
get size() {
|
|
1865
|
-
return this._index.size;
|
|
1866
|
-
}
|
|
1867
|
-
// ── Subscription ───────────────────────────────────────────────
|
|
1868
|
-
subscribe(fn) {
|
|
1869
|
-
this._guardDisposed();
|
|
1870
|
-
this._subscribers.add(fn);
|
|
1871
|
-
return () => {
|
|
1872
|
-
this._subscribers.delete(fn);
|
|
1873
|
-
};
|
|
1874
|
-
}
|
|
1875
|
-
// ── Patching ───────────────────────────────────────────────────
|
|
1876
|
-
patch(changed, removals) {
|
|
1877
|
-
this._guardDisposed();
|
|
1878
|
-
const patchStart = performance.now();
|
|
1879
|
-
for (const row of changed) {
|
|
1880
|
-
const mapKey = this._rowKey(row);
|
|
1881
|
-
this._index.set(mapKey, row);
|
|
1882
|
-
}
|
|
1883
|
-
if (removals) {
|
|
1884
|
-
for (const removal of removals) {
|
|
1885
|
-
const mapKey = this._removalKey(removal);
|
|
1886
|
-
this._index.delete(mapKey);
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
this._stats.patchMs = performance.now() - patchStart;
|
|
1890
|
-
this._stats.patchCount++;
|
|
1891
|
-
if (this._batchDepth > 0) {
|
|
1892
|
-
this._batchDirty = true;
|
|
1893
|
-
return;
|
|
1894
|
-
}
|
|
1895
|
-
if (this._executing) {
|
|
1896
|
-
this._dirtyDuringExec = true;
|
|
1897
|
-
return;
|
|
1898
|
-
}
|
|
1899
|
-
this._scheduleExecution();
|
|
1900
|
-
}
|
|
1901
|
-
// ── Reset ──────────────────────────────────────────────────────
|
|
1902
|
-
reset(data) {
|
|
1903
|
-
this._guardDisposed();
|
|
1904
|
-
this._index.clear();
|
|
1905
|
-
this._buildIndex(data);
|
|
1906
|
-
this._stats.patchMs = 0;
|
|
1907
|
-
this._scheduleExecution();
|
|
1908
|
-
}
|
|
1909
|
-
// ── Query change ───────────────────────────────────────────────
|
|
1910
|
-
setQuery(expression) {
|
|
1911
|
-
this._guardDisposed();
|
|
1912
|
-
const normalized = this._sourceName === "_data" ? normalizeExpression(expression) : expression;
|
|
1913
|
-
this._compiled = compile2(normalized);
|
|
1914
|
-
this._stats.patchMs = 0;
|
|
1915
|
-
this._scheduleExecution();
|
|
1916
|
-
}
|
|
1917
|
-
// ── Batching ───────────────────────────────────────────────────
|
|
1918
|
-
beginBatch() {
|
|
1919
|
-
this._guardDisposed();
|
|
1920
|
-
this._batchDepth++;
|
|
1921
|
-
}
|
|
1922
|
-
endBatch() {
|
|
1923
|
-
this._guardDisposed();
|
|
1924
|
-
if (this._batchDepth <= 0) {
|
|
1925
|
-
throw new RuntimeError("LiveQuery: endBatch() called without matching beginBatch()");
|
|
1926
|
-
}
|
|
1927
|
-
this._batchDepth--;
|
|
1928
|
-
if (this._batchDepth === 0 && this._batchDirty) {
|
|
1929
|
-
this._batchDirty = false;
|
|
1930
|
-
this._scheduleExecution();
|
|
1931
|
-
}
|
|
1932
|
-
}
|
|
1933
|
-
// ── Cleanup ────────────────────────────────────────────────────
|
|
1934
|
-
dispose() {
|
|
1935
|
-
if (this._pendingTimer !== null) {
|
|
1936
|
-
clearTimeout(this._pendingTimer);
|
|
1937
|
-
this._pendingTimer = null;
|
|
1938
|
-
}
|
|
1939
|
-
this._subscribers.clear();
|
|
1940
|
-
this._index.clear();
|
|
1941
|
-
this._result = void 0;
|
|
1942
|
-
this._disposed = true;
|
|
1943
|
-
}
|
|
1944
|
-
// ── Private: key helpers ───────────────────────────────────────
|
|
1945
|
-
_rowKey(row) {
|
|
1946
|
-
if (this._keyFields.length === 1) {
|
|
1947
|
-
const val = row[this._keyFields[0]];
|
|
1948
|
-
if (val == null) {
|
|
1949
|
-
throw new RuntimeError(
|
|
1950
|
-
`LiveQuery: row is missing key field "${this._keyFields[0]}"`
|
|
1951
|
-
);
|
|
1952
|
-
}
|
|
1953
|
-
return String(val);
|
|
1954
|
-
}
|
|
1955
|
-
let key = "";
|
|
1956
|
-
for (let i = 0; i < this._keyFields.length; i++) {
|
|
1957
|
-
const val = row[this._keyFields[i]];
|
|
1958
|
-
if (val == null) {
|
|
1959
|
-
throw new RuntimeError(
|
|
1960
|
-
`LiveQuery: row is missing key field "${this._keyFields[i]}"`
|
|
1961
|
-
);
|
|
1962
|
-
}
|
|
1963
|
-
if (i > 0) key += "\0";
|
|
1964
|
-
key += String(val);
|
|
1965
|
-
}
|
|
1966
|
-
return key;
|
|
1967
|
-
}
|
|
1968
|
-
_removalKey(removal) {
|
|
1969
|
-
if (typeof removal === "string") {
|
|
1970
|
-
return removal;
|
|
1971
|
-
}
|
|
1972
|
-
return removal.map(String).join("\0");
|
|
1973
|
-
}
|
|
1974
|
-
// ── Private: index ─────────────────────────────────────────────
|
|
1975
|
-
_buildIndex(rows) {
|
|
1976
|
-
for (const row of rows) {
|
|
1977
|
-
const key = this._rowKey(row);
|
|
1978
|
-
this._index.set(key, row);
|
|
1979
|
-
}
|
|
1980
|
-
}
|
|
1981
|
-
// ── Private: execution scheduling ──────────────────────────────
|
|
1982
|
-
_scheduleExecution() {
|
|
1983
|
-
if (this._pendingTimer !== null) return;
|
|
1984
|
-
if (this._throttle === 0) {
|
|
1985
|
-
this._pendingTimer = setTimeout(() => this._onTimer(), 0);
|
|
1986
|
-
} else {
|
|
1987
|
-
this._pendingTimer = setTimeout(() => this._onTimer(), this._throttle);
|
|
1988
|
-
}
|
|
1989
|
-
}
|
|
1990
|
-
_onTimer() {
|
|
1991
|
-
this._pendingTimer = null;
|
|
1992
|
-
if (this._disposed) return;
|
|
1993
|
-
this._executeSync();
|
|
1994
|
-
if (this._dirtyDuringExec) {
|
|
1995
|
-
this._dirtyDuringExec = false;
|
|
1996
|
-
this._scheduleExecution();
|
|
1997
|
-
}
|
|
1998
|
-
}
|
|
1999
|
-
_executeSync() {
|
|
2000
|
-
this._executing = true;
|
|
2001
|
-
this._dirtyDuringExec = false;
|
|
2002
|
-
const rows = Array.from(this._index.values());
|
|
2003
|
-
this._context[this._sourceName] = rows;
|
|
2004
|
-
const execStart = performance.now();
|
|
2005
|
-
this._result = this._compiled(this._context);
|
|
2006
|
-
const executionMs = performance.now() - execStart;
|
|
2007
|
-
this._stats.executionMs = executionMs;
|
|
2008
|
-
this._stats.totalMs = this._stats.patchMs + executionMs;
|
|
2009
|
-
this._stats.rowCount = rows.length;
|
|
2010
|
-
this._stats.resultCount = Array.isArray(this._result) ? this._result.length : this._result != null ? 1 : 0;
|
|
2011
|
-
this._stats.tick++;
|
|
2012
|
-
this._executing = false;
|
|
2013
|
-
this._notify();
|
|
2014
|
-
}
|
|
2015
|
-
_notify() {
|
|
2016
|
-
const result = this._result;
|
|
2017
|
-
const stats = { ...this._stats };
|
|
2018
|
-
for (const fn of this._subscribers) {
|
|
2019
|
-
try {
|
|
2020
|
-
fn(result, stats);
|
|
2021
|
-
} catch (err) {
|
|
2022
|
-
console.error("LiveQuery subscriber error:", err);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
}
|
|
2026
|
-
_guardDisposed() {
|
|
2027
|
-
if (this._disposed) {
|
|
2028
|
-
throw new RuntimeError("LiveQuery: instance has been disposed");
|
|
2029
|
-
}
|
|
2030
|
-
}
|
|
2031
|
-
};
|
|
2032
|
-
function liveQuery(data, expression, options) {
|
|
2033
|
-
return new LiveQuery(data, expression, options);
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// src/engine/index.ts
|
|
2037
|
-
var defaultCache = new QueryCache(128);
|
|
2038
|
-
var OPERATION_KEYWORDS2 = /* @__PURE__ */ new Set([
|
|
2039
|
-
"where",
|
|
2040
|
-
"select",
|
|
2041
|
-
"sort",
|
|
2042
|
-
"groupBy",
|
|
2043
|
-
"join",
|
|
2044
|
-
"first",
|
|
2045
|
-
"last",
|
|
2046
|
-
"distinct",
|
|
2047
|
-
"map",
|
|
2048
|
-
"reduce",
|
|
2049
|
-
"rollup",
|
|
2050
|
-
"pivot",
|
|
2051
|
-
"flatten",
|
|
2052
|
-
"transpose"
|
|
2053
|
-
]);
|
|
2054
|
-
function normalizeExpression2(expression) {
|
|
2055
|
-
const trimmed = expression.trimStart();
|
|
2056
|
-
const match = trimmed.match(/^(\w+)\s*\(/);
|
|
2057
|
-
if (match && OPERATION_KEYWORDS2.has(match[1])) {
|
|
2058
|
-
return `_data | ${trimmed}`;
|
|
2059
|
-
}
|
|
2060
|
-
return expression;
|
|
2061
|
-
}
|
|
2062
|
-
function compile2(expression, useCache = true) {
|
|
2063
|
-
if (useCache) {
|
|
2064
|
-
const cached = defaultCache.get(expression);
|
|
2065
|
-
if (cached) return cached;
|
|
2066
|
-
}
|
|
2067
|
-
const tokens = tokenize(expression);
|
|
2068
|
-
const ast = parse(tokens);
|
|
2069
|
-
const fn = compile(ast, expression);
|
|
2070
|
-
if (useCache) {
|
|
2071
|
-
defaultCache.set(expression, fn);
|
|
2072
|
-
}
|
|
2073
|
-
return fn;
|
|
2074
|
-
}
|
|
2075
|
-
function query(context, expression) {
|
|
2076
|
-
if (Array.isArray(context)) {
|
|
2077
|
-
const normalized = normalizeExpression2(expression);
|
|
2078
|
-
const fn2 = compile2(normalized);
|
|
2079
|
-
return fn2({ _data: context });
|
|
2080
|
-
}
|
|
2081
|
-
const fn = compile2(expression);
|
|
2082
|
-
return fn(context);
|
|
2083
|
-
}
|
|
2084
|
-
function parseQuery(expression) {
|
|
2085
|
-
const tokens = tokenize(expression);
|
|
2086
|
-
return parse(tokens);
|
|
2087
|
-
}
|
|
2088
|
-
function clearCache() {
|
|
2089
|
-
defaultCache.clear();
|
|
2090
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
DataWeaveError,
|
|
3
|
+
LexerError,
|
|
4
|
+
LiveQuery,
|
|
5
|
+
ParseError,
|
|
6
|
+
RuntimeError,
|
|
7
|
+
clearCache,
|
|
8
|
+
compile,
|
|
9
|
+
compileAst,
|
|
10
|
+
liveQuery,
|
|
11
|
+
parseQuery,
|
|
12
|
+
printExpression,
|
|
13
|
+
printQuery,
|
|
14
|
+
query
|
|
15
|
+
} from "../chunk-KNVFXND5.js";
|
|
2091
16
|
export {
|
|
2092
17
|
DataWeaveError,
|
|
2093
18
|
LexerError,
|
|
@@ -2095,8 +20,11 @@ export {
|
|
|
2095
20
|
ParseError,
|
|
2096
21
|
RuntimeError,
|
|
2097
22
|
clearCache,
|
|
2098
|
-
|
|
23
|
+
compile,
|
|
24
|
+
compileAst,
|
|
2099
25
|
liveQuery,
|
|
2100
26
|
parseQuery,
|
|
27
|
+
printExpression,
|
|
28
|
+
printQuery,
|
|
2101
29
|
query
|
|
2102
30
|
};
|