@atscript/db-memory 0.1.146 → 0.1.148

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/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, deletePath, getPath, getPath as getPath$1, isAtscriptDbView, walkFilter } from "@atscript/db";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, containsRelationFilter, deletePath, evaluateExpr, forEachResolvedRelation, getPath, getPath as getPath$1, isAtscriptDbView, isConflict, isResolvedRelationFilter, walkFilter } from "@atscript/db";
2
2
  import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
3
3
  import { bucketer } from "@uniqu/core";
4
4
  //#region src/lib/memory-filter.ts
@@ -73,15 +73,26 @@ function stringifyLeaf(v) {
73
73
  /** `$eq` semantics, factored out so `$ne` can be its exact negation. */
74
74
  function evalEq(row, field, value) {
75
75
  const fieldValue = getPath$1(row, field);
76
- if (value === null) return fieldValue == null;
76
+ if (value === null) return matchesScalar(fieldValue, null);
77
77
  if (value instanceof RegExp) return fieldValue != null && value.test(stringifyLeaf(fieldValue));
78
+ return matchesScalar(fieldValue, value);
79
+ }
80
+ /**
81
+ * Mongo containment: a scalar compared with an array field matches when ANY element
82
+ * equals it (`{ labels: "bug" }` matches `["bug", "ui"]`). Non-array fields compare as before.
83
+ * `null` also matches a missing field (`undefined`) and an array holding a `null` element,
84
+ * as `{ labels: null }` / `$in: [null]` do in Mongo.
85
+ */
86
+ function matchesScalar(fieldValue, value) {
87
+ if (value === null) return fieldValue == null || Array.isArray(fieldValue) && fieldValue.includes(null);
88
+ if (Array.isArray(fieldValue) && !Array.isArray(value)) return fieldValue.some((el) => valuesEqual(el, value));
78
89
  return valuesEqual(fieldValue, value);
79
90
  }
80
91
  /** `$in` membership, factored out so `$nin` can be its exact negation. */
81
92
  function evalIn(row, field, value) {
82
93
  if (!Array.isArray(value)) return false;
83
94
  const fieldValue = getPath$1(row, field);
84
- return value.some((element) => valuesEqual(fieldValue, element));
95
+ return value.some((element) => matchesScalar(fieldValue, element));
85
96
  }
86
97
  /**
87
98
  * Coerce a leaf to something the JS relational operators can order. `Date`s
@@ -155,18 +166,156 @@ const memoryVisitor = {
155
166
  message: `Unsupported filter operator: ${op}`
156
167
  }]);
157
168
  }
169
+ },
170
+ relation(field, op, operand) {
171
+ return relationPredicate(void 0, field, op, operand);
158
172
  }
159
173
  };
160
174
  /**
175
+ * Identity of the key tuple `fields` of `row` (dot-paths read nested), or
176
+ * `undefined` when a component is `null` / missing — a NULL key component
177
+ * never correlates (SQL `=` semantics: `$some` false, `$none` true).
178
+ * Every component is type-tagged (see {@link keyPart}), matching the strict
179
+ * equality the memory filter uses everywhere (`valuesEqual`): `1` ≠ `"1"`,
180
+ * `5n` ≠ `"5n"`, a `Date` ≠ its ISO string; `Date`s compare by instant.
181
+ */
182
+ function correlationKey(row, fields) {
183
+ const parts = [];
184
+ for (const field of fields) {
185
+ const part = keyPart(getPath$1(row, field));
186
+ if (part === void 0) return;
187
+ parts.push(part);
188
+ }
189
+ return JSON.stringify(parts);
190
+ }
191
+ /**
192
+ * Type-tagged identity of one key component, or `undefined` when it never
193
+ * equals anything (`null` / missing, `NaN`).
194
+ */
195
+ function keyPart(value) {
196
+ switch (typeof value) {
197
+ case "string": return `s:${value}`;
198
+ case "number": return Number.isNaN(value) ? void 0 : `n:${value === 0 ? 0 : value}`;
199
+ case "bigint": return `b:${value}`;
200
+ case "boolean": return `t:${value}`;
201
+ case "undefined": return;
202
+ default:
203
+ if (value === null) return void 0;
204
+ if (value instanceof Date) {
205
+ const time = value.getTime();
206
+ return Number.isNaN(time) ? void 0 : `d:${time}`;
207
+ }
208
+ return `o:${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? `${v}n` : v)}`;
209
+ }
210
+ }
211
+ /** Source-side correlation columns of a resolved predicate (physical names). */
212
+ function sourceColumns(node) {
213
+ return node.junction ? node.junction.toSource.map((p) => p.source) : node.pairs.map((p) => p.source);
214
+ }
215
+ /**
216
+ * The visitor's `relation` callback: a row satisfies `$some` when its source
217
+ * correlation tuple is in the predicate's prepared set, `$none` otherwise.
218
+ */
219
+ function relationPredicate(sets, field, op, operand) {
220
+ const node = operand;
221
+ const set = isResolvedRelationFilter(node) ? sets?.get(node) : void 0;
222
+ if (!set) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
223
+ path: field,
224
+ message: `Relational predicate "${op}" on "${field}" was not prepared — call prepareRelationSets() before buildMemoryPredicate()`
225
+ }]);
226
+ const columns = sourceColumns(node);
227
+ const some = op === "$some";
228
+ return (row) => {
229
+ const key = correlationKey(row, columns);
230
+ return (key !== void 0 && set.has(key)) === some;
231
+ };
232
+ }
233
+ /**
234
+ * Resolves every relational predicate of a TRANSLATED filter (nested ones
235
+ * included) into a correlation set, loading each related table through
236
+ * `load`:
237
+ *
238
+ * - `to` / `from`: the related rows matching the inner filter contribute their
239
+ * `pairs[].target` tuples — equal to the source tuple (`pairs[].source`) of
240
+ * every row that has such a related row.
241
+ * - `via`: the matching related rows' `junction.toTarget[].target` tuples
242
+ * select junction rows (also filtered by the junction part of
243
+ * `@db.rel.filter`), which contribute their `junction.toSource[].junction`
244
+ * tuples.
245
+ *
246
+ * Nested predicates are prepared against the related table's rows first
247
+ * (their source is this predicate's target). Returns `undefined` when the
248
+ * filter holds no predicate — callers then stay on the plain synchronous path.
249
+ *
250
+ * @since 0.1.147
251
+ */
252
+ async function prepareRelationSets(filter, load) {
253
+ if (!containsRelationFilter(filter)) return;
254
+ const sets = /* @__PURE__ */ new Map();
255
+ await prepareLevel(filter, load, sets);
256
+ return sets;
257
+ }
258
+ /** Prepares the predicates at one level of `filter` (and, recursively, their operands). */
259
+ async function prepareLevel(filter, load, sets) {
260
+ const nodes = [];
261
+ forEachResolvedRelation(filter, (node) => nodes.push(node), false);
262
+ for (const node of nodes) if (!sets.has(node)) sets.set(node, await correlationSet(node, load, sets));
263
+ }
264
+ /** Matching rows of `adapter`'s table for `filter` (its own predicates prepared first). */
265
+ async function matchingRows(adapter, filter, load, sets) {
266
+ const rows = await load(adapter);
267
+ if (!filter || Object.keys(filter).length === 0) return rows;
268
+ await prepareLevel(filter, load, sets);
269
+ return rows.filter(buildMemoryPredicate(filter, sets));
270
+ }
271
+ /** The source correlation tuples that have a related row matching `node`. */
272
+ async function correlationSet(node, load, sets) {
273
+ const targets = await matchingRows(node.target.adapter, node.filter, load, sets);
274
+ const result = /* @__PURE__ */ new Set();
275
+ if (!node.junction) {
276
+ const columns = node.pairs.map((p) => p.target);
277
+ for (const row of targets) {
278
+ const key = correlationKey(row, columns);
279
+ if (key !== void 0) result.add(key);
280
+ }
281
+ return result;
282
+ }
283
+ const { junction } = node;
284
+ const targetKeys = /* @__PURE__ */ new Set();
285
+ const targetColumns = junction.toTarget.map((p) => p.target);
286
+ for (const row of targets) {
287
+ const key = correlationKey(row, targetColumns);
288
+ if (key !== void 0) targetKeys.add(key);
289
+ }
290
+ if (targetKeys.size === 0) return result;
291
+ const toTarget = junction.toTarget.map((p) => p.junction);
292
+ const toSource = junction.toSource.map((p) => p.junction);
293
+ for (const row of await matchingRows(junction.adapter, junction.filter, load, sets)) {
294
+ const targetKey = correlationKey(row, toTarget);
295
+ if (targetKey === void 0 || !targetKeys.has(targetKey)) continue;
296
+ const sourceKey = correlationKey(row, toSource);
297
+ if (sourceKey !== void 0) result.add(sourceKey);
298
+ }
299
+ return result;
300
+ }
301
+ /**
161
302
  * Compiles a {@link FilterExpr} into an in-memory row predicate
162
303
  * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
163
304
  * structure matches the SQL/Mongo adapters by construction.
164
305
  *
165
306
  * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
166
307
  * to a match-everything predicate.
308
+ *
309
+ * A filter holding relational predicates (`ResolvedRelationFilter` operands,
310
+ * since 0.1.147) needs `relationSets` — prepared internally by
311
+ * `MemoryAdapter` from the related tables; a standalone call on such a
312
+ * filter throws `REL_FILTER_NOT_SUPPORTED`.
167
313
  */
168
- function buildMemoryPredicate(filter) {
169
- return walkFilter(filter, memoryVisitor) ?? (() => true);
314
+ function buildMemoryPredicate(filter, relationSets) {
315
+ return walkFilter(filter, relationSets ? {
316
+ ...memoryVisitor,
317
+ relation: (field, op, operand) => relationPredicate(relationSets, field, op, operand)
318
+ } : memoryVisitor) ?? (() => true);
170
319
  }
171
320
  //#endregion
172
321
  //#region src/lib/memory-engine.ts
@@ -325,10 +474,28 @@ function projectRow(row, projection, opts) {
325
474
  function aggregateRows(rows, controls) {
326
475
  const $select = controls.$select;
327
476
  const groupBy = controls.$groupBy ?? [];
328
- const aggregates = $select?.aggregates ?? [];
329
- const aliases = aggregates.map((expr) => resolveAlias(expr));
477
+ const entries = [
478
+ ...($select?.aggregates ?? []).map((expr) => ({
479
+ alias: resolveAlias(expr),
480
+ create: accumulatorFactory(expr)
481
+ })),
482
+ ...($select?.exprAggregates ?? []).map((e) => ({
483
+ alias: e.alias,
484
+ create: exprAccumulatorFactory(e)
485
+ })),
486
+ ...($select?.firstLast ?? []).map((fl) => ({
487
+ alias: fl.alias,
488
+ create: firstLastFactory(fl, $select?.rowOrder ?? [])
489
+ }))
490
+ ];
491
+ const aliases = entries.map((e) => e.alias);
492
+ const groupExprs = ($select?.exprs ?? []).map((e) => ({
493
+ alias: e.alias,
494
+ expr: e.expr,
495
+ readers: new Map(e.names.map((name) => [name, pathReader(name)]))
496
+ }));
330
497
  const keyReaders = groupBy.map((key) => groupKeyReader(key, controls));
331
- const accumulators = aggregates.map(accumulatorFactory);
498
+ const accumulators = entries.map((e) => e.create);
332
499
  const newGroup = (values) => ({
333
500
  values,
334
501
  accs: accumulators.map((create) => create())
@@ -357,6 +524,7 @@ function aggregateRows(rows, controls) {
357
524
  const row = {};
358
525
  for (let i = 0; i < groupBy.length; i++) setPath(row, groupBy[i], values[i]);
359
526
  for (let i = 0; i < aliases.length; i++) row[aliases[i]] = accs[i].result();
527
+ for (const e of groupExprs) row[e.alias] = evaluateExpr(e.expr, (name) => Object.hasOwn(row, name) ? row[name] : e.readers.get(name)(row));
360
528
  out.push(row);
361
529
  }
362
530
  const having = controls.$having;
@@ -364,7 +532,7 @@ function aggregateRows(rows, controls) {
364
532
  if (controls.$count) return [{ count: out.length }];
365
533
  const paged = paginate(sortRows(out, controls.$sort), controls.$skip, controls.$limit);
366
534
  const fields = ($select === void 0 ? groupBy : $select.asArray ?? []).map((field) => [field, pathReader(field)]);
367
- const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...aliases];
535
+ const outputAliases = $select === void 0 ? [] : [...($select.buckets ?? []).map((b) => b.alias), ...$select.computedAliases];
368
536
  return paged.map((row) => {
369
537
  const picked = {};
370
538
  for (const [field, read] of fields) setPath(picked, field, copyValue(read(row) ?? null));
@@ -430,76 +598,109 @@ function numericValue(value) {
430
598
  return Number.isNaN(n) ? void 0 : n;
431
599
  }
432
600
  }
601
+ /** `sum` / `avg` / `min` / `max` over a per-row arithmetic expression; NULL results are skipped like NULL fields. */
602
+ function exprAccumulatorFactory(e) {
603
+ const readers = new Map(e.names.map((name) => [name, pathReader(name)]));
604
+ return fnAccumulator(e.fn, (row) => evaluateExpr(e.expr, (field) => readers.get(field)(row)));
605
+ }
433
606
  /**
434
- * Resolves an aggregate expression to an accumulator factory (one accumulator
435
- * per group). Validated up front, so an unknown `$fn` is rejected even when
436
- * there are no rows.
607
+ * `first` / `last`: the value of `fl.column` on the group's representative
608
+ * row — the least (`first`) or greatest (`last`) row by `rowOrder` (NULL
609
+ * smallest; the primary key is the final key, so the pick is deterministic).
610
+ */
611
+ function firstLastFactory(fl, rowOrder) {
612
+ const keys = rowOrder.map((k) => ({
613
+ read: pathReader(k.column),
614
+ sign: k.desc ? -1 : 1
615
+ }));
616
+ const read = pathReader(fl.column);
617
+ const sign = fl.fn === "first" ? -1 : 1;
618
+ const compare = (a, b) => {
619
+ for (const k of keys) {
620
+ const c = compareLeaves(k.read(a), k.read(b)) * k.sign;
621
+ if (c !== 0) return c;
622
+ }
623
+ return 0;
624
+ };
625
+ return () => {
626
+ let best;
627
+ return {
628
+ add: (row) => {
629
+ if (best === void 0 || compare(row, best) * sign > 0) best = row;
630
+ },
631
+ result: () => best === void 0 ? null : copyValue(read(best) ?? null)
632
+ };
633
+ };
634
+ }
635
+ /**
636
+ * `sum` / `avg` / `min` / `max` over the values `read` yields per row — a
637
+ * field's, or a per-row expression's. `sum` / `avg` skip non-numeric values
638
+ * and are `null` over none; `min` / `max` skip null / missing and order with
639
+ * the `$sort` comparator.
640
+ */
641
+ function fnAccumulator(fn, read) {
642
+ if (fn === "sum" || fn === "avg") return () => {
643
+ let sum = 0;
644
+ let n = 0;
645
+ return {
646
+ add: (row) => {
647
+ const v = numericValue(read(row));
648
+ if (v !== void 0) {
649
+ sum += v;
650
+ n++;
651
+ }
652
+ },
653
+ result: () => n === 0 ? null : fn === "avg" ? sum / n : sum
654
+ };
655
+ };
656
+ const sign = fn === "min" ? -1 : 1;
657
+ return () => {
658
+ let best = null;
659
+ return {
660
+ add: (row) => {
661
+ const v = read(row);
662
+ if (v == null) return;
663
+ if (best === null || compareLeaves(v, best) * sign > 0) best = v;
664
+ },
665
+ result: () => best
666
+ };
667
+ };
668
+ }
669
+ /**
670
+ * Resolves a plain aggregate (`first` / `last` are {@link firstLastFactory}'s)
671
+ * to an accumulator factory (one accumulator per group). Validated up front,
672
+ * so an unknown `$fn` is rejected even when there are no rows.
437
673
  */
438
674
  function accumulatorFactory(expr) {
439
675
  assertAggregateFn(expr.$fn);
440
676
  const field = expr.$field;
441
677
  const read = pathReader(field);
442
- switch (expr.$fn) {
443
- case "count": {
444
- const all = field === "*";
445
- return () => {
446
- let n = 0;
447
- return {
448
- add: (row) => {
449
- if (all || read(row) != null) n++;
450
- },
451
- result: () => n
452
- };
453
- };
454
- }
455
- case "sum":
456
- case "avg": {
457
- const avg = expr.$fn === "avg";
458
- return () => {
459
- let sum = 0;
460
- let n = 0;
461
- return {
462
- add: (row) => {
463
- const v = numericValue(read(row));
464
- if (v !== void 0) {
465
- sum += v;
466
- n++;
467
- }
468
- },
469
- result: () => n === 0 ? null : avg ? sum / n : sum
470
- };
471
- };
472
- }
473
- case "countDistinct": return () => {
474
- const plain = /* @__PURE__ */ new Set();
475
- const tokens = /* @__PURE__ */ new Set();
678
+ if (expr.$fn === "count") {
679
+ const all = field === "*";
680
+ return () => {
681
+ let n = 0;
476
682
  return {
477
683
  add: (row) => {
478
- const v = read(row);
479
- if (v == null) return;
480
- if (typeof v === "string" || typeof v === "boolean") plain.add(v);
481
- else tokens.add(identityToken(v));
684
+ if (all || read(row) != null) n++;
482
685
  },
483
- result: () => plain.size + tokens.size
686
+ result: () => n
484
687
  };
485
688
  };
486
- case "min":
487
- case "max": {
488
- const sign = expr.$fn === "min" ? -1 : 1;
489
- return () => {
490
- let best = null;
491
- return {
492
- add: (row) => {
493
- const v = read(row);
494
- if (v == null) return;
495
- if (best === null || compareLeaves(v, best) * sign > 0) best = v;
496
- },
497
- result: () => best
498
- };
499
- };
500
- }
501
- default: return expr.$fn;
502
689
  }
690
+ if (expr.$fn === "countDistinct") return () => {
691
+ const plain = /* @__PURE__ */ new Set();
692
+ const tokens = /* @__PURE__ */ new Set();
693
+ return {
694
+ add: (row) => {
695
+ const v = read(row);
696
+ if (v == null) return;
697
+ if (typeof v === "string" || typeof v === "boolean") plain.add(v);
698
+ else tokens.add(identityToken(v));
699
+ },
700
+ result: () => plain.size + tokens.size
701
+ };
702
+ };
703
+ return fnAccumulator(expr.$fn, read);
503
704
  }
504
705
  //#endregion
505
706
  //#region src/lib/memory-adapter.ts
@@ -526,7 +727,7 @@ const databases = /* @__PURE__ */ new WeakMap();
526
727
  * runtime-owned entity with NO database can be observed as a READ-ONLY
527
728
  * atscript table; all writes are rejected (see {@link _assertWritable}).
528
729
  */
529
- var MemoryAdapter = class extends BaseDbAdapter {
730
+ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
530
731
  /**
531
732
  * The in-memory database: the space's (see {@link databases}), resolved once
532
733
  * in {@link registerSpace}; an adapter used without a space keeps this own one.
@@ -582,6 +783,19 @@ var MemoryAdapter = class extends BaseDbAdapter {
582
783
  return !fd.encrypted;
583
784
  }
584
785
  /**
786
+ * Relational predicates (`{ nav: { $some | $none: … } }`) are evaluated in
787
+ * reads AND mutation filters. Before a filter holding them is compiled, every
788
+ * related table (and junction) is loaded ONCE for that operation — through
789
+ * its own adapter's snapshot seam, so a provider-backed related table works
790
+ * too — and each predicate becomes a set of matching correlation keys
791
+ * (see {@link _relationSets}). Predicate-free filters take the plain path.
792
+ *
793
+ * @since 0.1.147
794
+ */
795
+ supportsRelationFilters(_mode) {
796
+ return true;
797
+ }
798
+ /**
585
799
  * Coerces a by-id value to the id field's declared leaf type. The framework
586
800
  * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
587
801
  * URL id like `"21"` reaches this adapter as the STRING `"21"`. Memory does
@@ -642,14 +856,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
642
856
  /**
643
857
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
644
858
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
645
- * PK path through `pathToPhysical` (falling back to the name itself). Memoized
859
+ * PK path through `physicalPath` (`@db.column` renames included). Memoized
646
860
  * because it is stable per adapter (1:1 with a fixed table) yet read on every
647
861
  * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
648
862
  */
649
863
  _physicalPkFields() {
650
864
  if (this._pkFieldsCache) return this._pkFieldsCache;
651
865
  const metaIdPhysical = this._table.metaIdPhysical;
652
- const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.pathToPhysical.get(pk) ?? pk);
866
+ const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.physicalPath(pk));
653
867
  this._pkFieldsCache = fields;
654
868
  return fields;
655
869
  }
@@ -807,6 +1021,39 @@ var MemoryAdapter = class extends BaseDbAdapter {
807
1021
  return [...this._peekState()?.rows.values() ?? []];
808
1022
  }
809
1023
  /**
1024
+ * A per-operation row loader for relational predicates: each related table
1025
+ * is snapshotted at most once per operation (the first predicate that needs
1026
+ * it loads it; later ones — and nested ones — reuse that snapshot), so a
1027
+ * provider-backed related table is invoked once per read. `own` seeds this
1028
+ * table's snapshot so a self relation reads the same rows as the outer scan.
1029
+ */
1030
+ _snapshotLoader(own) {
1031
+ const snapshots = /* @__PURE__ */ new Map();
1032
+ if (own) snapshots.set(this, own);
1033
+ return (adapter) => {
1034
+ let rows = snapshots.get(adapter);
1035
+ if (!rows) {
1036
+ if (!(adapter instanceof MemoryAdapter)) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
1037
+ path: "",
1038
+ message: "Relational predicate: the related table is not served by the memory adapter"
1039
+ }]);
1040
+ rows = Promise.resolve(adapter._loadRows());
1041
+ snapshots.set(adapter, rows);
1042
+ }
1043
+ return rows;
1044
+ };
1045
+ }
1046
+ /**
1047
+ * The prepared correlation sets for `filter`'s relational predicates.
1048
+ * Callers only await it when `containsRelationFilter(filter)` — a
1049
+ * predicate-free filter keeps its fully synchronous path. Write methods call
1050
+ * it BEFORE reading the table state, so the related tables (a self relation
1051
+ * included) are snapshotted before any row changes, like SQL.
1052
+ */
1053
+ async _relationSets(filter, own) {
1054
+ return prepareRelationSets(filter, this._snapshotLoader(own));
1055
+ }
1056
+ /**
810
1057
  * Clones the payload in, applies the version default, generates any
811
1058
  * `@db.default.increment` values, enforces PK + unique constraints, stores
812
1059
  * the row, and returns the resolved inserted id. Clone-in (`structuredClone`)
@@ -875,6 +1122,22 @@ var MemoryAdapter = class extends BaseDbAdapter {
875
1122
  insertedIds
876
1123
  };
877
1124
  }
1125
+ supportsInsertIgnore() {
1126
+ return true;
1127
+ }
1128
+ /** A row colliding on the primary key or a unique index (`CONFLICT`) becomes a skipped slot. */
1129
+ async insertManyIgnore(data) {
1130
+ this._assertWritable();
1131
+ const state = this._state();
1132
+ return data.map((item) => {
1133
+ try {
1134
+ return { insertedId: this._insertRow(state, item) };
1135
+ } catch (error) {
1136
+ if (isConflict(error)) return null;
1137
+ throw error;
1138
+ }
1139
+ });
1140
+ }
878
1141
  /**
879
1142
  * Selects the stored rows a write should touch, as `{ key, row }` pairs so
880
1143
  * callers can mutate in place and re-key. Stored mode scans the table's Map
@@ -890,11 +1153,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
890
1153
  *
891
1154
  * When `many` is `false` at most the first match is returned. A table that
892
1155
  * does not exist (`state` undefined) matches nothing and is not created.
1156
+ *
1157
+ * A filter with relational predicates needs `relationSets`, prepared by the
1158
+ * caller BEFORE it reads `state` (see {@link _relationSets}).
893
1159
  */
894
- _selectForWrite(state, filter, expectedVersion, many) {
1160
+ _selectForWrite(state, filter, expectedVersion, many, relationSets) {
895
1161
  const versionColumn = this._table.versionColumnPhysical;
896
1162
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
897
- const match = buildMemoryPredicate(filter);
1163
+ const match = buildMemoryPredicate(filter, relationSets);
898
1164
  const matched = [];
899
1165
  for (const [key, row] of state?.rows ?? []) {
900
1166
  if (!match(row)) continue;
@@ -974,8 +1240,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
974
1240
  }
975
1241
  async replaceOne(filter, data, expectedVersion) {
976
1242
  this._assertWritable();
1243
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
977
1244
  const state = this._peekState();
978
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1245
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
979
1246
  if (!state || matched.length === 0) return {
980
1247
  matchedCount: 0,
981
1248
  modifiedCount: 0
@@ -992,8 +1259,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
992
1259
  }
993
1260
  async updateOne(filter, data, ops, expectedVersion) {
994
1261
  this._assertWritable();
1262
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
995
1263
  const state = this._peekState();
996
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1264
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
997
1265
  if (!state || matched.length === 0) return {
998
1266
  matchedCount: 0,
999
1267
  modifiedCount: 0
@@ -1007,8 +1275,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
1007
1275
  }
1008
1276
  async deleteOne(filter) {
1009
1277
  this._assertWritable();
1278
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1010
1279
  const state = this._peekState();
1011
- const matched = this._selectForWrite(state, filter, void 0, false);
1280
+ const matched = this._selectForWrite(state, filter, void 0, false, sets);
1012
1281
  if (!state || matched.length === 0) return { deletedCount: 0 };
1013
1282
  state.rows.delete(matched[0].key);
1014
1283
  return { deletedCount: 1 };
@@ -1060,8 +1329,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
1060
1329
  * working set — one provider invocation per logical read.
1061
1330
  */
1062
1331
  async _filteredRows(query) {
1063
- const match = buildMemoryPredicate(query.filter);
1064
- return (await this._loadRows()).filter(match);
1332
+ if (!containsRelationFilter(query.filter)) {
1333
+ const match = buildMemoryPredicate(query.filter);
1334
+ return (await this._loadRows()).filter(match);
1335
+ }
1336
+ const own = Promise.resolve(this._loadRows());
1337
+ own.catch(() => {});
1338
+ const sets = await this._relationSets(query.filter, own);
1339
+ return (await own).filter(buildMemoryPredicate(query.filter, sets));
1065
1340
  }
1066
1341
  async findOne(query) {
1067
1342
  const filtered = await this._filteredRows(query);
@@ -1112,18 +1387,27 @@ var MemoryAdapter = class extends BaseDbAdapter {
1112
1387
  calendarBucketUnits() {
1113
1388
  return ALL_BUCKET_UNITS;
1114
1389
  }
1115
- /** Every aggregate function, `countDistinct` included. */
1390
+ /** Every aggregate function: `countDistinct`, `first` and `last` included. */
1116
1391
  aggregateFns() {
1117
1392
  return ALL_AGGREGATE_FNS;
1118
1393
  }
1394
+ /** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
1395
+ supportsAggregateExpressions() {
1396
+ return true;
1397
+ }
1398
+ /** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
1399
+ viewCapabilities() {
1400
+ return ALL_VIEW_CAPABILITIES;
1401
+ }
1119
1402
  async updateMany(filter, data, ops) {
1120
1403
  this._assertWritable();
1404
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1121
1405
  const state = this._peekState();
1122
1406
  if (!state) return {
1123
1407
  matchedCount: 0,
1124
1408
  modifiedCount: 0
1125
1409
  };
1126
- const matched = this._selectForWrite(state, filter, void 0, true);
1410
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1127
1411
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1128
1412
  return {
1129
1413
  matchedCount: matched.length,
@@ -1132,12 +1416,13 @@ var MemoryAdapter = class extends BaseDbAdapter {
1132
1416
  }
1133
1417
  async replaceMany(filter, data) {
1134
1418
  this._assertWritable();
1419
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1135
1420
  const state = this._peekState();
1136
1421
  if (!state) return {
1137
1422
  matchedCount: 0,
1138
1423
  modifiedCount: 0
1139
1424
  };
1140
- const matched = this._selectForWrite(state, filter, void 0, true);
1425
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1141
1426
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data);
1142
1427
  return {
1143
1428
  matchedCount: matched.length,
@@ -1146,9 +1431,10 @@ var MemoryAdapter = class extends BaseDbAdapter {
1146
1431
  }
1147
1432
  async deleteMany(filter) {
1148
1433
  this._assertWritable();
1434
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1149
1435
  const state = this._peekState();
1150
1436
  if (!state) return { deletedCount: 0 };
1151
- const matched = this._selectForWrite(state, filter, void 0, true);
1437
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1152
1438
  for (const { key } of matched) state.rows.delete(key);
1153
1439
  return { deletedCount: matched.length };
1154
1440
  }
@@ -1231,6 +1517,10 @@ function setMemoryProvider(space, type, fn) {
1231
1517
  if (!(adapter instanceof MemoryAdapter)) throw new Error("setMemoryProvider: table is not backed by MemoryAdapter");
1232
1518
  adapter.setProvider(fn);
1233
1519
  }
1520
+ /** @internal Drops the in-memory database of a closed space (called from `createAdapter`'s `onClose`). */
1521
+ function clearMemoryDatabase(space) {
1522
+ databases.get(space)?.clear();
1523
+ }
1234
1524
  //#endregion
1235
1525
  //#region src/lib/index.ts
1236
1526
  /**
@@ -1243,7 +1533,8 @@ function setMemoryProvider(space, type, fn) {
1243
1533
  * adapter has been built.
1244
1534
  */
1245
1535
  function createAdapter() {
1246
- return new DbSpace(() => new MemoryAdapter());
1536
+ const space = new DbSpace(() => new MemoryAdapter(), { onClose: () => clearMemoryDatabase(space) });
1537
+ return space;
1247
1538
  }
1248
1539
  //#endregion
1249
1540
  export { MemoryAdapter, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-memory",
3
- "version": "0.1.146",
3
+ "version": "0.1.148",
4
4
  "description": "In-memory adapter for atscript-db.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,16 +36,16 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@atscript/core": "^0.1.98",
40
- "@atscript/typescript": "^0.1.98",
41
- "@uniqu/core": "^0.1.11",
42
- "unplugin-atscript": "^0.1.98"
39
+ "@atscript/core": "^0.1.100",
40
+ "@atscript/typescript": "^0.1.100",
41
+ "@uniqu/core": "^0.1.13",
42
+ "unplugin-atscript": "^0.1.100"
43
43
  },
44
44
  "peerDependencies": {
45
- "@atscript/core": "^0.1.98",
46
- "@atscript/typescript": "^0.1.98",
47
- "@uniqu/core": "^0.1.11",
48
- "@atscript/db": "^0.1.146"
45
+ "@atscript/core": "^0.1.100",
46
+ "@atscript/typescript": "^0.1.100",
47
+ "@uniqu/core": "^0.1.13",
48
+ "@atscript/db": "^0.1.148"
49
49
  },
50
50
  "scripts": {
51
51
  "postinstall": "asc -f dts",