@vaultgradient/pipequery-cli 0.10.3 → 0.10.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3854 @@
1
+ // src/utils/parseDuration.ts
2
+ var UNITS = {
3
+ ms: 1,
4
+ s: 1e3,
5
+ m: 6e4,
6
+ h: 36e5
7
+ };
8
+ function parseDuration(input) {
9
+ const match = input.trim().match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h)$/);
10
+ if (!match) throw new Error(`Invalid duration: "${input}". Use format like "30s", "5m", "1h".`);
11
+ return parseFloat(match[1]) * UNITS[match[2]];
12
+ }
13
+
14
+ // src/utils/expandEnv.ts
15
+ function expandEnv(input, context) {
16
+ return input.replace(/\$\{([A-Z0-9_]+)\}/gi, (_, name) => {
17
+ const value = process.env[name];
18
+ if (value === void 0) {
19
+ process.stderr.write(
20
+ `[pipequery] env var "${name}" referenced in ${context} is not set
21
+ `
22
+ );
23
+ return "";
24
+ }
25
+ return value;
26
+ });
27
+ }
28
+
29
+ // src/server/sources/rest.ts
30
+ function extractPath(obj, path) {
31
+ let current = obj;
32
+ for (const key of path.split(".")) {
33
+ if (current == null || typeof current !== "object") return [];
34
+ current = current[key];
35
+ }
36
+ return Array.isArray(current) ? current : [];
37
+ }
38
+ var RestSourceAdapter = class {
39
+ constructor(config) {
40
+ this.config = config;
41
+ }
42
+ config;
43
+ data = [];
44
+ timer = null;
45
+ lastFetch = null;
46
+ error;
47
+ listeners = /* @__PURE__ */ new Set();
48
+ async start() {
49
+ await this.fetch();
50
+ const interval = this.config.interval ? parseDuration(this.config.interval) : 3e4;
51
+ this.timer = setInterval(() => this.fetch(), interval);
52
+ }
53
+ stop() {
54
+ if (this.timer) {
55
+ clearInterval(this.timer);
56
+ this.timer = null;
57
+ }
58
+ }
59
+ getData() {
60
+ return this.data;
61
+ }
62
+ getStatus() {
63
+ return {
64
+ healthy: !this.error,
65
+ rowCount: this.data.length,
66
+ lastFetch: this.lastFetch,
67
+ error: this.error
68
+ };
69
+ }
70
+ onUpdate(callback) {
71
+ this.listeners.add(callback);
72
+ return () => this.listeners.delete(callback);
73
+ }
74
+ async refresh() {
75
+ await this.fetch();
76
+ }
77
+ async fetch() {
78
+ try {
79
+ let url = expandEnv(this.config.url, "rest source URL");
80
+ if (this.config.params) {
81
+ const expanded = {};
82
+ for (const [k, v] of Object.entries(this.config.params)) {
83
+ expanded[k] = expandEnv(v, `rest source param "${k}"`);
84
+ }
85
+ const params = new URLSearchParams(expanded);
86
+ url += (url.includes("?") ? "&" : "?") + params.toString();
87
+ }
88
+ const headers = this.buildHeaders();
89
+ const res = await globalThis.fetch(url, { headers });
90
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
91
+ const json = await res.json();
92
+ this.data = this.config.dataPath ? extractPath(json, this.config.dataPath) : Array.isArray(json) ? json : [json];
93
+ this.lastFetch = /* @__PURE__ */ new Date();
94
+ this.error = void 0;
95
+ this.notify();
96
+ } catch (err) {
97
+ this.error = err instanceof Error ? err.message : String(err);
98
+ }
99
+ }
100
+ notify() {
101
+ for (const cb of this.listeners) cb(this.data);
102
+ }
103
+ /**
104
+ * Build the final HTTP headers for one fetch.
105
+ *
106
+ * Two sources contribute, in order: any user-declared `headers` (with
107
+ * env-var interpolation applied per value), then the `auth` helper
108
+ * (currently just `bearer`). Auth wins on collision — if a user sets
109
+ * both `headers.Authorization` and `auth.kind === 'bearer'`, the bearer
110
+ * helper is what they actually meant.
111
+ */
112
+ buildHeaders() {
113
+ const headers = {};
114
+ if (this.config.headers) {
115
+ for (const [k, v] of Object.entries(this.config.headers)) {
116
+ headers[k] = expandEnv(v, `rest source header "${k}"`);
117
+ }
118
+ }
119
+ if (this.config.auth?.kind === "bearer") {
120
+ const token = expandEnv(this.config.auth.token, "rest source bearer token");
121
+ headers["Authorization"] = `Bearer ${token}`;
122
+ }
123
+ return Object.keys(headers).length > 0 ? headers : void 0;
124
+ }
125
+ };
126
+
127
+ // src/server/sources/websocket.ts
128
+ import WebSocket from "ws";
129
+ var WebSocketSourceAdapter = class {
130
+ constructor(config) {
131
+ this.config = config;
132
+ this.maxBuffer = config.maxBuffer ?? 1e3;
133
+ }
134
+ config;
135
+ data = [];
136
+ ws = null;
137
+ lastFetch = null;
138
+ error;
139
+ listeners = /* @__PURE__ */ new Set();
140
+ maxBuffer;
141
+ heartbeatTimer = null;
142
+ /** Tracks whether stop() was called so reconnect respects intent. */
143
+ stopped = false;
144
+ async start() {
145
+ this.stopped = false;
146
+ return new Promise((resolve3, reject) => {
147
+ const ws = new WebSocket(this.config.url);
148
+ this.ws = ws;
149
+ let settled = false;
150
+ ws.on("open", () => {
151
+ this.error = void 0;
152
+ try {
153
+ this.sendSubscribePayloads(ws);
154
+ this.startHeartbeat(ws);
155
+ } catch (err) {
156
+ this.error = err instanceof Error ? err.message : String(err);
157
+ ws.close();
158
+ if (!settled) {
159
+ settled = true;
160
+ reject(err instanceof Error ? err : new Error(this.error));
161
+ }
162
+ return;
163
+ }
164
+ if (!settled) {
165
+ settled = true;
166
+ resolve3();
167
+ }
168
+ });
169
+ ws.on("message", (raw) => {
170
+ try {
171
+ const parsed = JSON.parse(raw.toString());
172
+ if (Array.isArray(parsed)) {
173
+ this.data = parsed;
174
+ } else {
175
+ this.data.push(parsed);
176
+ if (this.data.length > this.maxBuffer) {
177
+ this.data = this.data.slice(-this.maxBuffer);
178
+ }
179
+ }
180
+ this.lastFetch = /* @__PURE__ */ new Date();
181
+ this.error = void 0;
182
+ this.notify();
183
+ } catch {
184
+ }
185
+ });
186
+ ws.on("error", (err) => {
187
+ this.error = err.message;
188
+ if (!settled) {
189
+ settled = true;
190
+ reject(err);
191
+ }
192
+ });
193
+ ws.on("close", () => {
194
+ this.stopHeartbeat();
195
+ if (this.stopped) return;
196
+ setTimeout(() => {
197
+ if (!this.stopped) this.start().catch(() => {
198
+ });
199
+ }, 5e3);
200
+ });
201
+ });
202
+ }
203
+ stop() {
204
+ this.stopped = true;
205
+ this.stopHeartbeat();
206
+ const ws = this.ws;
207
+ this.ws = null;
208
+ ws?.close();
209
+ }
210
+ getData() {
211
+ return this.data;
212
+ }
213
+ getStatus() {
214
+ return {
215
+ healthy: this.ws?.readyState === WebSocket.OPEN,
216
+ rowCount: this.data.length,
217
+ lastFetch: this.lastFetch,
218
+ error: this.error
219
+ };
220
+ }
221
+ onUpdate(callback) {
222
+ this.listeners.add(callback);
223
+ return () => this.listeners.delete(callback);
224
+ }
225
+ notify() {
226
+ for (const cb of this.listeners) cb(this.data);
227
+ }
228
+ sendSubscribePayloads(ws) {
229
+ const subscribe = this.config.subscribe;
230
+ if (subscribe === void 0 || subscribe === null) return;
231
+ const list = Array.isArray(subscribe) ? subscribe : [subscribe];
232
+ for (const payload of list) {
233
+ ws.send(JSON.stringify(payload));
234
+ }
235
+ }
236
+ startHeartbeat(ws) {
237
+ this.stopHeartbeat();
238
+ const hb = this.config.heartbeat;
239
+ if (!hb) return;
240
+ const intervalMs = parseDuration(hb.interval);
241
+ this.heartbeatTimer = setInterval(() => {
242
+ if (ws.readyState !== WebSocket.OPEN) return;
243
+ try {
244
+ ws.send(JSON.stringify(hb.payload));
245
+ } catch {
246
+ }
247
+ }, intervalMs);
248
+ }
249
+ stopHeartbeat() {
250
+ if (this.heartbeatTimer) {
251
+ clearInterval(this.heartbeatTimer);
252
+ this.heartbeatTimer = null;
253
+ }
254
+ }
255
+ };
256
+
257
+ // src/server/sources/file.ts
258
+ import { readFile } from "fs/promises";
259
+ import { extname, resolve } from "path";
260
+ import { watch } from "chokidar";
261
+ import { parse as csvParse } from "csv-parse/sync";
262
+ var FileSourceAdapter = class {
263
+ constructor(config, cwd) {
264
+ this.config = config;
265
+ this.cwd = cwd;
266
+ this.resolvedPath = resolve(cwd, config.path);
267
+ }
268
+ config;
269
+ cwd;
270
+ data = [];
271
+ watcher = null;
272
+ lastFetch = null;
273
+ error;
274
+ listeners = /* @__PURE__ */ new Set();
275
+ resolvedPath;
276
+ async start() {
277
+ await this.load();
278
+ if (this.config.watch) {
279
+ this.watcher = watch(this.resolvedPath, { ignoreInitial: true });
280
+ this.watcher.on("change", () => this.load());
281
+ }
282
+ }
283
+ stop() {
284
+ this.watcher?.close();
285
+ this.watcher = null;
286
+ }
287
+ getData() {
288
+ return this.data;
289
+ }
290
+ getStatus() {
291
+ return {
292
+ healthy: !this.error,
293
+ rowCount: this.data.length,
294
+ lastFetch: this.lastFetch,
295
+ error: this.error
296
+ };
297
+ }
298
+ onUpdate(callback) {
299
+ this.listeners.add(callback);
300
+ return () => this.listeners.delete(callback);
301
+ }
302
+ async load() {
303
+ try {
304
+ const content = await readFile(this.resolvedPath, "utf-8");
305
+ const format = this.config.format ?? this.detectFormat();
306
+ if (format === "csv") {
307
+ this.data = csvParse(content, { columns: true, skip_empty_lines: true, cast: true });
308
+ } else {
309
+ const parsed = JSON.parse(content);
310
+ this.data = Array.isArray(parsed) ? parsed : [parsed];
311
+ }
312
+ this.lastFetch = /* @__PURE__ */ new Date();
313
+ this.error = void 0;
314
+ this.notify();
315
+ } catch (err) {
316
+ this.error = err instanceof Error ? err.message : String(err);
317
+ }
318
+ }
319
+ detectFormat() {
320
+ const ext = extname(this.resolvedPath).toLowerCase();
321
+ return ext === ".csv" || ext === ".tsv" ? "csv" : "json";
322
+ }
323
+ notify() {
324
+ for (const cb of this.listeners) cb(this.data);
325
+ }
326
+ };
327
+
328
+ // src/server/sources/static.ts
329
+ var StaticSourceAdapter = class {
330
+ constructor(config) {
331
+ this.config = config;
332
+ }
333
+ config;
334
+ listeners = /* @__PURE__ */ new Set();
335
+ async start() {
336
+ }
337
+ stop() {
338
+ }
339
+ getData() {
340
+ return this.config.data;
341
+ }
342
+ getStatus() {
343
+ return {
344
+ healthy: true,
345
+ rowCount: this.config.data.length,
346
+ lastFetch: /* @__PURE__ */ new Date()
347
+ };
348
+ }
349
+ onUpdate(callback) {
350
+ this.listeners.add(callback);
351
+ return () => this.listeners.delete(callback);
352
+ }
353
+ };
354
+
355
+ // src/server/sources/postgres.ts
356
+ import pg from "pg";
357
+
358
+ // ../src/engine/types.ts
359
+ var AGGREGATE_NAMES = /* @__PURE__ */ new Set([
360
+ "sum",
361
+ "avg",
362
+ "min",
363
+ "max",
364
+ "count",
365
+ "median",
366
+ "stddev",
367
+ "var",
368
+ "percentile",
369
+ "skew",
370
+ "kurt",
371
+ "vwap",
372
+ "wavg",
373
+ "drawdown",
374
+ "pct",
375
+ "sharpe",
376
+ "calmar",
377
+ "sortino",
378
+ "info_ratio",
379
+ "distinct_count",
380
+ "sum_abs",
381
+ "abs_sum",
382
+ "first_value",
383
+ "last_value"
384
+ ]);
385
+ var TWO_ARG_AGGREGATES = /* @__PURE__ */ new Set([
386
+ "percentile",
387
+ "vwap",
388
+ "wavg",
389
+ "info_ratio"
390
+ ]);
391
+ var DataWeaveError = class extends Error {
392
+ position;
393
+ line;
394
+ column;
395
+ constructor(message, position, line, column) {
396
+ super(message);
397
+ this.name = "DataWeaveError";
398
+ this.position = position;
399
+ this.line = line;
400
+ this.column = column;
401
+ }
402
+ };
403
+ var LexerError = class extends DataWeaveError {
404
+ constructor(message, position, line, column) {
405
+ super(message, position, line, column);
406
+ this.name = "LexerError";
407
+ }
408
+ };
409
+ var ParseError = class extends DataWeaveError {
410
+ constructor(message, position, line, column) {
411
+ super(message, position, line, column);
412
+ this.name = "ParseError";
413
+ }
414
+ };
415
+ var RuntimeError = class extends Error {
416
+ constructor(message) {
417
+ super(message);
418
+ this.name = "RuntimeError";
419
+ }
420
+ };
421
+
422
+ // ../src/engine/lexer.ts
423
+ var KEYWORDS = {
424
+ as: "AS",
425
+ asc: "ASC",
426
+ desc: "DESC",
427
+ true: "BOOLEAN",
428
+ false: "BOOLEAN",
429
+ null: "NULL"
430
+ };
431
+ function tokenize(source) {
432
+ const tokens = [];
433
+ let pos = 0;
434
+ let line = 1;
435
+ let column = 1;
436
+ function advance() {
437
+ const ch = source[pos];
438
+ pos++;
439
+ if (ch === "\n") {
440
+ line++;
441
+ column = 1;
442
+ } else {
443
+ column++;
444
+ }
445
+ return ch;
446
+ }
447
+ function peek() {
448
+ return source[pos];
449
+ }
450
+ function peekNext() {
451
+ return source[pos + 1];
452
+ }
453
+ function addToken(type, value, startPos, startLine, startCol) {
454
+ tokens.push({ type, value, position: startPos, line: startLine, column: startCol });
455
+ }
456
+ function error(msg) {
457
+ throw new LexerError(msg, pos, line, column);
458
+ }
459
+ while (pos < source.length) {
460
+ const startPos = pos;
461
+ const startLine = line;
462
+ const startCol = column;
463
+ const ch = peek();
464
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
465
+ advance();
466
+ continue;
467
+ }
468
+ if (ch === "|") {
469
+ advance();
470
+ addToken("PIPE", "|", startPos, startLine, startCol);
471
+ continue;
472
+ }
473
+ if (ch === "(") {
474
+ advance();
475
+ addToken("LPAREN", "(", startPos, startLine, startCol);
476
+ continue;
477
+ }
478
+ if (ch === ")") {
479
+ advance();
480
+ addToken("RPAREN", ")", startPos, startLine, startCol);
481
+ continue;
482
+ }
483
+ if (ch === ",") {
484
+ advance();
485
+ addToken("COMMA", ",", startPos, startLine, startCol);
486
+ continue;
487
+ }
488
+ if (ch === ".") {
489
+ advance();
490
+ addToken("DOT", ".", startPos, startLine, startCol);
491
+ continue;
492
+ }
493
+ if (ch === "+") {
494
+ advance();
495
+ addToken("PLUS", "+", startPos, startLine, startCol);
496
+ continue;
497
+ }
498
+ if (ch === "-") {
499
+ advance();
500
+ addToken("MINUS", "-", startPos, startLine, startCol);
501
+ continue;
502
+ }
503
+ if (ch === "*") {
504
+ advance();
505
+ addToken("STAR", "*", startPos, startLine, startCol);
506
+ continue;
507
+ }
508
+ if (ch === "/") {
509
+ advance();
510
+ addToken("SLASH", "/", startPos, startLine, startCol);
511
+ continue;
512
+ }
513
+ if (ch === "%") {
514
+ advance();
515
+ addToken("MOD", "%", startPos, startLine, startCol);
516
+ continue;
517
+ }
518
+ if (ch === ">" && peekNext() === "=") {
519
+ advance();
520
+ advance();
521
+ addToken("GTE", ">=", startPos, startLine, startCol);
522
+ continue;
523
+ }
524
+ if (ch === ">") {
525
+ advance();
526
+ addToken("GT", ">", startPos, startLine, startCol);
527
+ continue;
528
+ }
529
+ if (ch === "<" && peekNext() === "=") {
530
+ advance();
531
+ advance();
532
+ addToken("LTE", "<=", startPos, startLine, startCol);
533
+ continue;
534
+ }
535
+ if (ch === "<") {
536
+ advance();
537
+ addToken("LT", "<", startPos, startLine, startCol);
538
+ continue;
539
+ }
540
+ if (ch === "=" && peekNext() === "=") {
541
+ advance();
542
+ advance();
543
+ addToken("EQ", "==", startPos, startLine, startCol);
544
+ continue;
545
+ }
546
+ if (ch === "!" && peekNext() === "=") {
547
+ advance();
548
+ advance();
549
+ addToken("NEQ", "!=", startPos, startLine, startCol);
550
+ continue;
551
+ }
552
+ if (ch === "!") {
553
+ advance();
554
+ addToken("NOT", "!", startPos, startLine, startCol);
555
+ continue;
556
+ }
557
+ if (ch === "&" && peekNext() === "&") {
558
+ advance();
559
+ advance();
560
+ addToken("AND", "&&", startPos, startLine, startCol);
561
+ continue;
562
+ }
563
+ if (ch === "|" && peekNext() === "|") {
564
+ advance();
565
+ advance();
566
+ addToken("OR", "||", startPos, startLine, startCol);
567
+ continue;
568
+ }
569
+ if (ch >= "0" && ch <= "9") {
570
+ const start = pos;
571
+ while (pos < source.length && peek() >= "0" && peek() <= "9") advance();
572
+ if (pos < source.length && peek() === "." && pos + 1 < source.length && source[pos + 1] >= "0" && source[pos + 1] <= "9") {
573
+ advance();
574
+ while (pos < source.length && peek() >= "0" && peek() <= "9") advance();
575
+ }
576
+ addToken("NUMBER", source.slice(start, pos), startPos, startLine, startCol);
577
+ continue;
578
+ }
579
+ if (ch === '"' || ch === "'") {
580
+ const quote = ch;
581
+ advance();
582
+ const start = pos;
583
+ let value = "";
584
+ while (pos < source.length && peek() !== quote) {
585
+ if (peek() === "\\" && pos + 1 < source.length) {
586
+ advance();
587
+ const escaped = advance();
588
+ if (escaped === "n") value += "\n";
589
+ else if (escaped === "t") value += " ";
590
+ else value += escaped;
591
+ } else {
592
+ value += advance();
593
+ }
594
+ }
595
+ if (pos >= source.length) {
596
+ error(`Unterminated string starting at line ${startLine}, column ${startCol}`);
597
+ }
598
+ advance();
599
+ void start;
600
+ addToken("STRING", value, startPos, startLine, startCol);
601
+ continue;
602
+ }
603
+ if (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_") {
604
+ const start = pos;
605
+ while (pos < source.length && (peek() >= "a" && peek() <= "z" || peek() >= "A" && peek() <= "Z" || peek() >= "0" && peek() <= "9" || peek() === "_")) {
606
+ advance();
607
+ }
608
+ const word = source.slice(start, pos);
609
+ const keywordType = KEYWORDS[word];
610
+ if (keywordType) {
611
+ addToken(keywordType, word, startPos, startLine, startCol);
612
+ } else {
613
+ addToken("IDENTIFIER", word, startPos, startLine, startCol);
614
+ }
615
+ continue;
616
+ }
617
+ error(`Unexpected character '${ch}'`);
618
+ }
619
+ addToken("EOF", "", pos, line, column);
620
+ return tokens;
621
+ }
622
+
623
+ // ../src/engine/parser.ts
624
+ function parse(tokens) {
625
+ let pos = 0;
626
+ function peek() {
627
+ return tokens[pos];
628
+ }
629
+ function advance() {
630
+ const token = tokens[pos];
631
+ pos++;
632
+ return token;
633
+ }
634
+ function expect(type, context) {
635
+ const token = peek();
636
+ if (token.type !== type) {
637
+ const ctx = context ? ` ${context}` : "";
638
+ throw new ParseError(
639
+ `Expected ${type} but found ${token.type} ('${token.value}')${ctx}`,
640
+ token.position,
641
+ token.line,
642
+ token.column
643
+ );
644
+ }
645
+ return advance();
646
+ }
647
+ function match(type) {
648
+ if (peek().type === type) {
649
+ advance();
650
+ return true;
651
+ }
652
+ return false;
653
+ }
654
+ function parseExpression() {
655
+ let expr = parseOrExpr();
656
+ if (peek().type === "AS") {
657
+ advance();
658
+ const nameToken = expect("IDENTIFIER", 'after "as"');
659
+ expr = { kind: "AliasExpr", expression: expr, alias: nameToken.value };
660
+ }
661
+ return expr;
662
+ }
663
+ function parseOrExpr() {
664
+ let left = parseAndExpr();
665
+ while (peek().type === "OR") {
666
+ advance();
667
+ const right = parseAndExpr();
668
+ left = { kind: "BinaryExpr", operator: "||", left, right };
669
+ }
670
+ return left;
671
+ }
672
+ function parseAndExpr() {
673
+ let left = parseEqualityExpr();
674
+ while (peek().type === "AND") {
675
+ advance();
676
+ const right = parseEqualityExpr();
677
+ left = { kind: "BinaryExpr", operator: "&&", left, right };
678
+ }
679
+ return left;
680
+ }
681
+ function parseEqualityExpr() {
682
+ let left = parseComparisonExpr();
683
+ while (peek().type === "EQ" || peek().type === "NEQ") {
684
+ const op = advance().value;
685
+ const right = parseComparisonExpr();
686
+ left = { kind: "BinaryExpr", operator: op, left, right };
687
+ }
688
+ return left;
689
+ }
690
+ function parseComparisonExpr() {
691
+ let left = parseAdditiveExpr();
692
+ while (peek().type === "GT" || peek().type === "GTE" || peek().type === "LT" || peek().type === "LTE") {
693
+ const op = advance().value;
694
+ const right = parseAdditiveExpr();
695
+ left = { kind: "BinaryExpr", operator: op, left, right };
696
+ }
697
+ return left;
698
+ }
699
+ function parseAdditiveExpr() {
700
+ let left = parseMultiplicativeExpr();
701
+ while (peek().type === "PLUS" || peek().type === "MINUS") {
702
+ const op = advance().value;
703
+ const right = parseMultiplicativeExpr();
704
+ left = { kind: "BinaryExpr", operator: op, left, right };
705
+ }
706
+ return left;
707
+ }
708
+ function parseMultiplicativeExpr() {
709
+ let left = parseUnaryExpr();
710
+ while (peek().type === "STAR" || peek().type === "SLASH" || peek().type === "MOD") {
711
+ const op = advance().value;
712
+ const right = parseUnaryExpr();
713
+ left = { kind: "BinaryExpr", operator: op, left, right };
714
+ }
715
+ return left;
716
+ }
717
+ function parseUnaryExpr() {
718
+ if (peek().type === "NOT") {
719
+ advance();
720
+ const operand = parseUnaryExpr();
721
+ return { kind: "UnaryExpr", operator: "!", operand };
722
+ }
723
+ if (peek().type === "MINUS") {
724
+ advance();
725
+ const operand = parseUnaryExpr();
726
+ return { kind: "UnaryExpr", operator: "-", operand };
727
+ }
728
+ return parsePrimaryExpr();
729
+ }
730
+ function parsePrimaryExpr() {
731
+ const token = peek();
732
+ if (token.type === "NUMBER") {
733
+ advance();
734
+ return { kind: "NumberLiteral", value: Number(token.value) };
735
+ }
736
+ if (token.type === "STRING") {
737
+ advance();
738
+ return { kind: "StringLiteral", value: token.value };
739
+ }
740
+ if (token.type === "BOOLEAN") {
741
+ advance();
742
+ return { kind: "BooleanLiteral", value: token.value === "true" };
743
+ }
744
+ if (token.type === "NULL") {
745
+ advance();
746
+ return { kind: "NullLiteral" };
747
+ }
748
+ if (token.type === "IDENTIFIER") {
749
+ advance();
750
+ const name = token.value;
751
+ if (peek().type === "LPAREN") {
752
+ advance();
753
+ const args = [];
754
+ if (peek().type !== "RPAREN") {
755
+ args.push(parseExpression());
756
+ while (match("COMMA")) {
757
+ args.push(parseExpression());
758
+ }
759
+ }
760
+ expect("RPAREN", "after function arguments");
761
+ return { kind: "FunctionCall", name, args };
762
+ }
763
+ const path = [name];
764
+ while (peek().type === "DOT") {
765
+ advance();
766
+ const next = expect("IDENTIFIER", 'after "."');
767
+ path.push(next.value);
768
+ }
769
+ return { kind: "FieldAccess", path };
770
+ }
771
+ if (token.type === "LPAREN") {
772
+ advance();
773
+ const expr = parseExpression();
774
+ expect("RPAREN", "after grouped expression");
775
+ return expr;
776
+ }
777
+ throw new ParseError(
778
+ `Unexpected token ${token.type} ('${token.value}')`,
779
+ token.position,
780
+ token.line,
781
+ token.column
782
+ );
783
+ }
784
+ function isAggregateExpression(expr) {
785
+ if (expr.kind === "AliasExpr") return isAggregateExpression(expr.expression);
786
+ if (expr.kind === "FunctionCall") return AGGREGATE_NAMES.has(expr.name);
787
+ return false;
788
+ }
789
+ function parseOperation() {
790
+ const nameToken = expect("IDENTIFIER", 'after "|"');
791
+ const name = nameToken.value;
792
+ expect("LPAREN", `after "${name}"`);
793
+ let op;
794
+ switch (name) {
795
+ case "where": {
796
+ const condition = parseExpression();
797
+ op = { kind: "WhereOp", condition };
798
+ break;
799
+ }
800
+ case "select": {
801
+ const fields = [];
802
+ if (peek().type !== "RPAREN") {
803
+ fields.push(parseExpression());
804
+ while (match("COMMA")) {
805
+ fields.push(parseExpression());
806
+ }
807
+ }
808
+ op = { kind: "SelectOp", fields };
809
+ break;
810
+ }
811
+ case "sort": {
812
+ const criteria = [];
813
+ if (peek().type !== "RPAREN") {
814
+ const expr = parseOrExpr();
815
+ let direction = "asc";
816
+ if (peek().type === "ASC") {
817
+ advance();
818
+ direction = "asc";
819
+ } else if (peek().type === "DESC") {
820
+ advance();
821
+ direction = "desc";
822
+ }
823
+ criteria.push({ expression: expr, direction });
824
+ while (match("COMMA")) {
825
+ const e = parseOrExpr();
826
+ let d = "asc";
827
+ if (peek().type === "ASC") {
828
+ advance();
829
+ d = "asc";
830
+ } else if (peek().type === "DESC") {
831
+ advance();
832
+ d = "desc";
833
+ }
834
+ criteria.push({ expression: e, direction: d });
835
+ }
836
+ }
837
+ op = { kind: "SortOp", criteria };
838
+ break;
839
+ }
840
+ case "groupBy": {
841
+ const keys = [];
842
+ if (peek().type !== "RPAREN") {
843
+ keys.push(parseOrExpr());
844
+ while (match("COMMA")) {
845
+ keys.push(parseOrExpr());
846
+ }
847
+ }
848
+ op = { kind: "GroupByOp", keys };
849
+ break;
850
+ }
851
+ case "join": {
852
+ const rightToken = expect("IDENTIFIER", "for join source");
853
+ const right = rightToken.value;
854
+ expect("COMMA", "after join source");
855
+ const condition = parseExpression();
856
+ op = { kind: "JoinOp", right, condition };
857
+ break;
858
+ }
859
+ case "first": {
860
+ const numToken = expect("NUMBER", "for first count");
861
+ op = { kind: "FirstOp", count: Number(numToken.value) };
862
+ break;
863
+ }
864
+ case "last": {
865
+ const numToken = expect("NUMBER", "for last count");
866
+ op = { kind: "LastOp", count: Number(numToken.value) };
867
+ break;
868
+ }
869
+ case "distinct": {
870
+ const fields = [];
871
+ if (peek().type !== "RPAREN") {
872
+ fields.push(parseOrExpr());
873
+ while (match("COMMA")) {
874
+ fields.push(parseOrExpr());
875
+ }
876
+ }
877
+ op = { kind: "DistinctOp", fields: fields.length > 0 ? fields : void 0 };
878
+ break;
879
+ }
880
+ case "flatten": {
881
+ let field2;
882
+ if (peek().type !== "RPAREN") {
883
+ field2 = parseOrExpr();
884
+ }
885
+ op = { kind: "FlattenOp", field: field2 };
886
+ break;
887
+ }
888
+ case "transpose": {
889
+ let headerField;
890
+ if (peek().type !== "RPAREN") {
891
+ headerField = parseOrExpr();
892
+ }
893
+ op = { kind: "TransposeOp", headerField };
894
+ break;
895
+ }
896
+ case "rollup": {
897
+ const keys = [];
898
+ const aggregates = [];
899
+ let seenAggregate = false;
900
+ if (peek().type !== "RPAREN") {
901
+ const expr = parseExpression();
902
+ if (isAggregateExpression(expr)) {
903
+ seenAggregate = true;
904
+ aggregates.push(expr);
905
+ } else {
906
+ keys.push(expr);
907
+ }
908
+ while (match("COMMA")) {
909
+ const e = parseExpression();
910
+ if (isAggregateExpression(e)) {
911
+ seenAggregate = true;
912
+ aggregates.push(e);
913
+ } else if (seenAggregate) {
914
+ const t = peek();
915
+ throw new ParseError(
916
+ "rollup: group keys must come before aggregate expressions",
917
+ t.position,
918
+ t.line,
919
+ t.column
920
+ );
921
+ } else {
922
+ keys.push(e);
923
+ }
924
+ }
925
+ }
926
+ if (aggregates.length === 0) {
927
+ const t = peek();
928
+ throw new ParseError(
929
+ "rollup: at least one aggregate expression is required",
930
+ t.position,
931
+ t.line,
932
+ t.column
933
+ );
934
+ }
935
+ op = { kind: "RollupOp", keys, aggregates };
936
+ break;
937
+ }
938
+ case "pivot": {
939
+ const pivotField = parseOrExpr();
940
+ expect("COMMA", "after pivot field");
941
+ const aggregates = [];
942
+ aggregates.push(parseExpression());
943
+ while (match("COMMA")) {
944
+ aggregates.push(parseExpression());
945
+ }
946
+ op = { kind: "PivotOp", pivotField, aggregates };
947
+ break;
948
+ }
949
+ case "map": {
950
+ const fields = [];
951
+ if (peek().type !== "RPAREN") {
952
+ fields.push(parseExpression());
953
+ while (match("COMMA")) {
954
+ fields.push(parseExpression());
955
+ }
956
+ }
957
+ op = { kind: "MapOp", fields };
958
+ break;
959
+ }
960
+ case "reduce": {
961
+ const initial = parseExpression();
962
+ expect("COMMA", "after reduce initial value");
963
+ const accumulator = parseExpression();
964
+ op = { kind: "ReduceOp", initial, accumulator };
965
+ break;
966
+ }
967
+ default: {
968
+ if (AGGREGATE_NAMES.has(name)) {
969
+ if (TWO_ARG_AGGREGATES.has(name)) {
970
+ const args = [];
971
+ args.push(parseOrExpr());
972
+ expect("COMMA", `after first argument of ${name}`);
973
+ args.push(parseOrExpr());
974
+ op = { kind: "AggregateOp", function: name, args };
975
+ } else {
976
+ let field2;
977
+ if (peek().type !== "RPAREN") {
978
+ field2 = parseOrExpr();
979
+ }
980
+ op = { kind: "AggregateOp", function: name, field: field2 };
981
+ }
982
+ break;
983
+ }
984
+ throw new ParseError(
985
+ `Unknown operation "${name}"`,
986
+ nameToken.position,
987
+ nameToken.line,
988
+ nameToken.column
989
+ );
990
+ }
991
+ }
992
+ expect("RPAREN", `after ${name} arguments`);
993
+ return op;
994
+ }
995
+ function parsePipeline() {
996
+ const sourceToken = expect("IDENTIFIER", "at start of pipeline");
997
+ const operations = [];
998
+ while (peek().type === "PIPE") {
999
+ advance();
1000
+ operations.push(parseOperation());
1001
+ }
1002
+ if (peek().type !== "EOF") {
1003
+ const t = peek();
1004
+ throw new ParseError(
1005
+ `Unexpected token ${t.type} ('${t.value}') after pipeline`,
1006
+ t.position,
1007
+ t.line,
1008
+ t.column
1009
+ );
1010
+ }
1011
+ return { kind: "Pipeline", source: sourceToken.value, operations };
1012
+ }
1013
+ return parsePipeline();
1014
+ }
1015
+
1016
+ // ../src/engine/runtime.ts
1017
+ var accessorCache = /* @__PURE__ */ new Map();
1018
+ function getFieldAccessor(path) {
1019
+ const key = path.join(".");
1020
+ let fn = accessorCache.get(key);
1021
+ if (fn) return fn;
1022
+ if (path.length === 1) {
1023
+ const field2 = path[0];
1024
+ fn = (obj) => obj[field2];
1025
+ } else {
1026
+ const segments = [...path];
1027
+ fn = (obj) => {
1028
+ let val = obj;
1029
+ for (const segment of segments) {
1030
+ if (val == null) return void 0;
1031
+ val = val[segment];
1032
+ }
1033
+ return val;
1034
+ };
1035
+ }
1036
+ accessorCache.set(key, fn);
1037
+ return fn;
1038
+ }
1039
+ function compareValues(a, b) {
1040
+ if (a === b) return 0;
1041
+ if (a == null && b == null) return 0;
1042
+ if (a == null) return -1;
1043
+ if (b == null) return 1;
1044
+ if (typeof a === "number" && typeof b === "number") return a - b;
1045
+ return String(a).localeCompare(String(b));
1046
+ }
1047
+ function groupByFn(data, keyFns) {
1048
+ const groups = /* @__PURE__ */ new Map();
1049
+ if (keyFns.length === 1) {
1050
+ const { fn, name } = keyFns[0];
1051
+ for (const row of data) {
1052
+ const val = fn(row);
1053
+ const groupKey = String(val);
1054
+ let group = groups.get(groupKey);
1055
+ if (!group) {
1056
+ group = { keys: { [name]: val }, rows: [] };
1057
+ groups.set(groupKey, group);
1058
+ }
1059
+ group.rows.push(row);
1060
+ }
1061
+ } else {
1062
+ for (const row of data) {
1063
+ const keys = {};
1064
+ let groupKey = "";
1065
+ for (let i = 0; i < keyFns.length; i++) {
1066
+ const { fn, name } = keyFns[i];
1067
+ const val = fn(row);
1068
+ keys[name] = val;
1069
+ groupKey += (i > 0 ? "\0" : "") + String(val);
1070
+ }
1071
+ let group = groups.get(groupKey);
1072
+ if (!group) {
1073
+ group = { keys, rows: [] };
1074
+ groups.set(groupKey, group);
1075
+ }
1076
+ group.rows.push(row);
1077
+ }
1078
+ }
1079
+ return Array.from(groups.values());
1080
+ }
1081
+ function nestedLoopJoin(left, right, conditionFn) {
1082
+ const result = [];
1083
+ for (const l of left) {
1084
+ for (const r of right) {
1085
+ const combined = { ...l, ...r };
1086
+ if (conditionFn(combined)) {
1087
+ result.push(combined);
1088
+ }
1089
+ }
1090
+ }
1091
+ return result;
1092
+ }
1093
+ function hashJoin(left, right, leftKeyFn, rightKeyFn) {
1094
+ const hashMap = /* @__PURE__ */ new Map();
1095
+ for (const r of right) {
1096
+ const key = rightKeyFn(r);
1097
+ let bucket = hashMap.get(key);
1098
+ if (!bucket) {
1099
+ bucket = [];
1100
+ hashMap.set(key, bucket);
1101
+ }
1102
+ bucket.push(r);
1103
+ }
1104
+ const result = [];
1105
+ for (const l of left) {
1106
+ const matches = hashMap.get(leftKeyFn(l));
1107
+ if (matches) {
1108
+ for (const r of matches) {
1109
+ result.push({ ...l, ...r });
1110
+ }
1111
+ }
1112
+ }
1113
+ return result;
1114
+ }
1115
+ function distinctFn(data, keyFn) {
1116
+ const seen = /* @__PURE__ */ new Set();
1117
+ return data.filter((row) => {
1118
+ const key = keyFn ? keyFn(row) : JSON.stringify(row);
1119
+ if (seen.has(key)) return false;
1120
+ seen.add(key);
1121
+ return true;
1122
+ });
1123
+ }
1124
+ function transposeFn(data, headerFn, headerFieldName) {
1125
+ if (data.length === 0) return [];
1126
+ const colNames = data.map(
1127
+ (row, i) => headerFn ? String(headerFn(row)) : `col_${i}`
1128
+ );
1129
+ const allFields = /* @__PURE__ */ new Set();
1130
+ for (const row of data) {
1131
+ for (const key of Object.keys(row)) {
1132
+ if (key !== "_group" && key !== headerFieldName) {
1133
+ allFields.add(key);
1134
+ }
1135
+ }
1136
+ }
1137
+ const results = [];
1138
+ for (const field2 of allFields) {
1139
+ const row = { _field: field2 };
1140
+ for (let i = 0; i < data.length; i++) {
1141
+ row[colNames[i]] = data[i][field2] ?? null;
1142
+ }
1143
+ results.push(row);
1144
+ }
1145
+ return results;
1146
+ }
1147
+ function rollupFn(data, keyFns, aggFns) {
1148
+ const results = [];
1149
+ const numKeys = keyFns.length;
1150
+ for (let level = 0; level <= numKeys; level++) {
1151
+ const activeKeys = keyFns.slice(0, numKeys - level);
1152
+ const groups = groupByFn(data, activeKeys);
1153
+ for (const group of groups) {
1154
+ const row = {};
1155
+ for (const { name } of activeKeys) {
1156
+ row[name] = group.keys[name];
1157
+ }
1158
+ for (let i = numKeys - level; i < numKeys; i++) {
1159
+ row[keyFns[i].name] = null;
1160
+ }
1161
+ const groupedRow = { ...row, _group: group.rows };
1162
+ for (const { fn, name } of aggFns) {
1163
+ row[name] = fn(groupedRow);
1164
+ }
1165
+ row._rollupLevel = level;
1166
+ results.push(row);
1167
+ }
1168
+ }
1169
+ return results;
1170
+ }
1171
+ function defaultForAggregate(name) {
1172
+ if (name === "min" || name === "max") return null;
1173
+ return 0;
1174
+ }
1175
+ function pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate) {
1176
+ const pivotGroups = /* @__PURE__ */ new Map();
1177
+ for (const row of data) {
1178
+ const pivotVal = String(pivotFn(row));
1179
+ if (!pivotGroups.has(pivotVal)) pivotGroups.set(pivotVal, []);
1180
+ pivotGroups.get(pivotVal).push(row);
1181
+ }
1182
+ const result = {};
1183
+ for (const [pivotVal, groupRows] of pivotGroups) {
1184
+ const syntheticRow = { _group: groupRows };
1185
+ const safeName = pivotVal.replace(/\s+/g, "_");
1186
+ for (const { fn, name } of aggCompilers) {
1187
+ const colName = singleAggregate ? safeName : `${name}_${safeName}`;
1188
+ result[colName] = fn(syntheticRow);
1189
+ }
1190
+ }
1191
+ return [result];
1192
+ }
1193
+ function pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate) {
1194
+ const allPivotValues = /* @__PURE__ */ new Set();
1195
+ for (const groupRow of data) {
1196
+ const groupData = groupRow._group;
1197
+ for (const row of groupData) {
1198
+ allPivotValues.add(String(pivotFn(row)));
1199
+ }
1200
+ }
1201
+ return data.map((groupRow) => {
1202
+ const groupData = groupRow._group;
1203
+ const result = {};
1204
+ for (const key of Object.keys(groupRow)) {
1205
+ if (key !== "_group") result[key] = groupRow[key];
1206
+ }
1207
+ const subGroups = /* @__PURE__ */ new Map();
1208
+ for (const row of groupData) {
1209
+ const pivotVal = String(pivotFn(row));
1210
+ if (!subGroups.has(pivotVal)) subGroups.set(pivotVal, []);
1211
+ subGroups.get(pivotVal).push(row);
1212
+ }
1213
+ for (const pivotVal of allPivotValues) {
1214
+ const subGroupRows = subGroups.get(pivotVal) || [];
1215
+ const syntheticRow = { _group: subGroupRows };
1216
+ const safeName = pivotVal.replace(/\s+/g, "_");
1217
+ for (const { fn, name } of aggCompilers) {
1218
+ const colName = singleAggregate ? safeName : `${name}_${safeName}`;
1219
+ result[colName] = subGroupRows.length > 0 ? fn(syntheticRow) : defaultForAggregate(name);
1220
+ }
1221
+ }
1222
+ return result;
1223
+ });
1224
+ }
1225
+ function computeWindowValues(data, windowDefs) {
1226
+ const n = data.length;
1227
+ const results = Array.from({ length: n }, () => ({}));
1228
+ for (const def of windowDefs) {
1229
+ switch (def.name) {
1230
+ case "row_number":
1231
+ case "running_count":
1232
+ for (let i = 0; i < n; i++) results[i][def.id] = i + 1;
1233
+ break;
1234
+ case "running_sum": {
1235
+ let acc = 0;
1236
+ for (let i = 0; i < n; i++) {
1237
+ acc += Number(def.fieldFn(data[i]));
1238
+ results[i][def.id] = acc;
1239
+ }
1240
+ break;
1241
+ }
1242
+ case "running_avg": {
1243
+ let acc = 0;
1244
+ for (let i = 0; i < n; i++) {
1245
+ acc += Number(def.fieldFn(data[i]));
1246
+ results[i][def.id] = acc / (i + 1);
1247
+ }
1248
+ break;
1249
+ }
1250
+ case "running_min": {
1251
+ let min = Infinity;
1252
+ for (let i = 0; i < n; i++) {
1253
+ const val = Number(def.fieldFn(data[i]));
1254
+ if (val < min) min = val;
1255
+ results[i][def.id] = min;
1256
+ }
1257
+ break;
1258
+ }
1259
+ case "running_max": {
1260
+ let max = -Infinity;
1261
+ for (let i = 0; i < n; i++) {
1262
+ const val = Number(def.fieldFn(data[i]));
1263
+ if (val > max) max = val;
1264
+ results[i][def.id] = max;
1265
+ }
1266
+ break;
1267
+ }
1268
+ case "lag": {
1269
+ const offset = def.offset;
1270
+ for (let i = 0; i < n; i++) {
1271
+ results[i][def.id] = i >= offset ? def.fieldFn(data[i - offset]) : null;
1272
+ }
1273
+ break;
1274
+ }
1275
+ case "lead": {
1276
+ const offset = def.offset;
1277
+ for (let i = 0; i < n; i++) {
1278
+ results[i][def.id] = i + offset < n ? def.fieldFn(data[i + offset]) : null;
1279
+ }
1280
+ break;
1281
+ }
1282
+ }
1283
+ }
1284
+ return results;
1285
+ }
1286
+
1287
+ // ../src/engine/compiler.ts
1288
+ function computeMedian(values) {
1289
+ if (values.length === 0) return 0;
1290
+ const sorted = [...values].sort((a, b) => a - b);
1291
+ const mid = Math.floor(sorted.length / 2);
1292
+ return sorted.length % 2 !== 0 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
1293
+ }
1294
+ function computePercentile(values, p) {
1295
+ if (values.length === 0) return 0;
1296
+ const sorted = [...values].sort((a, b) => a - b);
1297
+ const idx = p / 100 * (sorted.length - 1);
1298
+ const lo = Math.floor(idx);
1299
+ const hi = Math.ceil(idx);
1300
+ if (lo === hi) return sorted[lo];
1301
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (idx - lo);
1302
+ }
1303
+ function computeVariance(values) {
1304
+ if (values.length === 0) return 0;
1305
+ const mean = values.reduce((a, b) => a + b, 0) / values.length;
1306
+ return values.reduce((acc, v) => acc + (v - mean) ** 2, 0) / values.length;
1307
+ }
1308
+ function computeStddev(values) {
1309
+ return Math.sqrt(computeVariance(values));
1310
+ }
1311
+ function computeSkew(values) {
1312
+ const n = values.length;
1313
+ if (n < 3) return 0;
1314
+ const mean = values.reduce((a, b) => a + b, 0) / n;
1315
+ const sd = computeStddev(values);
1316
+ if (sd === 0) return 0;
1317
+ return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 3, 0) / n;
1318
+ }
1319
+ function computeKurt(values) {
1320
+ const n = values.length;
1321
+ if (n < 4) return 0;
1322
+ const mean = values.reduce((a, b) => a + b, 0) / n;
1323
+ const sd = computeStddev(values);
1324
+ if (sd === 0) return 0;
1325
+ return values.reduce((acc, v) => acc + ((v - mean) / sd) ** 4, 0) / n - 3;
1326
+ }
1327
+ function computeDrawdown(values) {
1328
+ if (values.length === 0) return 0;
1329
+ let peak = values[0];
1330
+ let maxDd = 0;
1331
+ for (const v of values) {
1332
+ if (v > peak) peak = v;
1333
+ const dd = peak === 0 ? 0 : (v - peak) / peak;
1334
+ if (dd < maxDd) maxDd = dd;
1335
+ }
1336
+ return maxDd;
1337
+ }
1338
+ function computeDownsideDeviation(values, target = 0) {
1339
+ if (values.length === 0) return 0;
1340
+ const downside = values.filter((v) => v < target).map((v) => (v - target) ** 2);
1341
+ if (downside.length === 0) return 0;
1342
+ return Math.sqrt(downside.reduce((a, b) => a + b, 0) / values.length);
1343
+ }
1344
+ var WINDOW_FUNCTION_NAMES = /* @__PURE__ */ new Set([
1345
+ "running_sum",
1346
+ "running_avg",
1347
+ "running_count",
1348
+ "running_min",
1349
+ "running_max",
1350
+ "row_number",
1351
+ "lag",
1352
+ "lead"
1353
+ ]);
1354
+ var windowIdCounter = 0;
1355
+ function compileExpression(expr, grouped = false) {
1356
+ switch (expr.kind) {
1357
+ case "NumberLiteral": {
1358
+ const v = expr.value;
1359
+ return () => v;
1360
+ }
1361
+ case "StringLiteral": {
1362
+ const v = expr.value;
1363
+ return () => v;
1364
+ }
1365
+ case "BooleanLiteral": {
1366
+ const v = expr.value;
1367
+ return () => v;
1368
+ }
1369
+ case "NullLiteral":
1370
+ return () => null;
1371
+ case "FieldAccess": {
1372
+ const accessor = getFieldAccessor(expr.path);
1373
+ return accessor;
1374
+ }
1375
+ case "BinaryExpr": {
1376
+ const leftFn = compileExpression(expr.left, grouped);
1377
+ const rightFn = compileExpression(expr.right, grouped);
1378
+ const op = expr.operator;
1379
+ switch (op) {
1380
+ case ">":
1381
+ return (row) => leftFn(row) > rightFn(row);
1382
+ case ">=":
1383
+ return (row) => leftFn(row) >= rightFn(row);
1384
+ case "<":
1385
+ return (row) => leftFn(row) < rightFn(row);
1386
+ case "<=":
1387
+ return (row) => leftFn(row) <= rightFn(row);
1388
+ case "==":
1389
+ return (row) => leftFn(row) === rightFn(row);
1390
+ case "!=":
1391
+ return (row) => leftFn(row) !== rightFn(row);
1392
+ case "&&":
1393
+ return (row) => leftFn(row) && rightFn(row);
1394
+ case "||":
1395
+ return (row) => leftFn(row) || rightFn(row);
1396
+ case "+":
1397
+ return (row) => leftFn(row) + rightFn(row);
1398
+ case "-":
1399
+ return (row) => leftFn(row) - rightFn(row);
1400
+ case "*":
1401
+ return (row) => leftFn(row) * rightFn(row);
1402
+ case "/":
1403
+ return (row) => leftFn(row) / rightFn(row);
1404
+ case "%":
1405
+ return (row) => leftFn(row) % rightFn(row);
1406
+ }
1407
+ break;
1408
+ }
1409
+ case "UnaryExpr": {
1410
+ const operandFn = compileExpression(expr.operand, grouped);
1411
+ if (expr.operator === "!") return (row) => !operandFn(row);
1412
+ return (row) => -operandFn(row);
1413
+ }
1414
+ case "FunctionCall":
1415
+ return compileFunctionCall(expr, grouped);
1416
+ case "AliasExpr":
1417
+ return compileExpression(expr.expression, grouped);
1418
+ }
1419
+ throw new RuntimeError(`Unknown expression kind: ${expr.kind}`);
1420
+ }
1421
+ function compileFunctionCall(expr, grouped) {
1422
+ const { name, args } = expr;
1423
+ if (grouped && AGGREGATE_NAMES.has(name)) {
1424
+ return compileGroupAggregate(name, args);
1425
+ }
1426
+ if (WINDOW_FUNCTION_NAMES.has(name)) {
1427
+ const id = `_w${windowIdCounter++}`;
1428
+ const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
1429
+ const offset = args.length > 1 ? args[1].value || 1 : 1;
1430
+ const fn = ((row) => {
1431
+ const ctx = row._windowCtx;
1432
+ return ctx?.[id];
1433
+ });
1434
+ fn._windowDef = { id, name, fieldFn, offset };
1435
+ return fn;
1436
+ }
1437
+ switch (name) {
1438
+ case "if": {
1439
+ if (args.length < 2 || args.length > 3) {
1440
+ throw new RuntimeError(`if() expects 2 or 3 arguments, got ${args.length}`);
1441
+ }
1442
+ const condFn = compileExpression(args[0], grouped);
1443
+ const thenFn = compileExpression(args[1], grouped);
1444
+ const elseFn = args.length === 3 ? compileExpression(args[2], grouped) : () => null;
1445
+ return (row) => condFn(row) ? thenFn(row) : elseFn(row);
1446
+ }
1447
+ case "coalesce": {
1448
+ const fns = args.map((a) => compileExpression(a, grouped));
1449
+ return (row) => {
1450
+ for (const fn of fns) {
1451
+ const val = fn(row);
1452
+ if (val != null) return val;
1453
+ }
1454
+ return null;
1455
+ };
1456
+ }
1457
+ case "lower": {
1458
+ const fn = compileExpression(args[0], grouped);
1459
+ return (row) => String(fn(row)).toLowerCase();
1460
+ }
1461
+ case "upper": {
1462
+ const fn = compileExpression(args[0], grouped);
1463
+ return (row) => String(fn(row)).toUpperCase();
1464
+ }
1465
+ case "len": {
1466
+ const fn = compileExpression(args[0], grouped);
1467
+ return (row) => {
1468
+ const val = fn(row);
1469
+ if (Array.isArray(val)) return val.length;
1470
+ if (typeof val === "string") return val.length;
1471
+ return 0;
1472
+ };
1473
+ }
1474
+ case "abs": {
1475
+ const fn = compileExpression(args[0], grouped);
1476
+ return (row) => Math.abs(fn(row));
1477
+ }
1478
+ case "round": {
1479
+ const fn = compileExpression(args[0], grouped);
1480
+ const precFn = args.length > 1 ? compileExpression(args[1], grouped) : () => 0;
1481
+ return (row) => {
1482
+ const val = fn(row);
1483
+ const prec = precFn(row);
1484
+ const factor = Math.pow(10, prec);
1485
+ return Math.round(val * factor) / factor;
1486
+ };
1487
+ }
1488
+ case "concat": {
1489
+ const fns = args.map((a) => compileExpression(a, grouped));
1490
+ return (row) => fns.map((fn) => String(fn(row))).join("");
1491
+ }
1492
+ // ── String functions ────────────────────────────────────
1493
+ case "contains": {
1494
+ if (args.length !== 2) throw new RuntimeError(`contains() expects 2 arguments, got ${args.length}`);
1495
+ const strFn = compileExpression(args[0], grouped);
1496
+ const searchFn = compileExpression(args[1], grouped);
1497
+ return (row) => String(strFn(row)).includes(String(searchFn(row)));
1498
+ }
1499
+ case "startsWith": {
1500
+ if (args.length !== 2) throw new RuntimeError(`startsWith() expects 2 arguments, got ${args.length}`);
1501
+ const strFn = compileExpression(args[0], grouped);
1502
+ const prefixFn = compileExpression(args[1], grouped);
1503
+ return (row) => String(strFn(row)).startsWith(String(prefixFn(row)));
1504
+ }
1505
+ case "endsWith": {
1506
+ if (args.length !== 2) throw new RuntimeError(`endsWith() expects 2 arguments, got ${args.length}`);
1507
+ const strFn = compileExpression(args[0], grouped);
1508
+ const suffixFn = compileExpression(args[1], grouped);
1509
+ return (row) => String(strFn(row)).endsWith(String(suffixFn(row)));
1510
+ }
1511
+ case "trim": {
1512
+ if (args.length !== 1) throw new RuntimeError(`trim() expects 1 argument, got ${args.length}`);
1513
+ const fn = compileExpression(args[0], grouped);
1514
+ return (row) => String(fn(row)).trim();
1515
+ }
1516
+ case "substring": {
1517
+ if (args.length < 2 || args.length > 3) throw new RuntimeError(`substring() expects 2 or 3 arguments, got ${args.length}`);
1518
+ const strFn = compileExpression(args[0], grouped);
1519
+ const startFn = compileExpression(args[1], grouped);
1520
+ const lenFn = args.length === 3 ? compileExpression(args[2], grouped) : void 0;
1521
+ return (row) => {
1522
+ const s = String(strFn(row));
1523
+ const start = Number(startFn(row));
1524
+ if (lenFn) {
1525
+ return s.substring(start, start + Number(lenFn(row)));
1526
+ }
1527
+ return s.substring(start);
1528
+ };
1529
+ }
1530
+ case "replace": {
1531
+ if (args.length !== 3) throw new RuntimeError(`replace() expects 3 arguments, got ${args.length}`);
1532
+ const strFn = compileExpression(args[0], grouped);
1533
+ const searchFn = compileExpression(args[1], grouped);
1534
+ const replaceFn = compileExpression(args[2], grouped);
1535
+ return (row) => String(strFn(row)).replaceAll(String(searchFn(row)), String(replaceFn(row)));
1536
+ }
1537
+ // Non-grouped aggregates used as functions return NaN or undefined — they're
1538
+ // only meaningful as pipeline-level AggregateOps or in grouped selects
1539
+ default:
1540
+ throw new RuntimeError(`Unknown function: ${name}`);
1541
+ }
1542
+ }
1543
+ function compileGroupAggregate(name, args) {
1544
+ const fieldFn = args.length > 0 ? compileExpression(args[0], false) : void 0;
1545
+ const secondFn = args.length > 1 ? compileExpression(args[1], false) : void 0;
1546
+ const extractValues = (row) => row._group.map((r) => Number(fieldFn(r)));
1547
+ switch (name) {
1548
+ // ── Core ───────────────────────────────────────────────
1549
+ case "sum":
1550
+ return (row) => {
1551
+ const group = row._group;
1552
+ return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1553
+ };
1554
+ case "avg":
1555
+ return (row) => {
1556
+ const group = row._group;
1557
+ if (group.length === 0) return 0;
1558
+ const total = group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1559
+ return total / group.length;
1560
+ };
1561
+ case "min":
1562
+ return (row) => {
1563
+ const group = row._group;
1564
+ if (group.length === 0) return null;
1565
+ return group.reduce((min, r) => {
1566
+ const val = fieldFn(r);
1567
+ return val < min ? val : min;
1568
+ }, fieldFn(group[0]));
1569
+ };
1570
+ case "max":
1571
+ return (row) => {
1572
+ const group = row._group;
1573
+ if (group.length === 0) return null;
1574
+ return group.reduce((max, r) => {
1575
+ const val = fieldFn(r);
1576
+ return val > max ? val : max;
1577
+ }, fieldFn(group[0]));
1578
+ };
1579
+ case "count":
1580
+ return (row) => {
1581
+ const group = row._group;
1582
+ return group.length;
1583
+ };
1584
+ // ── Statistical ───────────────────────────────────────
1585
+ case "median":
1586
+ return (row) => computeMedian(extractValues(row));
1587
+ case "stddev":
1588
+ return (row) => computeStddev(extractValues(row));
1589
+ case "var":
1590
+ return (row) => computeVariance(extractValues(row));
1591
+ case "percentile":
1592
+ return (row) => computePercentile(extractValues(row), Number(secondFn(row)));
1593
+ case "skew":
1594
+ return (row) => computeSkew(extractValues(row));
1595
+ case "kurt":
1596
+ return (row) => computeKurt(extractValues(row));
1597
+ // ── Finance ───────────────────────────────────────────
1598
+ case "vwap":
1599
+ return (row) => {
1600
+ const group = row._group;
1601
+ let sumPV = 0, sumV = 0;
1602
+ for (const r of group) {
1603
+ const p = Number(fieldFn(r));
1604
+ const v = Number(secondFn(r));
1605
+ sumPV += p * v;
1606
+ sumV += v;
1607
+ }
1608
+ return sumV === 0 ? 0 : sumPV / sumV;
1609
+ };
1610
+ case "wavg":
1611
+ return (row) => {
1612
+ const group = row._group;
1613
+ let sumWV = 0, sumW = 0;
1614
+ for (const r of group) {
1615
+ const val = Number(fieldFn(r));
1616
+ const w = Number(secondFn(r));
1617
+ sumWV += val * w;
1618
+ sumW += w;
1619
+ }
1620
+ return sumW === 0 ? 0 : sumWV / sumW;
1621
+ };
1622
+ case "drawdown":
1623
+ return (row) => computeDrawdown(extractValues(row));
1624
+ // ── Ratios ────────────────────────────────────────────
1625
+ case "pct":
1626
+ return (row) => {
1627
+ const group = row._group;
1628
+ return group.reduce((acc, r) => acc + Number(fieldFn(r)), 0);
1629
+ };
1630
+ case "sharpe":
1631
+ return (row) => {
1632
+ const vals = extractValues(row);
1633
+ if (vals.length === 0) return 0;
1634
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1635
+ const sd = computeStddev(vals);
1636
+ return sd === 0 ? 0 : mean / sd;
1637
+ };
1638
+ case "calmar":
1639
+ return (row) => {
1640
+ const vals = extractValues(row);
1641
+ if (vals.length === 0) return 0;
1642
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1643
+ const dd = Math.abs(computeDrawdown(vals));
1644
+ return dd === 0 ? 0 : mean / dd;
1645
+ };
1646
+ case "sortino":
1647
+ return (row) => {
1648
+ const vals = extractValues(row);
1649
+ if (vals.length === 0) return 0;
1650
+ const mean = vals.reduce((a, b) => a + b, 0) / vals.length;
1651
+ const dsd = computeDownsideDeviation(vals, 0);
1652
+ return dsd === 0 ? 0 : mean / dsd;
1653
+ };
1654
+ case "info_ratio":
1655
+ return (row) => {
1656
+ const group = row._group;
1657
+ const excess = group.map((r) => Number(fieldFn(r)) - Number(secondFn(r)));
1658
+ if (excess.length === 0) return 0;
1659
+ const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
1660
+ const sd = computeStddev(excess);
1661
+ return sd === 0 ? 0 : mean / sd;
1662
+ };
1663
+ // ── Counting ──────────────────────────────────────────
1664
+ case "distinct_count":
1665
+ return (row) => {
1666
+ const group = row._group;
1667
+ return new Set(group.map((r) => fieldFn(r))).size;
1668
+ };
1669
+ case "sum_abs":
1670
+ return (row) => {
1671
+ const group = row._group;
1672
+ return group.reduce((acc, r) => acc + Math.abs(Number(fieldFn(r))), 0);
1673
+ };
1674
+ case "abs_sum":
1675
+ return (row) => {
1676
+ const group = row._group;
1677
+ return Math.abs(group.reduce((acc, r) => acc + Number(fieldFn(r)), 0));
1678
+ };
1679
+ // ── Range ─────────────────────────────────────────────
1680
+ case "first_value":
1681
+ return (row) => {
1682
+ const group = row._group;
1683
+ return group.length > 0 ? fieldFn(group[0]) : null;
1684
+ };
1685
+ case "last_value":
1686
+ return (row) => {
1687
+ const group = row._group;
1688
+ return group.length > 0 ? fieldFn(group[group.length - 1]) : null;
1689
+ };
1690
+ default:
1691
+ throw new RuntimeError(`Unknown aggregate function: ${name}`);
1692
+ }
1693
+ }
1694
+ function resolveFieldName(expr) {
1695
+ switch (expr.kind) {
1696
+ case "AliasExpr":
1697
+ return expr.alias;
1698
+ case "FieldAccess":
1699
+ return expr.path[expr.path.length - 1];
1700
+ case "FunctionCall":
1701
+ return expr.name;
1702
+ default:
1703
+ return "value";
1704
+ }
1705
+ }
1706
+ function compileOperation(op, isGrouped) {
1707
+ switch (op.kind) {
1708
+ case "WhereOp": {
1709
+ const condFn = compileExpression(op.condition);
1710
+ return {
1711
+ fn: (data) => data.filter((row) => condFn(row)),
1712
+ producesGrouped: false
1713
+ };
1714
+ }
1715
+ case "SelectOp":
1716
+ return compileSelectOp(op, isGrouped);
1717
+ case "SortOp": {
1718
+ const criteria = op.criteria.map((c) => ({
1719
+ fn: compileExpression(c.expression),
1720
+ dir: c.direction === "desc" ? -1 : 1
1721
+ }));
1722
+ return {
1723
+ fn: (data) => {
1724
+ return [...data].sort((a, b) => {
1725
+ for (const { fn, dir } of criteria) {
1726
+ const cmp = compareValues(fn(a), fn(b));
1727
+ if (cmp !== 0) return cmp * dir;
1728
+ }
1729
+ return 0;
1730
+ });
1731
+ },
1732
+ producesGrouped: false
1733
+ };
1734
+ }
1735
+ case "GroupByOp": {
1736
+ const keyFns = op.keys.map((k) => {
1737
+ const fn = compileExpression(k);
1738
+ const name = k.kind === "FieldAccess" ? k.path[k.path.length - 1] : "key";
1739
+ return { fn, name };
1740
+ });
1741
+ return {
1742
+ fn: (data) => {
1743
+ const groups = groupByFn(data, keyFns);
1744
+ return groups.map((g) => ({ ...g.keys, _group: g.rows }));
1745
+ },
1746
+ producesGrouped: true
1747
+ };
1748
+ }
1749
+ case "JoinOp": {
1750
+ const rightSource = op.right;
1751
+ if (op.condition.kind === "BinaryExpr" && op.condition.operator === "==" && op.condition.left.kind === "FieldAccess" && op.condition.right.kind === "FieldAccess") {
1752
+ const leftKeyFn = compileExpression(op.condition.left);
1753
+ const rightKeyFn = compileExpression(op.condition.right);
1754
+ return {
1755
+ fn: (data, context) => {
1756
+ const rightData = context[rightSource];
1757
+ if (!rightData) {
1758
+ throw new RuntimeError(`Join source "${rightSource}" not found in context`);
1759
+ }
1760
+ return hashJoin(data, rightData, leftKeyFn, rightKeyFn);
1761
+ },
1762
+ producesGrouped: false
1763
+ };
1764
+ }
1765
+ const condFn = compileExpression(op.condition);
1766
+ return {
1767
+ fn: (data, context) => {
1768
+ const rightData = context[rightSource];
1769
+ if (!rightData) {
1770
+ throw new RuntimeError(`Join source "${rightSource}" not found in context`);
1771
+ }
1772
+ return nestedLoopJoin(data, rightData, condFn);
1773
+ },
1774
+ producesGrouped: false
1775
+ };
1776
+ }
1777
+ case "FirstOp": {
1778
+ const count = op.count;
1779
+ return {
1780
+ fn: (data) => data.slice(0, count),
1781
+ producesGrouped: false
1782
+ };
1783
+ }
1784
+ case "LastOp": {
1785
+ const count = op.count;
1786
+ return {
1787
+ fn: (data) => data.slice(-count),
1788
+ producesGrouped: false
1789
+ };
1790
+ }
1791
+ case "DistinctOp": {
1792
+ const keyFn = op.fields ? (() => {
1793
+ const fns = op.fields.map((f) => compileExpression(f));
1794
+ return (row) => JSON.stringify(fns.map((fn) => fn(row)));
1795
+ })() : void 0;
1796
+ return {
1797
+ fn: (data) => distinctFn(data, keyFn),
1798
+ producesGrouped: false
1799
+ };
1800
+ }
1801
+ case "FlattenOp": {
1802
+ if (op.field) {
1803
+ const fieldFn = compileExpression(op.field);
1804
+ return {
1805
+ fn: (data) => data.flatMap((row) => {
1806
+ const val = fieldFn(row);
1807
+ return Array.isArray(val) ? val : [val];
1808
+ }),
1809
+ producesGrouped: false
1810
+ };
1811
+ }
1812
+ return {
1813
+ fn: (data) => data.flat(),
1814
+ producesGrouped: false
1815
+ };
1816
+ }
1817
+ case "TransposeOp": {
1818
+ const headerFn = op.headerField ? compileExpression(op.headerField) : void 0;
1819
+ const headerFieldName = op.headerField ? resolveFieldName(op.headerField) : void 0;
1820
+ return {
1821
+ fn: (data) => transposeFn(data, headerFn, headerFieldName),
1822
+ producesGrouped: false
1823
+ };
1824
+ }
1825
+ case "RollupOp": {
1826
+ const keyFns = op.keys.map((k) => {
1827
+ const fn = compileExpression(k);
1828
+ const name = resolveFieldName(k);
1829
+ return { fn, name };
1830
+ });
1831
+ const aggFns = op.aggregates.map((a) => {
1832
+ const fn = compileExpression(a, true);
1833
+ const name = resolveFieldName(a);
1834
+ return { fn, name };
1835
+ });
1836
+ const pctCols = /* @__PURE__ */ new Set();
1837
+ for (const a of op.aggregates) {
1838
+ const expr = a.kind === "AliasExpr" ? a.expression : a;
1839
+ if (expr.kind === "FunctionCall" && expr.name === "pct") {
1840
+ pctCols.add(resolveFieldName(a));
1841
+ }
1842
+ }
1843
+ return {
1844
+ fn: (data) => {
1845
+ const results = rollupFn(data, keyFns, aggFns);
1846
+ if (pctCols.size > 0) {
1847
+ const numKeys = keyFns.length;
1848
+ const grandRow = results.find((r) => r._rollupLevel === numKeys);
1849
+ for (const col of pctCols) {
1850
+ const total = Number(grandRow?.[col]) || 1;
1851
+ for (const row of results) {
1852
+ row[col] = Number(row[col]) / total * 100;
1853
+ }
1854
+ }
1855
+ if (pctCols.size === aggFns.length) {
1856
+ return results.filter((r) => r._rollupLevel !== numKeys);
1857
+ }
1858
+ }
1859
+ return results;
1860
+ },
1861
+ producesGrouped: false
1862
+ };
1863
+ }
1864
+ case "PivotOp": {
1865
+ const pivotFn = compileExpression(op.pivotField);
1866
+ const aggCompilers = op.aggregates.map((a) => ({
1867
+ fn: compileExpression(a, true),
1868
+ // grouped = true
1869
+ name: resolveFieldName(a)
1870
+ }));
1871
+ const singleAggregate = aggCompilers.length === 1;
1872
+ return {
1873
+ fn: (data) => {
1874
+ if (isGrouped) {
1875
+ return pivotGroupedFn(data, pivotFn, aggCompilers, singleAggregate);
1876
+ }
1877
+ return pivotFlatFn(data, pivotFn, aggCompilers, singleAggregate);
1878
+ },
1879
+ producesGrouped: false
1880
+ };
1881
+ }
1882
+ case "MapOp": {
1883
+ const fieldCompilers = op.fields.map((field2) => {
1884
+ const name = resolveFieldName(field2);
1885
+ const fn = compileExpression(field2, isGrouped);
1886
+ return { name, fn };
1887
+ });
1888
+ return {
1889
+ fn: (data) => data.map((row) => {
1890
+ const result = { ...row };
1891
+ for (const { name, fn } of fieldCompilers) {
1892
+ result[name] = fn(row);
1893
+ }
1894
+ return result;
1895
+ }),
1896
+ producesGrouped: isGrouped
1897
+ };
1898
+ }
1899
+ case "ReduceOp": {
1900
+ const initialFn = compileExpression(op.initial);
1901
+ const accFn = compileExpression(op.accumulator);
1902
+ return {
1903
+ fn: (data) => {
1904
+ let acc = initialFn({});
1905
+ for (const row of data) {
1906
+ acc = accFn({ ...row, _acc: acc });
1907
+ }
1908
+ return acc;
1909
+ },
1910
+ producesGrouped: false
1911
+ };
1912
+ }
1913
+ case "AggregateOp": {
1914
+ const argFns = op.args?.map((a) => compileExpression(a));
1915
+ const primaryFn = argFns ? argFns[0] : op.field ? compileExpression(op.field) : void 0;
1916
+ const secondFn = argFns ? argFns[1] : void 0;
1917
+ const aggName = op.function;
1918
+ return {
1919
+ fn: (data) => {
1920
+ const vals = () => data.map((row) => Number(primaryFn(row)));
1921
+ switch (aggName) {
1922
+ // ── Core ──────────────────────────────────────
1923
+ case "count":
1924
+ return data.length;
1925
+ case "sum":
1926
+ return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
1927
+ case "avg": {
1928
+ if (data.length === 0) return 0;
1929
+ const total = data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
1930
+ return total / data.length;
1931
+ }
1932
+ case "min":
1933
+ return data.reduce((min, row) => {
1934
+ const val = primaryFn(row);
1935
+ return val < min ? val : min;
1936
+ }, primaryFn(data[0]));
1937
+ case "max":
1938
+ return data.reduce((max, row) => {
1939
+ const val = primaryFn(row);
1940
+ return val > max ? val : max;
1941
+ }, primaryFn(data[0]));
1942
+ // ── Statistical ───────────────────────────────
1943
+ case "median":
1944
+ return computeMedian(vals());
1945
+ case "stddev":
1946
+ return computeStddev(vals());
1947
+ case "var":
1948
+ return computeVariance(vals());
1949
+ case "percentile":
1950
+ return computePercentile(vals(), Number(secondFn(data[0])));
1951
+ case "skew":
1952
+ return computeSkew(vals());
1953
+ case "kurt":
1954
+ return computeKurt(vals());
1955
+ // ── Finance ───────────────────────────────────
1956
+ case "vwap": {
1957
+ let sumPV = 0, sumV = 0;
1958
+ for (const row of data) {
1959
+ const p = Number(primaryFn(row));
1960
+ const v = Number(secondFn(row));
1961
+ sumPV += p * v;
1962
+ sumV += v;
1963
+ }
1964
+ return sumV === 0 ? 0 : sumPV / sumV;
1965
+ }
1966
+ case "wavg": {
1967
+ let sumWV = 0, sumW = 0;
1968
+ for (const row of data) {
1969
+ const val = Number(primaryFn(row));
1970
+ const w = Number(secondFn(row));
1971
+ sumWV += val * w;
1972
+ sumW += w;
1973
+ }
1974
+ return sumW === 0 ? 0 : sumWV / sumW;
1975
+ }
1976
+ case "drawdown":
1977
+ return computeDrawdown(vals());
1978
+ // ── Ratios ────────────────────────────────────
1979
+ case "pct": {
1980
+ return data.reduce((acc, row) => acc + Number(primaryFn(row)), 0);
1981
+ }
1982
+ case "sharpe": {
1983
+ const v = vals();
1984
+ if (v.length === 0) return 0;
1985
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
1986
+ const sd = computeStddev(v);
1987
+ return sd === 0 ? 0 : mean / sd;
1988
+ }
1989
+ case "calmar": {
1990
+ const v = vals();
1991
+ if (v.length === 0) return 0;
1992
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
1993
+ const dd = Math.abs(computeDrawdown(v));
1994
+ return dd === 0 ? 0 : mean / dd;
1995
+ }
1996
+ case "sortino": {
1997
+ const v = vals();
1998
+ if (v.length === 0) return 0;
1999
+ const mean = v.reduce((a, b) => a + b, 0) / v.length;
2000
+ const dsd = computeDownsideDeviation(v, 0);
2001
+ return dsd === 0 ? 0 : mean / dsd;
2002
+ }
2003
+ case "info_ratio": {
2004
+ const excess = data.map((row) => Number(primaryFn(row)) - Number(secondFn(row)));
2005
+ if (excess.length === 0) return 0;
2006
+ const mean = excess.reduce((a, b) => a + b, 0) / excess.length;
2007
+ const sd = computeStddev(excess);
2008
+ return sd === 0 ? 0 : mean / sd;
2009
+ }
2010
+ // ── Counting ──────────────────────────────────
2011
+ case "distinct_count":
2012
+ return new Set(data.map((row) => primaryFn(row))).size;
2013
+ case "sum_abs":
2014
+ return data.reduce((acc, row) => acc + Math.abs(Number(primaryFn(row))), 0);
2015
+ case "abs_sum":
2016
+ return Math.abs(data.reduce((acc, row) => acc + Number(primaryFn(row)), 0));
2017
+ // ── Range ─────────────────────────────────────
2018
+ case "first_value":
2019
+ return data.length > 0 ? primaryFn(data[0]) : null;
2020
+ case "last_value":
2021
+ return data.length > 0 ? primaryFn(data[data.length - 1]) : null;
2022
+ }
2023
+ },
2024
+ producesGrouped: false
2025
+ };
2026
+ }
2027
+ }
2028
+ }
2029
+ function compileSelectOp(op, isGrouped) {
2030
+ const fieldCompilers = op.fields.map((field2) => {
2031
+ const name = resolveFieldName(field2);
2032
+ const fn = compileExpression(field2, isGrouped);
2033
+ return { name, fn };
2034
+ });
2035
+ const windowDefs = [];
2036
+ for (const { fn } of fieldCompilers) {
2037
+ const wDef = fn._windowDef;
2038
+ if (wDef) windowDefs.push(wDef);
2039
+ }
2040
+ if (windowDefs.length > 0) {
2041
+ return {
2042
+ fn: (data) => {
2043
+ const windowValues = computeWindowValues(data, windowDefs);
2044
+ return data.map((row, i) => {
2045
+ const enrichedRow = { ...row, _windowCtx: windowValues[i] };
2046
+ const result = {};
2047
+ for (const { name, fn } of fieldCompilers) {
2048
+ result[name] = fn(enrichedRow);
2049
+ }
2050
+ return result;
2051
+ });
2052
+ },
2053
+ producesGrouped: false
2054
+ };
2055
+ }
2056
+ return {
2057
+ fn: (data) => {
2058
+ return data.map((row) => {
2059
+ const result = {};
2060
+ for (const { name, fn } of fieldCompilers) {
2061
+ result[name] = fn(row);
2062
+ }
2063
+ return result;
2064
+ });
2065
+ },
2066
+ producesGrouped: false
2067
+ };
2068
+ }
2069
+ function compile(ast, source) {
2070
+ const compiledOps = [];
2071
+ let isGrouped = false;
2072
+ for (const op of ast.operations) {
2073
+ const { fn, producesGrouped } = compileOperation(op, isGrouped);
2074
+ compiledOps.push(fn);
2075
+ isGrouped = producesGrouped;
2076
+ }
2077
+ const sourceName = ast.source;
2078
+ const queryFn = (context) => {
2079
+ const sourceData = context[sourceName];
2080
+ if (!sourceData) {
2081
+ throw new RuntimeError(`Source "${sourceName}" not found in context`);
2082
+ }
2083
+ if (!Array.isArray(sourceData)) {
2084
+ throw new RuntimeError(`Source "${sourceName}" is not an array`);
2085
+ }
2086
+ let data = sourceData;
2087
+ for (const opFn of compiledOps) {
2088
+ if (!Array.isArray(data)) {
2089
+ return data;
2090
+ }
2091
+ data = opFn(data, context);
2092
+ }
2093
+ return data;
2094
+ };
2095
+ const compiled = queryFn;
2096
+ Object.defineProperty(compiled, "source", { value: source, writable: false });
2097
+ Object.defineProperty(compiled, "ast", { value: ast, writable: false });
2098
+ return compiled;
2099
+ }
2100
+
2101
+ // ../src/engine/cache.ts
2102
+ var QueryCache = class {
2103
+ cache = /* @__PURE__ */ new Map();
2104
+ maxSize;
2105
+ constructor(maxSize = 128) {
2106
+ this.maxSize = maxSize;
2107
+ }
2108
+ get(key) {
2109
+ const value = this.cache.get(key);
2110
+ if (value) {
2111
+ this.cache.delete(key);
2112
+ this.cache.set(key, value);
2113
+ }
2114
+ return value;
2115
+ }
2116
+ set(key, value) {
2117
+ if (this.cache.has(key)) {
2118
+ this.cache.delete(key);
2119
+ } else if (this.cache.size >= this.maxSize) {
2120
+ const firstKey = this.cache.keys().next().value;
2121
+ if (firstKey !== void 0) {
2122
+ this.cache.delete(firstKey);
2123
+ }
2124
+ }
2125
+ this.cache.set(key, value);
2126
+ }
2127
+ clear() {
2128
+ this.cache.clear();
2129
+ }
2130
+ get size() {
2131
+ return this.cache.size;
2132
+ }
2133
+ };
2134
+
2135
+ // ../src/engine/index.ts
2136
+ var defaultCache = new QueryCache(128);
2137
+ var OPERATION_KEYWORDS = /* @__PURE__ */ new Set([
2138
+ "where",
2139
+ "select",
2140
+ "sort",
2141
+ "groupBy",
2142
+ "join",
2143
+ "first",
2144
+ "last",
2145
+ "distinct",
2146
+ "map",
2147
+ "reduce",
2148
+ "rollup",
2149
+ "pivot",
2150
+ "flatten",
2151
+ "transpose"
2152
+ ]);
2153
+ function normalizeExpression(expression) {
2154
+ const trimmed = expression.trimStart();
2155
+ const match = trimmed.match(/^(\w+)\s*\(/);
2156
+ if (match && OPERATION_KEYWORDS.has(match[1])) {
2157
+ return `_data | ${trimmed}`;
2158
+ }
2159
+ return expression;
2160
+ }
2161
+ function compile2(expression, useCache = true) {
2162
+ if (useCache) {
2163
+ const cached = defaultCache.get(expression);
2164
+ if (cached) return cached;
2165
+ }
2166
+ const tokens = tokenize(expression);
2167
+ const ast = parse(tokens);
2168
+ const fn = compile(ast, expression);
2169
+ if (useCache) {
2170
+ defaultCache.set(expression, fn);
2171
+ }
2172
+ return fn;
2173
+ }
2174
+ function query(context, expression) {
2175
+ if (Array.isArray(context)) {
2176
+ const normalized = normalizeExpression(expression);
2177
+ const fn2 = compile2(normalized);
2178
+ return fn2({ _data: context });
2179
+ }
2180
+ const fn = compile2(expression);
2181
+ return fn(context);
2182
+ }
2183
+ function parseQuery(expression) {
2184
+ const tokens = tokenize(expression);
2185
+ return parse(tokens);
2186
+ }
2187
+
2188
+ // src/server/sources/pushdown/compile.ts
2189
+ var BINARY_OP_SQL = {
2190
+ ">": ">",
2191
+ ">=": ">=",
2192
+ "<": "<",
2193
+ "<=": "<=",
2194
+ "==": "=",
2195
+ "!=": "<>",
2196
+ "&&": "AND",
2197
+ "||": "OR",
2198
+ "+": "+",
2199
+ "-": "-",
2200
+ "*": "*",
2201
+ "/": "/",
2202
+ "%": "%"
2203
+ };
2204
+ var SQL_AGGREGATE_FN = {
2205
+ sum: (arg) => `SUM(${arg ?? "*"})`,
2206
+ avg: (arg) => `AVG(${arg ?? "*"})`,
2207
+ min: (arg) => `MIN(${arg ?? "*"})`,
2208
+ max: (arg) => `MAX(${arg ?? "*"})`,
2209
+ count: (arg) => `COUNT(${arg ?? "*"})`,
2210
+ distinct_count: (arg) => `COUNT(DISTINCT ${arg ?? "*"})`
2211
+ };
2212
+ function compileForDialect(pipeline, baseQuery, dialect) {
2213
+ const ctx = {
2214
+ baseSelect: stripTrailingSemicolon(baseQuery),
2215
+ params: [],
2216
+ dialect
2217
+ };
2218
+ let where = null;
2219
+ let orderBy = null;
2220
+ let limit = null;
2221
+ let selectClause = null;
2222
+ let distinctClause = false;
2223
+ let groupByClause = null;
2224
+ let grouped = false;
2225
+ for (const op of pipeline.operations) {
2226
+ switch (op.kind) {
2227
+ case "WhereOp": {
2228
+ if (grouped) {
2229
+ return { ok: false, reason: "where after grouping (HAVING) is not yet pushable" };
2230
+ }
2231
+ const compiled = compileExpr(op.condition, ctx);
2232
+ if (compiled === null) {
2233
+ return { ok: false, reason: "where(): unsupported expression node" };
2234
+ }
2235
+ where = where ? `(${where}) AND (${compiled})` : compiled;
2236
+ break;
2237
+ }
2238
+ case "SortOp": {
2239
+ const parts = [];
2240
+ for (const c of op.criteria) {
2241
+ if (c.expression.kind !== "FieldAccess" || c.expression.path.length !== 1) {
2242
+ return { ok: false, reason: "sort(): only single-field paths supported" };
2243
+ }
2244
+ const ident = ctx.dialect.quoteIdent(c.expression.path[0]);
2245
+ parts.push(`${ident} ${c.direction === "desc" ? "DESC" : "ASC"}`);
2246
+ }
2247
+ orderBy = parts.join(", ");
2248
+ break;
2249
+ }
2250
+ case "FirstOp": {
2251
+ limit = op.count;
2252
+ break;
2253
+ }
2254
+ case "SelectOp": {
2255
+ if (grouped) {
2256
+ return { ok: false, reason: "select after rollup is not yet pushable" };
2257
+ }
2258
+ if (selectClause !== null) {
2259
+ return { ok: false, reason: "multiple select() ops in one pipeline" };
2260
+ }
2261
+ const parts = [];
2262
+ for (const f of op.fields) {
2263
+ const compiled = compileSelectField(f, ctx, false);
2264
+ if (compiled === null) {
2265
+ return { ok: false, reason: "select(): unsupported field expression" };
2266
+ }
2267
+ parts.push(compiled);
2268
+ }
2269
+ selectClause = parts.join(", ");
2270
+ break;
2271
+ }
2272
+ case "DistinctOp": {
2273
+ if (op.fields !== void 0 && op.fields.length > 0) {
2274
+ return { ok: false, reason: "distinct(field) is not yet pushable; full-row distinct works" };
2275
+ }
2276
+ if (grouped) {
2277
+ return { ok: false, reason: "distinct after grouping is not yet pushable" };
2278
+ }
2279
+ distinctClause = true;
2280
+ break;
2281
+ }
2282
+ case "RollupOp": {
2283
+ if (grouped) {
2284
+ return { ok: false, reason: "multiple grouping ops (rollup/aggregate) in one pipeline" };
2285
+ }
2286
+ if (selectClause !== null) {
2287
+ return { ok: false, reason: "rollup after select is not yet pushable" };
2288
+ }
2289
+ const keyParts = [];
2290
+ const groupByParts = [];
2291
+ for (const k of op.keys) {
2292
+ if (k.kind !== "FieldAccess" || k.path.length !== 1) {
2293
+ return { ok: false, reason: "rollup(): keys must be single-field paths" };
2294
+ }
2295
+ const ident = ctx.dialect.quoteIdent(k.path[0]);
2296
+ keyParts.push(ident);
2297
+ groupByParts.push(ident);
2298
+ }
2299
+ const aggParts = [];
2300
+ for (const a of op.aggregates) {
2301
+ const compiled = compileSelectField(a, ctx, true);
2302
+ if (compiled === null) {
2303
+ return { ok: false, reason: "rollup(): unsupported aggregate expression" };
2304
+ }
2305
+ aggParts.push(compiled);
2306
+ }
2307
+ selectClause = [...keyParts, ...aggParts].join(", ");
2308
+ groupByClause = groupByParts.length > 0 ? groupByParts.join(", ") : null;
2309
+ grouped = true;
2310
+ break;
2311
+ }
2312
+ case "AggregateOp": {
2313
+ if (grouped) {
2314
+ return { ok: false, reason: "multiple grouping ops (rollup/aggregate) in one pipeline" };
2315
+ }
2316
+ if (selectClause !== null) {
2317
+ return { ok: false, reason: "aggregate after select is not yet pushable" };
2318
+ }
2319
+ const fn = SQL_AGGREGATE_FN[op.function];
2320
+ if (!fn) {
2321
+ return { ok: false, reason: `aggregate "${op.function}" has no SQL push-down` };
2322
+ }
2323
+ if (op.args && op.args.length > 0) {
2324
+ return { ok: false, reason: `aggregate "${op.function}" with extra args is not yet pushable` };
2325
+ }
2326
+ const argSql = op.field === void 0 ? null : compileExpr(op.field, ctx);
2327
+ if (op.field !== void 0 && argSql === null) {
2328
+ return { ok: false, reason: `aggregate "${op.function}": unsupported argument expression` };
2329
+ }
2330
+ selectClause = `${fn(argSql)} AS ${ctx.dialect.quoteIdent(op.function)}`;
2331
+ grouped = true;
2332
+ break;
2333
+ }
2334
+ default:
2335
+ return { ok: false, reason: `${op.kind}: not yet pushable` };
2336
+ }
2337
+ }
2338
+ let sql = `SELECT ${distinctClause ? "DISTINCT " : ""}${selectClause ?? "*"} FROM (${ctx.baseSelect}) AS pq_src`;
2339
+ if (where) sql += ` WHERE ${where}`;
2340
+ if (groupByClause) sql += ` GROUP BY ${groupByClause}`;
2341
+ if (orderBy) sql += ` ORDER BY ${orderBy}`;
2342
+ if (limit !== null) sql += ` LIMIT ${limit}`;
2343
+ return { ok: true, compiled: { sql, params: ctx.params } };
2344
+ }
2345
+ function compileSelectField(expr, ctx, aggregateContext) {
2346
+ if (expr.kind === "AliasExpr") {
2347
+ const inner = compileSelectField(expr.expression, ctx, aggregateContext);
2348
+ if (inner === null) return null;
2349
+ return `${inner} AS ${ctx.dialect.quoteIdent(expr.alias)}`;
2350
+ }
2351
+ if (expr.kind === "FunctionCall") {
2352
+ if (!aggregateContext) return null;
2353
+ return compileAggregateCall(expr.name, expr.args, ctx);
2354
+ }
2355
+ return compileExpr(expr, ctx);
2356
+ }
2357
+ function compileAggregateCall(name, args, ctx) {
2358
+ const fn = SQL_AGGREGATE_FN[name];
2359
+ if (!fn) return null;
2360
+ if (args.length === 0) return fn(null);
2361
+ if (args.length > 1) return null;
2362
+ const argSql = compileExpr(args[0], ctx);
2363
+ if (argSql === null) return null;
2364
+ return fn(argSql);
2365
+ }
2366
+ function compileExpr(expr, ctx) {
2367
+ switch (expr.kind) {
2368
+ case "NumberLiteral":
2369
+ case "StringLiteral":
2370
+ case "BooleanLiteral":
2371
+ return ctx.dialect.bindLiteral(expr.value, ctx.params);
2372
+ case "NullLiteral":
2373
+ return "NULL";
2374
+ case "FieldAccess": {
2375
+ if (expr.path.length !== 1) return null;
2376
+ return ctx.dialect.quoteIdent(expr.path[0]);
2377
+ }
2378
+ case "BinaryExpr": {
2379
+ const opSql = BINARY_OP_SQL[expr.operator];
2380
+ if (!opSql) return null;
2381
+ const left = compileExpr(expr.left, ctx);
2382
+ const right = compileExpr(expr.right, ctx);
2383
+ if (left === null || right === null) return null;
2384
+ if (expr.right.kind === "NullLiteral") {
2385
+ if (expr.operator === "==") return `${left} IS NULL`;
2386
+ if (expr.operator === "!=") return `${left} IS NOT NULL`;
2387
+ }
2388
+ if (expr.left.kind === "NullLiteral") {
2389
+ if (expr.operator === "==") return `${right} IS NULL`;
2390
+ if (expr.operator === "!=") return `${right} IS NOT NULL`;
2391
+ }
2392
+ return `(${left} ${opSql} ${right})`;
2393
+ }
2394
+ case "UnaryExpr": {
2395
+ const operand = compileExpr(expr.operand, ctx);
2396
+ if (operand === null) return null;
2397
+ if (expr.operator === "!") return `(NOT ${operand})`;
2398
+ if (expr.operator === "-") return `(-${operand})`;
2399
+ return null;
2400
+ }
2401
+ case "FunctionCall":
2402
+ case "AliasExpr":
2403
+ return null;
2404
+ }
2405
+ }
2406
+ function stripTrailingSemicolon(sql) {
2407
+ return sql.trim().replace(/;+\s*$/, "");
2408
+ }
2409
+
2410
+ // src/server/sources/pushdown/dialect.ts
2411
+ var POSTGRES_DIALECT = {
2412
+ quoteIdent: (name) => `"${name.replace(/"/g, '""')}"`,
2413
+ bindLiteral: (value, params) => {
2414
+ params.push(value);
2415
+ return `$${params.length}`;
2416
+ }
2417
+ };
2418
+ var MYSQL_DIALECT = {
2419
+ quoteIdent: (name) => `\`${name.replace(/`/g, "``")}\``,
2420
+ bindLiteral: (value, params) => {
2421
+ params.push(value);
2422
+ return "?";
2423
+ }
2424
+ };
2425
+ var SNOWFLAKE_DIALECT = {
2426
+ quoteIdent: (name) => `"${name.replace(/"/g, '""')}"`,
2427
+ bindLiteral: (value, params) => {
2428
+ params.push(value);
2429
+ return "?";
2430
+ }
2431
+ };
2432
+ var CLICKHOUSE_DIALECT = {
2433
+ quoteIdent: (name) => `\`${name.replace(/`/g, "``")}\``,
2434
+ bindLiteral: (value) => clickhouseLiteral(value)
2435
+ };
2436
+ function clickhouseLiteral(value) {
2437
+ if (value === null || value === void 0) return "NULL";
2438
+ if (typeof value === "boolean") return value ? "1" : "0";
2439
+ if (typeof value === "number") {
2440
+ if (!Number.isFinite(value)) return "NULL";
2441
+ return String(value);
2442
+ }
2443
+ if (typeof value === "string") {
2444
+ const escaped = value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2445
+ return `'${escaped}'`;
2446
+ }
2447
+ return clickhouseLiteral(String(value));
2448
+ }
2449
+
2450
+ // src/server/sources/pushdown/postgres.ts
2451
+ function compilePostgresPushdown(pipeline, baseQuery) {
2452
+ return compileForDialect(pipeline, baseQuery, POSTGRES_DIALECT);
2453
+ }
2454
+
2455
+ // src/server/sources/postgres.ts
2456
+ var DEFAULT_INTERVAL_MS = 3e4;
2457
+ var DEFAULT_MAX_ROWS = 1e4;
2458
+ function resolveSsl(ssl) {
2459
+ if (ssl === false) return false;
2460
+ if (ssl === "no-verify") return { rejectUnauthorized: false };
2461
+ return { rejectUnauthorized: true };
2462
+ }
2463
+ var PostgresSourceAdapter = class {
2464
+ constructor(config) {
2465
+ this.config = config;
2466
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS;
2467
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS;
2468
+ }
2469
+ config;
2470
+ pool = null;
2471
+ data = [];
2472
+ timer = null;
2473
+ lastFetch = null;
2474
+ error;
2475
+ listeners = /* @__PURE__ */ new Set();
2476
+ intervalMs;
2477
+ maxRows;
2478
+ async start() {
2479
+ const connectionString = expandEnv(this.config.url, "postgres source URL");
2480
+ this.pool = new pg.Pool({
2481
+ connectionString,
2482
+ ssl: resolveSsl(this.config.ssl),
2483
+ // Small pool — we're read-only polling, not a transactional app.
2484
+ min: 0,
2485
+ max: 2,
2486
+ idleTimeoutMillis: 1e4
2487
+ });
2488
+ this.pool.on("error", (err) => {
2489
+ this.error = err.message;
2490
+ });
2491
+ await this.fetch();
2492
+ this.timer = setInterval(() => {
2493
+ void this.fetch();
2494
+ }, this.intervalMs);
2495
+ }
2496
+ stop() {
2497
+ if (this.timer) {
2498
+ clearInterval(this.timer);
2499
+ this.timer = null;
2500
+ }
2501
+ if (this.pool) {
2502
+ void this.pool.end();
2503
+ this.pool = null;
2504
+ }
2505
+ }
2506
+ getData() {
2507
+ return this.data;
2508
+ }
2509
+ getStatus() {
2510
+ return {
2511
+ healthy: !this.error,
2512
+ rowCount: this.data.length,
2513
+ lastFetch: this.lastFetch,
2514
+ error: this.error
2515
+ };
2516
+ }
2517
+ onUpdate(callback) {
2518
+ this.listeners.add(callback);
2519
+ return () => this.listeners.delete(callback);
2520
+ }
2521
+ async refresh() {
2522
+ await this.fetch();
2523
+ }
2524
+ /**
2525
+ * Push-down execution prototype. Compiles a pipequery expression rooted at
2526
+ * this source into a single Postgres query (where / sort / first only in
2527
+ * v1) and runs it directly against the database — no in-memory
2528
+ * materialization of the user's source query, no polling, no ring buffer.
2529
+ *
2530
+ * Returns:
2531
+ * - `{ ok: true, rows, sql, params, fullyPushed: true }` on a clean push-down
2532
+ * - `{ ok: false, reason }` when the AST contains operators we don't yet
2533
+ * translate. Caller should fall back to the in-process engine on the
2534
+ * ring-buffered data instead.
2535
+ *
2536
+ * This is an opt-in API; the default getData() / fetch() polling loop is
2537
+ * unchanged so existing users see no behaviour change.
2538
+ */
2539
+ async runPushdown(expression) {
2540
+ if (!this.pool) {
2541
+ return { ok: false, reason: "Postgres pool not initialized \u2014 call start() first" };
2542
+ }
2543
+ let pipeline;
2544
+ try {
2545
+ pipeline = parseQuery(expression);
2546
+ } catch (err) {
2547
+ return { ok: false, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` };
2548
+ }
2549
+ const compiled = compilePostgresPushdown(pipeline, this.config.query);
2550
+ if (!compiled.ok) {
2551
+ return { ok: false, reason: compiled.reason };
2552
+ }
2553
+ try {
2554
+ const res = await this.pool.query(compiled.compiled.sql, compiled.compiled.params);
2555
+ return {
2556
+ ok: true,
2557
+ rows: res.rows,
2558
+ sql: compiled.compiled.sql,
2559
+ params: compiled.compiled.params,
2560
+ fullyPushed: true
2561
+ };
2562
+ } catch (err) {
2563
+ return { ok: false, reason: `postgres error: ${err instanceof Error ? err.message : String(err)}` };
2564
+ }
2565
+ }
2566
+ async fetch() {
2567
+ if (!this.pool) {
2568
+ this.error = "Postgres pool not initialized";
2569
+ return;
2570
+ }
2571
+ try {
2572
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon2(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
2573
+ const res = await this.pool.query(capQuery);
2574
+ if (res.rows.length > this.maxRows) {
2575
+ throw new Error(
2576
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
2577
+ );
2578
+ }
2579
+ this.data = res.rows;
2580
+ this.lastFetch = /* @__PURE__ */ new Date();
2581
+ this.error = void 0;
2582
+ this.notify();
2583
+ } catch (err) {
2584
+ this.error = err instanceof Error ? err.message : String(err);
2585
+ }
2586
+ }
2587
+ notify() {
2588
+ for (const cb of this.listeners) cb(this.data);
2589
+ }
2590
+ };
2591
+ function stripTrailingSemicolon2(sql) {
2592
+ return sql.trim().replace(/;+\s*$/, "");
2593
+ }
2594
+
2595
+ // src/server/sources/mysql.ts
2596
+ import mysql from "mysql2/promise";
2597
+
2598
+ // src/server/sources/pushdown/mysql.ts
2599
+ function compileMysqlPushdown(pipeline, baseQuery) {
2600
+ return compileForDialect(pipeline, baseQuery, MYSQL_DIALECT);
2601
+ }
2602
+
2603
+ // src/server/sources/mysql.ts
2604
+ var DEFAULT_INTERVAL_MS2 = 3e4;
2605
+ var DEFAULT_MAX_ROWS2 = 1e4;
2606
+ function resolveSsl2(ssl) {
2607
+ if (ssl === false) return void 0;
2608
+ if (ssl === "no-verify") return { rejectUnauthorized: false };
2609
+ return { rejectUnauthorized: true };
2610
+ }
2611
+ var MysqlSourceAdapter = class {
2612
+ constructor(config) {
2613
+ this.config = config;
2614
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS2;
2615
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS2;
2616
+ }
2617
+ config;
2618
+ pool = null;
2619
+ data = [];
2620
+ timer = null;
2621
+ lastFetch = null;
2622
+ error;
2623
+ listeners = /* @__PURE__ */ new Set();
2624
+ intervalMs;
2625
+ maxRows;
2626
+ async start() {
2627
+ const uri = expandEnv(this.config.url, "mysql source URL");
2628
+ this.pool = mysql.createPool({
2629
+ uri,
2630
+ ssl: resolveSsl2(this.config.ssl),
2631
+ connectionLimit: 2,
2632
+ waitForConnections: true,
2633
+ // mysql2 returns BigInt for large integers by default — stringify so the
2634
+ // data serializes cleanly to JSON (which can't encode BigInt).
2635
+ supportBigNumbers: true,
2636
+ bigNumberStrings: true
2637
+ });
2638
+ await this.fetch();
2639
+ this.timer = setInterval(() => {
2640
+ void this.fetch();
2641
+ }, this.intervalMs);
2642
+ }
2643
+ stop() {
2644
+ if (this.timer) {
2645
+ clearInterval(this.timer);
2646
+ this.timer = null;
2647
+ }
2648
+ if (this.pool) {
2649
+ void this.pool.end();
2650
+ this.pool = null;
2651
+ }
2652
+ }
2653
+ getData() {
2654
+ return this.data;
2655
+ }
2656
+ getStatus() {
2657
+ return {
2658
+ healthy: !this.error,
2659
+ rowCount: this.data.length,
2660
+ lastFetch: this.lastFetch,
2661
+ error: this.error
2662
+ };
2663
+ }
2664
+ onUpdate(callback) {
2665
+ this.listeners.add(callback);
2666
+ return () => this.listeners.delete(callback);
2667
+ }
2668
+ async refresh() {
2669
+ await this.fetch();
2670
+ }
2671
+ /**
2672
+ * Adapter-native push-down. Compiles the pipe expression to MySQL SQL
2673
+ * (see ./pushdown/mysql.ts) and runs it directly. SourceManager.runQuery
2674
+ * dispatches here automatically when the pipeline shape is eligible —
2675
+ * unsupported shapes return `{ ok: false, reason }` and the caller falls
2676
+ * back to the in-process engine.
2677
+ *
2678
+ * Mirrors PostgresSourceAdapter.runPushdown — same shape, same result
2679
+ * type. Both adapters route through the dialect-parametric compiler in
2680
+ * compile.ts.
2681
+ */
2682
+ async runPushdown(expression) {
2683
+ if (!this.pool) {
2684
+ return { ok: false, reason: "MySQL pool not initialized \u2014 call start() first" };
2685
+ }
2686
+ let pipeline;
2687
+ try {
2688
+ pipeline = parseQuery(expression);
2689
+ } catch (err) {
2690
+ return { ok: false, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` };
2691
+ }
2692
+ const compiled = compileMysqlPushdown(pipeline, this.config.query);
2693
+ if (!compiled.ok) {
2694
+ return { ok: false, reason: compiled.reason };
2695
+ }
2696
+ try {
2697
+ const [rows] = await this.pool.query(compiled.compiled.sql, compiled.compiled.params);
2698
+ const asArray = Array.isArray(rows) ? rows : [];
2699
+ return {
2700
+ ok: true,
2701
+ rows: asArray,
2702
+ sql: compiled.compiled.sql,
2703
+ params: compiled.compiled.params
2704
+ };
2705
+ } catch (err) {
2706
+ return { ok: false, reason: `mysql error: ${err instanceof Error ? err.message : String(err)}` };
2707
+ }
2708
+ }
2709
+ async fetch() {
2710
+ if (!this.pool) {
2711
+ this.error = "MySQL pool not initialized";
2712
+ return;
2713
+ }
2714
+ try {
2715
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon3(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
2716
+ const [rows] = await this.pool.query(capQuery);
2717
+ const asArray = Array.isArray(rows) ? rows : [];
2718
+ if (asArray.length > this.maxRows) {
2719
+ throw new Error(
2720
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
2721
+ );
2722
+ }
2723
+ this.data = asArray;
2724
+ this.lastFetch = /* @__PURE__ */ new Date();
2725
+ this.error = void 0;
2726
+ this.notify();
2727
+ } catch (err) {
2728
+ this.error = err instanceof Error ? err.message : String(err);
2729
+ }
2730
+ }
2731
+ notify() {
2732
+ for (const cb of this.listeners) cb(this.data);
2733
+ }
2734
+ };
2735
+ function stripTrailingSemicolon3(sql) {
2736
+ return sql.trim().replace(/;+\s*$/, "");
2737
+ }
2738
+
2739
+ // src/server/sources/sqlite.ts
2740
+ import { resolve as resolve2 } from "path";
2741
+ import Database from "better-sqlite3";
2742
+ var DEFAULT_INTERVAL_MS3 = 3e4;
2743
+ var DEFAULT_MAX_ROWS3 = 1e4;
2744
+ var IN_MEMORY = ":memory:";
2745
+ var SqliteSourceAdapter = class {
2746
+ constructor(config, cwd) {
2747
+ this.config = config;
2748
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS3;
2749
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS3;
2750
+ this.readonly = config.readonly ?? true;
2751
+ this.resolvedPath = config.path === IN_MEMORY ? IN_MEMORY : resolve2(cwd, config.path);
2752
+ }
2753
+ config;
2754
+ db = null;
2755
+ data = [];
2756
+ timer = null;
2757
+ lastFetch = null;
2758
+ error;
2759
+ listeners = /* @__PURE__ */ new Set();
2760
+ intervalMs;
2761
+ maxRows;
2762
+ resolvedPath;
2763
+ readonly;
2764
+ async start() {
2765
+ const opts = this.resolvedPath === IN_MEMORY ? {} : { readonly: this.readonly, fileMustExist: this.readonly };
2766
+ this.db = new Database(this.resolvedPath, opts);
2767
+ if (this.resolvedPath !== IN_MEMORY && !this.readonly) {
2768
+ this.db.pragma("journal_mode = WAL");
2769
+ }
2770
+ await this.fetch();
2771
+ this.timer = setInterval(() => {
2772
+ void this.fetch();
2773
+ }, this.intervalMs);
2774
+ }
2775
+ stop() {
2776
+ if (this.timer) {
2777
+ clearInterval(this.timer);
2778
+ this.timer = null;
2779
+ }
2780
+ if (this.db) {
2781
+ this.db.close();
2782
+ this.db = null;
2783
+ }
2784
+ }
2785
+ getData() {
2786
+ return this.data;
2787
+ }
2788
+ getStatus() {
2789
+ return {
2790
+ healthy: !this.error,
2791
+ rowCount: this.data.length,
2792
+ lastFetch: this.lastFetch,
2793
+ error: this.error
2794
+ };
2795
+ }
2796
+ onUpdate(callback) {
2797
+ this.listeners.add(callback);
2798
+ return () => this.listeners.delete(callback);
2799
+ }
2800
+ async refresh() {
2801
+ await this.fetch();
2802
+ }
2803
+ async fetch() {
2804
+ if (!this.db) {
2805
+ this.error = "SQLite database not initialized";
2806
+ return;
2807
+ }
2808
+ try {
2809
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon4(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
2810
+ const rows = this.db.prepare(capQuery).all();
2811
+ if (rows.length > this.maxRows) {
2812
+ throw new Error(
2813
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
2814
+ );
2815
+ }
2816
+ this.data = rows;
2817
+ this.lastFetch = /* @__PURE__ */ new Date();
2818
+ this.error = void 0;
2819
+ this.notify();
2820
+ } catch (err) {
2821
+ this.error = err instanceof Error ? err.message : String(err);
2822
+ }
2823
+ }
2824
+ notify() {
2825
+ for (const cb of this.listeners) cb(this.data);
2826
+ }
2827
+ };
2828
+ function stripTrailingSemicolon4(sql) {
2829
+ return sql.trim().replace(/;+\s*$/, "");
2830
+ }
2831
+
2832
+ // src/server/sources/kafka.ts
2833
+ import { randomBytes } from "crypto";
2834
+ import { Kafka, logLevel } from "kafkajs";
2835
+ var DEFAULT_MAX_BUFFER = 1e3;
2836
+ function normalizeBrokers(brokers) {
2837
+ const list = Array.isArray(brokers) ? brokers : brokers.split(",");
2838
+ return list.map((b) => expandEnv(b.trim(), "kafka source brokers")).filter(Boolean);
2839
+ }
2840
+ var KafkaSourceAdapter = class {
2841
+ constructor(config) {
2842
+ this.config = config;
2843
+ this.maxBuffer = config.maxBuffer ?? DEFAULT_MAX_BUFFER;
2844
+ this.groupId = config.groupId ?? `pipequery-${randomBytes(6).toString("hex")}`;
2845
+ }
2846
+ config;
2847
+ data = [];
2848
+ kafka = null;
2849
+ consumer = null;
2850
+ lastFetch = null;
2851
+ error;
2852
+ connected = false;
2853
+ listeners = /* @__PURE__ */ new Set();
2854
+ maxBuffer;
2855
+ groupId;
2856
+ async start() {
2857
+ const brokers = normalizeBrokers(this.config.brokers);
2858
+ if (brokers.length === 0) {
2859
+ throw new Error("Kafka source has no brokers configured");
2860
+ }
2861
+ this.kafka = new Kafka({
2862
+ clientId: `pipequery-${this.config.topic}`,
2863
+ brokers,
2864
+ ssl: this.config.ssl ?? false,
2865
+ sasl: this.config.sasl ? {
2866
+ mechanism: this.config.sasl.mechanism,
2867
+ username: expandEnv(this.config.sasl.username, "kafka source SASL username"),
2868
+ password: expandEnv(this.config.sasl.password, "kafka source SASL password")
2869
+ } : void 0,
2870
+ // Silence kafkajs's own logger — we surface errors via SourceStatus.
2871
+ logLevel: logLevel.ERROR
2872
+ });
2873
+ this.consumer = this.kafka.consumer({ groupId: this.groupId });
2874
+ await this.consumer.connect();
2875
+ await this.consumer.subscribe({
2876
+ topic: this.config.topic,
2877
+ fromBeginning: this.config.fromBeginning ?? false
2878
+ });
2879
+ this.connected = true;
2880
+ this.error = void 0;
2881
+ void this.consumer.run({
2882
+ eachMessage: async ({ message, partition }) => {
2883
+ try {
2884
+ const row = this.decode(message.value, message.key, message.timestamp, partition, message.offset);
2885
+ this.data.push(row);
2886
+ if (this.data.length > this.maxBuffer) {
2887
+ this.data = this.data.slice(-this.maxBuffer);
2888
+ }
2889
+ this.lastFetch = /* @__PURE__ */ new Date();
2890
+ this.error = void 0;
2891
+ this.notify();
2892
+ } catch {
2893
+ }
2894
+ }
2895
+ });
2896
+ }
2897
+ stop() {
2898
+ this.connected = false;
2899
+ if (this.consumer) {
2900
+ const consumer = this.consumer;
2901
+ this.consumer = null;
2902
+ void consumer.disconnect().catch(() => {
2903
+ });
2904
+ }
2905
+ this.kafka = null;
2906
+ }
2907
+ getData() {
2908
+ return this.data;
2909
+ }
2910
+ getStatus() {
2911
+ return {
2912
+ healthy: this.connected && !this.error,
2913
+ rowCount: this.data.length,
2914
+ lastFetch: this.lastFetch,
2915
+ error: this.error
2916
+ };
2917
+ }
2918
+ onUpdate(callback) {
2919
+ this.listeners.add(callback);
2920
+ return () => this.listeners.delete(callback);
2921
+ }
2922
+ decode(value, key, timestamp, partition, offset) {
2923
+ const base = {
2924
+ _kafka_topic: this.config.topic,
2925
+ _kafka_partition: partition,
2926
+ _kafka_offset: offset,
2927
+ _kafka_timestamp: Number.parseInt(timestamp, 10),
2928
+ _kafka_key: key ? key.toString("utf8") : null
2929
+ };
2930
+ if (!value) {
2931
+ return { ...base, value: null };
2932
+ }
2933
+ const format = this.config.valueFormat ?? "json";
2934
+ if (format === "string") {
2935
+ return { ...base, value: value.toString("utf8") };
2936
+ }
2937
+ if (format === "raw") {
2938
+ return { ...base, value: value.toString("base64") };
2939
+ }
2940
+ const decoded = JSON.parse(value.toString("utf8"));
2941
+ if (decoded !== null && typeof decoded === "object" && !Array.isArray(decoded)) {
2942
+ return { ...base, ...decoded };
2943
+ }
2944
+ return { ...base, value: decoded };
2945
+ }
2946
+ notify() {
2947
+ for (const cb of this.listeners) cb(this.data);
2948
+ }
2949
+ };
2950
+
2951
+ // src/server/sources/snowflake.ts
2952
+ import snowflake from "snowflake-sdk";
2953
+
2954
+ // src/server/sources/pushdown/snowflake.ts
2955
+ function compileSnowflakePushdown(pipeline, baseQuery) {
2956
+ return compileForDialect(pipeline, baseQuery, SNOWFLAKE_DIALECT);
2957
+ }
2958
+
2959
+ // src/server/sources/snowflake.ts
2960
+ var DEFAULT_INTERVAL_MS4 = 6e4;
2961
+ var DEFAULT_MAX_ROWS4 = 1e4;
2962
+ var SnowflakeSourceAdapter = class {
2963
+ constructor(config) {
2964
+ this.config = config;
2965
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS4;
2966
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS4;
2967
+ }
2968
+ config;
2969
+ conn = null;
2970
+ data = [];
2971
+ timer = null;
2972
+ lastFetch = null;
2973
+ error;
2974
+ listeners = /* @__PURE__ */ new Set();
2975
+ intervalMs;
2976
+ maxRows;
2977
+ async start() {
2978
+ this.conn = snowflake.createConnection({
2979
+ account: expandEnv(this.config.account, "snowflake source account"),
2980
+ username: expandEnv(this.config.username, "snowflake source username"),
2981
+ password: expandEnv(this.config.password, "snowflake source password"),
2982
+ ...this.config.database ? { database: expandEnv(this.config.database, "snowflake source database") } : {},
2983
+ ...this.config.schema ? { schema: expandEnv(this.config.schema, "snowflake source schema") } : {},
2984
+ ...this.config.warehouse ? { warehouse: expandEnv(this.config.warehouse, "snowflake source warehouse") } : {},
2985
+ ...this.config.role ? { role: expandEnv(this.config.role, "snowflake source role") } : {}
2986
+ });
2987
+ await new Promise((resolve3, reject) => {
2988
+ this.conn.connect((err) => err ? reject(err) : resolve3());
2989
+ });
2990
+ await this.fetch();
2991
+ this.timer = setInterval(() => {
2992
+ void this.fetch();
2993
+ }, this.intervalMs);
2994
+ }
2995
+ stop() {
2996
+ if (this.timer) {
2997
+ clearInterval(this.timer);
2998
+ this.timer = null;
2999
+ }
3000
+ if (this.conn) {
3001
+ this.conn.destroy(() => void 0);
3002
+ this.conn = null;
3003
+ }
3004
+ }
3005
+ getData() {
3006
+ return this.data;
3007
+ }
3008
+ getStatus() {
3009
+ return {
3010
+ healthy: !this.error,
3011
+ rowCount: this.data.length,
3012
+ lastFetch: this.lastFetch,
3013
+ error: this.error
3014
+ };
3015
+ }
3016
+ onUpdate(callback) {
3017
+ this.listeners.add(callback);
3018
+ return () => this.listeners.delete(callback);
3019
+ }
3020
+ async refresh() {
3021
+ await this.fetch();
3022
+ }
3023
+ /**
3024
+ * Adapter-native push-down. SourceManager.runQuery dispatches here when a
3025
+ * pipeline targets a Snowflake source; declines fall back to in-process.
3026
+ */
3027
+ async runPushdown(expression) {
3028
+ if (!this.conn) {
3029
+ return { ok: false, reason: "Snowflake connection not initialized \u2014 call start() first" };
3030
+ }
3031
+ let pipeline;
3032
+ try {
3033
+ pipeline = parseQuery(expression);
3034
+ } catch (err) {
3035
+ return { ok: false, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` };
3036
+ }
3037
+ const compiled = compileSnowflakePushdown(pipeline, this.config.query);
3038
+ if (!compiled.ok) {
3039
+ return { ok: false, reason: compiled.reason };
3040
+ }
3041
+ try {
3042
+ const rows = await this.executeSql(compiled.compiled.sql, compiled.compiled.params);
3043
+ return {
3044
+ ok: true,
3045
+ rows,
3046
+ sql: compiled.compiled.sql,
3047
+ params: compiled.compiled.params
3048
+ };
3049
+ } catch (err) {
3050
+ return { ok: false, reason: `snowflake error: ${err instanceof Error ? err.message : String(err)}` };
3051
+ }
3052
+ }
3053
+ async fetch() {
3054
+ if (!this.conn) {
3055
+ this.error = "Snowflake connection not initialized";
3056
+ return;
3057
+ }
3058
+ try {
3059
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon5(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
3060
+ const rows = await this.executeSql(capQuery, []);
3061
+ if (rows.length > this.maxRows) {
3062
+ throw new Error(
3063
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
3064
+ );
3065
+ }
3066
+ this.data = rows;
3067
+ this.lastFetch = /* @__PURE__ */ new Date();
3068
+ this.error = void 0;
3069
+ this.notify();
3070
+ } catch (err) {
3071
+ this.error = err instanceof Error ? err.message : String(err);
3072
+ }
3073
+ }
3074
+ executeSql(sqlText, params) {
3075
+ return new Promise((resolve3, reject) => {
3076
+ this.conn.execute({
3077
+ sqlText,
3078
+ binds: params,
3079
+ complete: (err, _stmt, rows) => {
3080
+ if (err) reject(err);
3081
+ else resolve3(rows ?? []);
3082
+ }
3083
+ });
3084
+ });
3085
+ }
3086
+ notify() {
3087
+ for (const cb of this.listeners) cb(this.data);
3088
+ }
3089
+ };
3090
+ function stripTrailingSemicolon5(sql) {
3091
+ return sql.trim().replace(/;+\s*$/, "");
3092
+ }
3093
+
3094
+ // src/server/sources/clickhouse.ts
3095
+ import { createClient } from "@clickhouse/client";
3096
+
3097
+ // src/server/sources/pushdown/clickhouse.ts
3098
+ function compileClickhousePushdown(pipeline, baseQuery) {
3099
+ return compileForDialect(pipeline, baseQuery, CLICKHOUSE_DIALECT);
3100
+ }
3101
+
3102
+ // src/server/sources/clickhouse.ts
3103
+ var DEFAULT_INTERVAL_MS5 = 3e4;
3104
+ var DEFAULT_MAX_ROWS5 = 1e4;
3105
+ var ClickhouseSourceAdapter = class {
3106
+ constructor(config) {
3107
+ this.config = config;
3108
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS5;
3109
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS5;
3110
+ }
3111
+ config;
3112
+ client = null;
3113
+ data = [];
3114
+ timer = null;
3115
+ lastFetch = null;
3116
+ error;
3117
+ listeners = /* @__PURE__ */ new Set();
3118
+ intervalMs;
3119
+ maxRows;
3120
+ async start() {
3121
+ this.client = createClient({
3122
+ url: expandEnv(this.config.url, "clickhouse source URL"),
3123
+ ...this.config.username ? { username: expandEnv(this.config.username, "clickhouse source username") } : {},
3124
+ ...this.config.password ? { password: expandEnv(this.config.password, "clickhouse source password") } : {},
3125
+ ...this.config.database ? { database: this.config.database } : {}
3126
+ });
3127
+ await this.fetch();
3128
+ this.timer = setInterval(() => {
3129
+ void this.fetch();
3130
+ }, this.intervalMs);
3131
+ }
3132
+ stop() {
3133
+ if (this.timer) {
3134
+ clearInterval(this.timer);
3135
+ this.timer = null;
3136
+ }
3137
+ if (this.client) {
3138
+ void this.client.close();
3139
+ this.client = null;
3140
+ }
3141
+ }
3142
+ getData() {
3143
+ return this.data;
3144
+ }
3145
+ getStatus() {
3146
+ return {
3147
+ healthy: !this.error,
3148
+ rowCount: this.data.length,
3149
+ lastFetch: this.lastFetch,
3150
+ error: this.error
3151
+ };
3152
+ }
3153
+ onUpdate(callback) {
3154
+ this.listeners.add(callback);
3155
+ return () => this.listeners.delete(callback);
3156
+ }
3157
+ async refresh() {
3158
+ await this.fetch();
3159
+ }
3160
+ /**
3161
+ * Adapter-native push-down. ClickHouse dialect inlines literals (vs.
3162
+ * Postgres / MySQL / Snowflake which bind via placeholders) so the
3163
+ * `params` array returned in the success result is always empty;
3164
+ * the SQL is fully self-contained.
3165
+ */
3166
+ async runPushdown(expression) {
3167
+ if (!this.client) {
3168
+ return { ok: false, reason: "ClickHouse client not initialized \u2014 call start() first" };
3169
+ }
3170
+ let pipeline;
3171
+ try {
3172
+ pipeline = parseQuery(expression);
3173
+ } catch (err) {
3174
+ return { ok: false, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` };
3175
+ }
3176
+ const compiled = compileClickhousePushdown(pipeline, this.config.query);
3177
+ if (!compiled.ok) {
3178
+ return { ok: false, reason: compiled.reason };
3179
+ }
3180
+ try {
3181
+ const rows = await this.executeSql(compiled.compiled.sql);
3182
+ return {
3183
+ ok: true,
3184
+ rows,
3185
+ sql: compiled.compiled.sql,
3186
+ params: compiled.compiled.params
3187
+ };
3188
+ } catch (err) {
3189
+ return { ok: false, reason: `clickhouse error: ${err instanceof Error ? err.message : String(err)}` };
3190
+ }
3191
+ }
3192
+ async fetch() {
3193
+ if (!this.client) {
3194
+ this.error = "ClickHouse client not initialized";
3195
+ return;
3196
+ }
3197
+ try {
3198
+ const capQuery = `SELECT * FROM (${stripTrailingSemicolon6(this.config.query)}) AS pq_src LIMIT ${this.maxRows + 1}`;
3199
+ const rows = await this.executeSql(capQuery);
3200
+ if (rows.length > this.maxRows) {
3201
+ throw new Error(
3202
+ `Query returned more than maxRows=${this.maxRows}. Add LIMIT/WHERE to bound the result set, or raise maxRows in the source config.`
3203
+ );
3204
+ }
3205
+ this.data = rows;
3206
+ this.lastFetch = /* @__PURE__ */ new Date();
3207
+ this.error = void 0;
3208
+ this.notify();
3209
+ } catch (err) {
3210
+ this.error = err instanceof Error ? err.message : String(err);
3211
+ }
3212
+ }
3213
+ async executeSql(sql) {
3214
+ const result = await this.client.query({
3215
+ query: sql,
3216
+ format: "JSONEachRow"
3217
+ });
3218
+ const rows = await result.json();
3219
+ return Array.isArray(rows) ? rows : [];
3220
+ }
3221
+ notify() {
3222
+ for (const cb of this.listeners) cb(this.data);
3223
+ }
3224
+ };
3225
+ function stripTrailingSemicolon6(sql) {
3226
+ return sql.trim().replace(/;+\s*$/, "");
3227
+ }
3228
+
3229
+ // src/server/sources/mongodb.ts
3230
+ import { MongoClient } from "mongodb";
3231
+
3232
+ // src/server/sources/pushdown/mongodb.ts
3233
+ var CMP_TO_MONGO = {
3234
+ ">": "$gt",
3235
+ ">=": "$gte",
3236
+ "<": "$lt",
3237
+ "<=": "$lte",
3238
+ "==": "$eq",
3239
+ "!=": "$ne"
3240
+ };
3241
+ var MONGO_AGGREGATE_FN = {
3242
+ sum: (arg) => ({ $sum: arg ?? 1 }),
3243
+ // sum(*) → $sum: 1 (count rows)
3244
+ avg: (arg) => ({ $avg: arg ?? null }),
3245
+ min: (arg) => ({ $min: arg ?? null }),
3246
+ max: (arg) => ({ $max: arg ?? null }),
3247
+ count: () => ({ $sum: 1 }),
3248
+ // count(*) — Mongo idiom
3249
+ distinct_count: (arg) => ({ $addToSet: arg ?? null })
3250
+ // size taken in $project
3251
+ };
3252
+ function compileMongoPushdown(pipeline, defaultFilter) {
3253
+ let needsAggregate = false;
3254
+ for (const op of pipeline.operations) {
3255
+ if (op.kind === "RollupOp" || op.kind === "AggregateOp") needsAggregate = true;
3256
+ if (op.kind === "DistinctOp") {
3257
+ return { ok: false, reason: "mongo distinct() is not yet pushable; in-process handles it" };
3258
+ }
3259
+ if (op.kind !== "WhereOp" && op.kind !== "SortOp" && op.kind !== "FirstOp" && op.kind !== "SelectOp" && op.kind !== "RollupOp" && op.kind !== "AggregateOp") {
3260
+ return { ok: false, reason: `${op.kind}: not yet pushable to mongo` };
3261
+ }
3262
+ }
3263
+ return needsAggregate ? compileAggregatePipeline(pipeline, defaultFilter) : compileFindPlan(pipeline, defaultFilter);
3264
+ }
3265
+ function compileFindPlan(pipeline, defaultFilter) {
3266
+ let filter = null;
3267
+ let sort;
3268
+ let limit;
3269
+ let projection;
3270
+ for (const op of pipeline.operations) {
3271
+ if (op.kind === "WhereOp") {
3272
+ const compiled = compileFilter(op.condition);
3273
+ if (compiled === null) {
3274
+ return { ok: false, reason: "where(): unsupported expression node" };
3275
+ }
3276
+ filter = filter ? { $and: [filter, compiled] } : compiled;
3277
+ } else if (op.kind === "SortOp") {
3278
+ sort = {};
3279
+ for (const c of op.criteria) {
3280
+ if (c.expression.kind !== "FieldAccess" || c.expression.path.length !== 1) {
3281
+ return { ok: false, reason: "sort(): only single-field paths supported" };
3282
+ }
3283
+ sort[c.expression.path[0]] = c.direction === "desc" ? -1 : 1;
3284
+ }
3285
+ } else if (op.kind === "FirstOp") {
3286
+ limit = op.count;
3287
+ } else if (op.kind === "SelectOp") {
3288
+ projection = {};
3289
+ for (const f of op.fields) {
3290
+ if (f.kind === "FieldAccess" && f.path.length === 1) {
3291
+ projection[f.path[0]] = 1;
3292
+ } else if (f.kind === "AliasExpr" && f.expression.kind === "FieldAccess" && f.expression.path.length === 1) {
3293
+ return { ok: false, reason: "select() with alias requires aggregate path" };
3294
+ } else {
3295
+ return { ok: false, reason: "select(): only bare field projections in find() path" };
3296
+ }
3297
+ }
3298
+ projection._id = 0;
3299
+ }
3300
+ }
3301
+ const finalFilter = mergeFilters(defaultFilter, filter ?? void 0);
3302
+ return {
3303
+ ok: true,
3304
+ compiled: {
3305
+ kind: "find",
3306
+ filter: finalFilter,
3307
+ ...sort ? { sort } : {},
3308
+ ...limit !== void 0 ? { limit } : {},
3309
+ ...projection ? { projection } : {}
3310
+ }
3311
+ };
3312
+ }
3313
+ function compileAggregatePipeline(pipeline, defaultFilter) {
3314
+ const stages = [];
3315
+ if (defaultFilter && Object.keys(defaultFilter).length > 0) {
3316
+ stages.push({ $match: { ...defaultFilter } });
3317
+ }
3318
+ let grouped = false;
3319
+ let postGroupProjection = null;
3320
+ for (const op of pipeline.operations) {
3321
+ switch (op.kind) {
3322
+ case "WhereOp": {
3323
+ if (grouped) {
3324
+ return { ok: false, reason: "where after grouping (HAVING) is not yet pushable" };
3325
+ }
3326
+ const compiled = compileFilter(op.condition);
3327
+ if (compiled === null) {
3328
+ return { ok: false, reason: "where(): unsupported expression node" };
3329
+ }
3330
+ stages.push({ $match: compiled });
3331
+ break;
3332
+ }
3333
+ case "SortOp": {
3334
+ const sortStage = {};
3335
+ for (const c of op.criteria) {
3336
+ if (c.expression.kind !== "FieldAccess" || c.expression.path.length !== 1) {
3337
+ return { ok: false, reason: "sort(): only single-field paths supported" };
3338
+ }
3339
+ sortStage[c.expression.path[0]] = c.direction === "desc" ? -1 : 1;
3340
+ }
3341
+ stages.push({ $sort: sortStage });
3342
+ break;
3343
+ }
3344
+ case "FirstOp": {
3345
+ stages.push({ $limit: op.count });
3346
+ break;
3347
+ }
3348
+ case "SelectOp": {
3349
+ if (grouped) {
3350
+ return { ok: false, reason: "select after rollup is not yet pushable" };
3351
+ }
3352
+ const proj = { _id: 0 };
3353
+ for (const f of op.fields) {
3354
+ if (f.kind === "FieldAccess" && f.path.length === 1) {
3355
+ proj[f.path[0]] = 1;
3356
+ } else if (f.kind === "AliasExpr" && f.expression.kind === "FieldAccess" && f.expression.path.length === 1) {
3357
+ proj[f.alias] = `$${f.expression.path[0]}`;
3358
+ } else {
3359
+ return { ok: false, reason: "select(): unsupported field expression in mongo" };
3360
+ }
3361
+ }
3362
+ stages.push({ $project: proj });
3363
+ break;
3364
+ }
3365
+ case "RollupOp": {
3366
+ if (grouped) {
3367
+ return { ok: false, reason: "multiple grouping ops in one pipeline" };
3368
+ }
3369
+ const groupIdResult = compileGroupId(op.keys);
3370
+ if (!groupIdResult.ok) {
3371
+ return { ok: false, reason: "rollup(): keys must be single-field paths" };
3372
+ }
3373
+ const groupStage = { _id: groupIdResult.value };
3374
+ const distinctCountAliases = [];
3375
+ for (const a of op.aggregates) {
3376
+ const alias = a.kind === "AliasExpr" ? a.alias : null;
3377
+ const inner = a.kind === "AliasExpr" ? a.expression : a;
3378
+ if (inner.kind !== "FunctionCall") {
3379
+ return { ok: false, reason: "rollup(): aggregate must be a function call" };
3380
+ }
3381
+ const fn = MONGO_AGGREGATE_FN[inner.name];
3382
+ if (!fn) {
3383
+ return { ok: false, reason: `aggregate "${inner.name}" has no mongo push-down` };
3384
+ }
3385
+ let argRef = null;
3386
+ if (inner.args.length === 1) {
3387
+ const arg = inner.args[0];
3388
+ if (arg.kind !== "FieldAccess" || arg.path.length !== 1) {
3389
+ return { ok: false, reason: "rollup(): aggregate argument must be a single-field path" };
3390
+ }
3391
+ argRef = `$${arg.path[0]}`;
3392
+ } else if (inner.args.length > 1) {
3393
+ return { ok: false, reason: `aggregate "${inner.name}" with extra args not yet pushable` };
3394
+ }
3395
+ const fieldName = alias ?? inner.name;
3396
+ groupStage[fieldName] = fn(argRef);
3397
+ if (inner.name === "distinct_count") {
3398
+ distinctCountAliases.push(fieldName);
3399
+ }
3400
+ }
3401
+ stages.push({ $group: groupStage });
3402
+ const projAfterGroup = {
3403
+ _id: 0
3404
+ };
3405
+ if (op.keys.length === 1) {
3406
+ const k = op.keys[0];
3407
+ if (k.kind === "FieldAccess" && k.path.length === 1) {
3408
+ projAfterGroup[k.path[0]] = "$_id";
3409
+ }
3410
+ } else {
3411
+ for (const k of op.keys) {
3412
+ if (k.kind === "FieldAccess" && k.path.length === 1) {
3413
+ projAfterGroup[k.path[0]] = `$_id.${k.path[0]}`;
3414
+ }
3415
+ }
3416
+ }
3417
+ for (const key of Object.keys(groupStage)) {
3418
+ if (key === "_id") continue;
3419
+ if (distinctCountAliases.includes(key)) {
3420
+ projAfterGroup[key] = { $size: `$${key}` };
3421
+ } else {
3422
+ projAfterGroup[key] = `$${key}`;
3423
+ }
3424
+ }
3425
+ postGroupProjection = projAfterGroup;
3426
+ grouped = true;
3427
+ break;
3428
+ }
3429
+ case "AggregateOp": {
3430
+ if (grouped) {
3431
+ return { ok: false, reason: "multiple grouping ops in one pipeline" };
3432
+ }
3433
+ const fn = MONGO_AGGREGATE_FN[op.function];
3434
+ if (!fn) {
3435
+ return { ok: false, reason: `aggregate "${op.function}" has no mongo push-down` };
3436
+ }
3437
+ if (op.args && op.args.length > 0) {
3438
+ return { ok: false, reason: `aggregate "${op.function}" with extra args not yet pushable` };
3439
+ }
3440
+ let argRef = null;
3441
+ if (op.field !== void 0) {
3442
+ if (op.field.kind !== "FieldAccess" || op.field.path.length !== 1) {
3443
+ return { ok: false, reason: `aggregate "${op.function}": unsupported argument expression` };
3444
+ }
3445
+ argRef = `$${op.field.path[0]}`;
3446
+ }
3447
+ const groupStage = { _id: null, [op.function]: fn(argRef) };
3448
+ stages.push({ $group: groupStage });
3449
+ const proj = { _id: 0, [op.function]: `$${op.function}` };
3450
+ if (op.function === "distinct_count") {
3451
+ proj[op.function] = { $size: `$${op.function}` };
3452
+ }
3453
+ postGroupProjection = proj;
3454
+ grouped = true;
3455
+ break;
3456
+ }
3457
+ default:
3458
+ return { ok: false, reason: `${op.kind}: not yet pushable to mongo` };
3459
+ }
3460
+ }
3461
+ if (postGroupProjection) stages.push({ $project: postGroupProjection });
3462
+ return { ok: true, compiled: { kind: "aggregate", pipeline: stages } };
3463
+ }
3464
+ function compileFilter(expr) {
3465
+ switch (expr.kind) {
3466
+ case "BinaryExpr": {
3467
+ if (expr.operator === "&&") {
3468
+ const left = compileFilter(expr.left);
3469
+ const right = compileFilter(expr.right);
3470
+ if (left === null || right === null) return null;
3471
+ return { $and: [left, right] };
3472
+ }
3473
+ if (expr.operator === "||") {
3474
+ const left = compileFilter(expr.left);
3475
+ const right = compileFilter(expr.right);
3476
+ if (left === null || right === null) return null;
3477
+ return { $or: [left, right] };
3478
+ }
3479
+ const cmp = CMP_TO_MONGO[expr.operator];
3480
+ if (!cmp) return null;
3481
+ if (expr.right.kind === "NullLiteral") {
3482
+ if (expr.operator === "==") return field(expr.left, { $eq: null });
3483
+ if (expr.operator === "!=") return field(expr.left, { $ne: null });
3484
+ }
3485
+ if (expr.left.kind === "NullLiteral") {
3486
+ if (expr.operator === "==") return field(expr.right, { $eq: null });
3487
+ if (expr.operator === "!=") return field(expr.right, { $ne: null });
3488
+ }
3489
+ const lit = literalValue(expr.right);
3490
+ if (expr.left.kind === "FieldAccess" && lit.ok) {
3491
+ return field(expr.left, { [cmp]: lit.value });
3492
+ }
3493
+ const litLeft = literalValue(expr.left);
3494
+ if (expr.right.kind === "FieldAccess" && litLeft.ok) {
3495
+ const reversed = REVERSE_CMP[expr.operator] ?? expr.operator;
3496
+ const reverseCmp = CMP_TO_MONGO[reversed];
3497
+ if (!reverseCmp) return null;
3498
+ return field(expr.right, { [reverseCmp]: litLeft.value });
3499
+ }
3500
+ return null;
3501
+ }
3502
+ case "UnaryExpr": {
3503
+ if (expr.operator === "!") {
3504
+ const inner = compileFilter(expr.operand);
3505
+ if (inner === null) return null;
3506
+ return { $nor: [inner] };
3507
+ }
3508
+ return null;
3509
+ }
3510
+ default:
3511
+ return null;
3512
+ }
3513
+ }
3514
+ var REVERSE_CMP = {
3515
+ ">": "<",
3516
+ ">=": "<=",
3517
+ "<": ">",
3518
+ "<=": ">=",
3519
+ "==": "==",
3520
+ "!=": "!="
3521
+ };
3522
+ function field(expr, condition) {
3523
+ if (expr.kind !== "FieldAccess" || expr.path.length !== 1) return null;
3524
+ return { [expr.path[0]]: condition };
3525
+ }
3526
+ function literalValue(expr) {
3527
+ switch (expr.kind) {
3528
+ case "NumberLiteral":
3529
+ case "StringLiteral":
3530
+ case "BooleanLiteral":
3531
+ return { ok: true, value: expr.value };
3532
+ case "NullLiteral":
3533
+ return { ok: true, value: null };
3534
+ default:
3535
+ return { ok: false };
3536
+ }
3537
+ }
3538
+ function compileGroupId(keys) {
3539
+ if (keys.length === 0) return { ok: true, value: null };
3540
+ if (keys.length === 1) {
3541
+ const k = keys[0];
3542
+ if (k.kind !== "FieldAccess" || k.path.length !== 1) return { ok: false };
3543
+ return { ok: true, value: `$${k.path[0]}` };
3544
+ }
3545
+ const obj = {};
3546
+ for (const k of keys) {
3547
+ if (k.kind !== "FieldAccess" || k.path.length !== 1) return { ok: false };
3548
+ obj[k.path[0]] = `$${k.path[0]}`;
3549
+ }
3550
+ return { ok: true, value: obj };
3551
+ }
3552
+ function mergeFilters(base, added) {
3553
+ if (!base || Object.keys(base).length === 0) return added ?? {};
3554
+ if (!added || Object.keys(added).length === 0) return { ...base };
3555
+ return { $and: [{ ...base }, { ...added }] };
3556
+ }
3557
+
3558
+ // src/server/sources/mongodb.ts
3559
+ var DEFAULT_INTERVAL_MS6 = 3e4;
3560
+ var DEFAULT_MAX_ROWS6 = 1e4;
3561
+ var MongoSourceAdapter = class {
3562
+ constructor(config) {
3563
+ this.config = config;
3564
+ this.intervalMs = config.interval ? parseDuration(config.interval) : DEFAULT_INTERVAL_MS6;
3565
+ this.maxRows = config.maxRows ?? DEFAULT_MAX_ROWS6;
3566
+ }
3567
+ config;
3568
+ client = null;
3569
+ db = null;
3570
+ collection = null;
3571
+ data = [];
3572
+ timer = null;
3573
+ lastFetch = null;
3574
+ error;
3575
+ listeners = /* @__PURE__ */ new Set();
3576
+ intervalMs;
3577
+ maxRows;
3578
+ async start() {
3579
+ const uri = expandEnv(this.config.url, "mongodb source URL");
3580
+ this.client = new MongoClient(uri);
3581
+ await this.client.connect();
3582
+ this.db = this.client.db(expandEnv(this.config.database, "mongodb source database"));
3583
+ this.collection = this.db.collection(this.config.collection);
3584
+ await this.fetch();
3585
+ this.timer = setInterval(() => {
3586
+ void this.fetch();
3587
+ }, this.intervalMs);
3588
+ }
3589
+ stop() {
3590
+ if (this.timer) {
3591
+ clearInterval(this.timer);
3592
+ this.timer = null;
3593
+ }
3594
+ if (this.client) {
3595
+ void this.client.close();
3596
+ this.client = null;
3597
+ this.db = null;
3598
+ this.collection = null;
3599
+ }
3600
+ }
3601
+ getData() {
3602
+ return this.data;
3603
+ }
3604
+ getStatus() {
3605
+ return {
3606
+ healthy: !this.error,
3607
+ rowCount: this.data.length,
3608
+ lastFetch: this.lastFetch,
3609
+ error: this.error
3610
+ };
3611
+ }
3612
+ onUpdate(callback) {
3613
+ this.listeners.add(callback);
3614
+ return () => this.listeners.delete(callback);
3615
+ }
3616
+ async refresh() {
3617
+ await this.fetch();
3618
+ }
3619
+ /**
3620
+ * Adapter-native push-down to MongoDB. The compiler emits either a
3621
+ * `find()` plan (when the pipeline only uses where/sort/first/select)
3622
+ * or an `aggregate()` pipeline (when grouping is involved).
3623
+ *
3624
+ * The success result's `sql` field carries a JSON serialization of the
3625
+ * compiled plan (Mongo doesn't have SQL); `params` is empty since
3626
+ * Mongo plans bind values inline as part of the BSON document.
3627
+ */
3628
+ async runPushdown(expression) {
3629
+ if (!this.collection) {
3630
+ return { ok: false, reason: "Mongo collection not initialized \u2014 call start() first" };
3631
+ }
3632
+ let pipeline;
3633
+ try {
3634
+ pipeline = parseQuery(expression);
3635
+ } catch (err) {
3636
+ return { ok: false, reason: `parse error: ${err instanceof Error ? err.message : String(err)}` };
3637
+ }
3638
+ const compiled = compileMongoPushdown(pipeline, this.config.filter);
3639
+ if (!compiled.ok) {
3640
+ return { ok: false, reason: compiled.reason };
3641
+ }
3642
+ try {
3643
+ const rows = await this.runPlan(compiled.compiled);
3644
+ return {
3645
+ ok: true,
3646
+ rows,
3647
+ sql: JSON.stringify(compiled.compiled),
3648
+ params: []
3649
+ };
3650
+ } catch (err) {
3651
+ return { ok: false, reason: `mongo error: ${err instanceof Error ? err.message : String(err)}` };
3652
+ }
3653
+ }
3654
+ async runPlan(plan) {
3655
+ const coll = this.collection;
3656
+ if (plan.kind === "find") {
3657
+ let cursor = coll.find(plan.filter);
3658
+ if (plan.projection) cursor = cursor.project(plan.projection);
3659
+ if (plan.sort) cursor = cursor.sort(plan.sort);
3660
+ if (plan.limit !== void 0) cursor = cursor.limit(plan.limit);
3661
+ return cursor.toArray();
3662
+ }
3663
+ return coll.aggregate(plan.pipeline).toArray();
3664
+ }
3665
+ async fetch() {
3666
+ if (!this.collection) {
3667
+ this.error = "Mongo collection not initialized";
3668
+ return;
3669
+ }
3670
+ try {
3671
+ const filter = this.config.filter ?? {};
3672
+ const docs = await this.collection.find(filter).limit(this.maxRows + 1).toArray();
3673
+ if (docs.length > this.maxRows) {
3674
+ throw new Error(
3675
+ `Query returned more than maxRows=${this.maxRows}. Tighten the filter, or raise maxRows in the source config.`
3676
+ );
3677
+ }
3678
+ this.data = docs.map((d) => {
3679
+ const { _id, ...rest } = d;
3680
+ return rest;
3681
+ });
3682
+ this.lastFetch = /* @__PURE__ */ new Date();
3683
+ this.error = void 0;
3684
+ this.notify();
3685
+ } catch (err) {
3686
+ this.error = err instanceof Error ? err.message : String(err);
3687
+ }
3688
+ }
3689
+ notify() {
3690
+ for (const cb of this.listeners) cb(this.data);
3691
+ }
3692
+ };
3693
+
3694
+ // src/server/sources/manager.ts
3695
+ var SourceManager = class {
3696
+ constructor(cwd) {
3697
+ this.cwd = cwd;
3698
+ }
3699
+ cwd;
3700
+ sources = /* @__PURE__ */ new Map();
3701
+ globalListeners = /* @__PURE__ */ new Set();
3702
+ async addSource(name, config) {
3703
+ if (this.sources.has(name)) {
3704
+ this.removeSource(name);
3705
+ }
3706
+ const adapter = this.createAdapter(config);
3707
+ const entry = { config, adapter };
3708
+ entry.unsubscribe = adapter.onUpdate((data) => {
3709
+ for (const listener of this.globalListeners) {
3710
+ listener(name, data);
3711
+ }
3712
+ });
3713
+ this.sources.set(name, entry);
3714
+ await adapter.start();
3715
+ }
3716
+ removeSource(name) {
3717
+ const entry = this.sources.get(name);
3718
+ if (!entry) return false;
3719
+ entry.unsubscribe?.();
3720
+ entry.adapter.stop();
3721
+ this.sources.delete(name);
3722
+ return true;
3723
+ }
3724
+ getContext() {
3725
+ const ctx = {};
3726
+ for (const [name, { adapter }] of this.sources) {
3727
+ ctx[name] = adapter.getData();
3728
+ }
3729
+ return ctx;
3730
+ }
3731
+ getSourceData(name) {
3732
+ return this.sources.get(name)?.adapter.getData();
3733
+ }
3734
+ getSourceStatus(name) {
3735
+ return this.sources.get(name)?.adapter.getStatus();
3736
+ }
3737
+ getAllStatuses() {
3738
+ const result = {};
3739
+ for (const [name, { adapter }] of this.sources) {
3740
+ result[name] = adapter.getStatus();
3741
+ }
3742
+ return result;
3743
+ }
3744
+ getSourceNames() {
3745
+ return [...this.sources.keys()];
3746
+ }
3747
+ /**
3748
+ * Subscribe to data updates from any source.
3749
+ * Used by SSE endpoint for real-time push to TUI clients.
3750
+ * Returns unsubscribe function.
3751
+ */
3752
+ onSourceUpdate(callback) {
3753
+ this.globalListeners.add(callback);
3754
+ return () => this.globalListeners.delete(callback);
3755
+ }
3756
+ async refreshAll() {
3757
+ await Promise.all(
3758
+ [...this.sources.values()].map(({ adapter }) => adapter.refresh?.() ?? Promise.resolve())
3759
+ );
3760
+ }
3761
+ /**
3762
+ * Run a pipe expression, preferring adapter-native push-down when the
3763
+ * source supports it.
3764
+ *
3765
+ * Decision tree:
3766
+ * 1. Parse the expression. If it can't parse, fall straight through to
3767
+ * the in-process engine, which will throw the same parse error a
3768
+ * caller would have seen before push-down existed (no behaviour
3769
+ * change for malformed input).
3770
+ * 2. Look up the source named in the pipeline. If it exists and its
3771
+ * adapter exposes `runPushdown`, try it. On `{ ok: true }` we're
3772
+ * done — return the rows. On `{ ok: false }` (decline) or any
3773
+ * thrown error, fall back to in-process.
3774
+ * 3. In-process: query(getContext(), expression) — the existing path.
3775
+ *
3776
+ * The push-down attempt is best-effort and *invisible to callers*: same
3777
+ * return type as `query()`, no new failure modes. Telemetry (which
3778
+ * branch ran, latency, source-engine SQL) belongs in the audit log when
3779
+ * Phase 5 lands; not threaded through here.
3780
+ */
3781
+ async runQuery(expression) {
3782
+ const pushed = await this.tryPushdown(expression);
3783
+ if (pushed && pushed.ok) return pushed.rows;
3784
+ return query(this.getContext(), expression);
3785
+ }
3786
+ /**
3787
+ * Inspect the pipeline AST and dispatch to an adapter's runPushdown when
3788
+ * eligible. Returns `null` when push-down isn't even attempted (no parse,
3789
+ * no source, no capability). Returns a PushdownResult when it was tried —
3790
+ * `{ ok: true }` for success, `{ ok: false, reason }` for decline.
3791
+ *
3792
+ * Exposed on the manager (rather than tucked inside runQuery) so the
3793
+ * future `event.audit` channel can record `pushed_down: bool` and the
3794
+ * `source_engine_query` SQL string for governance. Today only Postgres
3795
+ * implements runPushdown; MySQL / SQLite / ClickHouse will follow the
3796
+ * same interface.
3797
+ */
3798
+ async tryPushdown(expression) {
3799
+ let pipeline;
3800
+ try {
3801
+ pipeline = parseQuery(expression);
3802
+ } catch {
3803
+ return null;
3804
+ }
3805
+ const entry = this.sources.get(pipeline.source);
3806
+ if (!entry) return null;
3807
+ if (!entry.adapter.runPushdown) return null;
3808
+ try {
3809
+ return await entry.adapter.runPushdown(expression);
3810
+ } catch (err) {
3811
+ const reason = err instanceof Error ? err.message : String(err);
3812
+ return { ok: false, reason: `pushdown threw: ${reason}` };
3813
+ }
3814
+ }
3815
+ async dispose() {
3816
+ this.globalListeners.clear();
3817
+ for (const entry of this.sources.values()) {
3818
+ entry.unsubscribe?.();
3819
+ entry.adapter.stop();
3820
+ }
3821
+ this.sources.clear();
3822
+ }
3823
+ createAdapter(config) {
3824
+ switch (config.type) {
3825
+ case "rest":
3826
+ return new RestSourceAdapter(config);
3827
+ case "websocket":
3828
+ return new WebSocketSourceAdapter(config);
3829
+ case "file":
3830
+ return new FileSourceAdapter(config, this.cwd);
3831
+ case "static":
3832
+ return new StaticSourceAdapter(config);
3833
+ case "postgres":
3834
+ return new PostgresSourceAdapter(config);
3835
+ case "mysql":
3836
+ return new MysqlSourceAdapter(config);
3837
+ case "sqlite":
3838
+ return new SqliteSourceAdapter(config, this.cwd);
3839
+ case "kafka":
3840
+ return new KafkaSourceAdapter(config);
3841
+ case "snowflake":
3842
+ return new SnowflakeSourceAdapter(config);
3843
+ case "clickhouse":
3844
+ return new ClickhouseSourceAdapter(config);
3845
+ case "mongodb":
3846
+ return new MongoSourceAdapter(config);
3847
+ default:
3848
+ throw new Error(`Unknown source type: ${config.type}`);
3849
+ }
3850
+ }
3851
+ };
3852
+ export {
3853
+ SourceManager
3854
+ };