@atscript/db-memory 0.1.145 → 0.1.147

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
@@ -156,18 +156,156 @@ const memoryVisitor = {
156
156
  message: `Unsupported filter operator: ${op}`
157
157
  }]);
158
158
  }
159
+ },
160
+ relation(field, op, operand) {
161
+ return relationPredicate(void 0, field, op, operand);
159
162
  }
160
163
  };
161
164
  /**
165
+ * Identity of the key tuple `fields` of `row` (dot-paths read nested), or
166
+ * `undefined` when a component is `null` / missing — a NULL key component
167
+ * never correlates (SQL `=` semantics: `$some` false, `$none` true).
168
+ * Every component is type-tagged (see {@link keyPart}), matching the strict
169
+ * equality the memory filter uses everywhere (`valuesEqual`): `1` ≠ `"1"`,
170
+ * `5n` ≠ `"5n"`, a `Date` ≠ its ISO string; `Date`s compare by instant.
171
+ */
172
+ function correlationKey(row, fields) {
173
+ const parts = [];
174
+ for (const field of fields) {
175
+ const part = keyPart((0, _atscript_db.getPath)(row, field));
176
+ if (part === void 0) return;
177
+ parts.push(part);
178
+ }
179
+ return JSON.stringify(parts);
180
+ }
181
+ /**
182
+ * Type-tagged identity of one key component, or `undefined` when it never
183
+ * equals anything (`null` / missing, `NaN`).
184
+ */
185
+ function keyPart(value) {
186
+ switch (typeof value) {
187
+ case "string": return `s:${value}`;
188
+ case "number": return Number.isNaN(value) ? void 0 : `n:${value === 0 ? 0 : value}`;
189
+ case "bigint": return `b:${value}`;
190
+ case "boolean": return `t:${value}`;
191
+ case "undefined": return;
192
+ default:
193
+ if (value === null) return void 0;
194
+ if (value instanceof Date) {
195
+ const time = value.getTime();
196
+ return Number.isNaN(time) ? void 0 : `d:${time}`;
197
+ }
198
+ return `o:${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? `${v}n` : v)}`;
199
+ }
200
+ }
201
+ /** Source-side correlation columns of a resolved predicate (physical names). */
202
+ function sourceColumns(node) {
203
+ return node.junction ? node.junction.toSource.map((p) => p.source) : node.pairs.map((p) => p.source);
204
+ }
205
+ /**
206
+ * The visitor's `relation` callback: a row satisfies `$some` when its source
207
+ * correlation tuple is in the predicate's prepared set, `$none` otherwise.
208
+ */
209
+ function relationPredicate(sets, field, op, operand) {
210
+ const node = operand;
211
+ const set = (0, _atscript_db.isResolvedRelationFilter)(node) ? sets?.get(node) : void 0;
212
+ if (!set) throw new _atscript_db.DbError("REL_FILTER_NOT_SUPPORTED", [{
213
+ path: field,
214
+ message: `Relational predicate "${op}" on "${field}" was not prepared — call prepareRelationSets() before buildMemoryPredicate()`
215
+ }]);
216
+ const columns = sourceColumns(node);
217
+ const some = op === "$some";
218
+ return (row) => {
219
+ const key = correlationKey(row, columns);
220
+ return (key !== void 0 && set.has(key)) === some;
221
+ };
222
+ }
223
+ /**
224
+ * Resolves every relational predicate of a TRANSLATED filter (nested ones
225
+ * included) into a correlation set, loading each related table through
226
+ * `load`:
227
+ *
228
+ * - `to` / `from`: the related rows matching the inner filter contribute their
229
+ * `pairs[].target` tuples — equal to the source tuple (`pairs[].source`) of
230
+ * every row that has such a related row.
231
+ * - `via`: the matching related rows' `junction.toTarget[].target` tuples
232
+ * select junction rows (also filtered by the junction part of
233
+ * `@db.rel.filter`), which contribute their `junction.toSource[].junction`
234
+ * tuples.
235
+ *
236
+ * Nested predicates are prepared against the related table's rows first
237
+ * (their source is this predicate's target). Returns `undefined` when the
238
+ * filter holds no predicate — callers then stay on the plain synchronous path.
239
+ *
240
+ * @since 0.1.147
241
+ */
242
+ async function prepareRelationSets(filter, load) {
243
+ if (!(0, _atscript_db.containsRelationFilter)(filter)) return;
244
+ const sets = /* @__PURE__ */ new Map();
245
+ await prepareLevel(filter, load, sets);
246
+ return sets;
247
+ }
248
+ /** Prepares the predicates at one level of `filter` (and, recursively, their operands). */
249
+ async function prepareLevel(filter, load, sets) {
250
+ const nodes = [];
251
+ (0, _atscript_db.forEachResolvedRelation)(filter, (node) => nodes.push(node), false);
252
+ for (const node of nodes) if (!sets.has(node)) sets.set(node, await correlationSet(node, load, sets));
253
+ }
254
+ /** Matching rows of `adapter`'s table for `filter` (its own predicates prepared first). */
255
+ async function matchingRows(adapter, filter, load, sets) {
256
+ const rows = await load(adapter);
257
+ if (!filter || Object.keys(filter).length === 0) return rows;
258
+ await prepareLevel(filter, load, sets);
259
+ return rows.filter(buildMemoryPredicate(filter, sets));
260
+ }
261
+ /** The source correlation tuples that have a related row matching `node`. */
262
+ async function correlationSet(node, load, sets) {
263
+ const targets = await matchingRows(node.target.adapter, node.filter, load, sets);
264
+ const result = /* @__PURE__ */ new Set();
265
+ if (!node.junction) {
266
+ const columns = node.pairs.map((p) => p.target);
267
+ for (const row of targets) {
268
+ const key = correlationKey(row, columns);
269
+ if (key !== void 0) result.add(key);
270
+ }
271
+ return result;
272
+ }
273
+ const { junction } = node;
274
+ const targetKeys = /* @__PURE__ */ new Set();
275
+ const targetColumns = junction.toTarget.map((p) => p.target);
276
+ for (const row of targets) {
277
+ const key = correlationKey(row, targetColumns);
278
+ if (key !== void 0) targetKeys.add(key);
279
+ }
280
+ if (targetKeys.size === 0) return result;
281
+ const toTarget = junction.toTarget.map((p) => p.junction);
282
+ const toSource = junction.toSource.map((p) => p.junction);
283
+ for (const row of await matchingRows(junction.adapter, junction.filter, load, sets)) {
284
+ const targetKey = correlationKey(row, toTarget);
285
+ if (targetKey === void 0 || !targetKeys.has(targetKey)) continue;
286
+ const sourceKey = correlationKey(row, toSource);
287
+ if (sourceKey !== void 0) result.add(sourceKey);
288
+ }
289
+ return result;
290
+ }
291
+ /**
162
292
  * Compiles a {@link FilterExpr} into an in-memory row predicate
163
293
  * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
164
294
  * structure matches the SQL/Mongo adapters by construction.
165
295
  *
166
296
  * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
167
297
  * to a match-everything predicate.
298
+ *
299
+ * A filter holding relational predicates (`ResolvedRelationFilter` operands,
300
+ * since 0.1.147) needs `relationSets` — prepared internally by
301
+ * `MemoryAdapter` from the related tables; a standalone call on such a
302
+ * filter throws `REL_FILTER_NOT_SUPPORTED`.
168
303
  */
169
- function buildMemoryPredicate(filter) {
170
- return (0, _atscript_db.walkFilter)(filter, memoryVisitor) ?? (() => true);
304
+ function buildMemoryPredicate(filter, relationSets) {
305
+ return (0, _atscript_db.walkFilter)(filter, relationSets ? {
306
+ ...memoryVisitor,
307
+ relation: (field, op, operand) => relationPredicate(relationSets, field, op, operand)
308
+ } : memoryVisitor) ?? (() => true);
171
309
  }
172
310
  //#endregion
173
311
  //#region src/lib/memory-engine.ts
@@ -527,7 +665,7 @@ const databases = /* @__PURE__ */ new WeakMap();
527
665
  * runtime-owned entity with NO database can be observed as a READ-ONLY
528
666
  * atscript table; all writes are rejected (see {@link _assertWritable}).
529
667
  */
530
- var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
668
+ var MemoryAdapter = class MemoryAdapter extends _atscript_db.BaseDbAdapter {
531
669
  /**
532
670
  * The in-memory database: the space's (see {@link databases}), resolved once
533
671
  * in {@link registerSpace}; an adapter used without a space keeps this own one.
@@ -583,6 +721,19 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
583
721
  return !fd.encrypted;
584
722
  }
585
723
  /**
724
+ * Relational predicates (`{ nav: { $some | $none: … } }`) are evaluated in
725
+ * reads AND mutation filters. Before a filter holding them is compiled, every
726
+ * related table (and junction) is loaded ONCE for that operation — through
727
+ * its own adapter's snapshot seam, so a provider-backed related table works
728
+ * too — and each predicate becomes a set of matching correlation keys
729
+ * (see {@link _relationSets}). Predicate-free filters take the plain path.
730
+ *
731
+ * @since 0.1.147
732
+ */
733
+ supportsRelationFilters(_mode) {
734
+ return true;
735
+ }
736
+ /**
586
737
  * Coerces a by-id value to the id field's declared leaf type. The framework
587
738
  * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
588
739
  * URL id like `"21"` reaches this adapter as the STRING `"21"`. Memory does
@@ -643,14 +794,14 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
643
794
  /**
644
795
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
645
796
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
646
- * PK path through `pathToPhysical` (falling back to the name itself). Memoized
797
+ * PK path through `physicalPath` (`@db.column` renames included). Memoized
647
798
  * because it is stable per adapter (1:1 with a fixed table) yet read on every
648
799
  * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
649
800
  */
650
801
  _physicalPkFields() {
651
802
  if (this._pkFieldsCache) return this._pkFieldsCache;
652
803
  const metaIdPhysical = this._table.metaIdPhysical;
653
- const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.pathToPhysical.get(pk) ?? pk);
804
+ const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.physicalPath(pk));
654
805
  this._pkFieldsCache = fields;
655
806
  return fields;
656
807
  }
@@ -808,6 +959,39 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
808
959
  return [...this._peekState()?.rows.values() ?? []];
809
960
  }
810
961
  /**
962
+ * A per-operation row loader for relational predicates: each related table
963
+ * is snapshotted at most once per operation (the first predicate that needs
964
+ * it loads it; later ones — and nested ones — reuse that snapshot), so a
965
+ * provider-backed related table is invoked once per read. `own` seeds this
966
+ * table's snapshot so a self relation reads the same rows as the outer scan.
967
+ */
968
+ _snapshotLoader(own) {
969
+ const snapshots = /* @__PURE__ */ new Map();
970
+ if (own) snapshots.set(this, own);
971
+ return (adapter) => {
972
+ let rows = snapshots.get(adapter);
973
+ if (!rows) {
974
+ if (!(adapter instanceof MemoryAdapter)) throw new _atscript_db.DbError("REL_FILTER_NOT_SUPPORTED", [{
975
+ path: "",
976
+ message: "Relational predicate: the related table is not served by the memory adapter"
977
+ }]);
978
+ rows = Promise.resolve(adapter._loadRows());
979
+ snapshots.set(adapter, rows);
980
+ }
981
+ return rows;
982
+ };
983
+ }
984
+ /**
985
+ * The prepared correlation sets for `filter`'s relational predicates.
986
+ * Callers only await it when `containsRelationFilter(filter)` — a
987
+ * predicate-free filter keeps its fully synchronous path. Write methods call
988
+ * it BEFORE reading the table state, so the related tables (a self relation
989
+ * included) are snapshotted before any row changes, like SQL.
990
+ */
991
+ async _relationSets(filter, own) {
992
+ return prepareRelationSets(filter, this._snapshotLoader(own));
993
+ }
994
+ /**
811
995
  * Clones the payload in, applies the version default, generates any
812
996
  * `@db.default.increment` values, enforces PK + unique constraints, stores
813
997
  * the row, and returns the resolved inserted id. Clone-in (`structuredClone`)
@@ -891,11 +1075,14 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
891
1075
  *
892
1076
  * When `many` is `false` at most the first match is returned. A table that
893
1077
  * does not exist (`state` undefined) matches nothing and is not created.
1078
+ *
1079
+ * A filter with relational predicates needs `relationSets`, prepared by the
1080
+ * caller BEFORE it reads `state` (see {@link _relationSets}).
894
1081
  */
895
- _selectForWrite(state, filter, expectedVersion, many) {
1082
+ _selectForWrite(state, filter, expectedVersion, many, relationSets) {
896
1083
  const versionColumn = this._table.versionColumnPhysical;
897
1084
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
898
- const match = buildMemoryPredicate(filter);
1085
+ const match = buildMemoryPredicate(filter, relationSets);
899
1086
  const matched = [];
900
1087
  for (const [key, row] of state?.rows ?? []) {
901
1088
  if (!match(row)) continue;
@@ -975,8 +1162,9 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
975
1162
  }
976
1163
  async replaceOne(filter, data, expectedVersion) {
977
1164
  this._assertWritable();
1165
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
978
1166
  const state = this._peekState();
979
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1167
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
980
1168
  if (!state || matched.length === 0) return {
981
1169
  matchedCount: 0,
982
1170
  modifiedCount: 0
@@ -993,8 +1181,9 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
993
1181
  }
994
1182
  async updateOne(filter, data, ops, expectedVersion) {
995
1183
  this._assertWritable();
1184
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
996
1185
  const state = this._peekState();
997
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1186
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
998
1187
  if (!state || matched.length === 0) return {
999
1188
  matchedCount: 0,
1000
1189
  modifiedCount: 0
@@ -1008,8 +1197,9 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1008
1197
  }
1009
1198
  async deleteOne(filter) {
1010
1199
  this._assertWritable();
1200
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
1011
1201
  const state = this._peekState();
1012
- const matched = this._selectForWrite(state, filter, void 0, false);
1202
+ const matched = this._selectForWrite(state, filter, void 0, false, sets);
1013
1203
  if (!state || matched.length === 0) return { deletedCount: 0 };
1014
1204
  state.rows.delete(matched[0].key);
1015
1205
  return { deletedCount: 1 };
@@ -1061,8 +1251,14 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1061
1251
  * working set — one provider invocation per logical read.
1062
1252
  */
1063
1253
  async _filteredRows(query) {
1064
- const match = buildMemoryPredicate(query.filter);
1065
- return (await this._loadRows()).filter(match);
1254
+ if (!(0, _atscript_db.containsRelationFilter)(query.filter)) {
1255
+ const match = buildMemoryPredicate(query.filter);
1256
+ return (await this._loadRows()).filter(match);
1257
+ }
1258
+ const own = Promise.resolve(this._loadRows());
1259
+ own.catch(() => {});
1260
+ const sets = await this._relationSets(query.filter, own);
1261
+ return (await own).filter(buildMemoryPredicate(query.filter, sets));
1066
1262
  }
1067
1263
  async findOne(query) {
1068
1264
  const filtered = await this._filteredRows(query);
@@ -1117,14 +1313,19 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1117
1313
  aggregateFns() {
1118
1314
  return _atscript_db.ALL_AGGREGATE_FNS;
1119
1315
  }
1316
+ /** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
1317
+ viewCapabilities() {
1318
+ return _atscript_db.ALL_VIEW_CAPABILITIES;
1319
+ }
1120
1320
  async updateMany(filter, data, ops) {
1121
1321
  this._assertWritable();
1322
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
1122
1323
  const state = this._peekState();
1123
1324
  if (!state) return {
1124
1325
  matchedCount: 0,
1125
1326
  modifiedCount: 0
1126
1327
  };
1127
- const matched = this._selectForWrite(state, filter, void 0, true);
1328
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1128
1329
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1129
1330
  return {
1130
1331
  matchedCount: matched.length,
@@ -1133,12 +1334,13 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1133
1334
  }
1134
1335
  async replaceMany(filter, data) {
1135
1336
  this._assertWritable();
1337
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
1136
1338
  const state = this._peekState();
1137
1339
  if (!state) return {
1138
1340
  matchedCount: 0,
1139
1341
  modifiedCount: 0
1140
1342
  };
1141
- const matched = this._selectForWrite(state, filter, void 0, true);
1343
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1142
1344
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data);
1143
1345
  return {
1144
1346
  matchedCount: matched.length,
@@ -1147,9 +1349,10 @@ var MemoryAdapter = class extends _atscript_db.BaseDbAdapter {
1147
1349
  }
1148
1350
  async deleteMany(filter) {
1149
1351
  this._assertWritable();
1352
+ const sets = (0, _atscript_db.containsRelationFilter)(filter) ? await this._relationSets(filter) : void 0;
1150
1353
  const state = this._peekState();
1151
1354
  if (!state) return { deletedCount: 0 };
1152
- const matched = this._selectForWrite(state, filter, void 0, true);
1355
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1153
1356
  for (const { key } of matched) state.rows.delete(key);
1154
1357
  return { deletedCount: matched.length };
1155
1358
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/lib/memory-adapter.d.ts
@@ -76,6 +76,17 @@ declare class MemoryAdapter extends BaseDbAdapter {
76
76
  * decomposes patches into dot-path `$set`s and loads relations app-level.
77
77
  */
78
78
  canFilterField(fd: TDbFieldMeta): boolean;
79
+ /**
80
+ * Relational predicates (`{ nav: { $some | $none: … } }`) are evaluated in
81
+ * reads AND mutation filters. Before a filter holding them is compiled, every
82
+ * related table (and junction) is loaded ONCE for that operation — through
83
+ * its own adapter's snapshot seam, so a provider-backed related table works
84
+ * too — and each predicate becomes a set of matching correlation keys
85
+ * (see {@link _relationSets}). Predicate-free filters take the plain path.
86
+ *
87
+ * @since 0.1.147
88
+ */
89
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
79
90
  /**
80
91
  * Coerces a by-id value to the id field's declared leaf type. The framework
81
92
  * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
@@ -107,7 +118,7 @@ declare class MemoryAdapter extends BaseDbAdapter {
107
118
  /**
108
119
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
109
120
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
110
- * PK path through `pathToPhysical` (falling back to the name itself). Memoized
121
+ * PK path through `physicalPath` (`@db.column` renames included). Memoized
111
122
  * because it is stable per adapter (1:1 with a fixed table) yet read on every
112
123
  * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
113
124
  */
@@ -221,6 +232,22 @@ declare class MemoryAdapter extends BaseDbAdapter {
221
232
  * AND single-snapshot-per-`findManyWithCount` both fall out for free.
222
233
  */
223
234
  protected _loadRows(): Record<string, unknown>[] | Promise<Record<string, unknown>[]>;
235
+ /**
236
+ * A per-operation row loader for relational predicates: each related table
237
+ * is snapshotted at most once per operation (the first predicate that needs
238
+ * it loads it; later ones — and nested ones — reuse that snapshot), so a
239
+ * provider-backed related table is invoked once per read. `own` seeds this
240
+ * table's snapshot so a self relation reads the same rows as the outer scan.
241
+ */
242
+ private _snapshotLoader;
243
+ /**
244
+ * The prepared correlation sets for `filter`'s relational predicates.
245
+ * Callers only await it when `containsRelationFilter(filter)` — a
246
+ * predicate-free filter keeps its fully synchronous path. Write methods call
247
+ * it BEFORE reading the table state, so the related tables (a self relation
248
+ * included) are snapshotted before any row changes, like SQL.
249
+ */
250
+ private _relationSets;
224
251
  /**
225
252
  * Clones the payload in, applies the version default, generates any
226
253
  * `@db.default.increment` values, enforces PK + unique constraints, stores
@@ -257,6 +284,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
257
284
  *
258
285
  * When `many` is `false` at most the first match is returned. A table that
259
286
  * does not exist (`state` undefined) matches nothing and is not created.
287
+ *
288
+ * A filter with relational predicates needs `relationSets`, prepared by the
289
+ * caller BEFORE it reads `state` (see {@link _relationSets}).
260
290
  */
261
291
  private _selectForWrite;
262
292
  /**
@@ -378,6 +408,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
378
408
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
379
409
  /** Every aggregate function, `countDistinct` included. */
380
410
  aggregateFns(): ReadonlySet<AggregateFn>;
411
+ /** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
412
+ viewCapabilities(): ReadonlySet<TViewCapability>;
381
413
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
382
414
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
383
415
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
@@ -449,6 +481,14 @@ type Predicate = (row: Record<string, unknown>) => boolean;
449
481
  * later concern; the SQL/Mongo adapters flatten differently and we do not want
450
482
  * to fake a semantic the store can't back yet.
451
483
  */
484
+ /**
485
+ * Per-predicate correlation sets: for every {@link ResolvedRelationFilter} of
486
+ * a filter, the SOURCE correlation tuples (see {@link correlationKey}) that
487
+ * have at least one related row matching the predicate's inner filter.
488
+ * Built asynchronously by {@link prepareRelationSets}; consumed synchronously
489
+ * by {@link buildMemoryPredicate}.
490
+ */
491
+ type RelationSets = ReadonlyMap<ResolvedRelationFilter, ReadonlySet<string>>;
452
492
  /**
453
493
  * Compiles a {@link FilterExpr} into an in-memory row predicate
454
494
  * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
@@ -456,8 +496,13 @@ type Predicate = (row: Record<string, unknown>) => boolean;
456
496
  *
457
497
  * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
458
498
  * to a match-everything predicate.
499
+ *
500
+ * A filter holding relational predicates (`ResolvedRelationFilter` operands,
501
+ * since 0.1.147) needs `relationSets` — prepared internally by
502
+ * `MemoryAdapter` from the related tables; a standalone call on such a
503
+ * filter throws `REL_FILTER_NOT_SUPPORTED`.
459
504
  */
460
- declare function buildMemoryPredicate(filter: FilterExpr): Predicate;
505
+ declare function buildMemoryPredicate(filter: FilterExpr, relationSets?: RelationSets): Predicate;
461
506
  //#endregion
462
507
  //#region src/lib/memory-engine.d.ts
463
508
  /**
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, ResolvedRelationFilter, TDbDeleteResult, TDbFieldMeta, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateResult, TFieldOps, TViewCapability } from "@atscript/db";
2
2
  import { TAtscriptAnnotatedType } from "@atscript/typescript/utils";
3
3
 
4
4
  //#region src/lib/memory-adapter.d.ts
@@ -76,6 +76,17 @@ declare class MemoryAdapter extends BaseDbAdapter {
76
76
  * decomposes patches into dot-path `$set`s and loads relations app-level.
77
77
  */
78
78
  canFilterField(fd: TDbFieldMeta): boolean;
79
+ /**
80
+ * Relational predicates (`{ nav: { $some | $none: … } }`) are evaluated in
81
+ * reads AND mutation filters. Before a filter holding them is compiled, every
82
+ * related table (and junction) is loaded ONCE for that operation — through
83
+ * its own adapter's snapshot seam, so a provider-backed related table works
84
+ * too — and each predicate becomes a set of matching correlation keys
85
+ * (see {@link _relationSets}). Predicate-free filters take the plain path.
86
+ *
87
+ * @since 0.1.147
88
+ */
89
+ supportsRelationFilters(_mode: "read" | "write"): boolean;
79
90
  /**
80
91
  * Coerces a by-id value to the id field's declared leaf type. The framework
81
92
  * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
@@ -107,7 +118,7 @@ declare class MemoryAdapter extends BaseDbAdapter {
107
118
  /**
108
119
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
109
120
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
110
- * PK path through `pathToPhysical` (falling back to the name itself). Memoized
121
+ * PK path through `physicalPath` (`@db.column` renames included). Memoized
111
122
  * because it is stable per adapter (1:1 with a fixed table) yet read on every
112
123
  * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
113
124
  */
@@ -221,6 +232,22 @@ declare class MemoryAdapter extends BaseDbAdapter {
221
232
  * AND single-snapshot-per-`findManyWithCount` both fall out for free.
222
233
  */
223
234
  protected _loadRows(): Record<string, unknown>[] | Promise<Record<string, unknown>[]>;
235
+ /**
236
+ * A per-operation row loader for relational predicates: each related table
237
+ * is snapshotted at most once per operation (the first predicate that needs
238
+ * it loads it; later ones — and nested ones — reuse that snapshot), so a
239
+ * provider-backed related table is invoked once per read. `own` seeds this
240
+ * table's snapshot so a self relation reads the same rows as the outer scan.
241
+ */
242
+ private _snapshotLoader;
243
+ /**
244
+ * The prepared correlation sets for `filter`'s relational predicates.
245
+ * Callers only await it when `containsRelationFilter(filter)` — a
246
+ * predicate-free filter keeps its fully synchronous path. Write methods call
247
+ * it BEFORE reading the table state, so the related tables (a self relation
248
+ * included) are snapshotted before any row changes, like SQL.
249
+ */
250
+ private _relationSets;
224
251
  /**
225
252
  * Clones the payload in, applies the version default, generates any
226
253
  * `@db.default.increment` values, enforces PK + unique constraints, stores
@@ -257,6 +284,9 @@ declare class MemoryAdapter extends BaseDbAdapter {
257
284
  *
258
285
  * When `many` is `false` at most the first match is returned. A table that
259
286
  * does not exist (`state` undefined) matches nothing and is not created.
287
+ *
288
+ * A filter with relational predicates needs `relationSets`, prepared by the
289
+ * caller BEFORE it reads `state` (see {@link _relationSets}).
260
290
  */
261
291
  private _selectForWrite;
262
292
  /**
@@ -378,6 +408,8 @@ declare class MemoryAdapter extends BaseDbAdapter {
378
408
  calendarBucketUnits(): ReadonlySet<BucketUnit>;
379
409
  /** Every aggregate function, `countDistinct` included. */
380
410
  aggregateFns(): ReadonlySet<AggregateFn>;
411
+ /** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
412
+ viewCapabilities(): ReadonlySet<TViewCapability>;
381
413
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
382
414
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
383
415
  deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
@@ -449,6 +481,14 @@ type Predicate = (row: Record<string, unknown>) => boolean;
449
481
  * later concern; the SQL/Mongo adapters flatten differently and we do not want
450
482
  * to fake a semantic the store can't back yet.
451
483
  */
484
+ /**
485
+ * Per-predicate correlation sets: for every {@link ResolvedRelationFilter} of
486
+ * a filter, the SOURCE correlation tuples (see {@link correlationKey}) that
487
+ * have at least one related row matching the predicate's inner filter.
488
+ * Built asynchronously by {@link prepareRelationSets}; consumed synchronously
489
+ * by {@link buildMemoryPredicate}.
490
+ */
491
+ type RelationSets = ReadonlyMap<ResolvedRelationFilter, ReadonlySet<string>>;
452
492
  /**
453
493
  * Compiles a {@link FilterExpr} into an in-memory row predicate
454
494
  * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
@@ -456,8 +496,13 @@ type Predicate = (row: Record<string, unknown>) => boolean;
456
496
  *
457
497
  * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
458
498
  * to a match-everything predicate.
499
+ *
500
+ * A filter holding relational predicates (`ResolvedRelationFilter` operands,
501
+ * since 0.1.147) needs `relationSets` — prepared internally by
502
+ * `MemoryAdapter` from the related tables; a standalone call on such a
503
+ * filter throws `REL_FILTER_NOT_SUPPORTED`.
459
504
  */
460
- declare function buildMemoryPredicate(filter: FilterExpr): Predicate;
505
+ declare function buildMemoryPredicate(filter: FilterExpr, relationSets?: RelationSets): Predicate;
461
506
  //#endregion
462
507
  //#region src/lib/memory-engine.d.ts
463
508
  /**
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, BaseDbAdapter, DbError, DbSpace, deletePath, getPath, getPath as getPath$1, isAtscriptDbView, walkFilter } from "@atscript/db";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, containsRelationFilter, deletePath, forEachResolvedRelation, getPath, getPath as getPath$1, isAtscriptDbView, isResolvedRelationFilter, walkFilter } from "@atscript/db";
2
2
  import { assertAggregateFn, resolveAlias } from "@atscript/db/agg";
3
3
  import { bucketer } from "@uniqu/core";
4
4
  //#region src/lib/memory-filter.ts
@@ -155,18 +155,156 @@ const memoryVisitor = {
155
155
  message: `Unsupported filter operator: ${op}`
156
156
  }]);
157
157
  }
158
+ },
159
+ relation(field, op, operand) {
160
+ return relationPredicate(void 0, field, op, operand);
158
161
  }
159
162
  };
160
163
  /**
164
+ * Identity of the key tuple `fields` of `row` (dot-paths read nested), or
165
+ * `undefined` when a component is `null` / missing — a NULL key component
166
+ * never correlates (SQL `=` semantics: `$some` false, `$none` true).
167
+ * Every component is type-tagged (see {@link keyPart}), matching the strict
168
+ * equality the memory filter uses everywhere (`valuesEqual`): `1` ≠ `"1"`,
169
+ * `5n` ≠ `"5n"`, a `Date` ≠ its ISO string; `Date`s compare by instant.
170
+ */
171
+ function correlationKey(row, fields) {
172
+ const parts = [];
173
+ for (const field of fields) {
174
+ const part = keyPart(getPath$1(row, field));
175
+ if (part === void 0) return;
176
+ parts.push(part);
177
+ }
178
+ return JSON.stringify(parts);
179
+ }
180
+ /**
181
+ * Type-tagged identity of one key component, or `undefined` when it never
182
+ * equals anything (`null` / missing, `NaN`).
183
+ */
184
+ function keyPart(value) {
185
+ switch (typeof value) {
186
+ case "string": return `s:${value}`;
187
+ case "number": return Number.isNaN(value) ? void 0 : `n:${value === 0 ? 0 : value}`;
188
+ case "bigint": return `b:${value}`;
189
+ case "boolean": return `t:${value}`;
190
+ case "undefined": return;
191
+ default:
192
+ if (value === null) return void 0;
193
+ if (value instanceof Date) {
194
+ const time = value.getTime();
195
+ return Number.isNaN(time) ? void 0 : `d:${time}`;
196
+ }
197
+ return `o:${JSON.stringify(value, (_k, v) => typeof v === "bigint" ? `${v}n` : v)}`;
198
+ }
199
+ }
200
+ /** Source-side correlation columns of a resolved predicate (physical names). */
201
+ function sourceColumns(node) {
202
+ return node.junction ? node.junction.toSource.map((p) => p.source) : node.pairs.map((p) => p.source);
203
+ }
204
+ /**
205
+ * The visitor's `relation` callback: a row satisfies `$some` when its source
206
+ * correlation tuple is in the predicate's prepared set, `$none` otherwise.
207
+ */
208
+ function relationPredicate(sets, field, op, operand) {
209
+ const node = operand;
210
+ const set = isResolvedRelationFilter(node) ? sets?.get(node) : void 0;
211
+ if (!set) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
212
+ path: field,
213
+ message: `Relational predicate "${op}" on "${field}" was not prepared — call prepareRelationSets() before buildMemoryPredicate()`
214
+ }]);
215
+ const columns = sourceColumns(node);
216
+ const some = op === "$some";
217
+ return (row) => {
218
+ const key = correlationKey(row, columns);
219
+ return (key !== void 0 && set.has(key)) === some;
220
+ };
221
+ }
222
+ /**
223
+ * Resolves every relational predicate of a TRANSLATED filter (nested ones
224
+ * included) into a correlation set, loading each related table through
225
+ * `load`:
226
+ *
227
+ * - `to` / `from`: the related rows matching the inner filter contribute their
228
+ * `pairs[].target` tuples — equal to the source tuple (`pairs[].source`) of
229
+ * every row that has such a related row.
230
+ * - `via`: the matching related rows' `junction.toTarget[].target` tuples
231
+ * select junction rows (also filtered by the junction part of
232
+ * `@db.rel.filter`), which contribute their `junction.toSource[].junction`
233
+ * tuples.
234
+ *
235
+ * Nested predicates are prepared against the related table's rows first
236
+ * (their source is this predicate's target). Returns `undefined` when the
237
+ * filter holds no predicate — callers then stay on the plain synchronous path.
238
+ *
239
+ * @since 0.1.147
240
+ */
241
+ async function prepareRelationSets(filter, load) {
242
+ if (!containsRelationFilter(filter)) return;
243
+ const sets = /* @__PURE__ */ new Map();
244
+ await prepareLevel(filter, load, sets);
245
+ return sets;
246
+ }
247
+ /** Prepares the predicates at one level of `filter` (and, recursively, their operands). */
248
+ async function prepareLevel(filter, load, sets) {
249
+ const nodes = [];
250
+ forEachResolvedRelation(filter, (node) => nodes.push(node), false);
251
+ for (const node of nodes) if (!sets.has(node)) sets.set(node, await correlationSet(node, load, sets));
252
+ }
253
+ /** Matching rows of `adapter`'s table for `filter` (its own predicates prepared first). */
254
+ async function matchingRows(adapter, filter, load, sets) {
255
+ const rows = await load(adapter);
256
+ if (!filter || Object.keys(filter).length === 0) return rows;
257
+ await prepareLevel(filter, load, sets);
258
+ return rows.filter(buildMemoryPredicate(filter, sets));
259
+ }
260
+ /** The source correlation tuples that have a related row matching `node`. */
261
+ async function correlationSet(node, load, sets) {
262
+ const targets = await matchingRows(node.target.adapter, node.filter, load, sets);
263
+ const result = /* @__PURE__ */ new Set();
264
+ if (!node.junction) {
265
+ const columns = node.pairs.map((p) => p.target);
266
+ for (const row of targets) {
267
+ const key = correlationKey(row, columns);
268
+ if (key !== void 0) result.add(key);
269
+ }
270
+ return result;
271
+ }
272
+ const { junction } = node;
273
+ const targetKeys = /* @__PURE__ */ new Set();
274
+ const targetColumns = junction.toTarget.map((p) => p.target);
275
+ for (const row of targets) {
276
+ const key = correlationKey(row, targetColumns);
277
+ if (key !== void 0) targetKeys.add(key);
278
+ }
279
+ if (targetKeys.size === 0) return result;
280
+ const toTarget = junction.toTarget.map((p) => p.junction);
281
+ const toSource = junction.toSource.map((p) => p.junction);
282
+ for (const row of await matchingRows(junction.adapter, junction.filter, load, sets)) {
283
+ const targetKey = correlationKey(row, toTarget);
284
+ if (targetKey === void 0 || !targetKeys.has(targetKey)) continue;
285
+ const sourceKey = correlationKey(row, toSource);
286
+ if (sourceKey !== void 0) result.add(sourceKey);
287
+ }
288
+ return result;
289
+ }
290
+ /**
161
291
  * Compiles a {@link FilterExpr} into an in-memory row predicate
162
292
  * `(row) => boolean`, reusing the shared {@link walkFilter} walker so filter
163
293
  * structure matches the SQL/Mongo adapters by construction.
164
294
  *
165
295
  * An empty/absent filter (for which `walkFilter` returns `undefined`) compiles
166
296
  * to a match-everything predicate.
297
+ *
298
+ * A filter holding relational predicates (`ResolvedRelationFilter` operands,
299
+ * since 0.1.147) needs `relationSets` — prepared internally by
300
+ * `MemoryAdapter` from the related tables; a standalone call on such a
301
+ * filter throws `REL_FILTER_NOT_SUPPORTED`.
167
302
  */
168
- function buildMemoryPredicate(filter) {
169
- return walkFilter(filter, memoryVisitor) ?? (() => true);
303
+ function buildMemoryPredicate(filter, relationSets) {
304
+ return walkFilter(filter, relationSets ? {
305
+ ...memoryVisitor,
306
+ relation: (field, op, operand) => relationPredicate(relationSets, field, op, operand)
307
+ } : memoryVisitor) ?? (() => true);
170
308
  }
171
309
  //#endregion
172
310
  //#region src/lib/memory-engine.ts
@@ -526,7 +664,7 @@ const databases = /* @__PURE__ */ new WeakMap();
526
664
  * runtime-owned entity with NO database can be observed as a READ-ONLY
527
665
  * atscript table; all writes are rejected (see {@link _assertWritable}).
528
666
  */
529
- var MemoryAdapter = class extends BaseDbAdapter {
667
+ var MemoryAdapter = class MemoryAdapter extends BaseDbAdapter {
530
668
  /**
531
669
  * The in-memory database: the space's (see {@link databases}), resolved once
532
670
  * in {@link registerSpace}; an adapter used without a space keeps this own one.
@@ -582,6 +720,19 @@ var MemoryAdapter = class extends BaseDbAdapter {
582
720
  return !fd.encrypted;
583
721
  }
584
722
  /**
723
+ * Relational predicates (`{ nav: { $some | $none: … } }`) are evaluated in
724
+ * reads AND mutation filters. Before a filter holding them is compiled, every
725
+ * related table (and junction) is loaded ONCE for that operation — through
726
+ * its own adapter's snapshot seam, so a provider-backed related table works
727
+ * too — and each predicate becomes a set of matching correlation keys
728
+ * (see {@link _relationSets}). Predicate-free filters take the plain path.
729
+ *
730
+ * @since 0.1.147
731
+ */
732
+ supportsRelationFilters(_mode) {
733
+ return true;
734
+ }
735
+ /**
585
736
  * Coerces a by-id value to the id field's declared leaf type. The framework
586
737
  * calls `adapter.prepareId(value, fieldType)` when building by-id filters, so a
587
738
  * URL id like `"21"` reaches this adapter as the STRING `"21"`. Memory does
@@ -642,14 +793,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
642
793
  /**
643
794
  * Physical names of the primary-key field(s). Single `@meta.id` resolves via
644
795
  * {@link AtscriptDbReadable.metaIdPhysical}; a composite key maps each logical
645
- * PK path through `pathToPhysical` (falling back to the name itself). Memoized
796
+ * PK path through `physicalPath` (`@db.column` renames included). Memoized
646
797
  * because it is stable per adapter (1:1 with a fixed table) yet read on every
647
798
  * `pkKey`/projection — recomputing would re-read `this._table.*` and re-allocate.
648
799
  */
649
800
  _physicalPkFields() {
650
801
  if (this._pkFieldsCache) return this._pkFieldsCache;
651
802
  const metaIdPhysical = this._table.metaIdPhysical;
652
- const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.pathToPhysical.get(pk) ?? pk);
803
+ const fields = metaIdPhysical ? [metaIdPhysical] : this._table.primaryKeys.map((pk) => this._table.physicalPath(pk));
653
804
  this._pkFieldsCache = fields;
654
805
  return fields;
655
806
  }
@@ -807,6 +958,39 @@ var MemoryAdapter = class extends BaseDbAdapter {
807
958
  return [...this._peekState()?.rows.values() ?? []];
808
959
  }
809
960
  /**
961
+ * A per-operation row loader for relational predicates: each related table
962
+ * is snapshotted at most once per operation (the first predicate that needs
963
+ * it loads it; later ones — and nested ones — reuse that snapshot), so a
964
+ * provider-backed related table is invoked once per read. `own` seeds this
965
+ * table's snapshot so a self relation reads the same rows as the outer scan.
966
+ */
967
+ _snapshotLoader(own) {
968
+ const snapshots = /* @__PURE__ */ new Map();
969
+ if (own) snapshots.set(this, own);
970
+ return (adapter) => {
971
+ let rows = snapshots.get(adapter);
972
+ if (!rows) {
973
+ if (!(adapter instanceof MemoryAdapter)) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
974
+ path: "",
975
+ message: "Relational predicate: the related table is not served by the memory adapter"
976
+ }]);
977
+ rows = Promise.resolve(adapter._loadRows());
978
+ snapshots.set(adapter, rows);
979
+ }
980
+ return rows;
981
+ };
982
+ }
983
+ /**
984
+ * The prepared correlation sets for `filter`'s relational predicates.
985
+ * Callers only await it when `containsRelationFilter(filter)` — a
986
+ * predicate-free filter keeps its fully synchronous path. Write methods call
987
+ * it BEFORE reading the table state, so the related tables (a self relation
988
+ * included) are snapshotted before any row changes, like SQL.
989
+ */
990
+ async _relationSets(filter, own) {
991
+ return prepareRelationSets(filter, this._snapshotLoader(own));
992
+ }
993
+ /**
810
994
  * Clones the payload in, applies the version default, generates any
811
995
  * `@db.default.increment` values, enforces PK + unique constraints, stores
812
996
  * the row, and returns the resolved inserted id. Clone-in (`structuredClone`)
@@ -890,11 +1074,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
890
1074
  *
891
1075
  * When `many` is `false` at most the first match is returned. A table that
892
1076
  * does not exist (`state` undefined) matches nothing and is not created.
1077
+ *
1078
+ * A filter with relational predicates needs `relationSets`, prepared by the
1079
+ * caller BEFORE it reads `state` (see {@link _relationSets}).
893
1080
  */
894
- _selectForWrite(state, filter, expectedVersion, many) {
1081
+ _selectForWrite(state, filter, expectedVersion, many, relationSets) {
895
1082
  const versionColumn = this._table.versionColumnPhysical;
896
1083
  if (expectedVersion !== void 0 && versionColumn === void 0) throw new Error("expectedVersion requires a versioned table");
897
- const match = buildMemoryPredicate(filter);
1084
+ const match = buildMemoryPredicate(filter, relationSets);
898
1085
  const matched = [];
899
1086
  for (const [key, row] of state?.rows ?? []) {
900
1087
  if (!match(row)) continue;
@@ -974,8 +1161,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
974
1161
  }
975
1162
  async replaceOne(filter, data, expectedVersion) {
976
1163
  this._assertWritable();
1164
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
977
1165
  const state = this._peekState();
978
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1166
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
979
1167
  if (!state || matched.length === 0) return {
980
1168
  matchedCount: 0,
981
1169
  modifiedCount: 0
@@ -992,8 +1180,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
992
1180
  }
993
1181
  async updateOne(filter, data, ops, expectedVersion) {
994
1182
  this._assertWritable();
1183
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
995
1184
  const state = this._peekState();
996
- const matched = this._selectForWrite(state, filter, expectedVersion, false);
1185
+ const matched = this._selectForWrite(state, filter, expectedVersion, false, sets);
997
1186
  if (!state || matched.length === 0) return {
998
1187
  matchedCount: 0,
999
1188
  modifiedCount: 0
@@ -1007,8 +1196,9 @@ var MemoryAdapter = class extends BaseDbAdapter {
1007
1196
  }
1008
1197
  async deleteOne(filter) {
1009
1198
  this._assertWritable();
1199
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1010
1200
  const state = this._peekState();
1011
- const matched = this._selectForWrite(state, filter, void 0, false);
1201
+ const matched = this._selectForWrite(state, filter, void 0, false, sets);
1012
1202
  if (!state || matched.length === 0) return { deletedCount: 0 };
1013
1203
  state.rows.delete(matched[0].key);
1014
1204
  return { deletedCount: 1 };
@@ -1060,8 +1250,14 @@ var MemoryAdapter = class extends BaseDbAdapter {
1060
1250
  * working set — one provider invocation per logical read.
1061
1251
  */
1062
1252
  async _filteredRows(query) {
1063
- const match = buildMemoryPredicate(query.filter);
1064
- return (await this._loadRows()).filter(match);
1253
+ if (!containsRelationFilter(query.filter)) {
1254
+ const match = buildMemoryPredicate(query.filter);
1255
+ return (await this._loadRows()).filter(match);
1256
+ }
1257
+ const own = Promise.resolve(this._loadRows());
1258
+ own.catch(() => {});
1259
+ const sets = await this._relationSets(query.filter, own);
1260
+ return (await own).filter(buildMemoryPredicate(query.filter, sets));
1065
1261
  }
1066
1262
  async findOne(query) {
1067
1263
  const filtered = await this._filteredRows(query);
@@ -1116,14 +1312,19 @@ var MemoryAdapter = class extends BaseDbAdapter {
1116
1312
  aggregateFns() {
1117
1313
  return ALL_AGGREGATE_FNS;
1118
1314
  }
1315
+ /** Managed views render nothing (non-goal) — both are accepted like `aggregateFns()`. */
1316
+ viewCapabilities() {
1317
+ return ALL_VIEW_CAPABILITIES;
1318
+ }
1119
1319
  async updateMany(filter, data, ops) {
1120
1320
  this._assertWritable();
1321
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1121
1322
  const state = this._peekState();
1122
1323
  if (!state) return {
1123
1324
  matchedCount: 0,
1124
1325
  modifiedCount: 0
1125
1326
  };
1126
- const matched = this._selectForWrite(state, filter, void 0, true);
1327
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1127
1328
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data, ops);
1128
1329
  return {
1129
1330
  matchedCount: matched.length,
@@ -1132,12 +1333,13 @@ var MemoryAdapter = class extends BaseDbAdapter {
1132
1333
  }
1133
1334
  async replaceMany(filter, data) {
1134
1335
  this._assertWritable();
1336
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1135
1337
  const state = this._peekState();
1136
1338
  if (!state) return {
1137
1339
  matchedCount: 0,
1138
1340
  modifiedCount: 0
1139
1341
  };
1140
- const matched = this._selectForWrite(state, filter, void 0, true);
1342
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1141
1343
  for (const { key, row } of matched) this._commitUpdate(state, key, row, data);
1142
1344
  return {
1143
1345
  matchedCount: matched.length,
@@ -1146,9 +1348,10 @@ var MemoryAdapter = class extends BaseDbAdapter {
1146
1348
  }
1147
1349
  async deleteMany(filter) {
1148
1350
  this._assertWritable();
1351
+ const sets = containsRelationFilter(filter) ? await this._relationSets(filter) : void 0;
1149
1352
  const state = this._peekState();
1150
1353
  if (!state) return { deletedCount: 0 };
1151
- const matched = this._selectForWrite(state, filter, void 0, true);
1354
+ const matched = this._selectForWrite(state, filter, void 0, true, sets);
1152
1355
  for (const { key } of matched) state.rows.delete(key);
1153
1356
  return { deletedCount: matched.length };
1154
1357
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-memory",
3
- "version": "0.1.145",
3
+ "version": "0.1.147",
4
4
  "description": "In-memory adapter for atscript-db.",
5
5
  "keywords": [
6
6
  "atscript",
@@ -36,16 +36,16 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@atscript/core": "^0.1.97",
40
- "@atscript/typescript": "^0.1.97",
41
- "@uniqu/core": "^0.1.11",
42
- "unplugin-atscript": "^0.1.97"
39
+ "@atscript/core": "^0.1.99",
40
+ "@atscript/typescript": "^0.1.99",
41
+ "@uniqu/core": "^0.1.12",
42
+ "unplugin-atscript": "^0.1.99"
43
43
  },
44
44
  "peerDependencies": {
45
- "@atscript/core": "^0.1.97",
46
- "@atscript/typescript": "^0.1.97",
47
- "@uniqu/core": "^0.1.11",
48
- "@atscript/db": "^0.1.145"
45
+ "@atscript/core": "^0.1.99",
46
+ "@atscript/typescript": "^0.1.99",
47
+ "@uniqu/core": "^0.1.12",
48
+ "@atscript/db": "^0.1.147"
49
49
  },
50
50
  "scripts": {
51
51
  "postinstall": "asc -f dts",