@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.mjs CHANGED
@@ -1,6 +1,69 @@
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";
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(row, field, value) {
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
- /** `$in` membership, factored out so `$nin` can be its exact negation. */
92
- function evalIn(row, field, value) {
93
- if (!Array.isArray(value)) return false;
94
- const fieldValue = getPath$1(row, field);
95
- return value.some((element) => matchesScalar(fieldValue, element));
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(row, field, op, value) {
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,24 +230,39 @@ const memoryVisitor = {
143
230
  return (row) => !child(row);
144
231
  },
145
232
  comparison(field, op, value) {
233
+ const read = pathReader(field);
234
+ if (op === INTEGER_REGEX_OP) {
235
+ const { pattern, flags } = parseRegexString(value);
236
+ const regex = new RegExp(pattern, flags);
237
+ return (row) => {
238
+ const fieldValue = read(row);
239
+ return (typeof fieldValue === "bigint" || typeof fieldValue === "number" && Number.isInteger(fieldValue)) && regex.test(String(fieldValue));
240
+ };
241
+ }
146
242
  switch (op) {
147
- case "$eq": return (row) => evalEq(row, field, value);
148
- case "$ne": return (row) => !evalEq(row, field, value);
243
+ case "$eq": return (row) => evalEq(read(row), value);
244
+ case "$ne": return (row) => !evalEq(read(row), value);
149
245
  case "$gt":
150
246
  case "$gte":
151
247
  case "$lt":
152
- case "$lte": return (row) => evalRelational(row, field, op, value);
153
- case "$in": return (row) => evalIn(row, field, value);
154
- case "$nin": return (row) => !evalIn(row, field, value);
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
+ }
155
257
  case "$regex": {
156
258
  const { pattern, flags } = parseRegexString(value);
157
259
  const regex = new RegExp(pattern, flags);
158
260
  return (row) => {
159
- const fieldValue = getPath$1(row, field);
261
+ const fieldValue = read(row);
160
262
  return fieldValue != null && regex.test(stringifyLeaf(fieldValue));
161
263
  };
162
264
  }
163
- case "$exists": return (row) => value === !evalEq(row, field, null);
265
+ case "$exists": return (row) => value === !evalEq(read(row), null);
164
266
  default: throw new DbError("INVALID_QUERY", [{
165
267
  path: field,
166
268
  message: `Unsupported filter operator: ${op}`
@@ -182,7 +284,7 @@ const memoryVisitor = {
182
284
  function correlationKey(row, fields) {
183
285
  const parts = [];
184
286
  for (const field of fields) {
185
- const part = keyPart(getPath$1(row, field));
287
+ const part = keyPart(getPath(row, field));
186
288
  if (part === void 0) return;
187
289
  parts.push(part);
188
290
  }
@@ -357,7 +459,10 @@ function compareLeaves(a, b) {
357
459
  * nested dot-paths both work.
358
460
  */
359
461
  function setPath(target, path, value) {
360
- const segments = path.split(".");
462
+ setSegments(target, path.split("."), value);
463
+ }
464
+ /** {@link setPath} over an already-split path. */
465
+ function setSegments(target, segments, value) {
361
466
  let current = target;
362
467
  for (let i = 0; i < segments.length - 1; i++) {
363
468
  const seg = segments[i];
@@ -377,31 +482,88 @@ function setPath(target, path, value) {
377
482
  * rows with equal sort keys still order deterministically. When ABSENT the
378
483
  * sort falls back to preserving input order among equal keys (via each row's
379
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.
380
489
  *
381
- * NEVER mutates the input array: `.map` decorates into a fresh array and
382
- * `.toSorted` returns another new sorted array (unlike `.sort`, which reorders
383
- * in place). The `tieBreak`/index is computed ONCE per row (O(n)), not inside
384
- * 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.
385
493
  */
386
- function sortRows(rows, $sort, tieBreak) {
494
+ function sortRows(rows, $sort, tieBreak, topK) {
387
495
  const keys = $sort ? Object.entries($sort) : [];
388
496
  if (keys.length === 0) return rows;
389
- return rows.map((row, index) => ({
497
+ const readers = keys.map(([field]) => pathReader(field));
498
+ const desc = keys.map(([, dir]) => dir === -1);
499
+ const decorated = rows.map((row, index) => ({
390
500
  row,
391
501
  index,
392
- tie: tieBreak?.(row)
393
- })).toSorted((a, b) => {
394
- for (const [field, dir] of keys) {
395
- const cmp = compareLeaves(getPath(a.row, field), getPath(b.row, field));
396
- if (cmp !== 0) return dir === -1 ? -cmp : cmp;
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;
397
512
  }
398
513
  if (tieBreak) {
399
- const at = a.tie;
400
- const bt = b.tie;
514
+ const at = a.tie ??= tieBreak(a.row);
515
+ const bt = b.tie ??= tieBreak(b.row);
401
516
  return at < bt ? -1 : at > bt ? 1 : 0;
402
517
  }
403
518
  return a.index - b.index;
404
- }).map((decorated) => decorated.row);
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;
405
567
  }
406
568
  /**
407
569
  * Applies `$skip` then `$limit` (both optional) via a single slice. No-op
@@ -430,24 +592,121 @@ function paginate(rows, skip, limit) {
430
592
  *
431
593
  * Top-level and nested dot-paths are supported; exotic Mongo projection quirks
432
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}.
433
598
  */
434
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) {
435
607
  const clone = opts?.clone ?? false;
436
608
  const entries = projection ? Object.entries(projection) : [];
437
- if (entries.length === 0) return clone ? structuredClone(row) : row;
609
+ if (entries.length === 0) return clone ? cloneValue : (row) => row;
438
610
  if (entries[0][1] === 1) {
439
611
  const paths = new Set(entries.filter(([, v]) => v === 1).map(([k]) => k));
440
612
  for (const pk of opts?.pkFields ?? []) paths.add(pk);
441
- const out = {};
442
- for (const path of paths) {
443
- const value = getPath(row, path);
444
- if (value !== void 0) setPath(out, path, value);
445
- }
446
- return clone ? structuredClone(out) : out;
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
+ };
625
+ }
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]));
447
693
  }
448
- const out = structuredClone(row);
449
- for (const [path, v] of entries) if (v === 0) deletePath(out, path);
450
- return out;
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;
451
710
  }
452
711
  //#endregion
453
712
  //#region src/lib/memory-aggregate.ts
@@ -542,7 +801,7 @@ function aggregateRows(rows, controls) {
542
801
  }
543
802
  /** Group values and min / max results may be store-owned objects (JSON values): hand back copies. */
544
803
  function copyValue(value) {
545
- return value !== null && typeof value === "object" ? structuredClone(value) : value;
804
+ return value !== null && typeof value === "object" ? cloneValue(value) : value;
546
805
  }
547
806
  /** Reads one `$groupBy` key off a row: a bucket alias → its label, a path → its value. */
548
807
  function groupKeyReader(key, controls) {
@@ -735,6 +994,8 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
735
994
  _db = /* @__PURE__ */ new Map();
736
995
  /** Memoized physical PK field names — stable for the adapter's lifetime. */
737
996
  _pkFieldsCache;
997
+ /** Memoized compiled readers of {@link _physicalPkFields}. */
998
+ _pkReadersCache;
738
999
  /**
739
1000
  * Memoized map of PHYSICAL field name → optional `start` for every field
740
1001
  * carrying `@db.default.increment`. Stable per adapter (see
@@ -847,6 +1108,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
847
1108
  kind: this._table.isView ? "view" : "table",
848
1109
  rows: /* @__PURE__ */ new Map(),
849
1110
  uniqueIndexes: [],
1111
+ opaquePk: false,
850
1112
  incrementCounters: /* @__PURE__ */ new Map()
851
1113
  };
852
1114
  db.set(name, state);
@@ -867,6 +1129,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
867
1129
  this._pkFieldsCache = fields;
868
1130
  return fields;
869
1131
  }
1132
+ /** Compiled readers of {@link _physicalPkFields} (memoized like it). */
1133
+ _pkReaders() {
1134
+ return this._pkReadersCache ??= this._physicalPkFields().map((field) => pathReader(field));
1135
+ }
870
1136
  /**
871
1137
  * PHYSICAL field name → optional `start` for every `@db.default.increment`
872
1138
  * field. Discovered lazily from the table's field descriptors — each carries
@@ -946,8 +1212,25 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
946
1212
  * `['a:b','c']` differ, and `1` differs from `'1'`.
947
1213
  */
948
1214
  pkKey(row) {
949
- const values = this._physicalPkFields().map((field) => getPath$1(row, field));
950
- return JSON.stringify(values);
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)));
951
1234
  }
952
1235
  /**
953
1236
  * Builds a DEFINED inserted-id for a table with NO single `@meta.id` — a
@@ -967,7 +1250,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
967
1250
  */
968
1251
  _compositeInsertedId(row) {
969
1252
  const id = {};
970
- for (const field of this._physicalPkFields()) id[field] = getPath$1(row, field);
1253
+ for (const field of this._physicalPkFields()) id[field] = getPath(row, field);
971
1254
  return id;
972
1255
  }
973
1256
  /**
@@ -1056,20 +1339,21 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1056
1339
  /**
1057
1340
  * Clones the payload in, applies the version default, generates any
1058
1341
  * `@db.default.increment` values, enforces PK + unique constraints, stores
1059
- * the row, and returns the resolved inserted id. Clone-in (`structuredClone`)
1060
- * is what makes post-insert mutation of the caller's object never leak into
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
1061
1345
  * the store. Called once per item by both `insertOne` and `insertMany`, so
1062
1346
  * increment values advance sequentially across a batch.
1063
1347
  */
1064
1348
  _insertRow(state, data) {
1065
- const row = structuredClone(data);
1349
+ const row = cloneValue(data);
1066
1350
  const versionColumn = this._table.versionColumnPhysical;
1067
1351
  if (versionColumn !== void 0 && !(versionColumn in row)) row[versionColumn] = 0;
1068
1352
  this._applyIncrements(state.incrementCounters, row);
1069
1353
  const key = this.pkKey(row);
1070
1354
  if (state.rows.has(key)) throw this._pkConflict();
1071
- this._enforceUniqueIndexes(state, row);
1072
- state.rows.set(key, row);
1355
+ const tuples = this._enforceUniqueIndexes(state, row);
1356
+ this._storeRow(state, key, row, tuples);
1073
1357
  const fallback = this._table.metaIdPhysical ? void 0 : this._compositeInsertedId(row);
1074
1358
  return this._resolveInsertedId(row, fallback);
1075
1359
  }
@@ -1077,38 +1361,40 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1077
1361
  * Enforces every recorded unique index against the current store. A row is
1078
1362
  * exempted from an index (present-only semantics) when ANY of that index's
1079
1363
  * optional fields is absent/`null`. Otherwise a stored row with an equal
1080
- * tuple → `CONFLICT`.
1364
+ * tuple → `CONFLICT`. Returns the row's tuple key per index (for
1365
+ * {@link _storeRow}).
1081
1366
  *
1082
1367
  * `excludeKey` (when given) skips the row stored under that {@link pkKey} — so
1083
1368
  * a row updating its own unique value does not false-conflict with itself.
1084
1369
  */
1085
1370
  _enforceUniqueIndexes(state, row, excludeKey) {
1086
- for (const index of state.uniqueIndexes) {
1087
- const tuple = [];
1088
- let skip = false;
1089
- for (const field of index.fields) {
1090
- const value = getPath$1(row, field);
1091
- if (index.optionalFields.has(field) && (value === null || value === void 0)) {
1092
- skip = true;
1093
- break;
1094
- }
1095
- tuple.push(value);
1096
- }
1097
- if (skip) continue;
1098
- for (const [existingKey, existing] of state.rows) {
1099
- if (excludeKey !== void 0 && existingKey === excludeKey) continue;
1100
- let equal = true;
1101
- for (let i = 0; i < index.fields.length; i++) if (!valuesEqual(getPath$1(existing, index.fields[i]), tuple[i])) {
1102
- equal = false;
1103
- break;
1104
- }
1105
- if (equal) throw new DbError("CONFLICT", [{
1106
- path: index.fields[0] ?? index.name,
1107
- message: `Duplicate value for unique index "${index.name}"`
1108
- }]);
1109
- }
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;
1110
1391
  }
1111
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
+ }
1112
1398
  async insertOne(data) {
1113
1399
  this._assertWritable();
1114
1400
  return { insertedId: this._insertRow(this._state(), data) };
@@ -1162,7 +1448,14 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1162
1448
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
1163
1449
  const match = buildMemoryPredicate(filter, relationSets);
1164
1450
  const matched = [];
1165
- for (const [key, row] of state?.rows ?? []) {
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) {
1166
1459
  if (!match(row)) continue;
1167
1460
  if (expectedVersion !== void 0 && row[versionColumn] !== expectedVersion) continue;
1168
1461
  matched.push({
@@ -1195,7 +1488,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1195
1488
  * because this adapter reports no {@link supportsNativePatch}), so they must
1196
1489
  * nest into the stored document — MERGING siblings — exactly like Mongo's
1197
1490
  * `$set: { "profile.city": v }`, not create a literal dotted key. Top-level
1198
- * (dot-free) keys behave as a plain assignment. `data` is `structuredClone`d
1491
+ * (dot-free) keys behave as a plain assignment. `data` is deep-cloned
1199
1492
  * first so nested subtrees from the caller never alias into the store.
1200
1493
  * - `ops.inc` / `ops.mul`: numeric increment / multiply on the (dot-path)
1201
1494
  * target, coercing a missing or non-numeric current value to `0` (parity with
@@ -1206,22 +1499,24 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1206
1499
  * bump always wins over whatever `data`/`inc` wrote and lands on `oldVersion + 1`.
1207
1500
  */
1208
1501
  _applyUpdate(row, data, ops) {
1209
- const patch = structuredClone(data);
1502
+ const patch = cloneValue(data);
1210
1503
  for (const k of Object.keys(patch)) setPath(row, k, patch[k]);
1211
- if (ops?.inc) for (const [col, n] of Object.entries(ops.inc)) setPath(row, col, (Number(getPath$1(row, col)) || 0) + n);
1212
- if (ops?.mul) for (const [col, n] of Object.entries(ops.mul)) setPath(row, col, (Number(getPath$1(row, col)) || 0) * n);
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);
1213
1506
  }
1214
1507
  /**
1215
- * Places `next` into the store under its (possibly changed) {@link pkKey},
1216
- * re-keying when a mutation/replace moved the primary key. A collision on the
1217
- * NEW key (some other row already owns it) throws `CONFLICT`. Throws BEFORE
1218
- * touching the Map so a failed re-key leaves the store unchanged.
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.
1219
1513
  */
1220
- _commitRow(rows, oldKey, next) {
1514
+ _commitRow(state, oldKey, oldRow, next, tuples) {
1221
1515
  const newKey = this.pkKey(next);
1222
- if (newKey !== oldKey && rows.has(newKey)) throw this._pkConflict();
1223
- if (newKey !== oldKey) rows.delete(oldKey);
1224
- rows.set(newKey, next);
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);
1225
1520
  }
1226
1521
  /**
1227
1522
  * Update-then-commit for a single matched row: clones the pristine `row`,
@@ -1229,14 +1524,15 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1229
1524
  * `row`), enforces unique indexes on the result EXCLUDING the row's own key (so
1230
1525
  * a row keeping/rewriting its own unique value never self-conflicts), then
1231
1526
  * commits. Nothing is written to the store until both the unique and PK checks
1232
- * pass, so a conflict leaves the store untouched.
1527
+ * pass, so a conflict leaves the store untouched. `keepVersion` skips the
1528
+ * bump (a version-exempt patch, since 0.1.150).
1233
1529
  */
1234
- _commitUpdate(state, oldKey, row, data, ops) {
1235
- const next = structuredClone(row);
1530
+ _commitUpdate(state, oldKey, row, data, ops, keepVersion = false) {
1531
+ const next = cloneValue(row);
1236
1532
  this._applyUpdate(next, data, ops);
1237
- this._bumpVersion(next, row);
1238
- this._enforceUniqueIndexes(state, next, oldKey);
1239
- this._commitRow(state.rows, oldKey, next);
1533
+ if (!keepVersion) this._bumpVersion(next, row);
1534
+ const tuples = this._enforceUniqueIndexes(state, next, oldKey);
1535
+ this._commitRow(state, oldKey, row, next, tuples);
1240
1536
  }
1241
1537
  async replaceOne(filter, data, expectedVersion) {
1242
1538
  this._assertWritable();
@@ -1248,16 +1544,17 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1248
1544
  modifiedCount: 0
1249
1545
  };
1250
1546
  const { key, row } = matched[0];
1251
- const next = structuredClone(data);
1547
+ const next = cloneValue(data);
1252
1548
  this._bumpVersion(next, row);
1253
- this._enforceUniqueIndexes(state, next, key);
1254
- this._commitRow(state.rows, key, next);
1549
+ const tuples = this._enforceUniqueIndexes(state, next, key);
1550
+ this._commitRow(state, key, row, next, tuples);
1255
1551
  return {
1256
1552
  matchedCount: 1,
1257
1553
  modifiedCount: 1
1258
1554
  };
1259
1555
  }
1260
- async updateOne(filter, data, ops, expectedVersion) {
1556
+ async updateOne(filter, data, ops, expectedVersion, opts) {
1557
+ const keepVersion = this._versionColumnFor(opts, expectedVersion) === void 0;
1261
1558
  this._assertWritable();
1262
1559
  const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1263
1560
  const state = this._peekState();
@@ -1267,7 +1564,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1267
1564
  modifiedCount: 0
1268
1565
  };
1269
1566
  const { key, row } = matched[0];
1270
- this._commitUpdate(state, key, row, data, ops);
1567
+ this._commitUpdate(state, key, row, data, ops, keepVersion);
1271
1568
  return {
1272
1569
  matchedCount: 1,
1273
1570
  modifiedCount: 1
@@ -1279,21 +1576,22 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1279
1576
  const state = this._peekState();
1280
1577
  const matched = this._selectForWrite(state, filter, void 0, false, sets);
1281
1578
  if (!state || matched.length === 0) return { deletedCount: 0 };
1282
- state.rows.delete(matched[0].key);
1579
+ this._removeRow(state, matched[0].key, matched[0].row);
1283
1580
  return { deletedCount: 1 };
1284
1581
  }
1285
1582
  /**
1286
1583
  * Stable multi-key comparator from `$sort`, with a final tie-break on
1287
1584
  * {@link pkKey} for a deterministic total order. Returns the input unchanged
1288
1585
  * (insertion order) when there is no `$sort`. Delegates to the shared pure
1289
- * {@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`).
1290
1588
  */
1291
- _sortRows(rows, $sort) {
1292
- return sortRows(rows, $sort, (r) => this.pkKey(r));
1589
+ _sortRows(rows, $sort, topK) {
1590
+ return sortRows(rows, $sort, (r) => this.pkKey(r), topK);
1293
1591
  }
1294
1592
  /**
1295
- * Projects a stored row per `$select` and returns a fresh, deep-cloned object
1296
- * so the store can never be mutated through a returned value.
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.
1297
1595
  *
1298
1596
  * - No projection → a full clone.
1299
1597
  * - INCLUSION form (`{ field: 1 }`) → a new object with exactly the selected
@@ -1304,8 +1602,8 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1304
1602
  * Top-level and nested dot-paths are supported; exotic Mongo projection
1305
1603
  * quirks (array positional, `$slice`, etc.) are intentionally NOT replicated.
1306
1604
  */
1307
- _projectAndClone(row, $select) {
1308
- return projectRow(row, $select?.asProjection, { clone: true });
1605
+ _projector($select) {
1606
+ return compileProjection($select?.asProjection, { clone: true });
1309
1607
  }
1310
1608
  /**
1311
1609
  * Reads the pagination/sort/projection controls with their intended types.
@@ -1327,11 +1625,25 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1327
1625
  * shares. Goes through the {@link _loadRows} seam exactly ONCE per call, so a
1328
1626
  * reader (and provider read-through mode) has one place that materializes the
1329
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`).
1330
1632
  */
1331
- async _filteredRows(query) {
1633
+ async _filteredRows(query, limit) {
1332
1634
  if (!containsRelationFilter(query.filter)) {
1333
1635
  const match = buildMemoryPredicate(query.filter);
1334
- return (await this._loadRows()).filter(match);
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);
1335
1647
  }
1336
1648
  const own = Promise.resolve(this._loadRows());
1337
1649
  own.catch(() => {});
@@ -1339,10 +1651,10 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1339
1651
  return (await own).filter(buildMemoryPredicate(query.filter, sets));
1340
1652
  }
1341
1653
  async findOne(query) {
1342
- const filtered = await this._filteredRows(query);
1343
1654
  const { $sort, $skip, $select } = this._readControls(query.controls ?? {});
1344
- const row = this._sortRows(filtered, $sort)[$skip ?? 0];
1345
- return row ? this._projectAndClone(row, $select) : null;
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;
1346
1658
  }
1347
1659
  async findMany(query) {
1348
1660
  return (await this.findManyWithCount(query)).data;
@@ -1360,8 +1672,9 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1360
1672
  async findManyWithCount(query) {
1361
1673
  const filtered = await this._filteredRows(query);
1362
1674
  const { $sort, $skip, $limit, $select } = this._readControls(query.controls ?? {});
1675
+ const topK = $limit === void 0 ? void 0 : ($skip ?? 0) + $limit;
1363
1676
  return {
1364
- data: paginate(this._sortRows(filtered, $sort), $skip, $limit).map((row) => this._projectAndClone(row, $select)),
1677
+ data: paginate(this._sortRows(filtered, $sort, topK), $skip, $limit).map(this._projector($select)),
1365
1678
  count: filtered.length
1366
1679
  };
1367
1680
  }
@@ -1399,7 +1712,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1399
1712
  viewCapabilities() {
1400
1713
  return ALL_VIEW_CAPABILITIES;
1401
1714
  }
1402
- async updateMany(filter, data, ops) {
1715
+ async updateMany(filter, data, ops, opts) {
1403
1716
  this._assertWritable();
1404
1717
  const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1405
1718
  const state = this._peekState();
@@ -1408,7 +1721,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1408
1721
  modifiedCount: 0
1409
1722
  };
1410
1723
  const matched = this._selectForWrite(state, filter, void 0, true, sets);
1411
- for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1724
+ for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops, opts?.keepVersion);
1412
1725
  return {
1413
1726
  matchedCount: matched.length,
1414
1727
  modifiedCount: matched.length
@@ -1435,7 +1748,7 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1435
1748
  const state = this._peekState();
1436
1749
  if (!state) return { deletedCount: 0 };
1437
1750
  const matched = this._selectForWrite(state, filter, void 0, true, sets);
1438
- for (const { key } of matched) state.rows.delete(key);
1751
+ for (const { key, row } of matched) this._removeRow(state, key, row);
1439
1752
  return { deletedCount: matched.length };
1440
1753
  }
1441
1754
  /**
@@ -1447,13 +1760,11 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1447
1760
  const uniqueIndexes = [];
1448
1761
  for (const index of this._table.indexes.values()) {
1449
1762
  if (index.type !== "unique") continue;
1450
- uniqueIndexes.push({
1451
- name: index.name,
1452
- fields: index.fields.map((f) => f.name),
1453
- optionalFields: new Set(index.fields.filter((f) => f.optional).map((f) => f.name))
1454
- });
1763
+ uniqueIndexes.push(recordUniqueIndex(index.name, index.fields.map((f) => f.name), new Set(index.fields.filter((f) => f.optional).map((f) => f.name))));
1455
1764
  }
1456
- this._state().uniqueIndexes = uniqueIndexes;
1765
+ const state = this._state();
1766
+ state.uniqueIndexes = uniqueIndexes;
1767
+ for (const [key, row] of state.rows) indexRow(uniqueIndexes, key, row);
1457
1768
  }
1458
1769
  /** Creates the table's (empty) state when it does not exist. Idempotent. */
1459
1770
  async ensureTable() {
@@ -1502,6 +1813,38 @@ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
1502
1813
  this._db.delete(name);
1503
1814
  }
1504
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
+ }
1505
1848
  /**
1506
1849
  * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
1507
1850
  * the ALREADY-BUILT {@link MemoryAdapter} backing `type` on `space` (reached