@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.
- package/README.md +19 -4
- package/client/asset_bundle/bundle.ts +3 -9
- package/client/data/datastore.ts +4 -5
- package/client/markdown_parser/constants.ts +5 -4
- package/client/plugos/hooks/code_widget.ts +3 -8
- package/client/plugos/hooks/command.ts +8 -8
- package/client/plugos/hooks/document_editor.ts +10 -15
- package/client/plugos/hooks/event.ts +33 -36
- package/client/plugos/hooks/mq.ts +17 -17
- package/client/plugos/hooks/plug_namespace.ts +3 -8
- package/client/plugos/hooks/slash_command.ts +13 -28
- package/client/plugos/hooks/syscall.ts +3 -3
- package/client/plugos/manifest_cache.ts +22 -15
- package/client/plugos/plug.ts +2 -6
- package/client/plugos/plug_compile.ts +79 -78
- package/client/plugos/protocol.ts +28 -28
- package/client/plugos/proxy_fetch.ts +7 -6
- package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
- package/client/plugos/sandboxes/worker_sandbox.ts +18 -18
- package/client/plugos/syscalls/asset.ts +1 -3
- package/client/plugos/syscalls/code_widget.ts +1 -3
- package/client/plugos/syscalls/config.ts +1 -5
- package/client/plugos/syscalls/datastore.ts +1 -1
- package/client/plugos/syscalls/editor.ts +72 -69
- package/client/plugos/syscalls/event.ts +9 -12
- package/client/plugos/syscalls/fetch.ts +31 -23
- package/client/plugos/syscalls/index.ts +10 -1
- package/client/plugos/syscalls/jsonschema.ts +72 -32
- package/client/plugos/syscalls/language.ts +9 -5
- package/client/plugos/syscalls/markdown.ts +29 -7
- package/client/plugos/syscalls/mq.ts +4 -12
- package/client/plugos/syscalls/service_registry.ts +1 -4
- package/client/plugos/syscalls/shell.ts +2 -5
- package/client/plugos/syscalls/space.ts +1 -1
- package/client/plugos/syscalls/sync.ts +69 -60
- package/client/plugos/syscalls/system.ts +2 -3
- package/client/plugos/system.ts +6 -12
- package/client/plugos/worker_runtime.ts +12 -33
- package/client/space_lua/aggregates.ts +782 -0
- package/client/space_lua/ast.ts +42 -8
- package/client/space_lua/ast_narrow.ts +4 -2
- package/client/space_lua/eval.ts +886 -575
- package/client/space_lua/labels.ts +7 -12
- package/client/space_lua/liq_null.ts +6 -0
- package/client/space_lua/numeric.ts +5 -8
- package/client/space_lua/parse.ts +346 -120
- package/client/space_lua/query_collection.ts +926 -82
- package/client/space_lua/query_env.ts +26 -0
- package/client/space_lua/render_lua_markdown.ts +369 -0
- package/client/space_lua/rp.ts +5 -4
- package/client/space_lua/runtime.ts +288 -155
- package/client/space_lua/stdlib/format.ts +53 -39
- package/client/space_lua/stdlib/js.ts +3 -7
- package/client/space_lua/stdlib/load.ts +1 -3
- package/client/space_lua/stdlib/math.ts +84 -58
- package/client/space_lua/stdlib/net.ts +27 -17
- package/client/space_lua/stdlib/os.ts +81 -85
- package/client/space_lua/stdlib/pattern.ts +695 -0
- package/client/space_lua/stdlib/prng.ts +148 -0
- package/client/space_lua/stdlib/space_lua.ts +17 -23
- package/client/space_lua/stdlib/string.ts +102 -190
- package/client/space_lua/stdlib/string_pack.ts +490 -0
- package/client/space_lua/stdlib/table.ts +76 -16
- package/client/space_lua/stdlib.ts +53 -39
- package/client/space_lua/tonumber.ts +82 -42
- package/client/space_lua/util.ts +53 -15
- package/dist/plug-compile.js +55 -98
- package/package.json +27 -20
- package/plug-api/constants.ts +0 -32
- package/plug-api/lib/async.ts +20 -7
- package/plug-api/lib/crypto.ts +16 -17
- package/plug-api/lib/dates.ts +15 -7
- package/plug-api/lib/json.ts +11 -5
- package/plug-api/lib/limited_map.ts +1 -1
- package/plug-api/lib/native_fetch.ts +2 -0
- package/plug-api/lib/ref.ts +23 -23
- package/plug-api/lib/resolve.ts +7 -11
- package/plug-api/lib/tags.ts +13 -4
- package/plug-api/lib/transclusion.ts +10 -21
- package/plug-api/lib/tree.ts +165 -45
- package/plug-api/lib/yaml.ts +35 -25
- package/plug-api/syscalls/asset.ts +1 -1
- package/plug-api/syscalls/config.ts +1 -4
- package/plug-api/syscalls/editor.ts +15 -15
- package/plug-api/syscalls/jsonschema.ts +1 -3
- package/plug-api/syscalls/lua.ts +3 -9
- package/plug-api/syscalls/mq.ts +1 -4
- package/plug-api/syscalls/shell.ts +4 -1
- package/plug-api/syscalls/space.ts +3 -10
- package/plug-api/syscalls/system.ts +1 -4
- package/plug-api/syscalls/yaml.ts +2 -6
- package/plug-api/system_mock.ts +0 -1
- package/plug-api/types/client.ts +16 -1
- package/plug-api/types/event.ts +6 -4
- package/plug-api/types/manifest.ts +8 -9
- package/plugs/builtin_plugs.ts +2 -2
- 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
|
+
}
|