@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.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
|
|
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) =>
|
|
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,
|
|
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
|
|
329
|
-
|
|
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 =
|
|
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),
|
|
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
|
-
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
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
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
1064
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
40
|
-
"@atscript/typescript": "^0.1.
|
|
41
|
-
"@uniqu/core": "^0.1.
|
|
42
|
-
"unplugin-atscript": "^0.1.
|
|
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.
|
|
46
|
-
"@atscript/typescript": "^0.1.
|
|
47
|
-
"@uniqu/core": "^0.1.
|
|
48
|
-
"@atscript/db": "^0.1.
|
|
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",
|