@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 +218 -15
- package/dist/index.d.cts +48 -3
- package/dist/index.d.mts +48 -3
- package/dist/index.mjs +219 -16
- package/package.json +9 -9
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,
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
1065
|
-
|
|
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 `
|
|
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 `
|
|
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,
|
|
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 `
|
|
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.
|
|
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
|
-
|
|
1064
|
-
|
|
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.
|
|
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.
|
|
40
|
-
"@atscript/typescript": "^0.1.
|
|
41
|
-
"@uniqu/core": "^0.1.
|
|
42
|
-
"unplugin-atscript": "^0.1.
|
|
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.
|
|
46
|
-
"@atscript/typescript": "^0.1.
|
|
47
|
-
"@uniqu/core": "^0.1.
|
|
48
|
-
"@atscript/db": "^0.1.
|
|
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",
|