@silverbulletmd/silverbullet 2.4.2 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/README.md +19 -4
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +5 -4
  5. package/client/plugos/hooks/code_widget.ts +3 -8
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -15
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -8
  11. package/client/plugos/hooks/slash_command.ts +13 -28
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -6
  15. package/client/plugos/plug_compile.ts +79 -78
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
  19. package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
  20. package/client/plugos/syscalls/asset.ts +1 -3
  21. package/client/plugos/syscalls/code_widget.ts +1 -3
  22. package/client/plugos/syscalls/config.ts +1 -5
  23. package/client/plugos/syscalls/datastore.ts +1 -1
  24. package/client/plugos/syscalls/editor.ts +72 -69
  25. package/client/plugos/syscalls/event.ts +9 -12
  26. package/client/plugos/syscalls/fetch.ts +31 -23
  27. package/client/plugos/syscalls/index.ts +10 -1
  28. package/client/plugos/syscalls/jsonschema.ts +72 -32
  29. package/client/plugos/syscalls/language.ts +9 -5
  30. package/client/plugos/syscalls/markdown.ts +29 -7
  31. package/client/plugos/syscalls/mq.ts +4 -12
  32. package/client/plugos/syscalls/service_registry.ts +1 -4
  33. package/client/plugos/syscalls/shell.ts +2 -5
  34. package/client/plugos/syscalls/space.ts +1 -1
  35. package/client/plugos/syscalls/sync.ts +69 -60
  36. package/client/plugos/syscalls/system.ts +2 -3
  37. package/client/plugos/system.ts +6 -12
  38. package/client/plugos/worker_runtime.ts +12 -33
  39. package/client/space_lua/aggregates.ts +782 -0
  40. package/client/space_lua/ast.ts +42 -8
  41. package/client/space_lua/ast_narrow.ts +4 -2
  42. package/client/space_lua/eval.ts +886 -575
  43. package/client/space_lua/labels.ts +7 -12
  44. package/client/space_lua/liq_null.ts +6 -0
  45. package/client/space_lua/numeric.ts +5 -8
  46. package/client/space_lua/parse.ts +346 -120
  47. package/client/space_lua/query_collection.ts +926 -82
  48. package/client/space_lua/query_env.ts +26 -0
  49. package/client/space_lua/render_lua_markdown.ts +369 -0
  50. package/client/space_lua/rp.ts +5 -4
  51. package/client/space_lua/runtime.ts +288 -155
  52. package/client/space_lua/stdlib/format.ts +53 -39
  53. package/client/space_lua/stdlib/js.ts +3 -7
  54. package/client/space_lua/stdlib/load.ts +1 -3
  55. package/client/space_lua/stdlib/math.ts +84 -58
  56. package/client/space_lua/stdlib/net.ts +27 -17
  57. package/client/space_lua/stdlib/os.ts +81 -85
  58. package/client/space_lua/stdlib/pattern.ts +695 -0
  59. package/client/space_lua/stdlib/prng.ts +148 -0
  60. package/client/space_lua/stdlib/space_lua.ts +17 -23
  61. package/client/space_lua/stdlib/string.ts +102 -190
  62. package/client/space_lua/stdlib/string_pack.ts +490 -0
  63. package/client/space_lua/stdlib/table.ts +76 -16
  64. package/client/space_lua/stdlib.ts +53 -39
  65. package/client/space_lua/tonumber.ts +82 -42
  66. package/client/space_lua/util.ts +53 -15
  67. package/dist/plug-compile.js +55 -98
  68. package/package.json +27 -20
  69. package/plug-api/constants.ts +0 -32
  70. package/plug-api/lib/async.ts +20 -7
  71. package/plug-api/lib/crypto.ts +16 -17
  72. package/plug-api/lib/dates.ts +15 -7
  73. package/plug-api/lib/json.ts +11 -5
  74. package/plug-api/lib/limited_map.ts +1 -1
  75. package/plug-api/lib/native_fetch.ts +2 -0
  76. package/plug-api/lib/ref.ts +23 -23
  77. package/plug-api/lib/resolve.ts +7 -11
  78. package/plug-api/lib/tags.ts +13 -4
  79. package/plug-api/lib/transclusion.ts +10 -21
  80. package/plug-api/lib/tree.ts +165 -45
  81. package/plug-api/lib/yaml.ts +35 -25
  82. package/plug-api/syscalls/asset.ts +1 -1
  83. package/plug-api/syscalls/config.ts +1 -4
  84. package/plug-api/syscalls/editor.ts +15 -15
  85. package/plug-api/syscalls/jsonschema.ts +1 -3
  86. package/plug-api/syscalls/lua.ts +3 -9
  87. package/plug-api/syscalls/mq.ts +1 -4
  88. package/plug-api/syscalls/shell.ts +4 -1
  89. package/plug-api/syscalls/space.ts +3 -10
  90. package/plug-api/syscalls/system.ts +1 -4
  91. package/plug-api/syscalls/yaml.ts +2 -6
  92. package/plug-api/system_mock.ts +0 -1
  93. package/plug-api/types/client.ts +16 -1
  94. package/plug-api/types/event.ts +6 -4
  95. package/plug-api/types/manifest.ts +8 -9
  96. package/plugs/builtin_plugs.ts +2 -2
  97. package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Aggregate function definitions and execution for LIQ.
3
+ *
4
+ * Builtins implement ILuaFunction via plain objects rather than
5
+ * LuaBuiltinFunction instances. This avoids ES module TDZ issues:
6
+ * `class` exports are not available during circular module init,
7
+ * but `interface`/`type` imports are.
8
+ */
9
+
10
+ import type { ILuaFunction, LuaStackFrame } from "./runtime.ts";
11
+ import {
12
+ luaCall,
13
+ type LuaEnv,
14
+ LuaTable,
15
+ luaTruthy,
16
+ luaValueToJS,
17
+ type LuaValue,
18
+ } from "./runtime.ts";
19
+ import { isSqlNull } from "./liq_null.ts";
20
+ import type { LuaExpression, LuaOrderBy } from "./ast.ts";
21
+ import { buildItemEnv } from "./query_env.ts";
22
+ import { asyncMergeSort } from "./util.ts";
23
+ import type { Config } from "../config.ts";
24
+ import { coerceToNumber, isTaggedFloat } from "./numeric.ts";
25
+ import YAML from "js-yaml";
26
+
27
+ export interface AggregateSpec {
28
+ name: string;
29
+ description?: string;
30
+ initialize: LuaValue; // ILuaFunction
31
+ iterate: LuaValue; // ILuaFunction
32
+ finish?: LuaValue; // ILuaFunction | undefined
33
+ }
34
+
35
+ // Helper to build an ILuaFunction from a plain function. Equivalent to
36
+ // LuaBuiltinFunction but without referencing the class.
37
+ function aggFn(
38
+ fn: (sf: LuaStackFrame, ...args: LuaValue[]) => LuaValue,
39
+ ): ILuaFunction {
40
+ return {
41
+ call(sf: LuaStackFrame, ...args: LuaValue[]) {
42
+ return fn(sf, ...args);
43
+ },
44
+ asString() {
45
+ return "<builtin aggregate>";
46
+ },
47
+ };
48
+ }
49
+
50
+ // Unwrap `LuaTaggedFloat` boxing to a plain JS number.
51
+ // Leaves all other values (strings, tables, etc.) untouched.
52
+ function unboxValue(value: LuaValue): LuaValue {
53
+ return isTaggedFloat(value) ? value.value : value;
54
+ }
55
+
56
+ // Welford's online algorithm (for variance and standard deviation)
57
+ interface WelfordState {
58
+ n: number;
59
+ mean: number;
60
+ m2: number;
61
+ }
62
+
63
+ function welfordInit(): WelfordState {
64
+ return { n: 0, mean: 0, m2: 0 };
65
+ }
66
+
67
+ function welfordIterate(state: WelfordState, value: any): WelfordState {
68
+ if (value === null || value === undefined || isSqlNull(value)) return state;
69
+ const x = coerceToNumber(value);
70
+ if (x === null) return state;
71
+ state.n += 1;
72
+ const delta = x - state.mean;
73
+ state.mean += delta / state.n;
74
+ const delta2 = x - state.mean;
75
+ state.m2 += delta * delta2;
76
+ return state;
77
+ }
78
+
79
+ interface CovarState extends WelfordState {
80
+ meanY: number;
81
+ m2y: number;
82
+ c: number; // co-moment
83
+ }
84
+
85
+ function covarInit(): CovarState {
86
+ return { n: 0, mean: 0, m2: 0, meanY: 0, m2y: 0, c: 0 };
87
+ }
88
+
89
+ function covarIterate(state: CovarState, x: any, y: any): CovarState {
90
+ if (
91
+ x === null ||
92
+ x === undefined ||
93
+ isSqlNull(x) ||
94
+ y === null ||
95
+ y === undefined ||
96
+ isSqlNull(y)
97
+ )
98
+ return state;
99
+ const xn = coerceToNumber(x);
100
+ const yn = coerceToNumber(y);
101
+ if (xn === null || yn === null) return state;
102
+ state.n += 1;
103
+ const dx = xn - state.mean;
104
+ state.mean += dx / state.n;
105
+ const dy = yn - state.meanY;
106
+ state.meanY += dy / state.n;
107
+ const dx2 = xn - state.mean;
108
+ const dy2 = yn - state.meanY;
109
+ state.c += dx * dy2;
110
+ state.m2 += dx * dx2;
111
+ state.m2y += dy * dy2;
112
+ return state;
113
+ }
114
+
115
+ // Quantile interpolation methods
116
+ type QuantileMethod =
117
+ | "linear" // percentile_cont
118
+ | "lower" // percentile_disc
119
+ | "higher"
120
+ | "nearest"
121
+ | "midpoint";
122
+
123
+ interface QuantileState {
124
+ values: number[];
125
+ q: number;
126
+ method: QuantileMethod;
127
+ }
128
+
129
+ // Default method based on aggregate invocation name
130
+ const quantileNameDefaults: Record<string, QuantileMethod> = {
131
+ percentile_cont: "linear",
132
+ percentile_disc: "lower",
133
+ };
134
+
135
+ function quantileFinish(state: QuantileState): number | null {
136
+ const { values, q, method } = state;
137
+ if (values.length === 0) return null;
138
+ const n = values.length;
139
+ if (n === 1) return values[0];
140
+ const idx = q * (n - 1);
141
+ const lo = Math.floor(idx);
142
+ const hi = Math.ceil(idx);
143
+ switch (method) {
144
+ case "lower":
145
+ return values[lo];
146
+ case "higher":
147
+ return values[hi];
148
+ case "nearest":
149
+ return idx - lo <= 0.5 ? values[lo] : values[hi];
150
+ case "midpoint":
151
+ return (values[lo] + values[hi]) / 2;
152
+ case "linear": {
153
+ if (lo === hi) return values[lo];
154
+ const frac = idx - lo;
155
+ return values[lo] + frac * (values[hi] - values[lo]);
156
+ }
157
+ default:
158
+ throw new Error(`quantile: unsupported interpolation method '${method}'`);
159
+ }
160
+ }
161
+
162
+ // Shared spec — branching on `ctx.name` for the default method
163
+ function makeQuantileSpec(name: string, description: string): AggregateSpec {
164
+ return {
165
+ name,
166
+ description,
167
+ initialize: aggFn((_sf, ctx: any, q: any, method: any) => {
168
+ const qVal = q ?? 0.5;
169
+ if (typeof qVal !== "number" || qVal < 0 || qVal > 1) {
170
+ throw new Error(`${name}: quantile must be between 0 and 1`);
171
+ }
172
+ const ctxName = ctx instanceof LuaTable ? ctx.rawGet("name") : name;
173
+ const m = (method ??
174
+ quantileNameDefaults[ctxName] ??
175
+ "linear") as QuantileMethod;
176
+ return { values: [] as number[], q: qVal, method: m } as QuantileState;
177
+ }),
178
+ iterate: aggFn((_sf, state: any, value: any) => {
179
+ if (value === null || value === undefined || isSqlNull(value))
180
+ return state;
181
+ const n = coerceToNumber(value);
182
+ if (n === null) return state;
183
+ state.values.push(n);
184
+ return state;
185
+ }),
186
+ finish: aggFn((_sf, state: any) => quantileFinish(state as QuantileState)),
187
+ };
188
+ }
189
+
190
+ // Built-in aggregate specs
191
+ const builtinAggregates: Record<string, AggregateSpec> = {
192
+ // General purpose
193
+ count: {
194
+ name: "count",
195
+ description:
196
+ "Non-null row count for arguments; total row count without argument",
197
+ initialize: aggFn((_sf) => 0),
198
+ iterate: aggFn((_sf, state: any, value: any) => {
199
+ if (value === null || value === undefined || isSqlNull(value))
200
+ return state;
201
+ return (state as number) + 1;
202
+ }),
203
+ },
204
+ sum: {
205
+ name: "sum",
206
+ description: "Arithmetic sum of all non-null input values",
207
+ initialize: aggFn((_sf) => ({ result: 0, hasValue: false })),
208
+ iterate: aggFn((_sf, state: any, value: any) => {
209
+ if (value === null || value === undefined || isSqlNull(value))
210
+ return state;
211
+ const n = coerceToNumber(value);
212
+ if (n === null) return state;
213
+ state.result += n;
214
+ state.hasValue = true;
215
+ return state;
216
+ }),
217
+ finish: aggFn((_sf, state: any) => {
218
+ return state.hasValue ? state.result : null;
219
+ }),
220
+ },
221
+ product: {
222
+ name: "product",
223
+ description: "Product of all non-null input values",
224
+ initialize: aggFn((_sf) => ({ result: 1, hasValue: false })),
225
+ iterate: aggFn((_sf, state: any, value: any) => {
226
+ if (value === null || value === undefined || isSqlNull(value))
227
+ return state;
228
+ const n = coerceToNumber(value);
229
+ if (n === null) return state;
230
+ state.result *= n;
231
+ state.hasValue = true;
232
+ return state;
233
+ }),
234
+ finish: aggFn((_sf, state: any) => {
235
+ return state.hasValue ? state.result : null;
236
+ }),
237
+ },
238
+ min: {
239
+ name: "min",
240
+ description: "Minimum value among non-null inputs",
241
+ initialize: aggFn((_sf) => null),
242
+ iterate: aggFn((_sf, state: any, value: any) => {
243
+ if (value === null || value === undefined || isSqlNull(value))
244
+ return state;
245
+ if (state === null || value < state) return value;
246
+ return state;
247
+ }),
248
+ },
249
+ max: {
250
+ name: "max",
251
+ description: "Maximum value among non-null inputs",
252
+ initialize: aggFn((_sf) => null),
253
+ iterate: aggFn((_sf, state: any, value: any) => {
254
+ if (value === null || value === undefined || isSqlNull(value))
255
+ return state;
256
+ if (state === null || value > state) return value;
257
+ return state;
258
+ }),
259
+ },
260
+ avg: {
261
+ name: "avg",
262
+ description: "Arithmetic mean of all non-null input values",
263
+ initialize: aggFn((_sf) => ({ sum: 0, count: 0 })),
264
+ iterate: aggFn((_sf, state: any, value: any) => {
265
+ if (value === null || value === undefined || isSqlNull(value))
266
+ return state;
267
+ const n = coerceToNumber(value);
268
+ if (n === null) return state;
269
+ state.sum += n;
270
+ state.count += 1;
271
+ return state;
272
+ }),
273
+ finish: aggFn((_sf, state: any) => {
274
+ if (state.count === 0) return null;
275
+ return state.sum / state.count;
276
+ }),
277
+ },
278
+ first: {
279
+ name: "first",
280
+ description: "First non-null input value (iteration order)",
281
+ initialize: aggFn((_sf) => ({ value: null, found: false })),
282
+ iterate: aggFn((_sf, state: any, value: any) => {
283
+ if (state.found) return state;
284
+ if (value === null || value === undefined || isSqlNull(value))
285
+ return state;
286
+ state.value = value;
287
+ state.found = true;
288
+ return state;
289
+ }),
290
+ finish: aggFn((_sf, state: any) => state.value),
291
+ },
292
+ last: {
293
+ name: "last",
294
+ description: "Last non-null input value (iteration order)",
295
+ initialize: aggFn((_sf) => null),
296
+ iterate: aggFn((_sf, state: any, value: any) => {
297
+ if (value === null || value === undefined || isSqlNull(value))
298
+ return state;
299
+ return value;
300
+ }),
301
+ },
302
+ // Collection and format
303
+ array_agg: {
304
+ name: "array_agg",
305
+ description: "Input values concatenated into an array",
306
+ initialize: aggFn((_sf) => new LuaTable()),
307
+ iterate: aggFn((_sf, state: any, value: any) => {
308
+ (state as LuaTable).rawSetArrayIndex(
309
+ (state as LuaTable).length + 1,
310
+ isSqlNull(value) ? null : value,
311
+ );
312
+ return state;
313
+ }),
314
+ },
315
+ string_agg: {
316
+ name: "string_agg",
317
+ description:
318
+ "Concatenated non-null values; argument: delimiter (default: ',')",
319
+ initialize: aggFn((_sf, _ctx: any, sep: any) => {
320
+ return { sep: sep ?? ",", parts: [] as string[] };
321
+ }),
322
+ iterate: aggFn((_sf, state: any, value: any) => {
323
+ if (value === null || value === undefined || isSqlNull(value))
324
+ return state;
325
+ state.parts.push(String(value));
326
+ return state;
327
+ }),
328
+ finish: aggFn((_sf, state: any) => {
329
+ return state.parts.join(state.sep);
330
+ }),
331
+ },
332
+ yaml_agg: {
333
+ name: "yaml_agg",
334
+ description: "Input values aggregated into a YAML string",
335
+ initialize: aggFn((_sf) => [] as any[]),
336
+ iterate: aggFn((sf, state: any, value: any) => {
337
+ if (isSqlNull(value)) {
338
+ state.push(null);
339
+ } else if (value instanceof LuaTable) {
340
+ state.push(luaValueToJS(value, sf));
341
+ } else {
342
+ state.push(value);
343
+ }
344
+ return state;
345
+ }),
346
+ finish: aggFn((_sf, state: any) => {
347
+ return YAML.dump(state, { quotingType: '"', noCompatMode: true });
348
+ }),
349
+ },
350
+ json_agg: {
351
+ name: "json_agg",
352
+ description: "Input values aggregated into a JSON string",
353
+ initialize: aggFn((_sf) => [] as any[]),
354
+ iterate: aggFn((sf, state: any, value: any) => {
355
+ if (isSqlNull(value)) {
356
+ state.push(null);
357
+ } else if (value instanceof LuaTable) {
358
+ state.push(luaValueToJS(value, sf));
359
+ } else {
360
+ state.push(value);
361
+ }
362
+ return state;
363
+ }),
364
+ finish: aggFn((_sf, state: any) => {
365
+ return JSON.stringify(state);
366
+ }),
367
+ },
368
+ // Bitwise and boolean
369
+ bit_and: {
370
+ name: "bit_and",
371
+ description: "Bitwise AND of all non-null input values",
372
+ initialize: aggFn((_sf) => ({ result: ~0, hasValue: false })),
373
+ iterate: aggFn((_sf, state: any, value: any) => {
374
+ if (value === null || value === undefined || isSqlNull(value))
375
+ return state;
376
+ const n = coerceToNumber(value);
377
+ if (n === null) return state;
378
+ state.result &= n;
379
+ state.hasValue = true;
380
+ return state;
381
+ }),
382
+ finish: aggFn((_sf, state: any) => {
383
+ return state.hasValue ? state.result : null;
384
+ }),
385
+ },
386
+ bit_or: {
387
+ name: "bit_or",
388
+ description: "Bitwise OR of all non-null input values",
389
+ initialize: aggFn((_sf) => ({ result: 0, hasValue: false })),
390
+ iterate: aggFn((_sf, state: any, value: any) => {
391
+ if (value === null || value === undefined || isSqlNull(value))
392
+ return state;
393
+ const n = coerceToNumber(value);
394
+ if (n === null) return state;
395
+ state.result |= n;
396
+ state.hasValue = true;
397
+ return state;
398
+ }),
399
+ finish: aggFn((_sf, state: any) => {
400
+ return state.hasValue ? state.result : null;
401
+ }),
402
+ },
403
+ bit_xor: {
404
+ name: "bit_xor",
405
+ description: "Bitwise exclusive OR of all non-null input values",
406
+ initialize: aggFn((_sf) => ({ result: 0, hasValue: false })),
407
+ iterate: aggFn((_sf, state: any, value: any) => {
408
+ if (value === null || value === undefined || isSqlNull(value))
409
+ return state;
410
+ const n = coerceToNumber(value);
411
+ if (n === null) return state;
412
+ state.result ^= n;
413
+ state.hasValue = true;
414
+ return state;
415
+ }),
416
+ finish: aggFn((_sf, state: any) => {
417
+ return state.hasValue ? state.result : null;
418
+ }),
419
+ },
420
+ bool_and: {
421
+ name: "bool_and",
422
+ description: "True if all non-null inputs are true, otherwise false",
423
+ initialize: aggFn((_sf) => ({ result: true, hasValue: false })),
424
+ iterate: aggFn((_sf, state: any, value: any) => {
425
+ if (value === null || value === undefined || isSqlNull(value))
426
+ return state;
427
+ state.result = state.result && !!value;
428
+ state.hasValue = true;
429
+ return state;
430
+ }),
431
+ finish: aggFn((_sf, state: any) => {
432
+ return state.hasValue ? state.result : null;
433
+ }),
434
+ },
435
+ bool_or: {
436
+ name: "bool_or",
437
+ description: "True if at least one non-null input is true, otherwise false",
438
+ initialize: aggFn((_sf) => ({ result: false, hasValue: false })),
439
+ iterate: aggFn((_sf, state: any, value: any) => {
440
+ if (value === null || value === undefined || isSqlNull(value))
441
+ return state;
442
+ state.result = state.result || !!value;
443
+ state.hasValue = true;
444
+ return state;
445
+ }),
446
+ finish: aggFn((_sf, state: any) => {
447
+ return state.hasValue ? state.result : null;
448
+ }),
449
+ },
450
+ // Statistical
451
+ stddev_pop: {
452
+ name: "stddev_pop",
453
+ description: "Population standard deviation of non-null inputs",
454
+ initialize: aggFn((_sf) => welfordInit()),
455
+ iterate: aggFn((_sf, state: any, value: any) =>
456
+ welfordIterate(state, value),
457
+ ),
458
+ finish: aggFn((_sf, state: any) => {
459
+ if (state.n === 0) return null;
460
+ return Math.sqrt(state.m2 / state.n);
461
+ }),
462
+ },
463
+ stddev_samp: {
464
+ name: "stddev_samp",
465
+ description: "Sample standard deviation of non-null inputs",
466
+ initialize: aggFn((_sf) => welfordInit()),
467
+ iterate: aggFn((_sf, state: any, value: any) =>
468
+ welfordIterate(state, value),
469
+ ),
470
+ finish: aggFn((_sf, state: any) => {
471
+ if (state.n < 2) return null;
472
+ return Math.sqrt(state.m2 / (state.n - 1));
473
+ }),
474
+ },
475
+ var_pop: {
476
+ name: "var_pop",
477
+ description:
478
+ "Population variance (square of population standard deviation)",
479
+ initialize: aggFn((_sf) => welfordInit()),
480
+ iterate: aggFn((_sf, state: any, value: any) =>
481
+ welfordIterate(state, value),
482
+ ),
483
+ finish: aggFn((_sf, state: any) => {
484
+ if (state.n === 0) return null;
485
+ return state.m2 / state.n;
486
+ }),
487
+ },
488
+ var_samp: {
489
+ name: "var_samp",
490
+ description: "Sample variance (square of sample standard deviation)",
491
+ initialize: aggFn((_sf) => welfordInit()),
492
+ iterate: aggFn((_sf, state: any, value: any) =>
493
+ welfordIterate(state, value),
494
+ ),
495
+ finish: aggFn((_sf, state: any) => {
496
+ if (state.n < 2) return null;
497
+ return state.m2 / (state.n - 1);
498
+ }),
499
+ },
500
+ covar_pop: {
501
+ name: "covar_pop",
502
+ description: "Population covariance of non-null input pairs",
503
+ initialize: aggFn((_sf) => covarInit()),
504
+ iterate: aggFn((_sf, state: any, y: any, _ctx: any, x: any) =>
505
+ covarIterate(state, x, y),
506
+ ),
507
+ finish: aggFn((_sf, state: any) => {
508
+ if (state.n === 0) return null;
509
+ return state.c / state.n;
510
+ }),
511
+ },
512
+ covar_samp: {
513
+ name: "covar_samp",
514
+ description: "Sample covariance of non-null input pairs",
515
+ initialize: aggFn((_sf) => covarInit()),
516
+ iterate: aggFn((_sf, state: any, y: any, _ctx: any, x: any) =>
517
+ covarIterate(state, x, y),
518
+ ),
519
+ finish: aggFn((_sf, state: any) => {
520
+ if (state.n < 2) return null;
521
+ return state.c / (state.n - 1);
522
+ }),
523
+ },
524
+ corr: {
525
+ name: "corr",
526
+ description: "Correlation coefficient of non-null input pairs",
527
+ initialize: aggFn((_sf) => covarInit()),
528
+ iterate: aggFn((_sf, state: any, y: any, _ctx: any, x: any) =>
529
+ covarIterate(state, x, y),
530
+ ),
531
+ finish: aggFn((_sf, state: any) => {
532
+ if (state.n < 2) return null;
533
+ const denom = Math.sqrt(state.m2 * state.m2y);
534
+ if (denom === 0) return null;
535
+ return state.c / denom;
536
+ }),
537
+ },
538
+ mode: {
539
+ name: "mode",
540
+ description: "Most frequent non-null input value",
541
+ initialize: aggFn((_sf) => ({
542
+ freq: new Map<LuaValue, number>(),
543
+ best: null as LuaValue,
544
+ bestCount: 0,
545
+ })),
546
+ iterate: aggFn((_sf, state: any, value: any) => {
547
+ if (value === null || value === undefined || isSqlNull(value))
548
+ return state;
549
+ const c = (state.freq.get(value) ?? 0) + 1;
550
+ state.freq.set(value, c);
551
+ if (c > state.bestCount) {
552
+ state.bestCount = c;
553
+ state.best = value;
554
+ }
555
+ return state;
556
+ }),
557
+ finish: aggFn((_sf, state: any) => {
558
+ return state.bestCount > 0 ? state.best : null;
559
+ }),
560
+ },
561
+ // Quantile and percentile
562
+ quantile: makeQuantileSpec(
563
+ "quantile",
564
+ "Quantile of ordered set of non-null inputs; arguments: value, quantile (0-1), interpolation ('lower', 'higher', 'nearest', 'midpoint' and default: 'linear')",
565
+ ),
566
+ percentile_cont: makeQuantileSpec(
567
+ "percentile_cont",
568
+ "Continuous percentile (linear interpolation) on ordered set of non-null inputs; arguments: value, fraction (0-1)",
569
+ ),
570
+ percentile_disc: makeQuantileSpec(
571
+ "percentile_disc",
572
+ "Discrete percentile (nearest lower value) on ordered set of non-null inputs; arguments: value, fraction (0-1)",
573
+ ),
574
+ median: {
575
+ name: "median",
576
+ description: "Median of non-null inputs (continuous percentile at 0.5)",
577
+ initialize: aggFn((_sf) => ({
578
+ values: [] as number[],
579
+ q: 0.5,
580
+ method: "linear" as QuantileMethod,
581
+ })),
582
+ iterate: aggFn((_sf, state: any, value: any) => {
583
+ if (value === null || value === undefined || isSqlNull(value))
584
+ return state;
585
+ const n = coerceToNumber(value);
586
+ if (n === null) return state;
587
+ state.values.push(n);
588
+ return state;
589
+ }),
590
+ finish: aggFn((_sf, state: any) => quantileFinish(state as QuantileState)),
591
+ },
592
+ };
593
+
594
+ const noCtx = {};
595
+
596
+ function buildAggCtx(name: string, config: Config): LuaTable {
597
+ const ctx = new LuaTable();
598
+ void ctx.rawSet("name", name);
599
+ void ctx.rawSet("config", config.get(`aggregateConfig.${name}`, {}));
600
+ return ctx;
601
+ }
602
+
603
+ /**
604
+ * Resolve name through config following alias chains (cycles detected)
605
+ */
606
+ export function getAggregateSpec(
607
+ name: string,
608
+ config?: Config,
609
+ ): AggregateSpec | null {
610
+ const visited = new Set<string>();
611
+ let current = name;
612
+
613
+ while (config) {
614
+ if (visited.has(current)) return null; // cycle
615
+ visited.add(current);
616
+
617
+ const spec: any = config.get(`aggregates.${current}`, null);
618
+ if (!spec) break;
619
+
620
+ // Check for alias redirect
621
+ const alias = spec instanceof LuaTable ? spec.rawGet("alias") : spec.alias;
622
+ if (typeof alias === "string") {
623
+ current = alias;
624
+ continue;
625
+ }
626
+
627
+ // Full definition in config
628
+ let candidate: AggregateSpec | null = null;
629
+ if (spec instanceof LuaTable) {
630
+ const init = spec.rawGet("initialize");
631
+ const iter = spec.rawGet("iterate");
632
+ if (init && iter) {
633
+ candidate = {
634
+ name: spec.rawGet("name") ?? current,
635
+ description: spec.rawGet("description"),
636
+ initialize: init,
637
+ iterate: iter,
638
+ finish: spec.rawGet("finish"),
639
+ };
640
+ }
641
+ } else if (spec.initialize && spec.iterate) {
642
+ candidate = spec as AggregateSpec;
643
+ }
644
+ if (candidate) return candidate;
645
+ break;
646
+ }
647
+
648
+ return builtinAggregates[current] ?? null;
649
+ }
650
+
651
+ /**
652
+ * Returns info about all built-in aggregates
653
+ */
654
+ export function getBuiltinAggregateEntries(): {
655
+ name: string;
656
+ description: string;
657
+ hasFinish: boolean;
658
+ }[] {
659
+ return Object.values(builtinAggregates).map((spec) => ({
660
+ name: spec.name,
661
+ description: spec.description ?? "",
662
+ hasFinish: !!spec.finish,
663
+ }));
664
+ }
665
+
666
+ /**
667
+ * Execute an aggregate function over a group of items.
668
+ */
669
+ export async function executeAggregate(
670
+ spec: AggregateSpec,
671
+ items: LuaTable,
672
+ valueExpr: LuaExpression | null,
673
+ extraArgExprs: LuaExpression[],
674
+ objectVariable: string | undefined,
675
+ env: LuaEnv,
676
+ sf: LuaStackFrame,
677
+ evalExprFn: (
678
+ e: LuaExpression,
679
+ env: LuaEnv,
680
+ sf: LuaStackFrame,
681
+ ) => Promise<LuaValue> | LuaValue,
682
+ config: Config,
683
+ filterExpr?: LuaExpression,
684
+ orderBy?: LuaOrderBy[],
685
+ ): Promise<LuaValue> {
686
+ const ctx = buildAggCtx(spec.name, config);
687
+
688
+ // Evaluate extra args using the first item's env so that references
689
+ // to the object variable (e.g. `data.x`) resolve correctly.
690
+ // These are used for initialize and finish; iterate re-evaluates per-item.
691
+ const extraArgs: LuaValue[] = [];
692
+ if (extraArgExprs.length > 0) {
693
+ const firstItem = items.length > 0 ? items.rawGet(1) : undefined;
694
+ const firstEnv =
695
+ firstItem !== undefined
696
+ ? buildItemEnv(objectVariable, firstItem, env, sf)
697
+ : env;
698
+ for (const argExpr of extraArgExprs) {
699
+ extraArgs.push(await evalExprFn(argExpr, firstEnv, sf));
700
+ }
701
+ }
702
+
703
+ // Initialize
704
+ let state = await luaCall(spec.initialize, [ctx, ...extraArgs], noCtx, sf);
705
+
706
+ // Collect filtered items
707
+ const filteredItems: LuaValue[] = [];
708
+ const len = items.length;
709
+ for (let i = 1; i <= len; i++) {
710
+ const item = items.rawGet(i);
711
+
712
+ // Filter
713
+ if (filterExpr) {
714
+ const filterEnv = buildItemEnv(objectVariable, item, env, sf);
715
+ const filterResult = await evalExprFn(filterExpr, filterEnv, sf);
716
+ if (!luaTruthy(filterResult)) {
717
+ continue;
718
+ }
719
+ }
720
+ filteredItems.push(item);
721
+ }
722
+
723
+ // Intra-aggregate ordering: sorts items before iteration.
724
+ // This is required for ordered-set aggregates (quantile, percentile_cont,
725
+ // percentile_disc) which expect values in a specific order. The user
726
+ // must provide `order by` for these aggregates to produce correct results.
727
+ if (orderBy && orderBy.length > 0) {
728
+ await asyncMergeSort(filteredItems, async (a: any, b: any) => {
729
+ for (const ob of orderBy) {
730
+ const envA = buildItemEnv(objectVariable, a, env, sf);
731
+ const envB = buildItemEnv(objectVariable, b, env, sf);
732
+ const valA = await evalExprFn(ob.expression, envA, sf);
733
+ const valB = await evalExprFn(ob.expression, envB, sf);
734
+ const aNull = valA === null || valA === undefined || isSqlNull(valA);
735
+ const bNull = valB === null || valB === undefined || isSqlNull(valB);
736
+ if (aNull && bNull) continue;
737
+ if (aNull) return ob.nulls === "first" ? -1 : 1;
738
+ if (bNull) return ob.nulls === "first" ? 1 : -1;
739
+ let cmp = 0;
740
+ if (valA < valB) cmp = -1;
741
+ else if (valA > valB) cmp = 1;
742
+ if (cmp !== 0) {
743
+ return ob.direction === "desc" ? -cmp : cmp;
744
+ }
745
+ }
746
+ return 0;
747
+ });
748
+ }
749
+
750
+ // Iterate
751
+ for (const item of filteredItems) {
752
+ const itemEnv = buildItemEnv(objectVariable, item, env, sf);
753
+ let value: LuaValue;
754
+ if (valueExpr === null) {
755
+ value = item;
756
+ } else {
757
+ value = await evalExprFn(valueExpr, itemEnv, sf);
758
+ }
759
+ // Unwrap internal `LuaTaggedFloat` boxing so that both builtin and
760
+ // user-defined iterate functions receive plain JS numbers instead
761
+ // of opaque `{value, isFloat}` objects.
762
+ value = unboxValue(value);
763
+ // Evaluate extra args per-item so they can reference item fields
764
+ const iterExtraArgs: LuaValue[] = [];
765
+ for (const argExpr of extraArgExprs) {
766
+ iterExtraArgs.push(unboxValue(await evalExprFn(argExpr, itemEnv, sf)));
767
+ }
768
+ state = await luaCall(
769
+ spec.iterate,
770
+ [state, value, ctx, ...iterExtraArgs],
771
+ noCtx,
772
+ sf,
773
+ );
774
+ }
775
+
776
+ // Finish
777
+ if (spec.finish) {
778
+ state = await luaCall(spec.finish, [state, ctx, ...extraArgs], noCtx, sf);
779
+ }
780
+
781
+ return state;
782
+ }