@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.
- package/README.md +4 -5
- package/client/asset_bundle/bundle.ts +3 -9
- package/client/data/datastore.ts +4 -5
- package/client/markdown_parser/constants.ts +3 -2
- package/client/plugos/hooks/code_widget.ts +3 -5
- package/client/plugos/hooks/command.ts +8 -8
- package/client/plugos/hooks/document_editor.ts +10 -12
- package/client/plugos/hooks/event.ts +33 -36
- package/client/plugos/hooks/mq.ts +17 -17
- package/client/plugos/hooks/plug_namespace.ts +3 -5
- package/client/plugos/hooks/slash_command.ts +12 -27
- package/client/plugos/hooks/syscall.ts +3 -3
- package/client/plugos/manifest_cache.ts +22 -15
- package/client/plugos/plug.ts +2 -5
- package/client/plugos/plug_compile.ts +67 -65
- package/client/plugos/protocol.ts +28 -28
- package/client/plugos/proxy_fetch.ts +7 -6
- package/client/plugos/sandboxes/worker_sandbox.ts +16 -15
- 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 +63 -60
- package/client/plugos/syscalls/event.ts +9 -12
- package/client/plugos/syscalls/fetch.ts +30 -22
- 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 +3 -11
- package/client/plugos/syscalls/service_registry.ts +1 -4
- package/client/plugos/syscalls/shell.ts +2 -5
- package/client/plugos/syscalls/sync.ts +69 -60
- package/client/plugos/syscalls/system.ts +2 -3
- package/client/plugos/system.ts +4 -10
- package/client/plugos/worker_runtime.ts +4 -3
- package/client/space_lua/aggregates.ts +632 -59
- package/client/space_lua/ast.ts +21 -9
- package/client/space_lua/ast_narrow.ts +4 -2
- package/client/space_lua/eval.ts +842 -536
- package/client/space_lua/labels.ts +6 -11
- package/client/space_lua/liq_null.ts +6 -0
- package/client/space_lua/numeric.ts +5 -8
- package/client/space_lua/parse.ts +290 -169
- package/client/space_lua/query_collection.ts +213 -149
- package/client/space_lua/render_lua_markdown.ts +369 -0
- package/client/space_lua/rp.ts +5 -4
- package/client/space_lua/runtime.ts +245 -142
- package/client/space_lua/stdlib/format.ts +34 -20
- 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 +15 -14
- package/client/space_lua/stdlib/net.ts +25 -15
- package/client/space_lua/stdlib/os.ts +76 -85
- package/client/space_lua/stdlib/pattern.ts +28 -35
- package/client/space_lua/stdlib/prng.ts +15 -12
- package/client/space_lua/stdlib/space_lua.ts +16 -17
- package/client/space_lua/stdlib/string.ts +7 -17
- package/client/space_lua/stdlib/string_pack.ts +23 -19
- package/client/space_lua/stdlib/table.ts +5 -9
- package/client/space_lua/stdlib.ts +20 -30
- package/client/space_lua/tonumber.ts +79 -40
- package/client/space_lua/util.ts +14 -10
- package/dist/plug-compile.js +44 -41
- package/package.json +24 -22
- package/plug-api/lib/async.ts +19 -6
- package/plug-api/lib/crypto.ts +5 -6
- package/plug-api/lib/dates.ts +15 -7
- package/plug-api/lib/json.ts +10 -4
- package/plug-api/lib/ref.ts +18 -18
- package/plug-api/lib/resolve.ts +7 -11
- package/plug-api/lib/tags.ts +13 -4
- package/plug-api/lib/transclusion.ts +6 -17
- package/plug-api/lib/tree.ts +115 -43
- package/plug-api/lib/yaml.ts +25 -15
- package/plug-api/syscalls/asset.ts +1 -1
- package/plug-api/syscalls/config.ts +1 -4
- package/plug-api/syscalls/editor.ts +14 -14
- 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/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/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
|
|
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: "
|
|
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
|
|
56
|
-
|
|
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
|
-
|
|
60
|
-
name: "
|
|
61
|
-
description: "
|
|
62
|
-
initialize: aggFn((_sf) =>
|
|
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
|
|
65
|
-
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
94
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
641
|
+
} else if (spec.initialize && spec.iterate) {
|
|
642
|
+
candidate = spec as AggregateSpec;
|
|
152
643
|
}
|
|
644
|
+
if (candidate) return candidate;
|
|
645
|
+
break;
|
|
153
646
|
}
|
|
154
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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;
|