@atscript/db-memory 0.1.113

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.
@@ -0,0 +1,484 @@
1
+ import { BaseDbAdapter, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbUpdateResult, TFieldOps } from "@atscript/db";
2
+ import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
+
4
+ //#region src/lib/memory-adapter.d.ts
5
+ /**
6
+ * Provider (read-through) backing closure. Recomputes and returns the table's
7
+ * rows on demand — a fresh snapshot every call (sync or async). See
8
+ * {@link MemoryAdapter.setProvider} / {@link setMemoryProvider}.
9
+ */
10
+ type MemoryProviderFn = () => Array<Record<string, unknown>> | Promise<Array<Record<string, unknown>>>;
11
+ /**
12
+ * In-memory {@link BaseDbAdapter} implementation.
13
+ *
14
+ * Runs in one of two modes:
15
+ * - STORED mode (default): storage is a plain `Map` living on the adapter
16
+ * instance — adapter instances are 1:1 with a readable (table/view), so the
17
+ * Map is this table's whole store. Documents are kept in their nested PHYSICAL
18
+ * shape (no flattening), which is why {@link supportsNestedObjects} is `true`.
19
+ * Full CRUD surface: inserts, reads, update / replace / delete with
20
+ * optimistic-concurrency CAS.
21
+ * - PROVIDER (read-through) mode: enabled via {@link setProvider} /
22
+ * {@link setMemoryProvider}. Reads are served from a runtime closure
23
+ * recomputed per request (e.g. a Redis/job-manager snapshot) so a
24
+ * runtime-owned entity with NO database can be observed as a READ-ONLY
25
+ * atscript table; all writes are rejected (see {@link _assertWritable}).
26
+ */
27
+ declare class MemoryAdapter extends BaseDbAdapter {
28
+ /**
29
+ * The table's store, keyed by {@link pkKey}. Values are the stored rows in
30
+ * nested physical shape. An instance field — no `ensureTable` DDL needed.
31
+ */
32
+ private rows;
33
+ /** Unique indexes recorded by {@link syncIndexes}. Enforced on insert. */
34
+ private uniqueIndexes;
35
+ /** Memoized physical PK field names — stable for the adapter's lifetime. */
36
+ private _pkFieldsCache?;
37
+ /**
38
+ * Memoized map of PHYSICAL field name → optional `start` for every field
39
+ * carrying `@db.default.increment`. Stable per adapter (see
40
+ * {@link _incrementFields}).
41
+ */
42
+ private _incrementFieldsCache?;
43
+ /**
44
+ * Running per-field auto-increment counters (PHYSICAL name → last value
45
+ * handed out). Lives on the adapter INSTANCE, so it resets whenever a new
46
+ * DbSpace/adapter is built — correct for an in-memory store (parity with
47
+ * SQLite `:memory:`, whose sequence also restarts with a fresh DB). Never
48
+ * persisted.
49
+ */
50
+ private _incrementCounters;
51
+ /**
52
+ * Provider (read-through) backing closure. When set, this adapter is
53
+ * PROVIDER-BACKED: reads recompute rows from this closure per request and all
54
+ * writes are rejected (see {@link _assertWritable}). `undefined` ⇒ stored mode.
55
+ *
56
+ * WHY late-binding only (no constructor provider option): a {@link DbSpace}'s
57
+ * zero-arg `TAdapterFactory` builds EVERY table's adapter with the SAME
58
+ * factory — INCLUDING the internal `__atscript_control` sync table. A provider
59
+ * injected at construction would therefore leak onto the control table and
60
+ * break schema sync. Provider mode must target ONE specific table's
61
+ * already-built adapter, so it is only settable AFTER construction via
62
+ * {@link setProvider}.
63
+ */
64
+ private _provider?;
65
+ /**
66
+ * The in-memory store keeps documents nested (no flattening), so the generic
67
+ * layer should pass nested objects through as-is.
68
+ */
69
+ supportsNestedObjects(): boolean;
70
+ /**
71
+ * Parity with the Mongo adapter (`return !fd.encrypted`). The in-memory
72
+ * dot-path filter visitor CAN filter into nested objects AND array
73
+ * (`storage === 'json'`) fields, so JSON storage is NOT a filterability
74
+ * blocker here. The base default vetoes `storage === 'json'` — correct for SQL
75
+ * engines that cannot reach into a raw JSON column, but WRONG for this
76
+ * nested-object-capable adapter, and it would under-report `filterable` to
77
+ * `/meta` for UIs. Overriding fixes that. The `@db.encrypted` veto is
78
+ * core-supplied and absolute (equality/range over ciphertext is meaningless),
79
+ * so it is preserved.
80
+ *
81
+ * NOTE on the capabilities left at their base defaults ON PURPOSE:
82
+ * - `canSortField` — its conservative JSON veto (array sort-by-min/max-element
83
+ * is a footgun for generic UI sort headers) is deliberate, matching Mongo.
84
+ * - `supportsNativePatch` / `supportsNativeRelations` — stay `false`: core
85
+ * decomposes patches into dot-path `$set`s and loads relations app-level.
86
+ */
87
+ canFilterField(fd: TDbFieldMeta): boolean;
88
+ /**
89
+ * Coerces a by-id value to the id field's declared leaf type. The framework
90
+ * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
91
+ * URL id like `"21"` reaches this adapter as the STRING `"21"`. Memory does
92
+ * STRICT JS comparison like the Mongo adapter, so the strict `$eq` would then
93
+ * compare `"21" === 21` against a numeric PK and never match — every
94
+ * fetch/patch/delete/replace-by-id of a numeric-PK row would 404. Coercing the
95
+ * id to the field type here fixes that. SQL adapters can rely on the DB to
96
+ * coerce the bound parameter; memory cannot, so it MUST coerce here.
97
+ *
98
+ * Mirrors the Mongo adapter's `prepareId` MINUS its `objectId` branch (an
99
+ * in-memory store has no ObjectId ids): a leaf `designType` of `"number"`
100
+ * coerces via `Number(id)`, everything else via `String(id)`.
101
+ */
102
+ prepareId(id: unknown, _fieldType: unknown): unknown;
103
+ /**
104
+ * Physical names of the primary-key field(s). Single `@meta.id` resolves via
105
+ * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
106
+ * PK path through `pathToPhysical` (falling back to the name itself). Memoized
107
+ * because it is stable per adapter (1:1 with a fixed table) yet read on every
108
+ * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
109
+ */
110
+ private _physicalPkFields;
111
+ /**
112
+ * PHYSICAL field name → optional `start` for every `@db.default.increment`
113
+ * field. Discovered lazily from the table's field descriptors — each carries
114
+ * both `physicalName` (the key the stored row uses) and `defaultValue`
115
+ * (sourced from `this._table.defaults`), so this resolves column renames
116
+ * correctly where iterating the logical-keyed `defaults` map would not.
117
+ * Memoized because it is stable per adapter (1:1 with a fixed table), like
118
+ * {@link _physicalPkFields}. An empty map ⇒ the insert fast-path skips all
119
+ * increment work.
120
+ *
121
+ * Mirrors the Mongo adapter's `_incrementFields`
122
+ * (`mongo-adapter.ts` — populated in `onFieldScanned`, keyed by physical
123
+ * name): core NEVER generates `increment` values (its `_applyDefaults`
124
+ * switch has no `increment` case, so the field reaches the adapter absent),
125
+ * whether or not the adapter claims it via `nativeDefaultFns()`. The adapter
126
+ * MUST fill it in. Mongo does not override `nativeDefaultFns()` /
127
+ * `supportsNativeValueDefaults()` for increment, so neither does this adapter.
128
+ */
129
+ private _incrementFields;
130
+ /**
131
+ * Assigns `@db.default.increment` values onto `row` (PHYSICAL shape) IN
132
+ * PLACE — called from {@link _insertRow} BEFORE `pkKey`/uniqueness/inserted-id
133
+ * are computed so an increment PRIMARY KEY produces a real `insertedId` and
134
+ * stores under a real key. The memory analogue of the Mongo adapter's
135
+ * insert-time increment (Mongo uses an atomic `__atscript_counters`
136
+ * collection; an in-memory store just keeps the counter on the instance):
137
+ *
138
+ * - No value for the field → assign the next counter value. First use starts
139
+ * at `start ?? 1` (`max(counter, (start ?? 1) - 1) + 1`); thereafter it is
140
+ * the previous value + 1. Sequential across an `insertMany` batch because
141
+ * {@link _insertRow} runs per item in `insertedIds` order against the shared
142
+ * counter.
143
+ * - Explicit value present → keep it, but advance the counter to
144
+ * `max(counter, value)` so a later auto value can never collide with it.
145
+ */
146
+ private _applyIncrements;
147
+ /**
148
+ * Builds the duplicate-primary-key {@link DbError}. The reported `path` is the
149
+ * physical `@meta.id` name, falling back to the first (logical) primary key,
150
+ * then `""`. Centralized so the insert and re-key paths raise an identical
151
+ * CONFLICT.
152
+ */
153
+ private _pkConflict;
154
+ /**
155
+ * Derives the storage key from a row's PRIMARY KEY value(s), read by PHYSICAL
156
+ * name. Encoded as `JSON.stringify` of the ordered PK values so it is
157
+ * collision-proof across both value shapes and types — `['a','b:c']` and
158
+ * `['a:b','c']` differ, and `1` differs from `'1'`.
159
+ */
160
+ private pkKey;
161
+ /**
162
+ * Builds a DEFINED inserted-id for a table with NO single `@meta.id` — a
163
+ * composite (or single non-meta) primary key, where
164
+ * {@link BaseDbAdapter._resolveInsertedId} would otherwise fall back to
165
+ * `undefined` (memory has no rowid/`_id` to hand back). Returns an object
166
+ * mapping each PRIMARY KEY PHYSICAL field name → its value in the stored row
167
+ * (e.g. `{ part1: "a", part2: "b" }`), read by physical name via
168
+ * {@link getPath} so it matches how {@link pkKey} derives the storage key and
169
+ * honours column renames. A single-field non-meta PK yields the one-key object
170
+ * form for consistency (documented shape).
171
+ *
172
+ * Passed as the `dbGeneratedId` fallback in {@link _insertRow} ONLY when
173
+ * {@link AtscriptDbReadable.metaIdPhysical} is null; single-`@meta.id` tables
174
+ * keep an `undefined` fallback, so their scalar `insertedId`
175
+ * (`row[metaIdPhysical]`) is byte-identical to before.
176
+ */
177
+ private _compositeInsertedId;
178
+ /**
179
+ * Switches this adapter into PROVIDER-BACKED mode: `fn` is invoked on every
180
+ * read to recompute the table's rows (e.g. a Redis/job-manager snapshot), so a
181
+ * runtime-owned entity with NO database can be observed as a read-only
182
+ * atscript table (and still carry `@DbAction`s). Rows are recomputed per read
183
+ * (no caching); once set, the table is READ-ONLY — all writes throw (see
184
+ * {@link _assertWritable}).
185
+ *
186
+ * Late-binding by design: set AFTER construction only, never via the
187
+ * constructor — see the {@link _provider} field comment for why a constructor
188
+ * option would leak onto the shared control-table adapter and break sync.
189
+ */
190
+ setProvider(fn: MemoryProviderFn): void;
191
+ /**
192
+ * Write guard for provider-backed (read-only) mode. Called first in every one
193
+ * of the 8 write methods so all mutation entry points reject identically. Uses
194
+ * `INVALID_QUERY` (moost-db's validation interceptor maps it to HTTP 400, NOT
195
+ * 500 — the same choice `aggregate()` makes) since there is no dedicated
196
+ * read-only error code.
197
+ */
198
+ private _assertWritable;
199
+ /**
200
+ * Snapshot seam for reads. Returns the current rows.
201
+ *
202
+ * - Stored mode reads the instance Map directly (insertion order preserved).
203
+ * - Provider (read-through) mode calls {@link _provider} to recompute a fresh
204
+ * snapshot per read.
205
+ *
206
+ * Does NOT clone — cloning happens only on OUTPUT (see {@link _projectAndClone})
207
+ * so the store stays authoritative and cheap. That same clone-on-output path
208
+ * ALSO covers provider rows: every value handed back to a caller is a
209
+ * `structuredClone`, so a provider that returns objects it still holds is
210
+ * protected from mutation by `reconstructFromRead`/callers.
211
+ *
212
+ * A single logical read invokes the provider EXACTLY ONCE: `findMany`
213
+ * delegates to `findManyWithCount` (one `_filteredRows` ⇒ one `_loadRows`),
214
+ * and `findOne`/`count` each call `_filteredRows` once — so recompute-per-read
215
+ * AND single-snapshot-per-`findManyWithCount` both fall out for free.
216
+ */
217
+ protected _loadRows(): Record<string, unknown>[] | Promise<Record<string, unknown>[]>;
218
+ /**
219
+ * Clones the payload in, applies the version default, generates any
220
+ * `@db.default.increment` values, enforces PK + unique constraints, stores
221
+ * the row, and returns the resolved inserted id. Clone-in (`structuredClone`)
222
+ * is what makes post-insert mutation of the caller's object never leak into
223
+ * the store. Called once per item by both `insertOne` and `insertMany`, so
224
+ * increment values advance sequentially across a batch.
225
+ */
226
+ private _insertRow;
227
+ /**
228
+ * Enforces every recorded unique index against the current store. A row is
229
+ * exempted from an index (present-only semantics) when ANY of that index's
230
+ * optional fields is absent/`null`. Otherwise a stored row with an equal
231
+ * tuple → `CONFLICT`.
232
+ *
233
+ * `excludeKey` (when given) skips the row stored under that {@link pkKey} — so
234
+ * a row updating its own unique value does not false-conflict with itself.
235
+ */
236
+ private _enforceUniqueIndexes;
237
+ insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
238
+ insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
239
+ /**
240
+ * Selects the stored rows a write should touch, as `{ key, row }` pairs so
241
+ * callers can mutate in place and re-key. Stored mode scans the instance Map
242
+ * directly (writes are authoritative against the store, unlike reads which go
243
+ * through the {@link _loadRows} snapshot seam).
244
+ *
245
+ * A defined `expectedVersion` layers an OCC (compare-and-set) predicate on top
246
+ * of `filter`: a row matches only when `row[versionColumn] === expectedVersion`.
247
+ * A version MISMATCH is NOT an error — it simply yields zero matches, so the
248
+ * caller reports `matchedCount: 0`. Supplying `expectedVersion` for a table
249
+ * that has no version column is a misconfiguration and throws (mirrors the
250
+ * Mongo adapter's `_buildCasFilter`).
251
+ *
252
+ * When `many` is `false` at most the first match is returned.
253
+ */
254
+ private _selectForWrite;
255
+ /**
256
+ * Sets `target`'s version column to `oldRow`'s version + 1, coercing a missing
257
+ * old version to `0`. No-op on an unversioned table. The OLD version is read
258
+ * from a pristine `oldRow` (not `target`) so the result is always
259
+ * `oldVersion + 1` regardless of what a patch/replace payload wrote onto
260
+ * `target`'s version column — the memory analogue of Mongo forcing
261
+ * `$inc: { version: 1 }` last. Shared by {@link _commitUpdate} (merge path) and
262
+ * {@link replaceOne} (full-replace path).
263
+ */
264
+ private _bumpVersion;
265
+ /**
266
+ * Applies a merge-style update to `row` IN PLACE — the memory analogue of
267
+ * `buildMongoUpdateDoc`:
268
+ *
269
+ * - `$set` (`data`): each key is set onto the row via {@link setPath}. Keys are
270
+ * DOT-PATHS (the table layer decomposes nested patches into `"profile.city"`
271
+ * because this adapter reports no {@link supportsNativePatch}), so they must
272
+ * nest into the stored document — MERGING siblings — exactly like Mongo's
273
+ * `$set: { "profile.city": v }`, not create a literal dotted key. Top-level
274
+ * (dot-free) keys behave as a plain assignment. `data` is `structuredClone`d
275
+ * first so nested subtrees from the caller never alias into the store.
276
+ * - `ops.inc` / `ops.mul`: numeric increment / multiply on the (dot-path)
277
+ * target, coercing a missing or non-numeric current value to `0` (parity with
278
+ * Mongo's `$inc`/`$mul`).
279
+ *
280
+ * Does NOT bump the version column — {@link _commitUpdate} does that LAST via
281
+ * {@link _bumpVersion} (after this merge, reading the pristine old row), so the
282
+ * bump always wins over whatever `data`/`inc` wrote and lands on `oldVersion + 1`.
283
+ */
284
+ private _applyUpdate;
285
+ /**
286
+ * Places `next` into the store under its (possibly changed) {@link pkKey},
287
+ * re-keying when a mutation/replace moved the primary key. A collision on the
288
+ * NEW key (some other row already owns it) throws `CONFLICT`. Throws BEFORE
289
+ * touching the Map so a failed re-key leaves the store unchanged.
290
+ */
291
+ private _commitRow;
292
+ /**
293
+ * Update-then-commit for a single matched row: clones the pristine `row`,
294
+ * applies the merge update, bumps the version LAST (read from the pristine old
295
+ * `row`), enforces unique indexes on the result EXCLUDING the row's own key (so
296
+ * a row keeping/rewriting its own unique value never self-conflicts), then
297
+ * commits. Nothing is written to the store until both the unique and PK checks
298
+ * pass, so a conflict leaves the store untouched.
299
+ */
300
+ private _commitUpdate;
301
+ replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
302
+ updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
303
+ deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
304
+ /**
305
+ * Stable multi-key comparator from `$sort`, with a final tie-break on
306
+ * {@link pkKey} for a deterministic total order. Returns the input unchanged
307
+ * (insertion order) when there is no `$sort`. Delegates to the shared pure
308
+ * {@link sortRows}, injecting {@link pkKey} as the total-order tie-break.
309
+ */
310
+ private _sortRows;
311
+ /** Applies `$skip` then `$limit` (both optional) via a single slice. */
312
+ private _paginate;
313
+ /**
314
+ * Projects a stored row per `$select` and returns a fresh, deep-cloned object
315
+ * so the store can never be mutated through a returned value.
316
+ *
317
+ * - No projection → a full clone.
318
+ * - INCLUSION form (`{ field: 1 }`) → a new object with only the selected
319
+ * paths PLUS the primary-key field(s) (mirrors Mongo including `_id`).
320
+ * - EXCLUSION form (`{ field: 0 }`) → a clone with those paths removed.
321
+ *
322
+ * Top-level and nested dot-paths are supported; exotic Mongo projection
323
+ * quirks (array positional, `$slice`, etc.) are intentionally NOT replicated.
324
+ */
325
+ private _projectAndClone;
326
+ /**
327
+ * Reads the pagination/sort/projection controls with their intended types.
328
+ * `DbControls` carries a `[key: `$${string}`]: unknown` index signature, and
329
+ * `Omit`-ing `$select` from `UniqueryControls` widens `$sort`/`$skip`/`$limit`
330
+ * back to `unknown` — so the casts here restore the declared shapes at a
331
+ * single, documented boundary. `$select` keeps its explicit `UniquSelect` type.
332
+ */
333
+ private _readControls;
334
+ /**
335
+ * The single "load a snapshot, apply the filter predicate" step every read
336
+ * shares. Goes through the {@link _loadRows} seam exactly ONCE per call, so a
337
+ * reader (and provider read-through mode) has one place that materializes the
338
+ * working set — one provider invocation per logical read.
339
+ */
340
+ private _filteredRows;
341
+ findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
342
+ findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
343
+ count(query: DbQuery): Promise<number>;
344
+ /**
345
+ * Overridden so the filtered snapshot is computed ONCE — the base default
346
+ * runs `findMany` and `count` separately (two `_loadRows` snapshots). A
347
+ * single snapshot is both cheaper here and the correct semantics for
348
+ * provider (read-through) mode, where two separate reads could otherwise
349
+ * observe different snapshots and make count/data disagree.
350
+ */
351
+ findManyWithCount(query: DbQuery): Promise<{
352
+ data: Array<Record<string, unknown>>;
353
+ count: number;
354
+ }>;
355
+ /**
356
+ * Aggregation (`$groupBy`) is a documented v1 non-goal for the in-memory
357
+ * adapter. The inherited base default throws a PLAIN `Error`, which a readable
358
+ * REST controller would surface as an unhandled HTTP 500 when a `?$groupBy=`
359
+ * query routes here. Throwing a typed {@link DbError} with `INVALID_QUERY`
360
+ * instead converts that into a clean client error — moost-db's validation
361
+ * interceptor maps `INVALID_QUERY` → HTTP 400.
362
+ */
363
+ aggregate(_query: DbQuery): Promise<Array<Record<string, unknown>>>;
364
+ updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
365
+ replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
366
+ deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
367
+ /**
368
+ * Records the model's `unique` indexes for insert-time enforcement. Idempotent
369
+ * (replaces the recorded set). Non-unique index types are ignored — an
370
+ * in-memory scan needs no plain/fulltext/geo index to answer queries.
371
+ */
372
+ syncIndexes(): Promise<void>;
373
+ /**
374
+ * No-op: the store is the instance-level {@link rows} Map, which already
375
+ * exists. Safe to call repeatedly.
376
+ */
377
+ ensureTable(): Promise<void>;
378
+ }
379
+ /**
380
+ * Ergonomic late-binding entry point for provider (read-through) mode. Resolves
381
+ * the ALREADY-BUILT {@link MemoryAdapter} backing `type` on `space` (reached
382
+ * after `getTable`/`syncSchema` has constructed it via `space.getAdapter`, which
383
+ * exists in core — so this helper needs NO core change) and installs `fn` as its
384
+ * provider, making that one table read-only and recomputed per read.
385
+ *
386
+ * Throws if the resolved adapter is not a {@link MemoryAdapter} (i.e. the space
387
+ * is backed by a different engine) so a misuse fails loudly, not silently.
388
+ */
389
+ declare function setMemoryProvider(space: DbSpace, type: TAtscriptAnnotatedType, fn: MemoryProviderFn): void;
390
+ //#endregion
391
+ //#region src/lib/memory-filter.d.ts
392
+ /**
393
+ * In-memory row predicate: given a document, decide whether it matches a
394
+ * filter. This is the single unit of currency the whole engine composes —
395
+ * leaves, logical nodes and the top-level filter all reduce to one of these.
396
+ */
397
+ type Predicate = (row: Record<string, unknown>) => boolean;
398
+ /**
399
+ * Dot-path getter. Splits `path` on `.` and walks plain objects, returning the
400
+ * value at the end of the path or `undefined` if any intermediate segment is
401
+ * missing or is not a plain object.
402
+ *
403
+ * LIMITATION (v1, accepted): this does NOT descend into arrays. If a segment
404
+ * resolves to an array, traversal stops and `undefined` is returned — there is
405
+ * no positional/`$elemMatch`-style indexing. Array-of-object matching is a
406
+ * later concern; the SQL/Mongo adapters flatten differently and we do not want
407
+ * to fake a semantic the store can't back yet.
408
+ */
409
+ /**
410
+ * Compiles a {@link FilterExpr} into an in-memory row predicate
411
+ * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
412
+ * structure matches the SQL/Mongo adapters by construction.
413
+ *
414
+ * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
415
+ * to a match-everything predicate.
416
+ */
417
+ declare function buildMemoryPredicate(filter: FilterExpr): Predicate;
418
+ //#endregion
419
+ //#region src/lib/memory-engine.d.ts
420
+ /**
421
+ * Stable multi-key sort from `$sort`, applied over plain rows.
422
+ *
423
+ * - No `$sort` (or an empty one) → returns the input array UNCHANGED (same
424
+ * reference, insertion order preserved) — the fast path for an unsorted read.
425
+ * - `tieBreak`, when supplied, is the FINAL deterministic tie-break key for a
426
+ * TOTAL order — the adapter injects its {@link MemoryAdapter.pkKey} here so
427
+ * rows with equal sort keys still order deterministically. When ABSENT the
428
+ * sort falls back to preserving input order among equal keys (via each row's
429
+ * original index), so a consumer with no primary key keeps insertion order.
430
+ *
431
+ * NEVER mutates the input array: `.map` decorates into a fresh array and
432
+ * `.toSorted` returns another new sorted array (unlike `.sort`, which reorders
433
+ * in place). The `tieBreak`/index is computed ONCE per row (O(n)), not inside
434
+ * the O(n log n) comparator.
435
+ */
436
+ declare function sortRows(rows: Record<string, unknown>[], $sort?: Partial<Record<string, 1 | -1>>, tieBreak?: (row: Record<string, unknown>) => string | number): Record<string, unknown>[];
437
+ /** Options for {@link projectRow}. */
438
+ interface ProjectRowOptions {
439
+ /**
440
+ * Physical field names ALWAYS kept by an inclusion projection (mirrors Mongo
441
+ * including `_id`). The adapter passes its primary-key field(s); a consumer
442
+ * with no PK (e.g. value-help) passes none. Ignored for exclusion / no
443
+ * projection.
444
+ */
445
+ pkFields?: string[];
446
+ /**
447
+ * When `true`, the returned object is `structuredClone`d so it shares NO
448
+ * structure with `row` (mutating the output leaves the input intact) — what
449
+ * the adapter needs to keep its store authoritative. When `false`/absent the
450
+ * output may alias nested subtrees of `row` (cheaper; for callers that own
451
+ * their rows).
452
+ */
453
+ clone?: boolean;
454
+ }
455
+ /**
456
+ * Projects a plain row per a `{ path: 0 | 1 }` projection map. Decoupled from
457
+ * `UniquSelect`: the caller passes the resolved projection map (e.g. from
458
+ * `$select.asProjection`) so the engine has no query-layer dependency.
459
+ *
460
+ * - No projection (undefined / empty) → the whole row (cloned per `clone`).
461
+ * - INCLUSION form (first entry is `1`) → a new object with only the selected
462
+ * paths PLUS `opts.pkFields`. Absent fields are omitted; a present-`null`
463
+ * (value === null) is kept.
464
+ * - EXCLUSION form (first entry is `0`) → a clone with those paths removed. This
465
+ * branch ALWAYS clones (it must own a copy to drop paths without mutating the
466
+ * input), so `clone: false` is a no-op here.
467
+ *
468
+ * Top-level and nested dot-paths are supported; exotic Mongo projection quirks
469
+ * (array positional, `$slice`, etc.) are intentionally NOT replicated.
470
+ */
471
+ declare function projectRow(row: Record<string, unknown>, projection?: Record<string, 0 | 1>, opts?: ProjectRowOptions): Record<string, unknown>;
472
+ //#endregion
473
+ //#region src/lib/index.d.ts
474
+ /**
475
+ * Creates a {@link DbSpace} backed by an in-memory {@link MemoryAdapter}.
476
+ *
477
+ * Tables default to STORED mode (an instance-level `Map`). To make one table
478
+ * read-only and read-through from a runtime closure, call `setMemoryProvider`
479
+ * (re-exported from {@link ./memory-adapter}) on the space AFTER the table's
480
+ * adapter has been built.
481
+ */
482
+ declare function createAdapter(): DbSpace;
483
+ //#endregion
484
+ export { MemoryAdapter, MemoryProviderFn, buildMemoryPredicate, createAdapter, projectRow, setMemoryProvider, sortRows };