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