@atscript/db-memory 0.1.149 → 0.1.151

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