@atscript/db-memory 0.1.150 → 0.1.152
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 +447 -114
- package/dist/index.d.cts +53 -17
- package/dist/index.d.mts +53 -17
- package/dist/index.mjs +452 -119
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,69 @@
|
|
|
1
|
-
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, INTEGER_REGEX_OP, containsRelationFilter, deletePath, evaluateExpr, forEachResolvedRelation, getPath,
|
|
1
|
+
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, INTEGER_REGEX_OP, containsRelationFilter, deletePath, evaluateExpr, forEachResolvedRelation, getPath, isAtscriptDbView, isConflict, isPlainObject, isResolvedRelationFilter, walkFilter } from "@atscript/db";
|
|
2
2
|
import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
|
|
3
3
|
import { bucketer } from "@uniqu/core";
|
|
4
|
+
//#region src/lib/memory-clone.ts
|
|
5
|
+
/**
|
|
6
|
+
* Deep clone with the exact observable result of `structuredClone` — fast for
|
|
7
|
+
* the plain data the store holds (plain objects, dense arrays, `Date`s and
|
|
8
|
+
* primitives), and delegating to `structuredClone` for everything else.
|
|
9
|
+
*
|
|
10
|
+
* The fast walk gives up — and the WHOLE value is re-cloned by
|
|
11
|
+
* `structuredClone` — on anything whose `structuredClone` result it would not
|
|
12
|
+
* reproduce exactly: a function or symbol (so the `DataCloneError` still
|
|
13
|
+
* throws), a non-plain prototype (class instances, `Map`, `Set`, typed arrays,
|
|
14
|
+
* `RegExp`, boxed primitives, …), an own `__proto__` key, a sparse array or
|
|
15
|
+
* one carrying extra own properties, and an object reached twice (a cycle or a
|
|
16
|
+
* shared subtree — `structuredClone` preserves both, a tree walk would not).
|
|
17
|
+
*/
|
|
18
|
+
function cloneValue(value) {
|
|
19
|
+
if (value === null || typeof value !== "object") return typeof value === "function" || typeof value === "symbol" ? structuredClone(value) : value;
|
|
20
|
+
const out = walk(value, /* @__PURE__ */ new Set());
|
|
21
|
+
return out === GIVE_UP ? structuredClone(value) : out;
|
|
22
|
+
}
|
|
23
|
+
const GIVE_UP = Symbol("give-up");
|
|
24
|
+
const OBJECT_PROTO = Object.prototype;
|
|
25
|
+
const ARRAY_PROTO = Array.prototype;
|
|
26
|
+
const DATE_PROTO = Date.prototype;
|
|
27
|
+
function walk(value, seen) {
|
|
28
|
+
if (seen.has(value)) return GIVE_UP;
|
|
29
|
+
seen.add(value);
|
|
30
|
+
const proto = Object.getPrototypeOf(value);
|
|
31
|
+
if (proto === ARRAY_PROTO) {
|
|
32
|
+
const src = value;
|
|
33
|
+
const len = src.length;
|
|
34
|
+
const arr = [];
|
|
35
|
+
for (let i = 0; i < len; i++) {
|
|
36
|
+
if (!(i in src)) return GIVE_UP;
|
|
37
|
+
const item = leaf(src[i], seen);
|
|
38
|
+
if (item === GIVE_UP) return GIVE_UP;
|
|
39
|
+
arr.push(item);
|
|
40
|
+
}
|
|
41
|
+
return Object.keys(src).length === len ? arr : GIVE_UP;
|
|
42
|
+
}
|
|
43
|
+
if (proto === OBJECT_PROTO || proto === null) {
|
|
44
|
+
const src = value;
|
|
45
|
+
const obj = {};
|
|
46
|
+
for (const key of Object.keys(src)) {
|
|
47
|
+
if (key === "__proto__") return GIVE_UP;
|
|
48
|
+
const item = leaf(src[key], seen);
|
|
49
|
+
if (item === GIVE_UP) return GIVE_UP;
|
|
50
|
+
obj[key] = item;
|
|
51
|
+
}
|
|
52
|
+
return obj;
|
|
53
|
+
}
|
|
54
|
+
if (proto === DATE_PROTO) return new Date(value.getTime());
|
|
55
|
+
return GIVE_UP;
|
|
56
|
+
}
|
|
57
|
+
function leaf(value, seen) {
|
|
58
|
+
if (value === null) return null;
|
|
59
|
+
switch (typeof value) {
|
|
60
|
+
case "object": return walk(value, seen);
|
|
61
|
+
case "function":
|
|
62
|
+
case "symbol": return GIVE_UP;
|
|
63
|
+
default: return value;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
4
67
|
//#region src/lib/memory-filter.ts
|
|
5
68
|
/**
|
|
6
69
|
* {@link getPath} compiled for one path: split once, and a single segment is
|
|
@@ -70,9 +133,8 @@ function parseRegexString(value) {
|
|
|
70
133
|
function stringifyLeaf(v) {
|
|
71
134
|
return String(v);
|
|
72
135
|
}
|
|
73
|
-
/** `$eq` semantics, factored out so `$ne` can be its exact negation. */
|
|
74
|
-
function evalEq(
|
|
75
|
-
const fieldValue = getPath$1(row, field);
|
|
136
|
+
/** `$eq` semantics over the field's value, factored out so `$ne` can be its exact negation. */
|
|
137
|
+
function evalEq(fieldValue, value) {
|
|
76
138
|
if (value === null) return matchesScalar(fieldValue, null);
|
|
77
139
|
if (value instanceof RegExp) return fieldValue != null && value.test(stringifyLeaf(fieldValue));
|
|
78
140
|
return matchesScalar(fieldValue, value);
|
|
@@ -88,11 +150,37 @@ function matchesScalar(fieldValue, value) {
|
|
|
88
150
|
if (Array.isArray(fieldValue) && !Array.isArray(value)) return fieldValue.some((el) => valuesEqual(el, value));
|
|
89
151
|
return valuesEqual(fieldValue, value);
|
|
90
152
|
}
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
153
|
+
/**
|
|
154
|
+
* `$in` membership compiled once per filter leaf, factored out so `$nin` can be
|
|
155
|
+
* its exact negation: true iff the field value {@link matchesScalar matches}
|
|
156
|
+
* SOME element. Primitive elements (and `Date`s, by instant) are looked up in a
|
|
157
|
+
* `Set` instead of scanned per row — `NaN` is left out of it (it equals
|
|
158
|
+
* nothing, while a `Set` would find it), `-0`/`0` collapse as under `===`.
|
|
159
|
+
* Any other element (an object / array — reference equality) keeps the scan.
|
|
160
|
+
*/
|
|
161
|
+
function compileIn(value) {
|
|
162
|
+
if (!Array.isArray(value)) return () => false;
|
|
163
|
+
const primitives = /* @__PURE__ */ new Set();
|
|
164
|
+
const instants = /* @__PURE__ */ new Set();
|
|
165
|
+
const rest = [];
|
|
166
|
+
let hasNull = false;
|
|
167
|
+
for (const element of value) if (element === null) hasNull = true;
|
|
168
|
+
else if (element instanceof Date) {
|
|
169
|
+
const time = element.getTime();
|
|
170
|
+
if (!Number.isNaN(time)) instants.add(time);
|
|
171
|
+
} else if (typeof element === "object") rest.push(element);
|
|
172
|
+
else if (!(typeof element === "number" && Number.isNaN(element))) primitives.add(element);
|
|
173
|
+
const one = (v) => v instanceof Date ? instants.has(v.getTime()) : (v === null || typeof v !== "object") && primitives.has(v);
|
|
174
|
+
return (fieldValue) => {
|
|
175
|
+
if (Array.isArray(fieldValue)) {
|
|
176
|
+
if (hasNull && fieldValue.includes(null)) return true;
|
|
177
|
+
for (const el of fieldValue) if (one(el)) return true;
|
|
178
|
+
return rest.some((el) => Array.isArray(el) ? el === fieldValue : fieldValue.includes(el));
|
|
179
|
+
}
|
|
180
|
+
if (fieldValue === null) return hasNull;
|
|
181
|
+
if (fieldValue === void 0) return hasNull || primitives.has(void 0);
|
|
182
|
+
return one(fieldValue) || rest.includes(fieldValue);
|
|
183
|
+
};
|
|
96
184
|
}
|
|
97
185
|
/**
|
|
98
186
|
* Coerce a leaf to something the JS relational operators can order. `Date`s
|
|
@@ -110,8 +198,7 @@ function toOrdinal(v) {
|
|
|
110
198
|
* lexicographically) — NO collation or locale awareness. This intentionally
|
|
111
199
|
* differs from SQL engines' collated ordering.
|
|
112
200
|
*/
|
|
113
|
-
function evalRelational(
|
|
114
|
-
const fieldValue = getPath$1(row, field);
|
|
201
|
+
function evalRelational(fieldValue, op, value) {
|
|
115
202
|
if (fieldValue === void 0 || fieldValue === null) return false;
|
|
116
203
|
const a = toOrdinal(fieldValue);
|
|
117
204
|
const b = toOrdinal(value);
|
|
@@ -143,32 +230,39 @@ const memoryVisitor = {
|
|
|
143
230
|
return (row) => !child(row);
|
|
144
231
|
},
|
|
145
232
|
comparison(field, op, value) {
|
|
233
|
+
const read = pathReader(field);
|
|
146
234
|
if (op === INTEGER_REGEX_OP) {
|
|
147
235
|
const { pattern, flags } = parseRegexString(value);
|
|
148
236
|
const regex = new RegExp(pattern, flags);
|
|
149
237
|
return (row) => {
|
|
150
|
-
const fieldValue =
|
|
238
|
+
const fieldValue = read(row);
|
|
151
239
|
return (typeof fieldValue === "bigint" || typeof fieldValue === "number" && Number.isInteger(fieldValue)) && regex.test(String(fieldValue));
|
|
152
240
|
};
|
|
153
241
|
}
|
|
154
242
|
switch (op) {
|
|
155
|
-
case "$eq": return (row) => evalEq(row,
|
|
156
|
-
case "$ne": return (row) => !evalEq(row,
|
|
243
|
+
case "$eq": return (row) => evalEq(read(row), value);
|
|
244
|
+
case "$ne": return (row) => !evalEq(read(row), value);
|
|
157
245
|
case "$gt":
|
|
158
246
|
case "$gte":
|
|
159
247
|
case "$lt":
|
|
160
|
-
case "$lte": return (row) => evalRelational(row,
|
|
161
|
-
case "$in":
|
|
162
|
-
|
|
248
|
+
case "$lte": return (row) => evalRelational(read(row), op, value);
|
|
249
|
+
case "$in": {
|
|
250
|
+
const isIn = compileIn(value);
|
|
251
|
+
return (row) => isIn(read(row));
|
|
252
|
+
}
|
|
253
|
+
case "$nin": {
|
|
254
|
+
const isIn = compileIn(value);
|
|
255
|
+
return (row) => !isIn(read(row));
|
|
256
|
+
}
|
|
163
257
|
case "$regex": {
|
|
164
258
|
const { pattern, flags } = parseRegexString(value);
|
|
165
259
|
const regex = new RegExp(pattern, flags);
|
|
166
260
|
return (row) => {
|
|
167
|
-
const fieldValue =
|
|
261
|
+
const fieldValue = read(row);
|
|
168
262
|
return fieldValue != null && regex.test(stringifyLeaf(fieldValue));
|
|
169
263
|
};
|
|
170
264
|
}
|
|
171
|
-
case "$exists": return (row) => value === !evalEq(row,
|
|
265
|
+
case "$exists": return (row) => value === !evalEq(read(row), null);
|
|
172
266
|
default: throw new DbError("INVALID_QUERY", [{
|
|
173
267
|
path: field,
|
|
174
268
|
message: `Unsupported filter operator: ${op}`
|
|
@@ -190,7 +284,7 @@ const memoryVisitor = {
|
|
|
190
284
|
function correlationKey(row, fields) {
|
|
191
285
|
const parts = [];
|
|
192
286
|
for (const field of fields) {
|
|
193
|
-
const part = keyPart(getPath
|
|
287
|
+
const part = keyPart(getPath(row, field));
|
|
194
288
|
if (part === void 0) return;
|
|
195
289
|
parts.push(part);
|
|
196
290
|
}
|
|
@@ -365,7 +459,10 @@ function compareLeaves(a, b) {
|
|
|
365
459
|
* nested dot-paths both work.
|
|
366
460
|
*/
|
|
367
461
|
function setPath(target, path, value) {
|
|
368
|
-
|
|
462
|
+
setSegments(target, path.split("."), value);
|
|
463
|
+
}
|
|
464
|
+
/** {@link setPath} over an already-split path. */
|
|
465
|
+
function setSegments(target, segments, value) {
|
|
369
466
|
let current = target;
|
|
370
467
|
for (let i = 0; i < segments.length - 1; i++) {
|
|
371
468
|
const seg = segments[i];
|
|
@@ -385,31 +482,88 @@ function setPath(target, path, value) {
|
|
|
385
482
|
* rows with equal sort keys still order deterministically. When ABSENT the
|
|
386
483
|
* sort falls back to preserving input order among equal keys (via each row's
|
|
387
484
|
* original index), so a consumer with no primary key keeps insertion order.
|
|
485
|
+
* - `topK` (since 0.1.151), when supplied, asks for only the first `topK` rows
|
|
486
|
+
* of that order (e.g. `$skip + $limit`): a small one is selected in one pass
|
|
487
|
+
* instead of sorting everything. The rows returned are exactly the head of
|
|
488
|
+
* the full sort — same comparator, same stable tie handling.
|
|
388
489
|
*
|
|
389
|
-
* NEVER mutates the input array
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* the O(n log n) comparator.
|
|
490
|
+
* NEVER mutates the input array. Each row's sort keys are read ONCE (O(n)) and
|
|
491
|
+
* its `tieBreak` only when two rows tie on every key — not inside the
|
|
492
|
+
* O(n log n) comparator.
|
|
393
493
|
*/
|
|
394
|
-
function sortRows(rows, $sort, tieBreak) {
|
|
494
|
+
function sortRows(rows, $sort, tieBreak, topK) {
|
|
395
495
|
const keys = $sort ? Object.entries($sort) : [];
|
|
396
496
|
if (keys.length === 0) return rows;
|
|
397
|
-
|
|
497
|
+
const readers = keys.map(([field]) => pathReader(field));
|
|
498
|
+
const desc = keys.map(([, dir]) => dir === -1);
|
|
499
|
+
const decorated = rows.map((row, index) => ({
|
|
398
500
|
row,
|
|
399
501
|
index,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
502
|
+
keys: readers.map((read) => {
|
|
503
|
+
const value = read(row);
|
|
504
|
+
return value instanceof Date ? value.getTime() : value;
|
|
505
|
+
}),
|
|
506
|
+
tie: void 0
|
|
507
|
+
}));
|
|
508
|
+
const compare = (a, b) => {
|
|
509
|
+
for (let i = 0; i < desc.length; i++) {
|
|
510
|
+
const cmp = compareLeaves(a.keys[i], b.keys[i]);
|
|
511
|
+
if (cmp !== 0) return desc[i] ? -cmp : cmp;
|
|
405
512
|
}
|
|
406
513
|
if (tieBreak) {
|
|
407
|
-
const at = a.tie;
|
|
408
|
-
const bt = b.tie;
|
|
514
|
+
const at = a.tie ??= tieBreak(a.row);
|
|
515
|
+
const bt = b.tie ??= tieBreak(b.row);
|
|
409
516
|
return at < bt ? -1 : at > bt ? 1 : 0;
|
|
410
517
|
}
|
|
411
518
|
return a.index - b.index;
|
|
412
|
-
}
|
|
519
|
+
};
|
|
520
|
+
return (topK !== void 0 && topK <= TOP_K_MAX && topK * 4 < decorated.length && totallyOrdered(decorated, desc.length) ? selectTop(decorated, topK, compare) : decorated.toSorted(compare)).map((entry) => entry.row);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Whether every sort key holds values of ONE ordered kind — numbers (`Date`s
|
|
524
|
+
* included) without `NaN`, strings, or booleans — besides `null`/missing.
|
|
525
|
+
* Only then is the comparator a total order, so selecting the head gives
|
|
526
|
+
* exactly the rows the full (stable) sort puts first; mixed kinds compare
|
|
527
|
+
* inconsistently (`"a" < 1` and `"a" > 1` are both false), and their full
|
|
528
|
+
* sort order depends on the algorithm, so it keeps the full sort.
|
|
529
|
+
*/
|
|
530
|
+
function totallyOrdered(entries, keyCount) {
|
|
531
|
+
for (let i = 0; i < keyCount; i++) {
|
|
532
|
+
let kind;
|
|
533
|
+
for (const entry of entries) {
|
|
534
|
+
const value = entry.keys[i];
|
|
535
|
+
if (value === null || value === void 0) continue;
|
|
536
|
+
const type = typeof value;
|
|
537
|
+
if (type === "number" ? Number.isNaN(value) : type !== "string" && type !== "boolean") return false;
|
|
538
|
+
if (kind === void 0) kind = type;
|
|
539
|
+
else if (kind !== type) return false;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
/** Largest `topK` {@link sortRows} selects in one pass instead of sorting. */
|
|
545
|
+
const TOP_K_MAX = 128;
|
|
546
|
+
/**
|
|
547
|
+
* The first `k` entries of the stable sort by `compare`, in order: a bounded
|
|
548
|
+
* sorted buffer, each entry inserted AFTER its equals (entries arrive in input
|
|
549
|
+
* order, so ties keep it — exactly as the stable full sort does).
|
|
550
|
+
*/
|
|
551
|
+
function selectTop(entries, k, compare) {
|
|
552
|
+
const top = [];
|
|
553
|
+
if (k <= 0) return top;
|
|
554
|
+
for (const entry of entries) {
|
|
555
|
+
if (top.length === k && compare(entry, top[k - 1]) >= 0) continue;
|
|
556
|
+
let lo = 0;
|
|
557
|
+
let hi = top.length;
|
|
558
|
+
while (lo < hi) {
|
|
559
|
+
const mid = lo + hi >>> 1;
|
|
560
|
+
if (compare(entry, top[mid]) < 0) hi = mid;
|
|
561
|
+
else lo = mid + 1;
|
|
562
|
+
}
|
|
563
|
+
top.splice(lo, 0, entry);
|
|
564
|
+
if (top.length > k) top.pop();
|
|
565
|
+
}
|
|
566
|
+
return top;
|
|
413
567
|
}
|
|
414
568
|
/**
|
|
415
569
|
* Applies `$skip` then `$limit` (both optional) via a single slice. No-op
|
|
@@ -438,24 +592,121 @@ function paginate(rows, skip, limit) {
|
|
|
438
592
|
*
|
|
439
593
|
* Top-level and nested dot-paths are supported; exotic Mongo projection quirks
|
|
440
594
|
* (array positional, `$slice`, etc.) are intentionally NOT replicated.
|
|
595
|
+
*
|
|
596
|
+
* Clones are `structuredClone`-equivalent. To project many rows the same way,
|
|
597
|
+
* compile once with {@link compileProjection}.
|
|
441
598
|
*/
|
|
442
599
|
function projectRow(row, projection, opts) {
|
|
600
|
+
return compileProjection(projection, opts)(row);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* {@link projectRow} compiled for one projection: the projection map is read
|
|
604
|
+
* and every dot-path split once, then applied to each row.
|
|
605
|
+
*/
|
|
606
|
+
function compileProjection(projection, opts) {
|
|
443
607
|
const clone = opts?.clone ?? false;
|
|
444
608
|
const entries = projection ? Object.entries(projection) : [];
|
|
445
|
-
if (entries.length === 0) return clone ?
|
|
609
|
+
if (entries.length === 0) return clone ? cloneValue : (row) => row;
|
|
446
610
|
if (entries[0][1] === 1) {
|
|
447
611
|
const paths = new Set(entries.filter(([, v]) => v === 1).map(([k]) => k));
|
|
448
612
|
for (const pk of opts?.pkFields ?? []) paths.add(pk);
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
613
|
+
const plan = [...paths].map((path) => ({
|
|
614
|
+
read: pathReader(path),
|
|
615
|
+
segments: path.split(".")
|
|
616
|
+
}));
|
|
617
|
+
return (row) => {
|
|
618
|
+
const out = {};
|
|
619
|
+
for (const { read, segments } of plan) {
|
|
620
|
+
const value = read(row);
|
|
621
|
+
if (value !== void 0) setSegments(out, segments, value);
|
|
622
|
+
}
|
|
623
|
+
return clone ? cloneValue(out) : out;
|
|
624
|
+
};
|
|
455
625
|
}
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
626
|
+
const dropped = entries.filter(([, v]) => v === 0).map(([path]) => path.split("."));
|
|
627
|
+
return (row) => {
|
|
628
|
+
const out = cloneValue(row);
|
|
629
|
+
for (const segments of dropped) deletePath(out, segments);
|
|
630
|
+
return out;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
//#endregion
|
|
634
|
+
//#region src/lib/memory-unique.ts
|
|
635
|
+
/** Builds an empty {@link RecordedUniqueIndex}. */
|
|
636
|
+
function recordUniqueIndex(name, fields, optionalFields) {
|
|
637
|
+
return {
|
|
638
|
+
name,
|
|
639
|
+
fields,
|
|
640
|
+
readers: fields.map((field) => pathReader(field)),
|
|
641
|
+
optional: fields.map((field) => optionalFields.has(field)),
|
|
642
|
+
entries: /* @__PURE__ */ new Map()
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Identity of `row`'s tuple in `index`, or `undefined` when the row can never
|
|
647
|
+
* collide in it. Two rows collide exactly when every component is equal under
|
|
648
|
+
* the memory filter's strict equality (`valuesEqual`: `===`, `Date`s by
|
|
649
|
+
* instant), so each component is type-tagged:
|
|
650
|
+
*
|
|
651
|
+
* - an optional member absent / `null` → `undefined` (present-only index);
|
|
652
|
+
* - `NaN`, an invalid `Date`, or any other object (arrays included — compared
|
|
653
|
+
* by reference, and rows never share one) → `undefined`: equal to nothing;
|
|
654
|
+
* - a required member that is `null` or missing is a value like any other
|
|
655
|
+
* (`null` ≠ missing).
|
|
656
|
+
*/
|
|
657
|
+
function uniqueTupleKey(index, row) {
|
|
658
|
+
const parts = [];
|
|
659
|
+
for (let i = 0; i < index.readers.length; i++) {
|
|
660
|
+
const value = index.readers[i](row);
|
|
661
|
+
if (index.optional[i] && (value === null || value === void 0)) return;
|
|
662
|
+
const part = tuplePart(value);
|
|
663
|
+
if (part === void 0) return;
|
|
664
|
+
parts.push(part);
|
|
665
|
+
}
|
|
666
|
+
return JSON.stringify(parts);
|
|
667
|
+
}
|
|
668
|
+
function tuplePart(value) {
|
|
669
|
+
switch (typeof value) {
|
|
670
|
+
case "string": return `s:${value}`;
|
|
671
|
+
case "number": return Number.isNaN(value) ? void 0 : `n:${value === 0 ? 0 : value}`;
|
|
672
|
+
case "bigint": return `b:${value}`;
|
|
673
|
+
case "boolean": return `t:${value}`;
|
|
674
|
+
case "undefined": return "u";
|
|
675
|
+
default:
|
|
676
|
+
if (value === null) return "z";
|
|
677
|
+
if (value instanceof Date) {
|
|
678
|
+
const time = value.getTime();
|
|
679
|
+
return Number.isNaN(time) ? void 0 : `d:${time}`;
|
|
680
|
+
}
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/** Records `row` (stored under `key`) in every index; `tuples` are its precomputed tuple keys. */
|
|
685
|
+
function indexRow(indexes, key, row, tuples) {
|
|
686
|
+
for (let i = 0; i < indexes.length; i++) {
|
|
687
|
+
const index = indexes[i];
|
|
688
|
+
const tuple = tuples ? tuples[i] : uniqueTupleKey(index, row);
|
|
689
|
+
if (tuple === void 0) continue;
|
|
690
|
+
const holders = index.entries.get(tuple);
|
|
691
|
+
if (holders) holders.add(key);
|
|
692
|
+
else index.entries.set(tuple, new Set([key]));
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/** Removes `row` (stored under `key`) from every index. */
|
|
696
|
+
function unindexRow(indexes, key, row) {
|
|
697
|
+
for (const index of indexes) {
|
|
698
|
+
const tuple = uniqueTupleKey(index, row);
|
|
699
|
+
if (tuple === void 0) continue;
|
|
700
|
+
const holders = index.entries.get(tuple);
|
|
701
|
+
if (holders?.delete(key) && holders.size === 0) index.entries.delete(tuple);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
/** Whether a row other than the one stored under `excludeKey` holds `tuple`. */
|
|
705
|
+
function tupleTaken(index, tuple, excludeKey) {
|
|
706
|
+
const holders = index.entries.get(tuple);
|
|
707
|
+
if (!holders) return false;
|
|
708
|
+
if (excludeKey === void 0 || !holders.has(excludeKey)) return holders.size > 0;
|
|
709
|
+
return holders.size > 1;
|
|
459
710
|
}
|
|
460
711
|
//#endregion
|
|
461
712
|
//#region src/lib/memory-aggregate.ts
|
|
@@ -550,7 +801,7 @@ function aggregateRows(rows, controls) {
|
|
|
550
801
|
}
|
|
551
802
|
/** Group values and min / max results may be store-owned objects (JSON values): hand back copies. */
|
|
552
803
|
function copyValue(value) {
|
|
553
|
-
return value !== null && typeof value === "object" ?
|
|
804
|
+
return value !== null && typeof value === "object" ? cloneValue(value) : value;
|
|
554
805
|
}
|
|
555
806
|
/** Reads one `$groupBy` key off a row: a bucket alias → its label, a path → its value. */
|
|
556
807
|
function groupKeyReader(key, controls) {
|
|
@@ -743,6 +994,8 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
743
994
|
_db = /* @__PURE__ */ new Map();
|
|
744
995
|
/** Memoized physical PK field names — stable for the adapter's lifetime. */
|
|
745
996
|
_pkFieldsCache;
|
|
997
|
+
/** Memoized compiled readers of {@link _physicalPkFields}. */
|
|
998
|
+
_pkReadersCache;
|
|
746
999
|
/**
|
|
747
1000
|
* Memoized map of PHYSICAL field name → optional `start` for every field
|
|
748
1001
|
* carrying `@db.default.increment`. Stable per adapter (see
|
|
@@ -855,6 +1108,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
855
1108
|
kind: this._table.isView ? "view" : "table",
|
|
856
1109
|
rows: /* @__PURE__ */ new Map(),
|
|
857
1110
|
uniqueIndexes: [],
|
|
1111
|
+
opaquePk: false,
|
|
858
1112
|
incrementCounters: /* @__PURE__ */ new Map()
|
|
859
1113
|
};
|
|
860
1114
|
db.set(name, state);
|
|
@@ -875,6 +1129,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
875
1129
|
this._pkFieldsCache = fields;
|
|
876
1130
|
return fields;
|
|
877
1131
|
}
|
|
1132
|
+
/** Compiled readers of {@link _physicalPkFields} (memoized like it). */
|
|
1133
|
+
_pkReaders() {
|
|
1134
|
+
return this._pkReadersCache ??= this._physicalPkFields().map((field) => pathReader(field));
|
|
1135
|
+
}
|
|
878
1136
|
/**
|
|
879
1137
|
* PHYSICAL field name → optional `start` for every `@db.default.increment`
|
|
880
1138
|
* field. Discovered lazily from the table's field descriptors — each carries
|
|
@@ -954,8 +1212,25 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
954
1212
|
* `['a:b','c']` differ, and `1` differs from `'1'`.
|
|
955
1213
|
*/
|
|
956
1214
|
pkKey(row) {
|
|
957
|
-
const
|
|
958
|
-
return JSON.stringify(
|
|
1215
|
+
const readers = this._pkReaders();
|
|
1216
|
+
return JSON.stringify(readers.length === 1 ? [readers[0](row)] : readers.map((r) => r(row)));
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* The storage key a filter pins by exact equality on EVERY primary-key field
|
|
1220
|
+
* — `{ id: v }`, `{ id: { $eq: v } }`, also inside (nested) `$and` — or
|
|
1221
|
+
* `undefined`. Only a string / number / boolean / `Date` value pins: for
|
|
1222
|
+
* those, a row the filter matches is stored exactly under this key (strict
|
|
1223
|
+
* equality ⇒ equal `JSON.stringify`), so a read can look the row up and
|
|
1224
|
+
* still run the full predicate on it. Unavailable once the table holds an
|
|
1225
|
+
* {@link MemoryTableState.opaquePk opaque} key.
|
|
1226
|
+
*/
|
|
1227
|
+
_pinnedKey(state, filter) {
|
|
1228
|
+
if (state.opaquePk) return;
|
|
1229
|
+
const fields = this._physicalPkFields();
|
|
1230
|
+
const pinned = /* @__PURE__ */ new Map();
|
|
1231
|
+
collectPins(filter, fields, pinned);
|
|
1232
|
+
if (pinned.size !== fields.length) return;
|
|
1233
|
+
return JSON.stringify(fields.map((field) => pinned.get(field)));
|
|
959
1234
|
}
|
|
960
1235
|
/**
|
|
961
1236
|
* Builds a DEFINED inserted-id for a table with NO single `@meta.id` — a
|
|
@@ -975,7 +1250,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
975
1250
|
*/
|
|
976
1251
|
_compositeInsertedId(row) {
|
|
977
1252
|
const id = {};
|
|
978
|
-
for (const field of this._physicalPkFields()) id[field] = getPath
|
|
1253
|
+
for (const field of this._physicalPkFields()) id[field] = getPath(row, field);
|
|
979
1254
|
return id;
|
|
980
1255
|
}
|
|
981
1256
|
/**
|
|
@@ -1064,20 +1339,21 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1064
1339
|
/**
|
|
1065
1340
|
* Clones the payload in, applies the version default, generates any
|
|
1066
1341
|
* `@db.default.increment` values, enforces PK + unique constraints, stores
|
|
1067
|
-
* the row, and returns the resolved inserted id. Clone-in (
|
|
1068
|
-
* is what makes post-insert mutation of the
|
|
1342
|
+
* the row, and returns the resolved inserted id. Clone-in ({@link cloneValue},
|
|
1343
|
+
* `structuredClone`-equivalent) is what makes post-insert mutation of the
|
|
1344
|
+
* caller's object never leak into
|
|
1069
1345
|
* the store. Called once per item by both `insertOne` and `insertMany`, so
|
|
1070
1346
|
* increment values advance sequentially across a batch.
|
|
1071
1347
|
*/
|
|
1072
1348
|
_insertRow(state, data) {
|
|
1073
|
-
const row =
|
|
1349
|
+
const row = cloneValue(data);
|
|
1074
1350
|
const versionColumn = this._table.versionColumnPhysical;
|
|
1075
1351
|
if (versionColumn !== void 0 && !(versionColumn in row)) row[versionColumn] = 0;
|
|
1076
1352
|
this._applyIncrements(state.incrementCounters, row);
|
|
1077
1353
|
const key = this.pkKey(row);
|
|
1078
1354
|
if (state.rows.has(key)) throw this._pkConflict();
|
|
1079
|
-
this._enforceUniqueIndexes(state, row);
|
|
1080
|
-
|
|
1355
|
+
const tuples = this._enforceUniqueIndexes(state, row);
|
|
1356
|
+
this._storeRow(state, key, row, tuples);
|
|
1081
1357
|
const fallback = this._table.metaIdPhysical ? void 0 : this._compositeInsertedId(row);
|
|
1082
1358
|
return this._resolveInsertedId(row, fallback);
|
|
1083
1359
|
}
|
|
@@ -1085,38 +1361,40 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1085
1361
|
* Enforces every recorded unique index against the current store. A row is
|
|
1086
1362
|
* exempted from an index (present-only semantics) when ANY of that index's
|
|
1087
1363
|
* optional fields is absent/`null`. Otherwise a stored row with an equal
|
|
1088
|
-
* tuple → `CONFLICT`.
|
|
1364
|
+
* tuple → `CONFLICT`. Returns the row's tuple key per index (for
|
|
1365
|
+
* {@link _storeRow}).
|
|
1089
1366
|
*
|
|
1090
1367
|
* `excludeKey` (when given) skips the row stored under that {@link pkKey} — so
|
|
1091
1368
|
* a row updating its own unique value does not false-conflict with itself.
|
|
1092
1369
|
*/
|
|
1093
1370
|
_enforceUniqueIndexes(state, row, excludeKey) {
|
|
1094
|
-
|
|
1095
|
-
const tuple =
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
path: index.fields[0] ?? index.name,
|
|
1115
|
-
message: `Duplicate value for unique index "${index.name}"`
|
|
1116
|
-
}]);
|
|
1117
|
-
}
|
|
1371
|
+
return state.uniqueIndexes.map((index) => {
|
|
1372
|
+
const tuple = uniqueTupleKey(index, row);
|
|
1373
|
+
if (tuple !== void 0 && tupleTaken(index, tuple, excludeKey)) throw new DbError("CONFLICT", [{
|
|
1374
|
+
path: index.fields[0] ?? index.name,
|
|
1375
|
+
message: `Duplicate value for unique index "${index.name}"`
|
|
1376
|
+
}]);
|
|
1377
|
+
return tuple;
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Stores `row` under `key` and records it in the unique indexes (`tuples`:
|
|
1382
|
+
* its precomputed tuple keys). The ONLY way a row enters the store, so the
|
|
1383
|
+
* indexes and {@link MemoryTableState.opaquePk} never drift from it.
|
|
1384
|
+
*/
|
|
1385
|
+
_storeRow(state, key, row, tuples) {
|
|
1386
|
+
state.rows.set(key, row);
|
|
1387
|
+
indexRow(state.uniqueIndexes, key, row, tuples);
|
|
1388
|
+
if (!state.opaquePk) for (const read of this._pkReaders()) {
|
|
1389
|
+
const value = read(row);
|
|
1390
|
+
if (value !== null && typeof value === "object" && !(value instanceof Date)) state.opaquePk = true;
|
|
1118
1391
|
}
|
|
1119
1392
|
}
|
|
1393
|
+
/** Removes the row stored under `key` (and its unique-index entries). */
|
|
1394
|
+
_removeRow(state, key, row) {
|
|
1395
|
+
state.rows.delete(key);
|
|
1396
|
+
unindexRow(state.uniqueIndexes, key, row);
|
|
1397
|
+
}
|
|
1120
1398
|
async insertOne(data) {
|
|
1121
1399
|
this._assertWritable();
|
|
1122
1400
|
return { insertedId: this._insertRow(this._state(), data) };
|
|
@@ -1170,7 +1448,14 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1170
1448
|
if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
|
|
1171
1449
|
const match = buildMemoryPredicate(filter, relationSets);
|
|
1172
1450
|
const matched = [];
|
|
1173
|
-
|
|
1451
|
+
if (!state) return matched;
|
|
1452
|
+
const pinned = this._pinnedKey(state, filter);
|
|
1453
|
+
let candidates = state.rows;
|
|
1454
|
+
if (pinned !== void 0) {
|
|
1455
|
+
const row = state.rows.get(pinned);
|
|
1456
|
+
candidates = row ? [[pinned, row]] : [];
|
|
1457
|
+
}
|
|
1458
|
+
for (const [key, row] of candidates) {
|
|
1174
1459
|
if (!match(row)) continue;
|
|
1175
1460
|
if (expectedVersion !== void 0 && row[versionColumn] !== expectedVersion) continue;
|
|
1176
1461
|
matched.push({
|
|
@@ -1203,7 +1488,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1203
1488
|
* because this adapter reports no {@link supportsNativePatch}), so they must
|
|
1204
1489
|
* nest into the stored document — MERGING siblings — exactly like Mongo's
|
|
1205
1490
|
* `$set: { "profile.city": v }`, not create a literal dotted key. Top-level
|
|
1206
|
-
* (dot-free) keys behave as a plain assignment. `data` is
|
|
1491
|
+
* (dot-free) keys behave as a plain assignment. `data` is deep-cloned
|
|
1207
1492
|
* first so nested subtrees from the caller never alias into the store.
|
|
1208
1493
|
* - `ops.inc` / `ops.mul`: numeric increment / multiply on the (dot-path)
|
|
1209
1494
|
* target, coercing a missing or non-numeric current value to `0` (parity with
|
|
@@ -1214,22 +1499,24 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1214
1499
|
* bump always wins over whatever `data`/`inc` wrote and lands on `oldVersion + 1`.
|
|
1215
1500
|
*/
|
|
1216
1501
|
_applyUpdate(row, data, ops) {
|
|
1217
|
-
const patch =
|
|
1502
|
+
const patch = cloneValue(data);
|
|
1218
1503
|
for (const k of Object.keys(patch)) setPath(row, k, patch[k]);
|
|
1219
|
-
if (ops?.inc) for (const [col, n] of Object.entries(ops.inc)) setPath(row, col, (Number(getPath
|
|
1220
|
-
if (ops?.mul) for (const [col, n] of Object.entries(ops.mul)) setPath(row, col, (Number(getPath
|
|
1504
|
+
if (ops?.inc) for (const [col, n] of Object.entries(ops.inc)) setPath(row, col, (Number(getPath(row, col)) || 0) + n);
|
|
1505
|
+
if (ops?.mul) for (const [col, n] of Object.entries(ops.mul)) setPath(row, col, (Number(getPath(row, col)) || 0) * n);
|
|
1221
1506
|
}
|
|
1222
1507
|
/**
|
|
1223
|
-
* Places `next` into the store under its (possibly changed) {@link pkKey}
|
|
1224
|
-
* re-keying when a mutation/replace
|
|
1225
|
-
* NEW key (some other row already
|
|
1226
|
-
* touching the Map so a failed
|
|
1508
|
+
* Places `next` into the store under its (possibly changed) {@link pkKey} in
|
|
1509
|
+
* place of `oldRow` (stored under `oldKey`), re-keying when a mutation/replace
|
|
1510
|
+
* moved the primary key. A collision on the NEW key (some other row already
|
|
1511
|
+
* owns it) throws `CONFLICT`. Throws BEFORE touching the Map so a failed
|
|
1512
|
+
* re-key leaves the store unchanged. `tuples` are `next`'s unique-index keys.
|
|
1227
1513
|
*/
|
|
1228
|
-
_commitRow(
|
|
1514
|
+
_commitRow(state, oldKey, oldRow, next, tuples) {
|
|
1229
1515
|
const newKey = this.pkKey(next);
|
|
1230
|
-
if (newKey !== oldKey && rows.has(newKey)) throw this._pkConflict();
|
|
1231
|
-
|
|
1232
|
-
|
|
1516
|
+
if (newKey !== oldKey && state.rows.has(newKey)) throw this._pkConflict();
|
|
1517
|
+
unindexRow(state.uniqueIndexes, oldKey, oldRow);
|
|
1518
|
+
if (newKey !== oldKey) state.rows.delete(oldKey);
|
|
1519
|
+
this._storeRow(state, newKey, next, tuples);
|
|
1233
1520
|
}
|
|
1234
1521
|
/**
|
|
1235
1522
|
* Update-then-commit for a single matched row: clones the pristine `row`,
|
|
@@ -1241,11 +1528,11 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1241
1528
|
* bump (a version-exempt patch, since 0.1.150).
|
|
1242
1529
|
*/
|
|
1243
1530
|
_commitUpdate(state, oldKey, row, data, ops, keepVersion = false) {
|
|
1244
|
-
const next =
|
|
1531
|
+
const next = cloneValue(row);
|
|
1245
1532
|
this._applyUpdate(next, data, ops);
|
|
1246
1533
|
if (!keepVersion) this._bumpVersion(next, row);
|
|
1247
|
-
this._enforceUniqueIndexes(state, next, oldKey);
|
|
1248
|
-
this._commitRow(state
|
|
1534
|
+
const tuples = this._enforceUniqueIndexes(state, next, oldKey);
|
|
1535
|
+
this._commitRow(state, oldKey, row, next, tuples);
|
|
1249
1536
|
}
|
|
1250
1537
|
async replaceOne(filter, data, expectedVersion) {
|
|
1251
1538
|
this._assertWritable();
|
|
@@ -1257,10 +1544,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1257
1544
|
modifiedCount: 0
|
|
1258
1545
|
};
|
|
1259
1546
|
const { key, row } = matched[0];
|
|
1260
|
-
const next =
|
|
1547
|
+
const next = cloneValue(data);
|
|
1261
1548
|
this._bumpVersion(next, row);
|
|
1262
|
-
this._enforceUniqueIndexes(state, next, key);
|
|
1263
|
-
this._commitRow(state
|
|
1549
|
+
const tuples = this._enforceUniqueIndexes(state, next, key);
|
|
1550
|
+
this._commitRow(state, key, row, next, tuples);
|
|
1264
1551
|
return {
|
|
1265
1552
|
matchedCount: 1,
|
|
1266
1553
|
modifiedCount: 1
|
|
@@ -1289,21 +1576,22 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1289
1576
|
const state = this._peekState();
|
|
1290
1577
|
const matched = this._selectForWrite(state, filter, void 0, false, sets);
|
|
1291
1578
|
if (!state || matched.length === 0) return { deletedCount: 0 };
|
|
1292
|
-
|
|
1579
|
+
this._removeRow(state, matched[0].key, matched[0].row);
|
|
1293
1580
|
return { deletedCount: 1 };
|
|
1294
1581
|
}
|
|
1295
1582
|
/**
|
|
1296
1583
|
* Stable multi-key comparator from `$sort`, with a final tie-break on
|
|
1297
1584
|
* {@link pkKey} for a deterministic total order. Returns the input unchanged
|
|
1298
1585
|
* (insertion order) when there is no `$sort`. Delegates to the shared pure
|
|
1299
|
-
* {@link sortRows}, injecting {@link pkKey} as the total-order tie-break
|
|
1586
|
+
* {@link sortRows}, injecting {@link pkKey} as the total-order tie-break;
|
|
1587
|
+
* `topK` asks for the head of that order only (`$skip + $limit`).
|
|
1300
1588
|
*/
|
|
1301
|
-
_sortRows(rows, $sort) {
|
|
1302
|
-
return sortRows(rows, $sort, (r) => this.pkKey(r));
|
|
1589
|
+
_sortRows(rows, $sort, topK) {
|
|
1590
|
+
return sortRows(rows, $sort, (r) => this.pkKey(r), topK);
|
|
1303
1591
|
}
|
|
1304
1592
|
/**
|
|
1305
|
-
*
|
|
1306
|
-
* so the store can never be mutated through
|
|
1593
|
+
* Compiles the `$select` projection of one read: each returned row is a
|
|
1594
|
+
* fresh, deep-cloned object so the store can never be mutated through it.
|
|
1307
1595
|
*
|
|
1308
1596
|
* - No projection → a full clone.
|
|
1309
1597
|
* - INCLUSION form (`{ field: 1 }`) → a new object with exactly the selected
|
|
@@ -1314,8 +1602,8 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1314
1602
|
* Top-level and nested dot-paths are supported; exotic Mongo projection
|
|
1315
1603
|
* quirks (array positional, `$slice`, etc.) are intentionally NOT replicated.
|
|
1316
1604
|
*/
|
|
1317
|
-
|
|
1318
|
-
return
|
|
1605
|
+
_projector($select) {
|
|
1606
|
+
return compileProjection($select?.asProjection, { clone: true });
|
|
1319
1607
|
}
|
|
1320
1608
|
/**
|
|
1321
1609
|
* Reads the pagination/sort/projection controls with their intended types.
|
|
@@ -1337,11 +1625,25 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1337
1625
|
* shares. Goes through the {@link _loadRows} seam exactly ONCE per call, so a
|
|
1338
1626
|
* reader (and provider read-through mode) has one place that materializes the
|
|
1339
1627
|
* working set — one provider invocation per logical read.
|
|
1628
|
+
*
|
|
1629
|
+
* Stored mode reads the table's Map in place (no snapshot copy), resolves a
|
|
1630
|
+
* filter pinning the whole primary key by direct lookup, and stops after
|
|
1631
|
+
* `limit` matches when given (an unsorted `findOne`).
|
|
1340
1632
|
*/
|
|
1341
|
-
async _filteredRows(query) {
|
|
1633
|
+
async _filteredRows(query, limit) {
|
|
1342
1634
|
if (!containsRelationFilter(query.filter)) {
|
|
1343
1635
|
const match = buildMemoryPredicate(query.filter);
|
|
1344
|
-
|
|
1636
|
+
if (!this._provider && this._loadRows === MemoryAdapter.prototype._loadRows) {
|
|
1637
|
+
const state = this._peekState();
|
|
1638
|
+
if (!state) return [];
|
|
1639
|
+
const pinned = this._pinnedKey(state, query.filter);
|
|
1640
|
+
if (pinned !== void 0) {
|
|
1641
|
+
const row = state.rows.get(pinned);
|
|
1642
|
+
return row && match(row) ? [row] : [];
|
|
1643
|
+
}
|
|
1644
|
+
return collectMatches(state.rows.values(), match, limit);
|
|
1645
|
+
}
|
|
1646
|
+
return collectMatches(await this._loadRows(), match, limit);
|
|
1345
1647
|
}
|
|
1346
1648
|
const own = Promise.resolve(this._loadRows());
|
|
1347
1649
|
own.catch(() => {});
|
|
@@ -1349,10 +1651,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1349
1651
|
return (await own).filter(buildMemoryPredicate(query.filter, sets));
|
|
1350
1652
|
}
|
|
1351
1653
|
async findOne(query) {
|
|
1352
|
-
const filtered = await this._filteredRows(query);
|
|
1353
1654
|
const { $sort, $skip, $select } = this._readControls(query.controls ?? {});
|
|
1354
|
-
const
|
|
1355
|
-
|
|
1655
|
+
const index = $skip ?? 0;
|
|
1656
|
+
const row = (hasSort($sort) ? this._sortRows(await this._filteredRows(query), $sort, index + 1) : await this._filteredRows(query, index + 1))[index];
|
|
1657
|
+
return row ? this._projector($select)(row) : null;
|
|
1356
1658
|
}
|
|
1357
1659
|
async findMany(query) {
|
|
1358
1660
|
return (await this.findManyWithCount(query)).data;
|
|
@@ -1370,8 +1672,9 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1370
1672
|
async findManyWithCount(query) {
|
|
1371
1673
|
const filtered = await this._filteredRows(query);
|
|
1372
1674
|
const { $sort, $skip, $limit, $select } = this._readControls(query.controls ?? {});
|
|
1675
|
+
const topK = $limit === void 0 ? void 0 : ($skip ?? 0) + $limit;
|
|
1373
1676
|
return {
|
|
1374
|
-
data: paginate(this._sortRows(filtered, $sort), $skip, $limit).map(
|
|
1677
|
+
data: paginate(this._sortRows(filtered, $sort, topK), $skip, $limit).map(this._projector($select)),
|
|
1375
1678
|
count: filtered.length
|
|
1376
1679
|
};
|
|
1377
1680
|
}
|
|
@@ -1445,7 +1748,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1445
1748
|
const state = this._peekState();
|
|
1446
1749
|
if (!state) return { deletedCount: 0 };
|
|
1447
1750
|
const matched = this._selectForWrite(state, filter, void 0, true, sets);
|
|
1448
|
-
for (const { key } of matched)
|
|
1751
|
+
for (const { key, row } of matched) this._removeRow(state, key, row);
|
|
1449
1752
|
return { deletedCount: matched.length };
|
|
1450
1753
|
}
|
|
1451
1754
|
/**
|
|
@@ -1457,13 +1760,11 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1457
1760
|
const uniqueIndexes = [];
|
|
1458
1761
|
for (const index of this._table.indexes.values()) {
|
|
1459
1762
|
if (index.type !== "unique") continue;
|
|
1460
|
-
uniqueIndexes.push(
|
|
1461
|
-
name: index.name,
|
|
1462
|
-
fields: index.fields.map((f) => f.name),
|
|
1463
|
-
optionalFields: new Set(index.fields.filter((f) => f.optional).map((f) => f.name))
|
|
1464
|
-
});
|
|
1763
|
+
uniqueIndexes.push(recordUniqueIndex(index.name, index.fields.map((f) => f.name), new Set(index.fields.filter((f) => f.optional).map((f) => f.name))));
|
|
1465
1764
|
}
|
|
1466
|
-
this._state()
|
|
1765
|
+
const state = this._state();
|
|
1766
|
+
state.uniqueIndexes = uniqueIndexes;
|
|
1767
|
+
for (const [key, row] of state.rows) indexRow(uniqueIndexes, key, row);
|
|
1467
1768
|
}
|
|
1468
1769
|
/** Creates the table's (empty) state when it does not exist. Idempotent. */
|
|
1469
1770
|
async ensureTable() {
|
|
@@ -1512,6 +1813,38 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
|
|
|
1512
1813
|
this._db.delete(name);
|
|
1513
1814
|
}
|
|
1514
1815
|
};
|
|
1816
|
+
/** Whether a `$sort` control orders by at least one key. */
|
|
1817
|
+
function hasSort($sort) {
|
|
1818
|
+
return $sort !== void 0 && Object.keys($sort).length > 0;
|
|
1819
|
+
}
|
|
1820
|
+
/** The rows of `rows` matching `match`, in order — at most `limit` when given. */
|
|
1821
|
+
function collectMatches(rows, match, limit) {
|
|
1822
|
+
const out = [];
|
|
1823
|
+
if (limit !== void 0 && limit <= 0) return out;
|
|
1824
|
+
for (const row of rows) if (match(row)) {
|
|
1825
|
+
out.push(row);
|
|
1826
|
+
if (out.length === limit) break;
|
|
1827
|
+
}
|
|
1828
|
+
return out;
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Collects into `pinned` the primary-key fields `filter` pins by exact equality
|
|
1832
|
+
* on a string / number / boolean / `Date` value — at its top level and inside
|
|
1833
|
+
* (nested) `$and`s, which `walkFilter` conjoins the same way. The first pin of
|
|
1834
|
+
* a field wins: a contradicting second one is still checked by the predicate.
|
|
1835
|
+
*/
|
|
1836
|
+
function collectPins(filter, fields, pinned) {
|
|
1837
|
+
if (filter === null || typeof filter !== "object" || Array.isArray(filter)) return;
|
|
1838
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1839
|
+
if (key === "$and") {
|
|
1840
|
+
if (Array.isArray(value)) for (const child of value) collectPins(child, fields, pinned);
|
|
1841
|
+
continue;
|
|
1842
|
+
}
|
|
1843
|
+
if (pinned.has(key) || !fields.includes(key)) continue;
|
|
1844
|
+
const operand = isPlainObject(value) ? value.$eq : value;
|
|
1845
|
+
if (typeof operand === "string" || typeof operand === "number" || typeof operand === "boolean" || operand instanceof Date) pinned.set(key, operand);
|
|
1846
|
+
}
|
|
1847
|
+
}
|
|
1515
1848
|
/**
|
|
1516
1849
|
* Ergonomic late-binding entry point for provider (read-through) mode. Resolves
|
|
1517
1850
|
* the ALREADY-BUILT {@link MemoryAdapter} backing `type` on `space` (reached
|