@silverbulletmd/silverbullet 2.4.1 → 2.5.3
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 +20 -4
- package/client/markdown_parser/constants.ts +2 -2
- package/client/plugos/hooks/code_widget.ts +0 -3
- package/client/plugos/hooks/document_editor.ts +0 -3
- package/client/plugos/hooks/event.ts +1 -1
- package/client/plugos/hooks/mq.ts +1 -1
- package/client/plugos/hooks/plug_namespace.ts +0 -3
- package/client/plugos/hooks/slash_command.ts +2 -2
- package/client/plugos/plug.ts +0 -1
- package/client/plugos/plug_compile.ts +28 -29
- package/client/plugos/proxy_fetch.ts +1 -1
- package/client/plugos/sandboxes/web_worker_sandbox.ts +1 -1
- package/client/plugos/sandboxes/worker_sandbox.ts +2 -3
- package/client/plugos/syscalls/editor.ts +12 -12
- package/client/plugos/syscalls/fetch.ts +1 -1
- package/client/plugos/syscalls/jsonschema.ts +1 -1
- package/client/plugos/syscalls/mq.ts +1 -1
- package/client/plugos/syscalls/space.ts +1 -1
- package/client/plugos/system.ts +2 -2
- package/client/plugos/worker_runtime.ts +8 -29
- package/client/space_lua/aggregates.ts +209 -0
- package/client/space_lua/ast.ts +24 -2
- package/client/space_lua/eval.ts +58 -53
- package/client/space_lua/labels.ts +1 -1
- package/client/space_lua/parse.ts +117 -12
- package/client/space_lua/query_collection.ts +850 -70
- package/client/space_lua/query_env.ts +26 -0
- package/client/space_lua/runtime.ts +47 -17
- package/client/space_lua/stdlib/format.ts +19 -19
- package/client/space_lua/stdlib/math.ts +73 -48
- package/client/space_lua/stdlib/net.ts +2 -2
- package/client/space_lua/stdlib/os.ts +5 -0
- package/client/space_lua/stdlib/pattern.ts +702 -0
- package/client/space_lua/stdlib/prng.ts +145 -0
- package/client/space_lua/stdlib/space_lua.ts +3 -8
- package/client/space_lua/stdlib/string.ts +103 -181
- package/client/space_lua/stdlib/string_pack.ts +486 -0
- package/client/space_lua/stdlib/table.ts +73 -9
- package/client/space_lua/stdlib.ts +38 -14
- package/client/space_lua/tonumber.ts +3 -2
- package/client/space_lua/util.ts +43 -9
- package/dist/plug-compile.js +23 -69
- package/dist/worker_runtime_bundle.js +233 -0
- package/package.json +16 -11
- package/plug-api/constants.ts +0 -32
- package/plug-api/lib/async.ts +2 -2
- package/plug-api/lib/crypto.ts +11 -11
- package/plug-api/lib/json.ts +1 -1
- package/plug-api/lib/limited_map.ts +1 -1
- package/plug-api/lib/native_fetch.ts +2 -0
- package/plug-api/lib/ref.ts +5 -5
- package/plug-api/lib/transclusion.ts +5 -5
- package/plug-api/lib/tree.ts +50 -2
- package/plug-api/lib/yaml.ts +10 -10
- package/plug-api/syscalls/editor.ts +1 -1
- package/plug-api/system_mock.ts +0 -1
- package/client/plugos/sandboxes/deno_worker_sandbox.ts +0 -6
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import type {
|
|
2
|
+
LuaBinaryExpression,
|
|
3
|
+
LuaDynamicField,
|
|
4
|
+
LuaExpression,
|
|
5
|
+
LuaExpressionField,
|
|
6
|
+
LuaFilteredCallExpression,
|
|
7
|
+
LuaFunctionBody,
|
|
8
|
+
LuaFunctionCallExpression,
|
|
9
|
+
LuaParenthesizedExpression,
|
|
10
|
+
LuaPropField,
|
|
11
|
+
LuaUnaryExpression,
|
|
12
|
+
} from "./ast.ts";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
jsToLuaValue,
|
|
16
|
+
luaCall,
|
|
17
|
+
LuaEnv,
|
|
18
|
+
LuaFunction,
|
|
19
|
+
luaGet,
|
|
20
|
+
luaKeys,
|
|
21
|
+
LuaRuntimeError,
|
|
22
|
+
LuaStackFrame,
|
|
23
|
+
LuaTable,
|
|
24
|
+
luaTruthy,
|
|
25
|
+
type LuaValue,
|
|
26
|
+
singleResult,
|
|
27
|
+
} from "./runtime.ts";
|
|
28
|
+
import { evalExpression, luaOp } from "./eval.ts";
|
|
29
|
+
import { asyncMergeSort } from "./util.ts";
|
|
5
30
|
import type { DataStore } from "../data/datastore.ts";
|
|
6
31
|
import type { KvPrimitives } from "../data/kv_primitives.ts";
|
|
7
32
|
|
|
@@ -9,29 +34,111 @@ import type { QueryCollationConfig } from "../../plug-api/types/config.ts";
|
|
|
9
34
|
|
|
10
35
|
import type { KvKey } from "../../plug-api/types/datastore.ts";
|
|
11
36
|
|
|
12
|
-
|
|
37
|
+
import { executeAggregate, getAggregateSpec } from "./aggregates.ts";
|
|
38
|
+
|
|
39
|
+
// Sentinel value representing SQL NULL in query results.
|
|
40
|
+
export const LIQ_NULL = Symbol.for("silverbullet.sqlNull");
|
|
41
|
+
|
|
42
|
+
export function isSqlNull(v: any): boolean {
|
|
43
|
+
return v === LIQ_NULL;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Build environment for post-`group by` clauses. Injects `key` and `group`
|
|
47
|
+
// as top-level variables. Unpacks first group item fields and group-by key
|
|
48
|
+
// fields as locals so that bare field access works after grouping.
|
|
49
|
+
function buildGroupItemEnv(
|
|
50
|
+
objectVariable: string | undefined,
|
|
51
|
+
groupByNames: string[] | undefined,
|
|
52
|
+
item: any,
|
|
53
|
+
parentGlobals: LuaEnv,
|
|
54
|
+
sf: LuaStackFrame,
|
|
55
|
+
): LuaEnv {
|
|
56
|
+
const itemEnv = new LuaEnv(parentGlobals);
|
|
57
|
+
itemEnv.setLocal("_", item);
|
|
58
|
+
if (item instanceof LuaTable) {
|
|
59
|
+
const keyVal = item.rawGet("key");
|
|
60
|
+
const groupVal = item.rawGet("group");
|
|
61
|
+
const firstItem = (groupVal instanceof LuaTable)
|
|
62
|
+
? groupVal.rawGet(1)
|
|
63
|
+
: undefined;
|
|
64
|
+
|
|
65
|
+
if (firstItem) {
|
|
66
|
+
for (const k of luaKeys(firstItem)) {
|
|
67
|
+
if (typeof k !== "string") continue;
|
|
68
|
+
itemEnv.setLocal(k, luaGet(firstItem, k, sf.astCtx ?? null, sf));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (objectVariable) {
|
|
73
|
+
itemEnv.setLocal(objectVariable, firstItem ?? item);
|
|
74
|
+
}
|
|
75
|
+
if (keyVal !== undefined) {
|
|
76
|
+
itemEnv.setLocal("key", keyVal);
|
|
77
|
+
}
|
|
78
|
+
if (groupVal !== undefined) {
|
|
79
|
+
itemEnv.setLocal("group", groupVal);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (keyVal instanceof LuaTable) {
|
|
83
|
+
for (const k of luaKeys(keyVal)) {
|
|
84
|
+
if (typeof k !== "string") continue;
|
|
85
|
+
itemEnv.setLocal(k, luaGet(keyVal, k, sf.astCtx ?? null, sf));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// For single-key group by, bind the key name as a local
|
|
89
|
+
if (
|
|
90
|
+
!(keyVal instanceof LuaTable) &&
|
|
91
|
+
groupByNames &&
|
|
92
|
+
groupByNames.length === 1
|
|
93
|
+
) {
|
|
94
|
+
itemEnv.setLocal(groupByNames[0], keyVal);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return itemEnv;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build an environment for evaluating per-item expressions in queries.
|
|
102
|
+
*
|
|
103
|
+
* When `objectVariable` is NOT set: item fields are unpacked as locals
|
|
104
|
+
* and shadow any globals. The item is also bound to `_`.
|
|
105
|
+
*
|
|
106
|
+
* When `objectVariable` IS set: only the object variable is bound.
|
|
107
|
+
* Item fields are NOT unpacked - the user opted into qualified access
|
|
108
|
+
* (e.g. `p.name`) and bare names must resolve from the parent env.
|
|
109
|
+
*/
|
|
110
|
+
function buildItemEnvLocal(
|
|
13
111
|
objectVariable: string | undefined,
|
|
14
112
|
item: any,
|
|
15
113
|
env: LuaEnv,
|
|
16
114
|
sf: LuaStackFrame,
|
|
17
115
|
): LuaEnv {
|
|
18
116
|
const itemEnv = new LuaEnv(env);
|
|
19
|
-
if (
|
|
20
|
-
// Inject all item keys as variables
|
|
21
|
-
for (const key of luaKeys(item)) {
|
|
22
|
-
itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
|
|
23
|
-
}
|
|
24
|
-
// As well as _
|
|
25
|
-
itemEnv.setLocal("_", item);
|
|
26
|
-
} else {
|
|
117
|
+
if (objectVariable) {
|
|
27
118
|
itemEnv.setLocal(objectVariable, item);
|
|
119
|
+
} else {
|
|
120
|
+
// Unpack item fields as locals so unqualified access works
|
|
121
|
+
itemEnv.setLocal("_", item);
|
|
122
|
+
if (item instanceof LuaTable) {
|
|
123
|
+
for (const key of luaKeys(item)) {
|
|
124
|
+
itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
|
|
125
|
+
}
|
|
126
|
+
} else if (typeof item === "object" && item !== null) {
|
|
127
|
+
for (const key of luaKeys(item)) {
|
|
128
|
+
itemEnv.setLocal(key, luaGet(item, key, sf.astCtx ?? null, sf));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
28
131
|
}
|
|
29
132
|
return itemEnv;
|
|
30
133
|
}
|
|
31
134
|
|
|
135
|
+
export { buildItemEnvLocal as buildItemEnv };
|
|
136
|
+
|
|
32
137
|
export type LuaOrderBy = {
|
|
33
138
|
expr: LuaExpression;
|
|
34
139
|
desc: boolean;
|
|
140
|
+
nulls?: "first" | "last";
|
|
141
|
+
using?: string | LuaFunctionBody;
|
|
35
142
|
};
|
|
36
143
|
|
|
37
144
|
/**
|
|
@@ -51,6 +158,10 @@ export type LuaCollectionQuery = {
|
|
|
51
158
|
offset?: number;
|
|
52
159
|
// Whether to return only distinct values
|
|
53
160
|
distinct?: boolean;
|
|
161
|
+
// The group by expressions evaluated with Lua
|
|
162
|
+
groupBy?: LuaExpression[];
|
|
163
|
+
// The having expression evaluated with Lua
|
|
164
|
+
having?: LuaExpression;
|
|
54
165
|
};
|
|
55
166
|
|
|
56
167
|
export interface LuaQueryCollection {
|
|
@@ -67,7 +178,6 @@ export interface LuaQueryCollection {
|
|
|
67
178
|
export class ArrayQueryCollection<T> implements LuaQueryCollection {
|
|
68
179
|
constructor(private readonly array: T[]) {
|
|
69
180
|
}
|
|
70
|
-
|
|
71
181
|
query(
|
|
72
182
|
query: LuaCollectionQuery,
|
|
73
183
|
env: LuaEnv,
|
|
@@ -78,9 +188,443 @@ export class ArrayQueryCollection<T> implements LuaQueryCollection {
|
|
|
78
188
|
}
|
|
79
189
|
}
|
|
80
190
|
|
|
191
|
+
// Wrap any object, array, or LuaQueryCollection as a queryable collection
|
|
192
|
+
export function toCollection(obj: any): LuaQueryCollection {
|
|
193
|
+
if (
|
|
194
|
+
obj instanceof ArrayQueryCollection ||
|
|
195
|
+
obj instanceof DataStoreQueryCollection
|
|
196
|
+
) {
|
|
197
|
+
return obj;
|
|
198
|
+
}
|
|
199
|
+
if (Array.isArray(obj)) {
|
|
200
|
+
return new ArrayQueryCollection(obj);
|
|
201
|
+
}
|
|
202
|
+
return new ArrayQueryCollection([obj]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function containsAggregate(expr: LuaExpression): boolean {
|
|
206
|
+
switch (expr.type) {
|
|
207
|
+
case "FilteredCall": {
|
|
208
|
+
const fc = (expr as LuaFilteredCallExpression).call;
|
|
209
|
+
if (fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return containsAggregate(fc) ||
|
|
213
|
+
containsAggregate((expr as LuaFilteredCallExpression).filter);
|
|
214
|
+
}
|
|
215
|
+
case "FunctionCall": {
|
|
216
|
+
const fc = expr as LuaFunctionCallExpression;
|
|
217
|
+
if (fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name)) {
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return fc.args.some(containsAggregate);
|
|
221
|
+
}
|
|
222
|
+
case "Binary": {
|
|
223
|
+
const bin = expr as LuaBinaryExpression;
|
|
224
|
+
return containsAggregate(bin.left) || containsAggregate(bin.right);
|
|
225
|
+
}
|
|
226
|
+
case "Unary": {
|
|
227
|
+
const un = expr as LuaUnaryExpression;
|
|
228
|
+
return containsAggregate(un.argument);
|
|
229
|
+
}
|
|
230
|
+
case "Parenthesized": {
|
|
231
|
+
const p = expr as LuaParenthesizedExpression;
|
|
232
|
+
return containsAggregate(p.expression);
|
|
233
|
+
}
|
|
234
|
+
case "TableConstructor":
|
|
235
|
+
return expr.fields.some((f) => {
|
|
236
|
+
switch (f.type) {
|
|
237
|
+
case "PropField":
|
|
238
|
+
return containsAggregate((f as LuaPropField).value);
|
|
239
|
+
case "DynamicField": {
|
|
240
|
+
const df = f as LuaDynamicField;
|
|
241
|
+
return containsAggregate(df.key) || containsAggregate(df.value);
|
|
242
|
+
}
|
|
243
|
+
case "ExpressionField":
|
|
244
|
+
return containsAggregate((f as LuaExpressionField).value);
|
|
245
|
+
default:
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
default:
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Evaluate an expression in aggregate-aware mode.
|
|
256
|
+
*
|
|
257
|
+
* When a FunctionCall matches a registered aggregate name, the aggregate
|
|
258
|
+
* protocol is executed instead of normal call semantics. All other
|
|
259
|
+
* expressions fall through to normal evalExpression.
|
|
260
|
+
*/
|
|
261
|
+
export async function evalExpressionWithAggregates(
|
|
262
|
+
expr: LuaExpression,
|
|
263
|
+
env: LuaEnv,
|
|
264
|
+
sf: LuaStackFrame,
|
|
265
|
+
groupItems: LuaTable,
|
|
266
|
+
objectVariable: string | undefined,
|
|
267
|
+
outerEnv: LuaEnv,
|
|
268
|
+
): Promise<LuaValue> {
|
|
269
|
+
if (!containsAggregate(expr)) {
|
|
270
|
+
return evalExpression(expr, env, sf);
|
|
271
|
+
}
|
|
272
|
+
const recurse = (e: LuaExpression) =>
|
|
273
|
+
evalExpressionWithAggregates(
|
|
274
|
+
e,
|
|
275
|
+
env,
|
|
276
|
+
sf,
|
|
277
|
+
groupItems,
|
|
278
|
+
objectVariable,
|
|
279
|
+
outerEnv,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
if (expr.type === "FilteredCall") {
|
|
283
|
+
const filtered = expr as LuaFilteredCallExpression;
|
|
284
|
+
const fc = filtered.call;
|
|
285
|
+
if (fc.prefix.type === "Variable") {
|
|
286
|
+
const name = fc.prefix.name;
|
|
287
|
+
const spec = getAggregateSpec(name);
|
|
288
|
+
if (spec) {
|
|
289
|
+
const valueExpr = fc.args.length > 0 ? fc.args[0] : null;
|
|
290
|
+
return executeAggregate(
|
|
291
|
+
spec,
|
|
292
|
+
groupItems,
|
|
293
|
+
valueExpr,
|
|
294
|
+
objectVariable,
|
|
295
|
+
outerEnv,
|
|
296
|
+
sf,
|
|
297
|
+
evalExpression,
|
|
298
|
+
filtered.filter,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return evalExpression(expr, env, sf);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (expr.type === "FunctionCall") {
|
|
307
|
+
const fc = expr as LuaFunctionCallExpression;
|
|
308
|
+
if (fc.prefix.type === "Variable") {
|
|
309
|
+
const name = fc.prefix.name;
|
|
310
|
+
const spec = getAggregateSpec(name);
|
|
311
|
+
if (spec) {
|
|
312
|
+
const valueExpr = fc.args.length > 0 ? fc.args[0] : null;
|
|
313
|
+
return executeAggregate(
|
|
314
|
+
spec,
|
|
315
|
+
groupItems,
|
|
316
|
+
valueExpr,
|
|
317
|
+
objectVariable,
|
|
318
|
+
outerEnv,
|
|
319
|
+
sf,
|
|
320
|
+
evalExpression,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (expr.type === "TableConstructor") {
|
|
326
|
+
const table = new LuaTable();
|
|
327
|
+
let nextArrayIndex = 1;
|
|
328
|
+
for (const field of expr.fields) {
|
|
329
|
+
switch (field.type) {
|
|
330
|
+
case "PropField": {
|
|
331
|
+
const pf = field as LuaPropField;
|
|
332
|
+
const value = await recurse(pf.value);
|
|
333
|
+
table.set(pf.key, value, sf);
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case "DynamicField": {
|
|
337
|
+
const df = field as LuaDynamicField;
|
|
338
|
+
const key = await evalExpression(df.key, env, sf);
|
|
339
|
+
const value = await recurse(df.value);
|
|
340
|
+
table.set(key, value, sf);
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "ExpressionField": {
|
|
344
|
+
const ef = field as LuaExpressionField;
|
|
345
|
+
const value = await recurse(ef.value);
|
|
346
|
+
table.rawSetArrayIndex(nextArrayIndex, value);
|
|
347
|
+
nextArrayIndex++;
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return table;
|
|
353
|
+
}
|
|
354
|
+
if (expr.type === "Binary") {
|
|
355
|
+
const bin = expr as LuaBinaryExpression;
|
|
356
|
+
if (bin.operator === "and") {
|
|
357
|
+
const left = singleResult(await recurse(bin.left));
|
|
358
|
+
if (!luaTruthy(left)) return left;
|
|
359
|
+
return singleResult(await recurse(bin.right));
|
|
360
|
+
}
|
|
361
|
+
if (bin.operator === "or") {
|
|
362
|
+
const left = singleResult(await recurse(bin.left));
|
|
363
|
+
if (luaTruthy(left)) return left;
|
|
364
|
+
return singleResult(await recurse(bin.right));
|
|
365
|
+
}
|
|
366
|
+
const left = singleResult(await recurse(bin.left));
|
|
367
|
+
const right = singleResult(await recurse(bin.right));
|
|
368
|
+
return luaOp(
|
|
369
|
+
bin.operator,
|
|
370
|
+
left,
|
|
371
|
+
right,
|
|
372
|
+
undefined,
|
|
373
|
+
undefined,
|
|
374
|
+
expr.ctx,
|
|
375
|
+
sf,
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
if (expr.type === "Unary") {
|
|
379
|
+
const un = expr as LuaUnaryExpression;
|
|
380
|
+
const arg = singleResult(await recurse(un.argument));
|
|
381
|
+
switch (un.operator) {
|
|
382
|
+
case "-":
|
|
383
|
+
return typeof arg === "number" ? -arg : luaOp(
|
|
384
|
+
"-",
|
|
385
|
+
0,
|
|
386
|
+
arg,
|
|
387
|
+
undefined,
|
|
388
|
+
undefined,
|
|
389
|
+
expr.ctx,
|
|
390
|
+
sf,
|
|
391
|
+
);
|
|
392
|
+
case "not":
|
|
393
|
+
return !luaTruthy(arg);
|
|
394
|
+
case "#":
|
|
395
|
+
return evalExpression(expr, env, sf);
|
|
396
|
+
case "~":
|
|
397
|
+
if (typeof arg === "number") return ~arg;
|
|
398
|
+
throw new Error("attempt to perform bitwise operation on a non-number");
|
|
399
|
+
default:
|
|
400
|
+
return evalExpression(expr, env, sf);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (expr.type === "Parenthesized") {
|
|
404
|
+
const paren = expr as LuaParenthesizedExpression;
|
|
405
|
+
return singleResult(await recurse(paren.expression));
|
|
406
|
+
}
|
|
407
|
+
return evalExpression(expr, env, sf);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Collect the canonical key order from an array of select results.
|
|
412
|
+
* Finds the first LuaTable that has the maximum number of string keys
|
|
413
|
+
* and returns its keys in insertion order. This represents the
|
|
414
|
+
* "complete" column set in the order the user wrote in `select { ... }`.
|
|
415
|
+
*/
|
|
416
|
+
function collectCanonicalKeyOrder(results: any[]): string[] | null {
|
|
417
|
+
let best: string[] | null = null;
|
|
418
|
+
for (const item of results) {
|
|
419
|
+
if (item instanceof LuaTable) {
|
|
420
|
+
const keys = luaKeys(item).filter(
|
|
421
|
+
(k): k is string => typeof k === "string",
|
|
422
|
+
);
|
|
423
|
+
if (!best || keys.length > best.length) {
|
|
424
|
+
best = keys;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return best;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function normalizeSelectResults(results: any[]): any[] {
|
|
432
|
+
if (results.length === 0) return results;
|
|
433
|
+
const canonicalKeys = collectCanonicalKeyOrder(results);
|
|
434
|
+
if (!canonicalKeys || canonicalKeys.length === 0) return results;
|
|
435
|
+
for (let i = 0; i < results.length; i++) {
|
|
436
|
+
const item = results[i];
|
|
437
|
+
if (!(item instanceof LuaTable)) continue;
|
|
438
|
+
let needsRebuild = false;
|
|
439
|
+
for (const k of canonicalKeys) {
|
|
440
|
+
const v = item.rawGet(k);
|
|
441
|
+
if (v === undefined || v === null) {
|
|
442
|
+
needsRebuild = true;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (!needsRebuild) continue;
|
|
447
|
+
const rebuilt = new LuaTable();
|
|
448
|
+
for (const k of canonicalKeys) {
|
|
449
|
+
const v = item.rawGet(k);
|
|
450
|
+
rebuilt.rawSet(
|
|
451
|
+
k,
|
|
452
|
+
(v === undefined || v === null) ? LIQ_NULL : v,
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
for (const k of luaKeys(item)) {
|
|
456
|
+
if (typeof k !== "string") {
|
|
457
|
+
rebuilt.rawSet(k, item.rawGet(k));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
results[i] = rebuilt;
|
|
461
|
+
}
|
|
462
|
+
return results;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function resolveUsing(
|
|
466
|
+
using: string | LuaFunctionBody | undefined,
|
|
467
|
+
env: LuaEnv,
|
|
468
|
+
_sf: LuaStackFrame,
|
|
469
|
+
): LuaValue | null {
|
|
470
|
+
if (using === undefined) return null;
|
|
471
|
+
if (typeof using === "string") {
|
|
472
|
+
return env.get(using) ?? null;
|
|
473
|
+
}
|
|
474
|
+
return new LuaFunction(using, env);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Compare values using a custom comparator with SWO violation detection
|
|
478
|
+
async function usingCompare(
|
|
479
|
+
luaCmp: LuaValue,
|
|
480
|
+
aVal: any,
|
|
481
|
+
bVal: any,
|
|
482
|
+
originalA: number,
|
|
483
|
+
originalB: number,
|
|
484
|
+
desc: boolean,
|
|
485
|
+
sf: LuaStackFrame,
|
|
486
|
+
violated: boolean[],
|
|
487
|
+
keyIdx: number,
|
|
488
|
+
): Promise<number> {
|
|
489
|
+
const res = luaTruthy(
|
|
490
|
+
singleResult(
|
|
491
|
+
await luaCall(luaCmp, [aVal, bVal], sf.astCtx ?? {}, sf),
|
|
492
|
+
),
|
|
493
|
+
);
|
|
494
|
+
const reverseRes = luaTruthy(
|
|
495
|
+
singleResult(
|
|
496
|
+
await luaCall(luaCmp, [bVal, aVal], sf.astCtx ?? {}, sf),
|
|
497
|
+
),
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// both true means SWO violation
|
|
501
|
+
if (res && reverseRes) {
|
|
502
|
+
violated[keyIdx] = true;
|
|
503
|
+
return originalA < originalB ? -1 : 1;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (res) return desc ? 1 : -1;
|
|
507
|
+
if (reverseRes) return desc ? -1 : 1;
|
|
508
|
+
return 0;
|
|
509
|
+
}
|
|
510
|
+
|
|
81
511
|
/**
|
|
82
|
-
*
|
|
512
|
+
* Pre-compute all sort keys for each result item (Schwartzian transform)
|
|
513
|
+
* and evaluate each `order by` expression exactly once per item
|
|
83
514
|
*/
|
|
515
|
+
async function precomputeSortKeys(
|
|
516
|
+
results: any[],
|
|
517
|
+
orderBy: LuaOrderBy[],
|
|
518
|
+
mkEnv: (
|
|
519
|
+
ov: string | undefined,
|
|
520
|
+
item: any,
|
|
521
|
+
e: LuaEnv,
|
|
522
|
+
s: LuaStackFrame,
|
|
523
|
+
) => LuaEnv,
|
|
524
|
+
objectVariable: string | undefined,
|
|
525
|
+
env: LuaEnv,
|
|
526
|
+
sf: LuaStackFrame,
|
|
527
|
+
grouped: boolean,
|
|
528
|
+
selectResults: any[] | undefined,
|
|
529
|
+
): Promise<any[][]> {
|
|
530
|
+
const allKeys: any[][] = new Array(results.length);
|
|
531
|
+
for (let i = 0; i < results.length; i++) {
|
|
532
|
+
const item = results[i];
|
|
533
|
+
const itemEnv = mkEnv(objectVariable, item, env, sf);
|
|
534
|
+
if (selectResults) {
|
|
535
|
+
const row = selectResults[i];
|
|
536
|
+
if (row) {
|
|
537
|
+
for (const k of luaKeys(row)) {
|
|
538
|
+
const v = luaGet(row, k, sf.astCtx ?? null, sf);
|
|
539
|
+
itemEnv.setLocal(k, isSqlNull(v) ? null : v);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const keys: any[] = new Array(orderBy.length);
|
|
544
|
+
for (let j = 0; j < orderBy.length; j++) {
|
|
545
|
+
if (grouped) {
|
|
546
|
+
const groupTable = (item as LuaTable).rawGet("group");
|
|
547
|
+
keys[j] = await evalExpressionWithAggregates(
|
|
548
|
+
orderBy[j].expr,
|
|
549
|
+
itemEnv,
|
|
550
|
+
sf,
|
|
551
|
+
groupTable,
|
|
552
|
+
objectVariable,
|
|
553
|
+
env,
|
|
554
|
+
);
|
|
555
|
+
} else {
|
|
556
|
+
keys[j] = await evalExpression(orderBy[j].expr, itemEnv, sf);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
allKeys[i] = keys;
|
|
560
|
+
}
|
|
561
|
+
return allKeys;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Compare two items by their pre-computed sort keys without Lua
|
|
566
|
+
* expressions evaluation.
|
|
567
|
+
*/
|
|
568
|
+
async function sortKeyCompare(
|
|
569
|
+
a: { val: any; idx: number },
|
|
570
|
+
b: { val: any; idx: number },
|
|
571
|
+
orderBy: LuaOrderBy[],
|
|
572
|
+
aKeys: any[],
|
|
573
|
+
bKeys: any[],
|
|
574
|
+
collation: QueryCollationConfig | undefined,
|
|
575
|
+
collator: Intl.Collator,
|
|
576
|
+
resolvedUsing: (LuaValue | null)[],
|
|
577
|
+
violated: boolean[],
|
|
578
|
+
sf: LuaStackFrame,
|
|
579
|
+
): Promise<number> {
|
|
580
|
+
for (let idx = 0; idx < orderBy.length; idx++) {
|
|
581
|
+
const { desc, nulls } = orderBy[idx];
|
|
582
|
+
const aVal = aKeys[idx];
|
|
583
|
+
const bVal = bKeys[idx];
|
|
584
|
+
|
|
585
|
+
// Handle nulls positioning
|
|
586
|
+
const aIsNull = aVal === null || aVal === undefined;
|
|
587
|
+
const bIsNull = bVal === null || bVal === undefined;
|
|
588
|
+
if (aIsNull || bIsNull) {
|
|
589
|
+
if (aIsNull && bIsNull) continue;
|
|
590
|
+
// Default: nulls last for asc, nulls first for desc
|
|
591
|
+
const nullsLast = nulls === "last" || (nulls === undefined && !desc);
|
|
592
|
+
if (aIsNull) return nullsLast ? 1 : -1;
|
|
593
|
+
return nullsLast ? -1 : 1;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
const usingFn = resolvedUsing[idx];
|
|
597
|
+
if (usingFn) {
|
|
598
|
+
const cmp = await usingCompare(
|
|
599
|
+
usingFn,
|
|
600
|
+
aVal,
|
|
601
|
+
bVal,
|
|
602
|
+
a.idx,
|
|
603
|
+
b.idx,
|
|
604
|
+
desc,
|
|
605
|
+
sf,
|
|
606
|
+
violated,
|
|
607
|
+
idx,
|
|
608
|
+
);
|
|
609
|
+
if (cmp !== 0) return cmp;
|
|
610
|
+
} else if (
|
|
611
|
+
collation?.enabled &&
|
|
612
|
+
typeof aVal === "string" &&
|
|
613
|
+
typeof bVal === "string"
|
|
614
|
+
) {
|
|
615
|
+
const order = collator.compare(aVal, bVal);
|
|
616
|
+
if (order !== 0) {
|
|
617
|
+
return desc ? -order : order;
|
|
618
|
+
}
|
|
619
|
+
} else if (aVal < bVal) {
|
|
620
|
+
return desc ? 1 : -1;
|
|
621
|
+
} else if (aVal > bVal) {
|
|
622
|
+
return desc ? -1 : 1;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return 0;
|
|
626
|
+
}
|
|
627
|
+
|
|
84
628
|
export async function applyQuery(
|
|
85
629
|
results: any[],
|
|
86
630
|
query: LuaCollectionQuery,
|
|
@@ -88,15 +632,16 @@ export async function applyQuery(
|
|
|
88
632
|
sf: LuaStackFrame,
|
|
89
633
|
collation?: QueryCollationConfig,
|
|
90
634
|
): Promise<any[]> {
|
|
91
|
-
// Shallow copy to avoid updating underlying data structures
|
|
92
635
|
results = results.slice();
|
|
93
|
-
|
|
94
|
-
// Filter results based on `where` clause first
|
|
95
636
|
if (query.where) {
|
|
96
637
|
const filteredResults = [];
|
|
97
638
|
for (const value of results) {
|
|
98
|
-
|
|
99
|
-
|
|
639
|
+
const itemEnv = buildItemEnvLocal(
|
|
640
|
+
query.objectVariable,
|
|
641
|
+
value,
|
|
642
|
+
env,
|
|
643
|
+
sf,
|
|
644
|
+
);
|
|
100
645
|
if (await evalExpression(query.where, itemEnv, sf)) {
|
|
101
646
|
filteredResults.push(value);
|
|
102
647
|
}
|
|
@@ -104,76 +649,281 @@ export async function applyQuery(
|
|
|
104
649
|
results = filteredResults;
|
|
105
650
|
}
|
|
106
651
|
|
|
107
|
-
|
|
652
|
+
const grouped = !!query.groupBy;
|
|
653
|
+
|
|
654
|
+
// Collect group-by key names for unpacking into the post-group environment.
|
|
655
|
+
let groupByNames: string[] | undefined;
|
|
656
|
+
|
|
657
|
+
if (query.groupBy) {
|
|
658
|
+
// Extract bare names from group-by expressions for local binding
|
|
659
|
+
groupByNames = query.groupBy.map((expr) => {
|
|
660
|
+
if (expr.type === "Variable") {
|
|
661
|
+
return expr.name;
|
|
662
|
+
}
|
|
663
|
+
if (expr.type === "PropertyAccess") {
|
|
664
|
+
return expr.property;
|
|
665
|
+
}
|
|
666
|
+
return undefined as unknown as string;
|
|
667
|
+
}).filter(Boolean);
|
|
668
|
+
|
|
669
|
+
const groups = new Map<
|
|
670
|
+
string,
|
|
671
|
+
{ key: any; items: any[] }
|
|
672
|
+
>();
|
|
673
|
+
const groupByExprs = query.groupBy as LuaExpression[];
|
|
674
|
+
|
|
675
|
+
for (const item of results) {
|
|
676
|
+
const itemEnv = buildItemEnvLocal(
|
|
677
|
+
query.objectVariable,
|
|
678
|
+
item,
|
|
679
|
+
env,
|
|
680
|
+
sf,
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
const keyParts: any[] = [];
|
|
684
|
+
const keyRecord: Record<string, any> = {};
|
|
685
|
+
|
|
686
|
+
for (const expr of groupByExprs) {
|
|
687
|
+
if (expr.type === "Variable") {
|
|
688
|
+
const v = await evalExpression(expr, itemEnv, sf);
|
|
689
|
+
keyParts.push(v);
|
|
690
|
+
keyRecord[expr.name] = v;
|
|
691
|
+
} else if (expr.type === "PropertyAccess") {
|
|
692
|
+
const v = await evalExpression(expr, itemEnv, sf);
|
|
693
|
+
keyParts.push(v);
|
|
694
|
+
keyRecord[expr.property] = v;
|
|
695
|
+
} else {
|
|
696
|
+
const v = await evalExpression(expr, itemEnv, sf);
|
|
697
|
+
keyParts.push(v);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const compositeKey = keyParts.length === 1
|
|
702
|
+
? generateKey(keyParts[0])
|
|
703
|
+
: JSON.stringify(keyParts.map(generateKey));
|
|
704
|
+
let entry = groups.get(compositeKey);
|
|
705
|
+
if (!entry) {
|
|
706
|
+
let keyVal: any;
|
|
707
|
+
if (keyParts.length === 1) {
|
|
708
|
+
keyVal = keyParts[0];
|
|
709
|
+
} else {
|
|
710
|
+
const kt = new LuaTable();
|
|
711
|
+
// Always populate array indices from keyParts
|
|
712
|
+
for (let i = 0; i < keyParts.length; i++) {
|
|
713
|
+
kt.rawSetArrayIndex(i + 1, keyParts[i]);
|
|
714
|
+
}
|
|
715
|
+
// Additionally set named fields for Variable/PropertyAccess exprs
|
|
716
|
+
for (const name in keyRecord) {
|
|
717
|
+
kt.rawSet(name, keyRecord[name]);
|
|
718
|
+
}
|
|
719
|
+
keyVal = kt;
|
|
720
|
+
}
|
|
721
|
+
entry = {
|
|
722
|
+
key: keyVal,
|
|
723
|
+
items: [],
|
|
724
|
+
};
|
|
725
|
+
groups.set(compositeKey, entry);
|
|
726
|
+
}
|
|
727
|
+
entry.items.push(item);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
results = [];
|
|
731
|
+
for (const { key, items } of groups.values()) {
|
|
732
|
+
const groupTable = new LuaTable();
|
|
733
|
+
for (let i = 0; i < items.length; i++) {
|
|
734
|
+
const item = items[i];
|
|
735
|
+
groupTable.rawSetArrayIndex(
|
|
736
|
+
i + 1,
|
|
737
|
+
(item instanceof LuaTable || typeof item !== "object" ||
|
|
738
|
+
item === null)
|
|
739
|
+
? item
|
|
740
|
+
: jsToLuaValue(item),
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
const row = new LuaTable();
|
|
744
|
+
row.rawSet("key", key);
|
|
745
|
+
row.rawSet("group", groupTable);
|
|
746
|
+
results.push(row);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (query.having) {
|
|
751
|
+
const filteredResults = [];
|
|
752
|
+
for (const value of results) {
|
|
753
|
+
let condResult;
|
|
754
|
+
if (grouped) {
|
|
755
|
+
const itemEnv = buildGroupItemEnv(
|
|
756
|
+
query.objectVariable,
|
|
757
|
+
groupByNames,
|
|
758
|
+
value,
|
|
759
|
+
env,
|
|
760
|
+
sf,
|
|
761
|
+
);
|
|
762
|
+
const groupTable = (value as LuaTable).rawGet("group");
|
|
763
|
+
condResult = await evalExpressionWithAggregates(
|
|
764
|
+
query.having,
|
|
765
|
+
itemEnv,
|
|
766
|
+
sf,
|
|
767
|
+
groupTable,
|
|
768
|
+
query.objectVariable,
|
|
769
|
+
env,
|
|
770
|
+
);
|
|
771
|
+
} else {
|
|
772
|
+
const itemEnv = buildItemEnvLocal(
|
|
773
|
+
query.objectVariable,
|
|
774
|
+
value,
|
|
775
|
+
env,
|
|
776
|
+
sf,
|
|
777
|
+
);
|
|
778
|
+
condResult = await evalExpression(query.having, itemEnv, sf);
|
|
779
|
+
}
|
|
780
|
+
if (condResult) {
|
|
781
|
+
filteredResults.push(value);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
results = filteredResults;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
const mkEnv = grouped
|
|
788
|
+
? (
|
|
789
|
+
ov: string | undefined,
|
|
790
|
+
item: any,
|
|
791
|
+
e: LuaEnv,
|
|
792
|
+
s: LuaStackFrame,
|
|
793
|
+
) => buildGroupItemEnv(ov, groupByNames, item, e, s)
|
|
794
|
+
: buildItemEnvLocal;
|
|
795
|
+
|
|
796
|
+
let selectResults: any[] | undefined;
|
|
797
|
+
|
|
798
|
+
if (grouped && query.select && query.orderBy) {
|
|
799
|
+
selectResults = [];
|
|
800
|
+
for (const item of results) {
|
|
801
|
+
const itemEnv = mkEnv(query.objectVariable, item, env, sf);
|
|
802
|
+
const groupTable = (item as LuaTable).rawGet("group");
|
|
803
|
+
const selected = await evalExpressionWithAggregates(
|
|
804
|
+
query.select,
|
|
805
|
+
itemEnv,
|
|
806
|
+
sf,
|
|
807
|
+
groupTable,
|
|
808
|
+
query.objectVariable,
|
|
809
|
+
env,
|
|
810
|
+
);
|
|
811
|
+
selectResults.push(selected);
|
|
812
|
+
}
|
|
813
|
+
selectResults = normalizeSelectResults(selectResults);
|
|
814
|
+
}
|
|
815
|
+
|
|
108
816
|
if (query.orderBy) {
|
|
109
|
-
// Retrieve from config API if not passed
|
|
110
817
|
if (collation === undefined) {
|
|
111
|
-
|
|
112
|
-
const config = globalThis.client.config; // HACK: Shouldn't be using client here directly
|
|
113
|
-
|
|
818
|
+
const config = globalThis.client.config;
|
|
114
819
|
collation = config.get("queryCollation", {});
|
|
115
820
|
}
|
|
116
|
-
// Both arguments are optional, so passing undefined is fine
|
|
117
821
|
const collator = Intl.Collator(collation?.locale, collation?.options);
|
|
118
822
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
823
|
+
const resolvedUsing: (LuaValue | null)[] = [];
|
|
824
|
+
const violated: boolean[] = [];
|
|
825
|
+
for (const ob of query.orderBy) {
|
|
826
|
+
resolvedUsing.push(resolveUsing(ob.using, env, sf));
|
|
827
|
+
violated.push(false);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// Decorate: pre-compute all sort keys once (Schwartzian transform)
|
|
831
|
+
const sortKeys = await precomputeSortKeys(
|
|
832
|
+
results,
|
|
833
|
+
query.orderBy,
|
|
834
|
+
mkEnv,
|
|
835
|
+
query.objectVariable,
|
|
836
|
+
env,
|
|
837
|
+
sf,
|
|
838
|
+
grouped,
|
|
839
|
+
selectResults,
|
|
840
|
+
);
|
|
841
|
+
|
|
842
|
+
// Tag each result with its original index for stable sorting
|
|
843
|
+
const tagged = results.map((val, idx) => ({ val, idx }));
|
|
844
|
+
|
|
845
|
+
// Sort: compare cached keys only, no Lua eval in comparator
|
|
846
|
+
await asyncMergeSort(tagged, (a, b) =>
|
|
847
|
+
sortKeyCompare(
|
|
848
|
+
a,
|
|
849
|
+
b,
|
|
850
|
+
query.orderBy!,
|
|
851
|
+
sortKeys[a.idx],
|
|
852
|
+
sortKeys[b.idx],
|
|
853
|
+
collation,
|
|
854
|
+
collator,
|
|
855
|
+
resolvedUsing,
|
|
856
|
+
violated,
|
|
857
|
+
sf,
|
|
858
|
+
));
|
|
859
|
+
|
|
860
|
+
// Check for SWO violations in comparators
|
|
861
|
+
for (let i = 0; i < violated.length; i++) {
|
|
862
|
+
if (violated[i]) {
|
|
863
|
+
throw new LuaRuntimeError(
|
|
864
|
+
`order by #${
|
|
865
|
+
i + 1
|
|
866
|
+
}: 'using' comparator violates strict weak ordering`,
|
|
867
|
+
sf,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
if (selectResults) {
|
|
873
|
+
const reorderedResults: any[] = new Array(tagged.length);
|
|
874
|
+
const reorderedSelect: any[] = new Array(tagged.length);
|
|
875
|
+
for (let i = 0; i < tagged.length; i++) {
|
|
876
|
+
reorderedResults[i] = tagged[i].val;
|
|
877
|
+
reorderedSelect[i] = selectResults[tagged[i].idx];
|
|
143
878
|
}
|
|
144
|
-
|
|
145
|
-
|
|
879
|
+
results = reorderedResults;
|
|
880
|
+
selectResults = reorderedSelect;
|
|
881
|
+
} else {
|
|
882
|
+
results = tagged.map((t) => t.val);
|
|
883
|
+
}
|
|
146
884
|
}
|
|
147
885
|
|
|
148
|
-
// Apply the select
|
|
149
886
|
if (query.select) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
newResult
|
|
887
|
+
if (selectResults) {
|
|
888
|
+
results = selectResults;
|
|
889
|
+
} else {
|
|
890
|
+
const newResult = [];
|
|
891
|
+
for (const item of results) {
|
|
892
|
+
const itemEnv = mkEnv(query.objectVariable, item, env, sf);
|
|
893
|
+
if (grouped) {
|
|
894
|
+
const groupTable = (item as LuaTable).rawGet("group");
|
|
895
|
+
newResult.push(
|
|
896
|
+
await evalExpressionWithAggregates(
|
|
897
|
+
query.select,
|
|
898
|
+
itemEnv,
|
|
899
|
+
sf,
|
|
900
|
+
groupTable,
|
|
901
|
+
query.objectVariable,
|
|
902
|
+
env,
|
|
903
|
+
),
|
|
904
|
+
);
|
|
905
|
+
} else {
|
|
906
|
+
newResult.push(await evalExpression(query.select, itemEnv, sf));
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
results = newResult;
|
|
154
910
|
}
|
|
155
|
-
results =
|
|
911
|
+
results = normalizeSelectResults(results);
|
|
156
912
|
}
|
|
157
913
|
|
|
158
|
-
// Apply distinct filter (after select to filter on selected values)
|
|
159
914
|
if (query.distinct) {
|
|
160
915
|
const seen = new Set();
|
|
161
916
|
const distinctResult = [];
|
|
162
|
-
|
|
163
917
|
for (const item of results) {
|
|
164
|
-
// For non-primitive values, we use a JSON string as the key for comparison
|
|
165
918
|
const key = generateKey(item);
|
|
166
|
-
|
|
167
919
|
if (!seen.has(key)) {
|
|
168
920
|
seen.add(key);
|
|
169
921
|
distinctResult.push(item);
|
|
170
922
|
}
|
|
171
923
|
}
|
|
172
|
-
|
|
173
924
|
results = distinctResult;
|
|
174
925
|
}
|
|
175
926
|
|
|
176
|
-
// Apply the limit and offset
|
|
177
927
|
if (query.limit !== undefined && query.offset !== undefined) {
|
|
178
928
|
results = results.slice(query.offset, query.offset + query.limit);
|
|
179
929
|
} else if (query.limit !== undefined) {
|
|
@@ -194,7 +944,6 @@ export async function queryLua<T = any>(
|
|
|
194
944
|
enricher?: (key: KvKey, item: any) => any,
|
|
195
945
|
): Promise<T[]> {
|
|
196
946
|
const results: T[] = [];
|
|
197
|
-
// Accumulate all results into an array
|
|
198
947
|
for await (
|
|
199
948
|
let { key, value } of kv.query({ prefix })
|
|
200
949
|
) {
|
|
@@ -203,25 +952,56 @@ export async function queryLua<T = any>(
|
|
|
203
952
|
}
|
|
204
953
|
results.push(value);
|
|
205
954
|
}
|
|
206
|
-
|
|
207
955
|
return applyQuery(results, query, env, sf);
|
|
208
956
|
}
|
|
209
957
|
|
|
210
958
|
function generateKey(value: any) {
|
|
959
|
+
if (isSqlNull(value)) {
|
|
960
|
+
return "__SQL_NULL__";
|
|
961
|
+
}
|
|
211
962
|
if (value instanceof LuaTable) {
|
|
212
|
-
return JSON.stringify(value
|
|
963
|
+
return JSON.stringify(luaTableToJSWithNulls(value));
|
|
213
964
|
}
|
|
214
965
|
return typeof value === "object" && value !== null
|
|
215
966
|
? JSON.stringify(value)
|
|
216
967
|
: value;
|
|
217
968
|
}
|
|
969
|
+
|
|
970
|
+
function luaTableToJSWithNulls(
|
|
971
|
+
table: LuaTable,
|
|
972
|
+
sf = LuaStackFrame.lostFrame,
|
|
973
|
+
): any {
|
|
974
|
+
if (table.length > 0) {
|
|
975
|
+
const arr: any[] = [];
|
|
976
|
+
for (let i = 1; i <= table.length; i++) {
|
|
977
|
+
const v = table.rawGet(i);
|
|
978
|
+
arr.push(
|
|
979
|
+
isSqlNull(v)
|
|
980
|
+
? "__SQL_NULL__"
|
|
981
|
+
: v instanceof LuaTable
|
|
982
|
+
? luaTableToJSWithNulls(v, sf)
|
|
983
|
+
: v,
|
|
984
|
+
);
|
|
985
|
+
}
|
|
986
|
+
return arr;
|
|
987
|
+
}
|
|
988
|
+
const obj: Record<string, any> = {};
|
|
989
|
+
for (const key of luaKeys(table)) {
|
|
990
|
+
const v = table.rawGet(key);
|
|
991
|
+
obj[key] = isSqlNull(v)
|
|
992
|
+
? "__SQL_NULL__"
|
|
993
|
+
: v instanceof LuaTable
|
|
994
|
+
? luaTableToJSWithNulls(v, sf)
|
|
995
|
+
: v;
|
|
996
|
+
}
|
|
997
|
+
return obj;
|
|
998
|
+
}
|
|
999
|
+
|
|
218
1000
|
export class DataStoreQueryCollection implements LuaQueryCollection {
|
|
219
1001
|
constructor(
|
|
220
1002
|
private readonly dataStore: DataStore,
|
|
221
1003
|
readonly prefix: string[],
|
|
222
|
-
) {
|
|
223
|
-
}
|
|
224
|
-
|
|
1004
|
+
) {}
|
|
225
1005
|
query(
|
|
226
1006
|
query: LuaCollectionQuery,
|
|
227
1007
|
env: LuaEnv,
|