@silverbulletmd/silverbullet 2.5.3 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +4 -5
  2. package/client/asset_bundle/bundle.ts +3 -9
  3. package/client/data/datastore.ts +4 -5
  4. package/client/markdown_parser/constants.ts +3 -2
  5. package/client/plugos/hooks/code_widget.ts +3 -5
  6. package/client/plugos/hooks/command.ts +8 -8
  7. package/client/plugos/hooks/document_editor.ts +10 -12
  8. package/client/plugos/hooks/event.ts +33 -36
  9. package/client/plugos/hooks/mq.ts +17 -17
  10. package/client/plugos/hooks/plug_namespace.ts +3 -5
  11. package/client/plugos/hooks/slash_command.ts +12 -27
  12. package/client/plugos/hooks/syscall.ts +3 -3
  13. package/client/plugos/manifest_cache.ts +22 -15
  14. package/client/plugos/plug.ts +2 -5
  15. package/client/plugos/plug_compile.ts +67 -65
  16. package/client/plugos/protocol.ts +28 -28
  17. package/client/plugos/proxy_fetch.ts +7 -6
  18. package/client/plugos/sandboxes/worker_sandbox.ts +16 -15
  19. package/client/plugos/syscalls/asset.ts +1 -3
  20. package/client/plugos/syscalls/code_widget.ts +1 -3
  21. package/client/plugos/syscalls/config.ts +1 -5
  22. package/client/plugos/syscalls/datastore.ts +1 -1
  23. package/client/plugos/syscalls/editor.ts +63 -60
  24. package/client/plugos/syscalls/event.ts +9 -12
  25. package/client/plugos/syscalls/fetch.ts +30 -22
  26. package/client/plugos/syscalls/index.ts +10 -1
  27. package/client/plugos/syscalls/jsonschema.ts +72 -32
  28. package/client/plugos/syscalls/language.ts +9 -5
  29. package/client/plugos/syscalls/markdown.ts +29 -7
  30. package/client/plugos/syscalls/mq.ts +3 -11
  31. package/client/plugos/syscalls/service_registry.ts +1 -4
  32. package/client/plugos/syscalls/shell.ts +2 -5
  33. package/client/plugos/syscalls/sync.ts +69 -60
  34. package/client/plugos/syscalls/system.ts +2 -3
  35. package/client/plugos/system.ts +4 -10
  36. package/client/plugos/worker_runtime.ts +4 -3
  37. package/client/space_lua/aggregates.ts +632 -59
  38. package/client/space_lua/ast.ts +21 -9
  39. package/client/space_lua/ast_narrow.ts +4 -2
  40. package/client/space_lua/eval.ts +842 -536
  41. package/client/space_lua/labels.ts +6 -11
  42. package/client/space_lua/liq_null.ts +6 -0
  43. package/client/space_lua/numeric.ts +5 -8
  44. package/client/space_lua/parse.ts +290 -169
  45. package/client/space_lua/query_collection.ts +213 -149
  46. package/client/space_lua/render_lua_markdown.ts +369 -0
  47. package/client/space_lua/rp.ts +5 -4
  48. package/client/space_lua/runtime.ts +245 -142
  49. package/client/space_lua/stdlib/format.ts +34 -20
  50. package/client/space_lua/stdlib/js.ts +3 -7
  51. package/client/space_lua/stdlib/load.ts +1 -3
  52. package/client/space_lua/stdlib/math.ts +15 -14
  53. package/client/space_lua/stdlib/net.ts +25 -15
  54. package/client/space_lua/stdlib/os.ts +76 -85
  55. package/client/space_lua/stdlib/pattern.ts +28 -35
  56. package/client/space_lua/stdlib/prng.ts +15 -12
  57. package/client/space_lua/stdlib/space_lua.ts +16 -17
  58. package/client/space_lua/stdlib/string.ts +7 -17
  59. package/client/space_lua/stdlib/string_pack.ts +23 -19
  60. package/client/space_lua/stdlib/table.ts +5 -9
  61. package/client/space_lua/stdlib.ts +20 -30
  62. package/client/space_lua/tonumber.ts +79 -40
  63. package/client/space_lua/util.ts +14 -10
  64. package/dist/plug-compile.js +44 -41
  65. package/package.json +24 -22
  66. package/plug-api/lib/async.ts +19 -6
  67. package/plug-api/lib/crypto.ts +5 -6
  68. package/plug-api/lib/dates.ts +15 -7
  69. package/plug-api/lib/json.ts +10 -4
  70. package/plug-api/lib/ref.ts +18 -18
  71. package/plug-api/lib/resolve.ts +7 -11
  72. package/plug-api/lib/tags.ts +13 -4
  73. package/plug-api/lib/transclusion.ts +6 -17
  74. package/plug-api/lib/tree.ts +115 -43
  75. package/plug-api/lib/yaml.ts +25 -15
  76. package/plug-api/syscalls/asset.ts +1 -1
  77. package/plug-api/syscalls/config.ts +1 -4
  78. package/plug-api/syscalls/editor.ts +14 -14
  79. package/plug-api/syscalls/jsonschema.ts +1 -3
  80. package/plug-api/syscalls/lua.ts +3 -9
  81. package/plug-api/syscalls/mq.ts +1 -4
  82. package/plug-api/syscalls/shell.ts +4 -1
  83. package/plug-api/syscalls/space.ts +3 -10
  84. package/plug-api/syscalls/system.ts +1 -4
  85. package/plug-api/syscalls/yaml.ts +2 -6
  86. package/plug-api/types/client.ts +16 -1
  87. package/plug-api/types/event.ts +6 -4
  88. package/plug-api/types/manifest.ts +8 -9
  89. package/plugs/builtin_plugs.ts +2 -2
  90. package/dist/worker_runtime_bundle.js +0 -233
@@ -1,4 +1,5 @@
1
1
  import type {
2
+ LuaAggregateCallExpression,
2
3
  LuaBinaryExpression,
3
4
  LuaDynamicField,
4
5
  LuaExpression,
@@ -25,6 +26,7 @@ import {
25
26
  type LuaValue,
26
27
  singleResult,
27
28
  } from "./runtime.ts";
29
+ import { isSqlNull, LIQ_NULL } from "./liq_null.ts";
28
30
  import { evalExpression, luaOp } from "./eval.ts";
29
31
  import { asyncMergeSort } from "./util.ts";
30
32
  import type { DataStore } from "../data/datastore.ts";
@@ -35,13 +37,10 @@ import type { QueryCollationConfig } from "../../plug-api/types/config.ts";
35
37
  import type { KvKey } from "../../plug-api/types/datastore.ts";
36
38
 
37
39
  import { executeAggregate, getAggregateSpec } from "./aggregates.ts";
40
+ import { Config } from "../config.ts";
38
41
 
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
- }
42
+ // Implicit single group map key (aggregates without `group by`)
43
+ const IMPLICIT_GROUP_KEY: unique symbol = Symbol("implicit-group");
45
44
 
46
45
  // Build environment for post-`group by` clauses. Injects `key` and `group`
47
46
  // as top-level variables. Unpacks first group item fields and group-by key
@@ -58,9 +57,8 @@ function buildGroupItemEnv(
58
57
  if (item instanceof LuaTable) {
59
58
  const keyVal = item.rawGet("key");
60
59
  const groupVal = item.rawGet("group");
61
- const firstItem = (groupVal instanceof LuaTable)
62
- ? groupVal.rawGet(1)
63
- : undefined;
60
+ const firstItem =
61
+ groupVal instanceof LuaTable ? groupVal.rawGet(1) : undefined;
64
62
 
65
63
  if (firstItem) {
66
64
  for (const k of luaKeys(firstItem)) {
@@ -79,19 +77,32 @@ function buildGroupItemEnv(
79
77
  itemEnv.setLocal("group", groupVal);
80
78
  }
81
79
 
80
+ // Unpack named fields from multi-key LuaTable keys
82
81
  if (keyVal instanceof LuaTable) {
83
82
  for (const k of luaKeys(keyVal)) {
84
83
  if (typeof k !== "string") continue;
85
84
  itemEnv.setLocal(k, luaGet(keyVal, k, sf.astCtx ?? null, sf));
86
85
  }
87
86
  }
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);
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
+ }
95
106
  }
96
107
  }
97
108
  return itemEnv;
@@ -141,6 +152,11 @@ export type LuaOrderBy = {
141
152
  using?: string | LuaFunctionBody;
142
153
  };
143
154
 
155
+ export type LuaGroupByEntry = {
156
+ expr: LuaExpression;
157
+ alias?: string;
158
+ };
159
+
144
160
  /**
145
161
  * Represents a query for a collection
146
162
  */
@@ -158,8 +174,8 @@ export type LuaCollectionQuery = {
158
174
  offset?: number;
159
175
  // Whether to return only distinct values
160
176
  distinct?: boolean;
161
- // The group by expressions evaluated with Lua
162
- groupBy?: LuaExpression[];
177
+ // The group by entries evaluated with Lua
178
+ groupBy?: LuaGroupByEntry[];
163
179
  // The having expression evaluated with Lua
164
180
  having?: LuaExpression;
165
181
  };
@@ -169,6 +185,7 @@ export interface LuaQueryCollection {
169
185
  query: LuaCollectionQuery,
170
186
  env: LuaEnv,
171
187
  sf: LuaStackFrame,
188
+ config?: Config,
172
189
  ): Promise<any[]>;
173
190
  }
174
191
 
@@ -176,15 +193,14 @@ export interface LuaQueryCollection {
176
193
  * Implements a query collection for a regular JavaScript array
177
194
  */
178
195
  export class ArrayQueryCollection<T> implements LuaQueryCollection {
179
- constructor(private readonly array: T[]) {
180
- }
196
+ constructor(private readonly array: T[]) {}
181
197
  query(
182
198
  query: LuaCollectionQuery,
183
199
  env: LuaEnv,
184
200
  sf: LuaStackFrame,
185
- collation?: QueryCollationConfig,
201
+ config?: Config,
186
202
  ): Promise<any[]> {
187
- return applyQuery(this.array, query, env, sf, collation);
203
+ return applyQuery(this.array, query, env, sf, config);
188
204
  }
189
205
  }
190
206
 
@@ -202,46 +218,71 @@ export function toCollection(obj: any): LuaQueryCollection {
202
218
  return new ArrayQueryCollection([obj]);
203
219
  }
204
220
 
205
- function containsAggregate(expr: LuaExpression): boolean {
221
+ function containsAggregate(expr: LuaExpression, config?: Config): boolean {
206
222
  switch (expr.type) {
207
223
  case "FilteredCall": {
208
224
  const fc = (expr as LuaFilteredCallExpression).call;
209
- if (fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name)) {
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
+ ) {
210
243
  return true;
211
244
  }
212
- return containsAggregate(fc) ||
213
- containsAggregate((expr as LuaFilteredCallExpression).filter);
245
+ return containsAggregate(fc, config);
214
246
  }
215
247
  case "FunctionCall": {
216
248
  const fc = expr as LuaFunctionCallExpression;
217
- if (fc.prefix.type === "Variable" && getAggregateSpec(fc.prefix.name)) {
249
+ if (
250
+ fc.prefix.type === "Variable" &&
251
+ getAggregateSpec(fc.prefix.name, config)
252
+ ) {
218
253
  return true;
219
254
  }
220
- return fc.args.some(containsAggregate);
255
+ return fc.args.some((a) => containsAggregate(a, config));
221
256
  }
222
257
  case "Binary": {
223
258
  const bin = expr as LuaBinaryExpression;
224
- return containsAggregate(bin.left) || containsAggregate(bin.right);
259
+ return (
260
+ containsAggregate(bin.left, config) ||
261
+ containsAggregate(bin.right, config)
262
+ );
225
263
  }
226
264
  case "Unary": {
227
265
  const un = expr as LuaUnaryExpression;
228
- return containsAggregate(un.argument);
266
+ return containsAggregate(un.argument, config);
229
267
  }
230
268
  case "Parenthesized": {
231
269
  const p = expr as LuaParenthesizedExpression;
232
- return containsAggregate(p.expression);
270
+ return containsAggregate(p.expression, config);
233
271
  }
234
272
  case "TableConstructor":
235
273
  return expr.fields.some((f) => {
236
274
  switch (f.type) {
237
275
  case "PropField":
238
- return containsAggregate((f as LuaPropField).value);
276
+ return containsAggregate((f as LuaPropField).value, config);
239
277
  case "DynamicField": {
240
278
  const df = f as LuaDynamicField;
241
- return containsAggregate(df.key) || containsAggregate(df.value);
279
+ return (
280
+ containsAggregate(df.key, config) ||
281
+ containsAggregate(df.value, config)
282
+ );
242
283
  }
243
284
  case "ExpressionField":
244
- return containsAggregate((f as LuaExpressionField).value);
285
+ return containsAggregate((f as LuaExpressionField).value, config);
245
286
  default:
246
287
  return false;
247
288
  }
@@ -251,6 +292,12 @@ function containsAggregate(expr: LuaExpression): boolean {
251
292
  }
252
293
  }
253
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
+
254
301
  /**
255
302
  * Evaluate an expression in aggregate-aware mode.
256
303
  *
@@ -265,8 +312,9 @@ export async function evalExpressionWithAggregates(
265
312
  groupItems: LuaTable,
266
313
  objectVariable: string | undefined,
267
314
  outerEnv: LuaEnv,
315
+ config: Config,
268
316
  ): Promise<LuaValue> {
269
- if (!containsAggregate(expr)) {
317
+ if (!containsAggregate(expr, config)) {
270
318
  return evalExpression(expr, env, sf);
271
319
  }
272
320
  const recurse = (e: LuaExpression) =>
@@ -277,6 +325,7 @@ export async function evalExpressionWithAggregates(
277
325
  groupItems,
278
326
  objectVariable,
279
327
  outerEnv,
328
+ config,
280
329
  );
281
330
 
282
331
  if (expr.type === "FilteredCall") {
@@ -284,18 +333,22 @@ export async function evalExpressionWithAggregates(
284
333
  const fc = filtered.call;
285
334
  if (fc.prefix.type === "Variable") {
286
335
  const name = fc.prefix.name;
287
- const spec = getAggregateSpec(name);
336
+ const spec = getAggregateSpec(name, config);
288
337
  if (spec) {
289
338
  const valueExpr = fc.args.length > 0 ? fc.args[0] : null;
339
+ const extraArgExprs = fc.args.length > 1 ? fc.args.slice(1) : [];
290
340
  return executeAggregate(
291
341
  spec,
292
342
  groupItems,
293
343
  valueExpr,
344
+ extraArgExprs,
294
345
  objectVariable,
295
346
  outerEnv,
296
347
  sf,
297
348
  evalExpression,
349
+ config,
298
350
  filtered.filter,
351
+ fc.orderBy,
299
352
  );
300
353
  }
301
354
  }
@@ -307,17 +360,22 @@ export async function evalExpressionWithAggregates(
307
360
  const fc = expr as LuaFunctionCallExpression;
308
361
  if (fc.prefix.type === "Variable") {
309
362
  const name = fc.prefix.name;
310
- const spec = getAggregateSpec(name);
363
+ const spec = getAggregateSpec(name, config);
311
364
  if (spec) {
312
365
  const valueExpr = fc.args.length > 0 ? fc.args[0] : null;
366
+ const extraArgExprs = fc.args.length > 1 ? fc.args.slice(1) : [];
313
367
  return executeAggregate(
314
368
  spec,
315
369
  groupItems,
316
370
  valueExpr,
371
+ extraArgExprs,
317
372
  objectVariable,
318
373
  outerEnv,
319
374
  sf,
320
375
  evalExpression,
376
+ config,
377
+ undefined,
378
+ fc.orderBy,
321
379
  );
322
380
  }
323
381
  }
@@ -330,20 +388,20 @@ export async function evalExpressionWithAggregates(
330
388
  case "PropField": {
331
389
  const pf = field as LuaPropField;
332
390
  const value = await recurse(pf.value);
333
- table.set(pf.key, value, sf);
391
+ void table.set(pf.key, selectVal(value), sf);
334
392
  break;
335
393
  }
336
394
  case "DynamicField": {
337
395
  const df = field as LuaDynamicField;
338
396
  const key = await evalExpression(df.key, env, sf);
339
397
  const value = await recurse(df.value);
340
- table.set(key, value, sf);
398
+ void table.set(key, selectVal(value), sf);
341
399
  break;
342
400
  }
343
401
  case "ExpressionField": {
344
402
  const ef = field as LuaExpressionField;
345
403
  const value = await recurse(ef.value);
346
- table.rawSetArrayIndex(nextArrayIndex, value);
404
+ table.rawSetArrayIndex(nextArrayIndex, selectVal(value));
347
405
  nextArrayIndex++;
348
406
  break;
349
407
  }
@@ -365,30 +423,16 @@ export async function evalExpressionWithAggregates(
365
423
  }
366
424
  const left = singleResult(await recurse(bin.left));
367
425
  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
- );
426
+ return luaOp(bin.operator, left, right, undefined, undefined, expr.ctx, sf);
377
427
  }
378
428
  if (expr.type === "Unary") {
379
429
  const un = expr as LuaUnaryExpression;
380
430
  const arg = singleResult(await recurse(un.argument));
381
431
  switch (un.operator) {
382
432
  case "-":
383
- return typeof arg === "number" ? -arg : luaOp(
384
- "-",
385
- 0,
386
- arg,
387
- undefined,
388
- undefined,
389
- expr.ctx,
390
- sf,
391
- );
433
+ return typeof arg === "number"
434
+ ? -arg
435
+ : luaOp("-", 0, arg, undefined, undefined, expr.ctx, sf);
392
436
  case "not":
393
437
  return !luaTruthy(arg);
394
438
  case "#":
@@ -447,14 +491,11 @@ function normalizeSelectResults(results: any[]): any[] {
447
491
  const rebuilt = new LuaTable();
448
492
  for (const k of canonicalKeys) {
449
493
  const v = item.rawGet(k);
450
- rebuilt.rawSet(
451
- k,
452
- (v === undefined || v === null) ? LIQ_NULL : v,
453
- );
494
+ void rebuilt.rawSet(k, v === undefined || v === null ? LIQ_NULL : v);
454
495
  }
455
496
  for (const k of luaKeys(item)) {
456
497
  if (typeof k !== "string") {
457
- rebuilt.rawSet(k, item.rawGet(k));
498
+ void rebuilt.rawSet(k, item.rawGet(k));
458
499
  }
459
500
  }
460
501
  results[i] = rebuilt;
@@ -487,14 +528,10 @@ async function usingCompare(
487
528
  keyIdx: number,
488
529
  ): Promise<number> {
489
530
  const res = luaTruthy(
490
- singleResult(
491
- await luaCall(luaCmp, [aVal, bVal], sf.astCtx ?? {}, sf),
492
- ),
531
+ singleResult(await luaCall(luaCmp, [aVal, bVal], sf.astCtx ?? {}, sf)),
493
532
  );
494
533
  const reverseRes = luaTruthy(
495
- singleResult(
496
- await luaCall(luaCmp, [bVal, aVal], sf.astCtx ?? {}, sf),
497
- ),
534
+ singleResult(await luaCall(luaCmp, [bVal, aVal], sf.astCtx ?? {}, sf)),
498
535
  );
499
536
 
500
537
  // both true means SWO violation
@@ -526,6 +563,7 @@ async function precomputeSortKeys(
526
563
  sf: LuaStackFrame,
527
564
  grouped: boolean,
528
565
  selectResults: any[] | undefined,
566
+ config: Config,
529
567
  ): Promise<any[][]> {
530
568
  const allKeys: any[][] = new Array(results.length);
531
569
  for (let i = 0; i < results.length; i++) {
@@ -551,6 +589,7 @@ async function precomputeSortKeys(
551
589
  groupTable,
552
590
  objectVariable,
553
591
  env,
592
+ config,
554
593
  );
555
594
  } else {
556
595
  keys[j] = await evalExpression(orderBy[j].expr, itemEnv, sf);
@@ -583,8 +622,8 @@ async function sortKeyCompare(
583
622
  const bVal = bKeys[idx];
584
623
 
585
624
  // Handle nulls positioning
586
- const aIsNull = aVal === null || aVal === undefined;
587
- const bIsNull = bVal === null || bVal === undefined;
625
+ const aIsNull = aVal === null || aVal === undefined || isSqlNull(aVal);
626
+ const bIsNull = bVal === null || bVal === undefined || isSqlNull(bVal);
588
627
  if (aIsNull || bIsNull) {
589
628
  if (aIsNull && bIsNull) continue;
590
629
  // Default: nulls last for asc, nulls first for desc
@@ -625,23 +664,35 @@ async function sortKeyCompare(
625
664
  return 0;
626
665
  }
627
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
+
628
684
  export async function applyQuery(
629
685
  results: any[],
630
686
  query: LuaCollectionQuery,
631
687
  env: LuaEnv,
632
688
  sf: LuaStackFrame,
633
- collation?: QueryCollationConfig,
689
+ config: Config = new Config(),
634
690
  ): Promise<any[]> {
635
691
  results = results.slice();
636
692
  if (query.where) {
637
693
  const filteredResults = [];
638
694
  for (const value of results) {
639
- const itemEnv = buildItemEnvLocal(
640
- query.objectVariable,
641
- value,
642
- env,
643
- sf,
644
- );
695
+ const itemEnv = buildItemEnvLocal(query.objectVariable, value, env, sf);
645
696
  if (await evalExpression(query.where, itemEnv, sf)) {
646
697
  filteredResults.push(value);
647
698
  }
@@ -649,62 +700,72 @@ export async function applyQuery(
649
700
  results = filteredResults;
650
701
  }
651
702
 
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
+ }
711
+
652
712
  const grouped = !!query.groupBy;
653
713
 
654
714
  // Collect group-by key names for unpacking into the post-group environment.
655
715
  let groupByNames: string[] | undefined;
656
716
 
657
717
  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);
718
+ // Extract expressions and names from `group by` entries
719
+ const groupByEntries = query.groupBy;
668
720
 
669
- const groups = new Map<
670
- string,
671
- { key: any; items: any[] }
672
- >();
673
- const groupByExprs = query.groupBy as LuaExpression[];
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[] }>();
674
732
 
675
733
  for (const item of results) {
676
- const itemEnv = buildItemEnvLocal(
677
- query.objectVariable,
678
- item,
679
- env,
680
- sf,
681
- );
734
+ const itemEnv = buildItemEnvLocal(query.objectVariable, item, env, sf);
682
735
 
683
736
  const keyParts: any[] = [];
684
737
  const keyRecord: Record<string, any> = {};
685
738
 
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);
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;
698
752
  }
699
753
  }
700
754
 
701
- const compositeKey = keyParts.length === 1
702
- ? generateKey(keyParts[0])
703
- : JSON.stringify(keyParts.map(generateKey));
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));
704
762
  let entry = groups.get(compositeKey);
705
763
  if (!entry) {
706
764
  let keyVal: any;
707
- if (keyParts.length === 1) {
765
+ if (keyParts.length === 0) {
766
+ // Implicit single group — key is `nil`
767
+ keyVal = null;
768
+ } else if (keyParts.length === 1) {
708
769
  keyVal = keyParts[0];
709
770
  } else {
710
771
  const kt = new LuaTable();
@@ -714,7 +775,7 @@ export async function applyQuery(
714
775
  }
715
776
  // Additionally set named fields for Variable/PropertyAccess exprs
716
777
  for (const name in keyRecord) {
717
- kt.rawSet(name, keyRecord[name]);
778
+ void kt.rawSet(name, keyRecord[name]);
718
779
  }
719
780
  keyVal = kt;
720
781
  }
@@ -734,15 +795,14 @@ export async function applyQuery(
734
795
  const item = items[i];
735
796
  groupTable.rawSetArrayIndex(
736
797
  i + 1,
737
- (item instanceof LuaTable || typeof item !== "object" ||
738
- item === null)
798
+ item instanceof LuaTable || typeof item !== "object" || item === null
739
799
  ? item
740
800
  : jsToLuaValue(item),
741
801
  );
742
802
  }
743
803
  const row = new LuaTable();
744
- row.rawSet("key", key);
745
- row.rawSet("group", groupTable);
804
+ void row.rawSet("key", key);
805
+ void row.rawSet("group", groupTable);
746
806
  results.push(row);
747
807
  }
748
808
  }
@@ -767,14 +827,10 @@ export async function applyQuery(
767
827
  groupTable,
768
828
  query.objectVariable,
769
829
  env,
830
+ config,
770
831
  );
771
832
  } else {
772
- const itemEnv = buildItemEnvLocal(
773
- query.objectVariable,
774
- value,
775
- env,
776
- sf,
777
- );
833
+ const itemEnv = buildItemEnvLocal(query.objectVariable, value, env, sf);
778
834
  condResult = await evalExpression(query.having, itemEnv, sf);
779
835
  }
780
836
  if (condResult) {
@@ -785,28 +841,27 @@ export async function applyQuery(
785
841
  }
786
842
 
787
843
  const mkEnv = grouped
788
- ? (
789
- ov: string | undefined,
790
- item: any,
791
- e: LuaEnv,
792
- s: LuaStackFrame,
793
- ) => buildGroupItemEnv(ov, groupByNames, item, e, s)
844
+ ? (ov: string | undefined, item: any, e: LuaEnv, s: LuaStackFrame) =>
845
+ buildGroupItemEnv(ov, groupByNames, item, e, s)
794
846
  : buildItemEnvLocal;
795
847
 
796
848
  let selectResults: any[] | undefined;
797
849
 
850
+ // Pre-compute select for grouped + ordered queries
798
851
  if (grouped && query.select && query.orderBy) {
852
+ const selectExpr = query.select;
799
853
  selectResults = [];
800
854
  for (const item of results) {
801
855
  const itemEnv = mkEnv(query.objectVariable, item, env, sf);
802
856
  const groupTable = (item as LuaTable).rawGet("group");
803
857
  const selected = await evalExpressionWithAggregates(
804
- query.select,
858
+ selectExpr,
805
859
  itemEnv,
806
860
  sf,
807
861
  groupTable,
808
862
  query.objectVariable,
809
863
  env,
864
+ config,
810
865
  );
811
866
  selectResults.push(selected);
812
867
  }
@@ -814,10 +869,7 @@ export async function applyQuery(
814
869
  }
815
870
 
816
871
  if (query.orderBy) {
817
- if (collation === undefined) {
818
- const config = globalThis.client.config;
819
- collation = config.get("queryCollation", {});
820
- }
872
+ const collation = config.get<QueryCollationConfig>("queryCollation", {});
821
873
  const collator = Intl.Collator(collation?.locale, collation?.options);
822
874
 
823
875
  const resolvedUsing: (LuaValue | null)[] = [];
@@ -837,6 +889,7 @@ export async function applyQuery(
837
889
  sf,
838
890
  grouped,
839
891
  selectResults,
892
+ config,
840
893
  );
841
894
 
842
895
  // Tag each result with its original index for stable sorting
@@ -855,7 +908,8 @@ export async function applyQuery(
855
908
  resolvedUsing,
856
909
  violated,
857
910
  sf,
858
- ));
911
+ ),
912
+ );
859
913
 
860
914
  // Check for SWO violations in comparators
861
915
  for (let i = 0; i < violated.length; i++) {
@@ -884,6 +938,7 @@ export async function applyQuery(
884
938
  }
885
939
 
886
940
  if (query.select) {
941
+ const selectExpr = query.select;
887
942
  if (selectResults) {
888
943
  results = selectResults;
889
944
  } else {
@@ -894,16 +949,17 @@ export async function applyQuery(
894
949
  const groupTable = (item as LuaTable).rawGet("group");
895
950
  newResult.push(
896
951
  await evalExpressionWithAggregates(
897
- query.select,
952
+ selectExpr,
898
953
  itemEnv,
899
954
  sf,
900
955
  groupTable,
901
956
  query.objectVariable,
902
957
  env,
958
+ config,
903
959
  ),
904
960
  );
905
961
  } else {
906
- newResult.push(await evalExpression(query.select, itemEnv, sf));
962
+ newResult.push(await evalSelectExpression(query.select, itemEnv, sf));
907
963
  }
908
964
  }
909
965
  results = newResult;
@@ -942,17 +998,16 @@ export async function queryLua<T = any>(
942
998
  env: LuaEnv,
943
999
  sf: LuaStackFrame = LuaStackFrame.lostFrame,
944
1000
  enricher?: (key: KvKey, item: any) => any,
1001
+ config?: Config,
945
1002
  ): Promise<T[]> {
946
1003
  const results: T[] = [];
947
- for await (
948
- let { key, value } of kv.query({ prefix })
949
- ) {
1004
+ for await (let { key, value } of kv.query({ prefix })) {
950
1005
  if (enricher) {
951
1006
  value = enricher(key, value);
952
1007
  }
953
1008
  results.push(value);
954
1009
  }
955
- return applyQuery(results, query, env, sf);
1010
+ return applyQuery(results, query, env, sf, config);
956
1011
  }
957
1012
 
958
1013
  function generateKey(value: any) {
@@ -979,8 +1034,8 @@ function luaTableToJSWithNulls(
979
1034
  isSqlNull(v)
980
1035
  ? "__SQL_NULL__"
981
1036
  : v instanceof LuaTable
982
- ? luaTableToJSWithNulls(v, sf)
983
- : v,
1037
+ ? luaTableToJSWithNulls(v, sf)
1038
+ : v,
984
1039
  );
985
1040
  }
986
1041
  return arr;
@@ -991,8 +1046,8 @@ function luaTableToJSWithNulls(
991
1046
  obj[key] = isSqlNull(v)
992
1047
  ? "__SQL_NULL__"
993
1048
  : v instanceof LuaTable
994
- ? luaTableToJSWithNulls(v, sf)
995
- : v;
1049
+ ? luaTableToJSWithNulls(v, sf)
1050
+ : v;
996
1051
  }
997
1052
  return obj;
998
1053
  }
@@ -1006,7 +1061,16 @@ export class DataStoreQueryCollection implements LuaQueryCollection {
1006
1061
  query: LuaCollectionQuery,
1007
1062
  env: LuaEnv,
1008
1063
  sf: LuaStackFrame,
1064
+ config?: Config,
1009
1065
  ): Promise<any[]> {
1010
- return queryLua(this.dataStore.kv, this.prefix, query, env, sf);
1066
+ return queryLua(
1067
+ this.dataStore.kv,
1068
+ this.prefix,
1069
+ query,
1070
+ env,
1071
+ sf,
1072
+ undefined,
1073
+ config,
1074
+ );
1011
1075
  }
1012
1076
  }