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