@silverbulletmd/silverbullet 2.5.3 → 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 (90) hide show
  1. package/README.md +4 -5
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +3 -2
  5. package/client/plugos/hooks/code_widget.ts +3 -5
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -12
  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 -5
  11. package/client/plugos/hooks/slash_command.ts +12 -27
  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 -5
  15. package/client/plugos/plug_compile.ts +67 -65
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/worker_sandbox.ts +16 -15
  19. package/client/plugos/syscalls/asset.ts +1 -3
  20. package/client/plugos/syscalls/code_widget.ts +1 -3
  21. package/client/plugos/syscalls/config.ts +1 -5
  22. package/client/plugos/syscalls/datastore.ts +1 -1
  23. package/client/plugos/syscalls/editor.ts +63 -60
  24. package/client/plugos/syscalls/event.ts +9 -12
  25. package/client/plugos/syscalls/fetch.ts +30 -22
  26. package/client/plugos/syscalls/index.ts +10 -1
  27. package/client/plugos/syscalls/jsonschema.ts +72 -32
  28. package/client/plugos/syscalls/language.ts +9 -5
  29. package/client/plugos/syscalls/markdown.ts +29 -7
  30. package/client/plugos/syscalls/mq.ts +3 -11
  31. package/client/plugos/syscalls/service_registry.ts +1 -4
  32. package/client/plugos/syscalls/shell.ts +2 -5
  33. package/client/plugos/syscalls/sync.ts +69 -60
  34. package/client/plugos/syscalls/system.ts +2 -3
  35. package/client/plugos/system.ts +4 -10
  36. package/client/plugos/worker_runtime.ts +4 -3
  37. package/client/space_lua/aggregates.ts +632 -59
  38. package/client/space_lua/ast.ts +21 -9
  39. package/client/space_lua/ast_narrow.ts +4 -2
  40. package/client/space_lua/eval.ts +842 -536
  41. package/client/space_lua/labels.ts +6 -11
  42. package/client/space_lua/liq_null.ts +6 -0
  43. package/client/space_lua/numeric.ts +5 -8
  44. package/client/space_lua/parse.ts +290 -169
  45. package/client/space_lua/query_collection.ts +213 -149
  46. package/client/space_lua/render_lua_markdown.ts +369 -0
  47. package/client/space_lua/rp.ts +5 -4
  48. package/client/space_lua/runtime.ts +245 -142
  49. package/client/space_lua/stdlib/format.ts +34 -20
  50. package/client/space_lua/stdlib/js.ts +3 -7
  51. package/client/space_lua/stdlib/load.ts +1 -3
  52. package/client/space_lua/stdlib/math.ts +15 -14
  53. package/client/space_lua/stdlib/net.ts +25 -15
  54. package/client/space_lua/stdlib/os.ts +76 -85
  55. package/client/space_lua/stdlib/pattern.ts +28 -35
  56. package/client/space_lua/stdlib/prng.ts +15 -12
  57. package/client/space_lua/stdlib/space_lua.ts +16 -17
  58. package/client/space_lua/stdlib/string.ts +7 -17
  59. package/client/space_lua/stdlib/string_pack.ts +23 -19
  60. package/client/space_lua/stdlib/table.ts +5 -9
  61. package/client/space_lua/stdlib.ts +20 -30
  62. package/client/space_lua/tonumber.ts +79 -40
  63. package/client/space_lua/util.ts +14 -10
  64. package/dist/plug-compile.js +44 -41
  65. package/package.json +24 -22
  66. package/plug-api/lib/async.ts +19 -6
  67. package/plug-api/lib/crypto.ts +5 -6
  68. package/plug-api/lib/dates.ts +15 -7
  69. package/plug-api/lib/json.ts +10 -4
  70. package/plug-api/lib/ref.ts +18 -18
  71. package/plug-api/lib/resolve.ts +7 -11
  72. package/plug-api/lib/tags.ts +13 -4
  73. package/plug-api/lib/transclusion.ts +6 -17
  74. package/plug-api/lib/tree.ts +115 -43
  75. package/plug-api/lib/yaml.ts +25 -15
  76. package/plug-api/syscalls/asset.ts +1 -1
  77. package/plug-api/syscalls/config.ts +1 -4
  78. package/plug-api/syscalls/editor.ts +14 -14
  79. package/plug-api/syscalls/jsonschema.ts +1 -3
  80. package/plug-api/syscalls/lua.ts +3 -9
  81. package/plug-api/syscalls/mq.ts +1 -4
  82. package/plug-api/syscalls/shell.ts +4 -1
  83. package/plug-api/syscalls/space.ts +3 -10
  84. package/plug-api/syscalls/system.ts +1 -4
  85. package/plug-api/syscalls/yaml.ts +2 -6
  86. package/plug-api/types/client.ts +16 -1
  87. package/plug-api/types/event.ts +6 -4
  88. package/plug-api/types/manifest.ts +8 -9
  89. package/plugs/builtin_plugs.ts +2 -2
  90. package/dist/worker_runtime_bundle.js +0 -233
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * Aggregate function definitions and execution for LIQ.
3
3
  *
4
- * Built-in aggregates (sum, count, min, max, avg, array_agg) are
5
- * implemented in TypeScript for speed. Users can override any builtin
6
- * via `aggregate.define` or `aggregate.update`.
7
- *
8
4
  * Builtins implement ILuaFunction via plain objects rather than
9
5
  * LuaBuiltinFunction instances. This avoids ES module TDZ issues:
10
6
  * `class` exports are not available during circular module init,
@@ -17,10 +13,16 @@ import {
17
13
  type LuaEnv,
18
14
  LuaTable,
19
15
  luaTruthy,
16
+ luaValueToJS,
20
17
  type LuaValue,
21
18
  } from "./runtime.ts";
22
- import type { LuaExpression } from "./ast.ts";
19
+ import { isSqlNull } from "./liq_null.ts";
20
+ import type { LuaExpression, LuaOrderBy } from "./ast.ts";
23
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";
24
26
 
25
27
  export interface AggregateSpec {
26
28
  name: string;
@@ -45,53 +47,226 @@ function aggFn(
45
47
  };
46
48
  }
47
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
+
48
190
  // Built-in aggregate specs
49
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
+ },
50
204
  sum: {
51
205
  name: "sum",
52
- description: "Sum of numeric values",
53
- initialize: aggFn((_sf) => 0),
206
+ description: "Arithmetic sum of all non-null input values",
207
+ initialize: aggFn((_sf) => ({ result: 0, hasValue: false })),
54
208
  iterate: aggFn((_sf, state: any, value: any) => {
55
- if (value === null || value === undefined) return state;
56
- return (state as number) + (value as number);
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;
57
219
  }),
58
220
  },
59
- count: {
60
- name: "count",
61
- description: "Count of values; count() with no argument counts all rows",
62
- initialize: aggFn((_sf) => 0),
221
+ product: {
222
+ name: "product",
223
+ description: "Product of all non-null input values",
224
+ initialize: aggFn((_sf) => ({ result: 1, hasValue: false })),
63
225
  iterate: aggFn((_sf, state: any, value: any) => {
64
- if (value === null || value === undefined) return state;
65
- return (state as number) + 1;
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;
66
236
  }),
67
237
  },
68
238
  min: {
69
239
  name: "min",
70
- description: "Minimum value",
240
+ description: "Minimum value among non-null inputs",
71
241
  initialize: aggFn((_sf) => null),
72
242
  iterate: aggFn((_sf, state: any, value: any) => {
73
- if (value === null || value === undefined) return state;
243
+ if (value === null || value === undefined || isSqlNull(value))
244
+ return state;
74
245
  if (state === null || value < state) return value;
75
246
  return state;
76
247
  }),
77
248
  },
78
249
  max: {
79
250
  name: "max",
80
- description: "Maximum value",
251
+ description: "Maximum value among non-null inputs",
81
252
  initialize: aggFn((_sf) => null),
82
253
  iterate: aggFn((_sf, state: any, value: any) => {
83
- if (value === null || value === undefined) return state;
254
+ if (value === null || value === undefined || isSqlNull(value))
255
+ return state;
84
256
  if (state === null || value > state) return value;
85
257
  return state;
86
258
  }),
87
259
  },
88
260
  avg: {
89
261
  name: "avg",
90
- description: "Average of numeric values",
262
+ description: "Arithmetic mean of all non-null input values",
91
263
  initialize: aggFn((_sf) => ({ sum: 0, count: 0 })),
92
264
  iterate: aggFn((_sf, state: any, value: any) => {
93
- if (value === null || value === undefined) return state;
94
- state.sum += value as number;
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;
95
270
  state.count += 1;
96
271
  return state;
97
272
  }),
@@ -100,58 +275,392 @@ const builtinAggregates: Record<string, AggregateSpec> = {
100
275
  return state.sum / state.count;
101
276
  }),
102
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
103
303
  array_agg: {
104
304
  name: "array_agg",
105
- description: "Collect values into an array",
305
+ description: "Input values concatenated into an array",
106
306
  initialize: aggFn((_sf) => new LuaTable()),
107
307
  iterate: aggFn((_sf, state: any, value: any) => {
108
308
  (state as LuaTable).rawSetArrayIndex(
109
309
  (state as LuaTable).length + 1,
110
- value,
310
+ isSqlNull(value) ? null : value,
111
311
  );
112
312
  return state;
113
313
  }),
114
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
+ },
115
592
  };
116
593
 
117
594
  const noCtx = {};
118
595
 
119
- function buildAggCtx(name: string): LuaTable {
596
+ function buildAggCtx(name: string, config: Config): LuaTable {
120
597
  const ctx = new LuaTable();
121
- ctx.rawSet("name", name);
122
- const clientConfig = globalThis.client?.config;
123
- const aggConfig = clientConfig
124
- ? clientConfig.get(`aggregateConfig.${name}`, {})
125
- : {};
126
- ctx.rawSet("config", aggConfig);
598
+ void ctx.rawSet("name", name);
599
+ void ctx.rawSet("config", config.get(`aggregateConfig.${name}`, {}));
127
600
  return ctx;
128
601
  }
129
602
 
130
- export function getAggregateSpec(name: string): AggregateSpec | null {
131
- const clientConfig = globalThis.client?.config;
132
- if (clientConfig) {
133
- const spec: any = clientConfig.get(`aggregates.${name}`, null);
134
- if (spec) {
135
- let candidate: AggregateSpec | null = null;
136
- if (spec instanceof LuaTable) {
137
- const init = spec.rawGet("initialize");
138
- const iter = spec.rawGet("iterate");
139
- if (init && iter) {
140
- candidate = {
141
- name: spec.rawGet("name") ?? name,
142
- description: spec.rawGet("description"),
143
- initialize: init,
144
- iterate: iter,
145
- finish: spec.rawGet("finish"),
146
- };
147
- }
148
- } else if (spec.initialize && spec.iterate) {
149
- candidate = spec as AggregateSpec;
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
+ };
150
640
  }
151
- if (candidate) return candidate;
641
+ } else if (spec.initialize && spec.iterate) {
642
+ candidate = spec as AggregateSpec;
152
643
  }
644
+ if (candidate) return candidate;
645
+ break;
153
646
  }
154
- return builtinAggregates[name] ?? null;
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
+ }));
155
664
  }
156
665
 
157
666
  /**
@@ -161,6 +670,7 @@ export async function executeAggregate(
161
670
  spec: AggregateSpec,
162
671
  items: LuaTable,
163
672
  valueExpr: LuaExpression | null,
673
+ extraArgExprs: LuaExpression[],
164
674
  objectVariable: string | undefined,
165
675
  env: LuaEnv,
166
676
  sf: LuaStackFrame,
@@ -169,14 +679,32 @@ export async function executeAggregate(
169
679
  env: LuaEnv,
170
680
  sf: LuaStackFrame,
171
681
  ) => Promise<LuaValue> | LuaValue,
682
+ config: Config,
172
683
  filterExpr?: LuaExpression,
684
+ orderBy?: LuaOrderBy[],
173
685
  ): Promise<LuaValue> {
174
- const ctx = buildAggCtx(spec.name);
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
+ }
175
702
 
176
703
  // Initialize
177
- let state = await luaCall(spec.initialize, [ctx], noCtx, sf);
704
+ let state = await luaCall(spec.initialize, [ctx, ...extraArgs], noCtx, sf);
178
705
 
179
- // Iterate
706
+ // Collect filtered items
707
+ const filteredItems: LuaValue[] = [];
180
708
  const len = items.length;
181
709
  for (let i = 1; i <= len; i++) {
182
710
  const item = items.rawGet(i);
@@ -189,20 +717,65 @@ export async function executeAggregate(
189
717
  continue;
190
718
  }
191
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
+ }
192
749
 
750
+ // Iterate
751
+ for (const item of filteredItems) {
752
+ const itemEnv = buildItemEnv(objectVariable, item, env, sf);
193
753
  let value: LuaValue;
194
754
  if (valueExpr === null) {
195
755
  value = item;
196
756
  } else {
197
- const itemEnv = buildItemEnv(objectVariable, item, env, sf);
198
757
  value = await evalExprFn(valueExpr, itemEnv, sf);
199
758
  }
200
- state = await luaCall(spec.iterate, [state, value, ctx], noCtx, sf);
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
+ );
201
774
  }
202
775
 
203
776
  // Finish
204
777
  if (spec.finish) {
205
- state = await luaCall(spec.finish, [state, ctx], noCtx, sf);
778
+ state = await luaCall(spec.finish, [state, ctx, ...extraArgs], noCtx, sf);
206
779
  }
207
780
 
208
781
  return state;