@prisma-idb/client-idb 0.10.0 → 0.12.0
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/README.md +6 -0
- package/dist/client-auto.d.mts +2 -2
- package/dist/client-auto.mjs +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{executor-CevtdBfz.d.mts → executor-CIEHpa1j.d.mts} +16 -9
- package/dist/{executor-CevtdBfz.d.mts.map → executor-CIEHpa1j.d.mts.map} +1 -1
- package/dist/{idb-orm-gEKHfG4H.d.mts → idb-orm-6OuwkeDM.d.mts} +5 -5
- package/dist/{idb-orm-gEKHfG4H.d.mts.map → idb-orm-6OuwkeDM.d.mts.map} +1 -1
- package/dist/{idb-orm-DsfllvzD.mjs → idb-orm-B1Br1C0F.mjs} +84 -427
- package/dist/idb-orm-B1Br1C0F.mjs.map +1 -0
- package/dist/internal.d.mts +2 -2
- package/dist/internal.d.mts.map +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/{managed-client-B_xGXdjz.d.mts → managed-client-DQlsxc4Z.d.mts} +3 -3
- package/dist/{managed-client-B_xGXdjz.d.mts.map → managed-client-DQlsxc4Z.d.mts.map} +1 -1
- package/dist/{managed-client-Cp01irrm.mjs → managed-client-M3xMUk0n.mjs} +3 -3
- package/dist/{managed-client-Cp01irrm.mjs.map → managed-client-M3xMUk0n.mjs.map} +1 -1
- package/dist/{mutation-executor-CD5n_kKT.mjs → mutation-executor-inLFb0TE.mjs} +699 -215
- package/dist/mutation-executor-inLFb0TE.mjs.map +1 -0
- package/dist/{mutation-scope-pOn0QzvV.d.mts → mutation-scope-DrxFhek3.d.mts} +2 -2
- package/dist/{mutation-scope-pOn0QzvV.d.mts.map → mutation-scope-DrxFhek3.d.mts.map} +1 -1
- package/dist/orm.d.mts +3 -3
- package/dist/orm.mjs +2 -2
- package/package.json +7 -6
- package/dist/idb-orm-DsfllvzD.mjs.map +0 -1
- package/dist/mutation-executor-CD5n_kKT.mjs.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { evaluateFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
1
|
+
import { andExpr, evaluateFilter, fieldFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
|
|
2
2
|
import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
|
|
3
|
-
import { fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyToken } from "@prisma-idb/target-idb/runtime";
|
|
3
|
+
import { compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyEquals as keyEquals$1, keyToken } from "@prisma-idb/target-idb/runtime";
|
|
4
4
|
import { generateId } from "@prisma/orm-framework/ids/runtime";
|
|
5
5
|
import { keyPathFields } from "@prisma-idb/target-idb/pack";
|
|
6
6
|
//#region src/core/types.ts
|
|
@@ -39,19 +39,6 @@ function extractKeyFromRow(row, keyPath) {
|
|
|
39
39
|
return keyPath.map((field) => row[field]);
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Build a field → indexName lookup for every single-field, non-multi-entry
|
|
43
|
-
* index on `storeName`. Top-level scans use this map to find indexed equality
|
|
44
|
-
* conditions in combined filter expressions. Compound and multi-entry indexes
|
|
45
|
-
* cannot serve these single-field point lookups.
|
|
46
|
-
*/
|
|
47
|
-
function buildFieldToIndexMap(contract, storeName) {
|
|
48
|
-
const storeDef = contract.storage.stores[storeName];
|
|
49
|
-
const result = {};
|
|
50
|
-
if (storeDef?.indexes === void 0) return result;
|
|
51
|
-
for (const [indexName, indexDef] of Object.entries(storeDef.indexes)) if (typeof indexDef.keyPath === "string" && indexDef.multiEntry !== true) result[indexDef.keyPath] = indexName;
|
|
52
|
-
return result;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
42
|
* Resolve a model's named relation to a {@link ContractReferenceRelation} at
|
|
56
43
|
* runtime, or `undefined` when the relation is absent or an embedded relation
|
|
57
44
|
* (no `on` join block). Used by `include()` to find the related model name and
|
|
@@ -63,6 +50,39 @@ function getRelation(contract, modelName, relName) {
|
|
|
63
50
|
return relation;
|
|
64
51
|
}
|
|
65
52
|
//#endregion
|
|
53
|
+
//#region src/core/query-shaping.ts
|
|
54
|
+
/**
|
|
55
|
+
* Combine accumulated filter expressions with AND.
|
|
56
|
+
*
|
|
57
|
+
* Returns `undefined` when no filter is present so callers can skip building a
|
|
58
|
+
* row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
|
|
59
|
+
* and the relation loader (refined `include()` child scans).
|
|
60
|
+
*/
|
|
61
|
+
function combineFilterExprs(filters) {
|
|
62
|
+
if (filters.length === 0) return void 0;
|
|
63
|
+
if (filters.length === 1) return filters[0];
|
|
64
|
+
return andExpr(filters);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Build an in-memory comparator from an `orderBy` spec (field → direction).
|
|
68
|
+
*
|
|
69
|
+
* Returns `undefined` when there is nothing to sort by. Compares fields in
|
|
70
|
+
* declaration order, with {@link compareFieldValues}: the same order an
|
|
71
|
+
* IndexedDB index uses, so equal `Date`s tie and fall through to the next
|
|
72
|
+
* field, and `null`s sort last (first when descending).
|
|
73
|
+
*/
|
|
74
|
+
function buildRowComparator(orderBy) {
|
|
75
|
+
if (orderBy === void 0) return void 0;
|
|
76
|
+
return (a, b) => {
|
|
77
|
+
for (const [field, dir] of Object.entries(orderBy)) {
|
|
78
|
+
const cmp = compareFieldValues(a[field], b[field]);
|
|
79
|
+
if (cmp === 0) continue;
|
|
80
|
+
return dir === "desc" ? -cmp : cmp;
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
66
86
|
//#region src/core/enum-validation.ts
|
|
67
87
|
const rulesByContract = /* @__PURE__ */ new WeakMap();
|
|
68
88
|
function resolveEnumRule(contract, field) {
|
|
@@ -191,6 +211,565 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
|
191
211
|
return result;
|
|
192
212
|
}
|
|
193
213
|
//#endregion
|
|
214
|
+
//#region src/core/planner/catalog.ts
|
|
215
|
+
const catalogs = /* @__PURE__ */ new WeakMap();
|
|
216
|
+
/** Build a catalog once per contract identity and store name. Contracts are immutable. */
|
|
217
|
+
function buildCatalog(contract, storeName) {
|
|
218
|
+
const cached = catalogs.get(contract)?.get(storeName);
|
|
219
|
+
if (cached) return cached;
|
|
220
|
+
const store = contract.storage.stores[storeName];
|
|
221
|
+
if (!store) throw new Error(`Store "${storeName}" is not in the contract`);
|
|
222
|
+
const fields = {};
|
|
223
|
+
for (const model of Object.values(domainModelsAtDefaultNamespace(contract.domain))) {
|
|
224
|
+
if (model.storage.storeName !== storeName) continue;
|
|
225
|
+
for (const [name, field] of Object.entries(model.fields)) fields[name] = Object.freeze({
|
|
226
|
+
codecId: field.type.kind === "scalar" ? field.type.codecId : void 0,
|
|
227
|
+
nullable: field.nullable,
|
|
228
|
+
collection: field.many === true || field.dict === true
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
const catalog = Object.freeze({
|
|
232
|
+
storeName,
|
|
233
|
+
primaryKey: source(store.keyPath, true, false),
|
|
234
|
+
indexes: Object.freeze(Object.entries(store.indexes ?? {}).map(([name, index]) => source(index.keyPath, index.unique, index.multiEntry ?? false, name))),
|
|
235
|
+
fields: Object.freeze(fields)
|
|
236
|
+
});
|
|
237
|
+
let byStore = catalogs.get(contract);
|
|
238
|
+
if (!byStore) {
|
|
239
|
+
byStore = /* @__PURE__ */ new Map();
|
|
240
|
+
catalogs.set(contract, byStore);
|
|
241
|
+
}
|
|
242
|
+
byStore.set(storeName, catalog);
|
|
243
|
+
return catalog;
|
|
244
|
+
}
|
|
245
|
+
function source(keyPath, unique, multiEntry, indexName) {
|
|
246
|
+
const path = typeof keyPath === "string" ? keyPath : Object.freeze([...keyPath]);
|
|
247
|
+
return Object.freeze({
|
|
248
|
+
keyPath: path,
|
|
249
|
+
fields: Object.freeze(typeof path === "string" ? [path] : [...path]),
|
|
250
|
+
unique,
|
|
251
|
+
multiEntry,
|
|
252
|
+
...indexName === void 0 ? {} : { indexName }
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/core/planner/plan.ts
|
|
257
|
+
const COST = {
|
|
258
|
+
UNIQUE_POINT: 0,
|
|
259
|
+
PRIMARY_POINT: 1,
|
|
260
|
+
INDEX_POINT: 2,
|
|
261
|
+
PREFIX_POINT: 3,
|
|
262
|
+
TWO_SIDED_RANGE: 4,
|
|
263
|
+
ONE_SIDED_RANGE: 5,
|
|
264
|
+
ORDER_ONLY_SCAN: 6
|
|
265
|
+
};
|
|
266
|
+
const KEY_PARTIAL_CODECS = /* @__PURE__ */ new Set(["idb/double@1", "idb/date@1"]);
|
|
267
|
+
const KEY_CODECS = /* @__PURE__ */ new Set([
|
|
268
|
+
"idb/string@1",
|
|
269
|
+
"idb/int32@1",
|
|
270
|
+
"idb/double@1",
|
|
271
|
+
"idb/date@1",
|
|
272
|
+
"idb/decimal@1",
|
|
273
|
+
"idb/bytes@1"
|
|
274
|
+
]);
|
|
275
|
+
/** Choose the cheapest safe access without opening IndexedDB or probing cardinality. */
|
|
276
|
+
function planQuery(catalog, request) {
|
|
277
|
+
const normalized = normalize(request.where);
|
|
278
|
+
if (normalized.empty) return {
|
|
279
|
+
access: { kind: "empty" },
|
|
280
|
+
exact: true
|
|
281
|
+
};
|
|
282
|
+
const orders = Object.entries(request.orderBy ?? {});
|
|
283
|
+
if (normalized.excluded || request.hasRelationFilter || orders.length > 1) return full(false);
|
|
284
|
+
const constraints = /* @__PURE__ */ new Map();
|
|
285
|
+
for (const atom of normalized.atoms) {
|
|
286
|
+
let constraint = constraints.get(atom.field);
|
|
287
|
+
if (!constraint) {
|
|
288
|
+
constraint = { consumed: 0 };
|
|
289
|
+
constraints.set(atom.field, constraint);
|
|
290
|
+
}
|
|
291
|
+
addAtom(constraint, atom, catalog.fields[atom.field]);
|
|
292
|
+
}
|
|
293
|
+
for (const [name, constraint] of constraints) {
|
|
294
|
+
if (keyPartial(catalog.fields[name]) && !constraint.rejectsNonKeys) {
|
|
295
|
+
delete constraint.lower;
|
|
296
|
+
delete constraint.upper;
|
|
297
|
+
constraint.consumed = 0;
|
|
298
|
+
}
|
|
299
|
+
if (contradictory(constraint)) return {
|
|
300
|
+
access: { kind: "empty" },
|
|
301
|
+
exact: true
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const sources = [catalog.primaryKey, ...catalog.indexes];
|
|
305
|
+
const candidates = sources.flatMap((source, declaration) => {
|
|
306
|
+
const candidate = filteringCandidate(catalog, source, constraints, declaration);
|
|
307
|
+
return candidate ? [candidate] : [];
|
|
308
|
+
});
|
|
309
|
+
candidates.sort((a, b) => a.cost - b.cost || a.ranges.length - b.ranges.length || Number(a.source.indexName !== void 0) - Number(b.source.indexName !== void 0) || a.declaration - b.declaration);
|
|
310
|
+
let selected = candidates[0];
|
|
311
|
+
let direction;
|
|
312
|
+
if (orders.length === 1 && request.take !== void 0) {
|
|
313
|
+
const [field, order] = orders[0];
|
|
314
|
+
if (selected) {
|
|
315
|
+
if (singleFieldSource(selected.source, field)) direction = order === "asc" ? "next" : "prev";
|
|
316
|
+
} else {
|
|
317
|
+
const ordering = sources.find((source) => !source.multiEntry && singleFieldSource(source, field) && completeKeyField(catalog.fields[field]));
|
|
318
|
+
if (ordering) {
|
|
319
|
+
selected = {
|
|
320
|
+
source: ordering,
|
|
321
|
+
ranges: [void 0],
|
|
322
|
+
cost: COST.ORDER_ONLY_SCAN,
|
|
323
|
+
consumed: 0,
|
|
324
|
+
declaration: sources.indexOf(ordering)
|
|
325
|
+
};
|
|
326
|
+
direction = order === "asc" ? "next" : "prev";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!selected) {
|
|
331
|
+
const partialOrder = orders.length === 1 && request.take !== void 0 && keyPartial(catalog.fields[orders[0][0]]);
|
|
332
|
+
return full(normalized.atoms.length + normalized.residual === 0 && !partialOrder);
|
|
333
|
+
}
|
|
334
|
+
return {
|
|
335
|
+
access: {
|
|
336
|
+
kind: "ranges",
|
|
337
|
+
source: selected.source,
|
|
338
|
+
ranges: selected.ranges
|
|
339
|
+
},
|
|
340
|
+
exact: selected.consumed === normalized.atoms.length && normalized.residual === 0,
|
|
341
|
+
...direction === void 0 ? {} : { direction }
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
function full(exact) {
|
|
345
|
+
return {
|
|
346
|
+
access: { kind: "full" },
|
|
347
|
+
exact
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function normalize(where) {
|
|
351
|
+
const result = {
|
|
352
|
+
atoms: [],
|
|
353
|
+
residual: 0,
|
|
354
|
+
excluded: false,
|
|
355
|
+
empty: false
|
|
356
|
+
};
|
|
357
|
+
function visit(expr) {
|
|
358
|
+
switch (expr.kind) {
|
|
359
|
+
case "field":
|
|
360
|
+
if (expr.op === "in" && (!Array.isArray(expr.value) || expr.value.length === 0)) result.empty = true;
|
|
361
|
+
result.atoms.push(expr);
|
|
362
|
+
break;
|
|
363
|
+
case "and":
|
|
364
|
+
expr.exprs.forEach(visit);
|
|
365
|
+
break;
|
|
366
|
+
case "or": {
|
|
367
|
+
if (expr.exprs.length === 0) {
|
|
368
|
+
result.empty = true;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
371
|
+
const branches = expr.exprs;
|
|
372
|
+
const first = branches[0];
|
|
373
|
+
if (first.kind !== "field" || !branches.every((branch) => branch.kind === "field" && branch.field === first.field && (branch.op === "eq" || branch.op === "in"))) {
|
|
374
|
+
result.excluded = true;
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
const values = branches.flatMap((branch) => {
|
|
378
|
+
const atom = branch;
|
|
379
|
+
return atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
|
|
380
|
+
});
|
|
381
|
+
visit({
|
|
382
|
+
kind: "field",
|
|
383
|
+
field: first.field,
|
|
384
|
+
op: "in",
|
|
385
|
+
value: values
|
|
386
|
+
});
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case "not":
|
|
390
|
+
result.excluded = true;
|
|
391
|
+
break;
|
|
392
|
+
case "null-check": result.residual++;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (where) visit(where);
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
function keyTyped(field) {
|
|
399
|
+
return field !== void 0 && !field.collection && KEY_CODECS.has(field.codecId ?? "");
|
|
400
|
+
}
|
|
401
|
+
function keyPartial(field) {
|
|
402
|
+
return KEY_PARTIAL_CODECS.has(field?.codecId ?? "");
|
|
403
|
+
}
|
|
404
|
+
function completeKeyField(field, constraint) {
|
|
405
|
+
return keyTyped(field) && field?.nullable === false && (!keyPartial(field) || constraint?.rejectsNonKeys === true);
|
|
406
|
+
}
|
|
407
|
+
function singleFieldSource(source, field) {
|
|
408
|
+
return source.fields.length === 1 && source.fields[0] === field;
|
|
409
|
+
}
|
|
410
|
+
function addAtom(constraint, atom, field) {
|
|
411
|
+
if (atom.op === "eq" || atom.op === "in") {
|
|
412
|
+
const values = atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
|
|
413
|
+
if (!values.every(isValidIdbKey)) return;
|
|
414
|
+
const points = uniqueKeys(values);
|
|
415
|
+
constraint.points = constraint.points === void 0 ? points : constraint.points.filter((point) => points.some((value) => keyEquals(point, value)));
|
|
416
|
+
constraint.rejectsNonKeys = true;
|
|
417
|
+
constraint.consumed++;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!keyTyped(field)) return;
|
|
421
|
+
if (atom.op === "startsWith") {
|
|
422
|
+
if (field?.codecId !== "idb/string@1" || typeof atom.value !== "string") return;
|
|
423
|
+
intersectBound(constraint, "lower", {
|
|
424
|
+
value: atom.value,
|
|
425
|
+
open: false
|
|
426
|
+
});
|
|
427
|
+
const upper = stringSuccessor(atom.value);
|
|
428
|
+
if (upper !== void 0) intersectBound(constraint, "upper", {
|
|
429
|
+
value: upper,
|
|
430
|
+
open: true
|
|
431
|
+
});
|
|
432
|
+
constraint.startsWith = true;
|
|
433
|
+
constraint.consumed++;
|
|
434
|
+
} else if ([
|
|
435
|
+
"gt",
|
|
436
|
+
"gte",
|
|
437
|
+
"lt",
|
|
438
|
+
"lte"
|
|
439
|
+
].includes(atom.op) && isValidIdbKey(atom.value)) {
|
|
440
|
+
const side = atom.op === "gt" || atom.op === "gte" ? "lower" : "upper";
|
|
441
|
+
const strict = atom.op === "gt" || atom.op === "lt";
|
|
442
|
+
intersectBound(constraint, side, {
|
|
443
|
+
value: atom.value,
|
|
444
|
+
open: strict
|
|
445
|
+
});
|
|
446
|
+
if (strict) constraint.rejectsNonKeys = true;
|
|
447
|
+
constraint.consumed++;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function uniqueKeys(values) {
|
|
451
|
+
const sorted = [...values].sort(compareFieldValues);
|
|
452
|
+
return sorted.filter((value, i) => i === 0 || compareFieldValues(sorted[i - 1], value) !== 0);
|
|
453
|
+
}
|
|
454
|
+
function intersectBound(constraint, side, bound) {
|
|
455
|
+
const previous = constraint[side];
|
|
456
|
+
const comparison = previous === void 0 ? 0 : compareFieldValues(bound.value, previous.value);
|
|
457
|
+
if (!previous || (side === "lower" ? comparison > 0 : comparison < 0) || comparison === 0 && bound.open) constraint[side] = bound;
|
|
458
|
+
}
|
|
459
|
+
function insideBounds(value, constraint) {
|
|
460
|
+
const lower = constraint.lower;
|
|
461
|
+
const upper = constraint.upper;
|
|
462
|
+
if (lower && (compareFieldValues(value, lower.value) < 0 || lower.open && keyEquals(value, lower.value))) return false;
|
|
463
|
+
if (upper && (compareFieldValues(value, upper.value) > 0 || upper.open && keyEquals(value, upper.value))) return false;
|
|
464
|
+
return true;
|
|
465
|
+
}
|
|
466
|
+
function contradictory(constraint) {
|
|
467
|
+
if (constraint.points) {
|
|
468
|
+
constraint.points = constraint.points.filter((value) => insideBounds(value, constraint));
|
|
469
|
+
return constraint.points.length === 0;
|
|
470
|
+
}
|
|
471
|
+
if (!constraint.lower || !constraint.upper) return false;
|
|
472
|
+
const comparison = compareFieldValues(constraint.lower.value, constraint.upper.value);
|
|
473
|
+
return comparison > 0 || comparison === 0 && (constraint.lower.open || constraint.upper.open);
|
|
474
|
+
}
|
|
475
|
+
function filteringCandidate(catalog, source, constraints, declaration) {
|
|
476
|
+
if (source.multiEntry || source.fields.length === 0) return void 0;
|
|
477
|
+
if (source.fields.some((name) => keyPartial(catalog.fields[name]) && !constraints.get(name)?.rejectsNonKeys)) return void 0;
|
|
478
|
+
let prefixes = [[]];
|
|
479
|
+
let consumed = 0;
|
|
480
|
+
let position = 0;
|
|
481
|
+
while (position < source.fields.length) {
|
|
482
|
+
const constraint = constraints.get(source.fields[position]);
|
|
483
|
+
if (constraint?.points === void 0) break;
|
|
484
|
+
if (prefixes.length * constraint.points.length > 1024) return void 0;
|
|
485
|
+
prefixes = prefixes.flatMap((prefix) => constraint.points.map((point) => [...prefix, point]));
|
|
486
|
+
consumed += constraint.consumed;
|
|
487
|
+
position++;
|
|
488
|
+
}
|
|
489
|
+
const constraint = constraints.get(source.fields[position] ?? "");
|
|
490
|
+
const hasRange = constraint !== void 0 && (constraint.lower !== void 0 || constraint.upper !== void 0);
|
|
491
|
+
if (position === 0 && !hasRange) return void 0;
|
|
492
|
+
const used = position + Number(hasRange);
|
|
493
|
+
if (!source.fields.slice(used).every((field) => completeKeyField(catalog.fields[field], constraints.get(field)))) return void 0;
|
|
494
|
+
const compound = typeof source.keyPath !== "string";
|
|
495
|
+
if (position === source.fields.length) return {
|
|
496
|
+
source,
|
|
497
|
+
ranges: prefixes.map((prefix) => ({
|
|
498
|
+
kind: "only",
|
|
499
|
+
key: compound ? prefix : prefix[0]
|
|
500
|
+
})),
|
|
501
|
+
cost: source.indexName !== void 0 && source.unique ? COST.UNIQUE_POINT : source.indexName === void 0 ? COST.PRIMARY_POINT : COST.INDEX_POINT,
|
|
502
|
+
consumed,
|
|
503
|
+
declaration
|
|
504
|
+
};
|
|
505
|
+
if (hasRange) consumed += constraint.consumed;
|
|
506
|
+
const ranges = prefixes.map((prefix) => compound ? compoundRange(prefix, constraint, source.fields.length - used) : scalarRange(constraint));
|
|
507
|
+
if (ranges.some((range) => range.kind === "bound" && (compareFieldValues(range.lower, range.upper) > 0 || keyEquals(range.lower, range.upper) && (range.lowerOpen || range.upperOpen)))) return void 0;
|
|
508
|
+
return {
|
|
509
|
+
source,
|
|
510
|
+
ranges,
|
|
511
|
+
cost: hasRange ? constraint.lower && constraint.upper && !constraint.startsWith ? COST.TWO_SIDED_RANGE : COST.ONE_SIDED_RANGE : COST.PREFIX_POINT,
|
|
512
|
+
consumed,
|
|
513
|
+
declaration
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function scalarRange(constraint) {
|
|
517
|
+
if (constraint.lower && constraint.upper) return {
|
|
518
|
+
kind: "bound",
|
|
519
|
+
lower: constraint.lower.value,
|
|
520
|
+
upper: constraint.upper.value,
|
|
521
|
+
lowerOpen: constraint.lower.open,
|
|
522
|
+
upperOpen: constraint.upper.open
|
|
523
|
+
};
|
|
524
|
+
if (constraint.lower) return {
|
|
525
|
+
kind: "lower",
|
|
526
|
+
key: constraint.lower.value,
|
|
527
|
+
open: constraint.lower.open
|
|
528
|
+
};
|
|
529
|
+
return {
|
|
530
|
+
kind: "upper",
|
|
531
|
+
key: constraint.upper.value,
|
|
532
|
+
open: constraint.upper.open
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function compoundRange(prefix, constraint, trailing) {
|
|
536
|
+
const lower = constraint?.lower;
|
|
537
|
+
const upper = constraint?.upper;
|
|
538
|
+
return {
|
|
539
|
+
kind: "bound",
|
|
540
|
+
lower: lower ? [
|
|
541
|
+
...prefix,
|
|
542
|
+
lower.value,
|
|
543
|
+
...lower.open && trailing > 0 ? [[]] : []
|
|
544
|
+
] : prefix.length ? prefix : [-Infinity],
|
|
545
|
+
upper: upper ? [
|
|
546
|
+
...prefix,
|
|
547
|
+
upper.value,
|
|
548
|
+
...!upper.open && trailing > 0 ? [[]] : []
|
|
549
|
+
] : [...prefix, []],
|
|
550
|
+
lowerOpen: lower?.open ?? false,
|
|
551
|
+
upperOpen: upper ? upper.open || trailing > 0 : true
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function stringSuccessor(prefix) {
|
|
555
|
+
for (let i = prefix.length - 1; i >= 0; i--) {
|
|
556
|
+
const code = prefix.charCodeAt(i);
|
|
557
|
+
if (code < 65535) return prefix.slice(0, i) + String.fromCharCode(code + 1);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/core/planner/lower.ts
|
|
562
|
+
/**
|
|
563
|
+
* Lower row access without creating IDBKeyRange objects or awaiting work.
|
|
564
|
+
* Run `finish` after the executor has materialized all returned rows.
|
|
565
|
+
* Without `orderBy`, row order and pagination selection are unspecified.
|
|
566
|
+
* Rows tied on every ordering field may appear in any order, including at
|
|
567
|
+
* pagination boundaries. Add a unique ordering field for deterministic pages.
|
|
568
|
+
*/
|
|
569
|
+
function lowerRows(catalog, logical, meta, request, additionalFilter) {
|
|
570
|
+
const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
|
|
571
|
+
const comparator = logical.direction === void 0 ? buildRowComparator(request.orderBy) : void 0;
|
|
572
|
+
const base = {
|
|
573
|
+
meta,
|
|
574
|
+
storeName: catalog.storeName
|
|
575
|
+
};
|
|
576
|
+
const pagination = {
|
|
577
|
+
...request.skip === void 0 ? {} : { skip: request.skip },
|
|
578
|
+
...request.take === void 0 ? {} : { take: request.take }
|
|
579
|
+
};
|
|
580
|
+
let shapedByDriver = false;
|
|
581
|
+
let idbPlan;
|
|
582
|
+
switch (logical.access.kind) {
|
|
583
|
+
case "empty":
|
|
584
|
+
idbPlan = {
|
|
585
|
+
meta,
|
|
586
|
+
kind: "batch",
|
|
587
|
+
storeNames: [catalog.storeName],
|
|
588
|
+
ops: []
|
|
589
|
+
};
|
|
590
|
+
break;
|
|
591
|
+
case "full":
|
|
592
|
+
idbPlan = {
|
|
593
|
+
...base,
|
|
594
|
+
kind: "cursor-scan",
|
|
595
|
+
filter,
|
|
596
|
+
...comparator === void 0 ? {} : { comparator },
|
|
597
|
+
...pagination
|
|
598
|
+
};
|
|
599
|
+
shapedByDriver = true;
|
|
600
|
+
break;
|
|
601
|
+
case "ranges": {
|
|
602
|
+
const { source } = logical.access;
|
|
603
|
+
const ranges = [...logical.access.ranges];
|
|
604
|
+
if (logical.direction === "prev") ranges.reverse();
|
|
605
|
+
const single = ranges.length === 1;
|
|
606
|
+
const ops = ranges.map((range) => {
|
|
607
|
+
if (range?.kind === "only" && source.indexName === void 0) return {
|
|
608
|
+
...base,
|
|
609
|
+
kind: "key-get",
|
|
610
|
+
key: range.key
|
|
611
|
+
};
|
|
612
|
+
const access = {
|
|
613
|
+
...base,
|
|
614
|
+
...source.indexName === void 0 ? {} : { indexName: source.indexName },
|
|
615
|
+
...range === void 0 ? {} : { range }
|
|
616
|
+
};
|
|
617
|
+
if (!single && logical.direction !== void 0 && request.take !== void 0) return {
|
|
618
|
+
...access,
|
|
619
|
+
kind: "cursor-scan",
|
|
620
|
+
direction: logical.direction,
|
|
621
|
+
filter,
|
|
622
|
+
take: (request.skip ?? 0) + request.take
|
|
623
|
+
};
|
|
624
|
+
if (single && request.take !== void 0) {
|
|
625
|
+
shapedByDriver = true;
|
|
626
|
+
return {
|
|
627
|
+
...access,
|
|
628
|
+
kind: "cursor-scan",
|
|
629
|
+
filter,
|
|
630
|
+
...logical.direction === void 0 ? {} : { direction: logical.direction },
|
|
631
|
+
...comparator === void 0 ? {} : { comparator },
|
|
632
|
+
...pagination
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
if (logical.direction === "prev") return {
|
|
636
|
+
...access,
|
|
637
|
+
kind: "cursor-scan",
|
|
638
|
+
direction: "prev",
|
|
639
|
+
filter
|
|
640
|
+
};
|
|
641
|
+
return {
|
|
642
|
+
...access,
|
|
643
|
+
kind: "get-all"
|
|
644
|
+
};
|
|
645
|
+
});
|
|
646
|
+
idbPlan = single ? ops[0] : {
|
|
647
|
+
meta,
|
|
648
|
+
kind: "batch",
|
|
649
|
+
storeNames: [catalog.storeName],
|
|
650
|
+
ops
|
|
651
|
+
};
|
|
652
|
+
break;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return {
|
|
656
|
+
idbPlan,
|
|
657
|
+
finish(rows) {
|
|
658
|
+
let result = rows.filter(filter);
|
|
659
|
+
if (!shapedByDriver) {
|
|
660
|
+
if (comparator !== void 0) result.sort(comparator);
|
|
661
|
+
const skip = request.skip ?? 0;
|
|
662
|
+
result = result.slice(skip, request.take === void 0 ? void 0 : skip + request.take);
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Lower a row count. An `exact` plan counts index entries and loads no rows.
|
|
670
|
+
* The planner emits disjoint ranges on a non-multi-entry source, so each row
|
|
671
|
+
* is counted once. Anything else counts the rows that pass the filter.
|
|
672
|
+
* Pagination and `additionalFilter` always take the row path.
|
|
673
|
+
*/
|
|
674
|
+
function lowerCount(catalog, logical, meta, request, additionalFilter) {
|
|
675
|
+
const paginated = request.skip !== void 0 || request.take !== void 0;
|
|
676
|
+
if (!logical.exact || paginated || additionalFilter !== void 0) {
|
|
677
|
+
const lowered = lowerRows(catalog, logical, meta, request, additionalFilter);
|
|
678
|
+
return {
|
|
679
|
+
idbPlan: lowered.idbPlan,
|
|
680
|
+
finish: (rows) => lowered.finish(rows).length
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
return {
|
|
684
|
+
idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
|
|
685
|
+
...target,
|
|
686
|
+
kind: "count"
|
|
687
|
+
}))),
|
|
688
|
+
finish: (rows) => rows.reduce((total, row) => total + row["count"], 0)
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
/**
|
|
692
|
+
* Lower an existence check. An `exact` plan reads one primary key per range
|
|
693
|
+
* and loads no rows. Anything else stops each range at its first row that
|
|
694
|
+
* passes the filter.
|
|
695
|
+
*/
|
|
696
|
+
function lowerExists(catalog, logical, meta, request, additionalFilter) {
|
|
697
|
+
if (logical.exact && additionalFilter === void 0) return {
|
|
698
|
+
idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
|
|
699
|
+
...target,
|
|
700
|
+
kind: "keys",
|
|
701
|
+
take: 1
|
|
702
|
+
}))),
|
|
703
|
+
finish: (rows) => rows.length > 0
|
|
704
|
+
};
|
|
705
|
+
const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
|
|
706
|
+
return {
|
|
707
|
+
idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
|
|
708
|
+
...target,
|
|
709
|
+
kind: "cursor-scan",
|
|
710
|
+
filter,
|
|
711
|
+
take: 1
|
|
712
|
+
}))),
|
|
713
|
+
finish: (rows) => rows.some(filter)
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Lower writes through the same access paths as reads. If the patch touches
|
|
718
|
+
* the walked key, collect every range first so moved rows cannot enter a
|
|
719
|
+
* later range or change the cursor's position. Multiple ranges with a limit
|
|
720
|
+
* also collect first, because the limit applies to their combined matches.
|
|
721
|
+
*/
|
|
722
|
+
function lowerWrite(catalog, logical, meta, request, additionalFilter) {
|
|
723
|
+
const { access } = logical;
|
|
724
|
+
const walkedFields = access.kind === "ranges" ? access.source.fields : catalog.primaryKey.fields;
|
|
725
|
+
const movesKey = request.write === "put-merged" && walkedFields.some((field) => field in (request.patch ?? {}));
|
|
726
|
+
const limitedRanges = access.kind === "ranges" && access.ranges.length > 1 && request.take !== void 0;
|
|
727
|
+
if (movesKey || limitedRanges) return {
|
|
728
|
+
kind: "collect",
|
|
729
|
+
rows: lowerRows(catalog, logical, meta, request, additionalFilter)
|
|
730
|
+
};
|
|
731
|
+
const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
|
|
732
|
+
return {
|
|
733
|
+
kind: "scan",
|
|
734
|
+
idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
|
|
735
|
+
...target,
|
|
736
|
+
kind: "scan-write",
|
|
737
|
+
write: request.write,
|
|
738
|
+
filter,
|
|
739
|
+
...request.patch === void 0 ? {} : { patch: request.patch },
|
|
740
|
+
...request.take === void 0 ? {} : { take: request.take }
|
|
741
|
+
})))
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
/** One operation per planned range, in ascending key order. */
|
|
745
|
+
function accessOps(catalog, meta, logical, build) {
|
|
746
|
+
const { access } = logical;
|
|
747
|
+
const base = {
|
|
748
|
+
meta,
|
|
749
|
+
storeName: catalog.storeName
|
|
750
|
+
};
|
|
751
|
+
switch (access.kind) {
|
|
752
|
+
case "empty": return [];
|
|
753
|
+
case "full": return [build(base)];
|
|
754
|
+
case "ranges": {
|
|
755
|
+
const { indexName } = access.source;
|
|
756
|
+
return access.ranges.map((range) => build({
|
|
757
|
+
...base,
|
|
758
|
+
...indexName === void 0 ? {} : { indexName },
|
|
759
|
+
...range === void 0 ? {} : { range }
|
|
760
|
+
}));
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function combine(catalog, meta, ops) {
|
|
765
|
+
return ops.length === 1 ? ops[0] : {
|
|
766
|
+
meta,
|
|
767
|
+
kind: "batch",
|
|
768
|
+
storeNames: [catalog.storeName],
|
|
769
|
+
ops
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
//#endregion
|
|
194
773
|
//#region src/core/mutation-scope.ts
|
|
195
774
|
/**
|
|
196
775
|
* Run a callback inside a single multi-store IDB readwrite transaction.
|
|
@@ -475,47 +1054,36 @@ async function applyChildOwnedMutation(scope, contract, parentModelName, parentR
|
|
|
475
1054
|
const setValues = {};
|
|
476
1055
|
for (const [childField, parentValue] of parentValues.entries()) setValues[childField] = parentValue;
|
|
477
1056
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
await scope.execute({
|
|
481
|
-
meta,
|
|
482
|
-
kind: "scan-write",
|
|
483
|
-
storeName: relation.relatedStoreName,
|
|
1057
|
+
const where = shorthandToFilterExpr(criterion);
|
|
1058
|
+
await writeMutationRows(scope, contract, relation.relatedModelName, {
|
|
484
1059
|
write: "put-merged",
|
|
485
1060
|
patch,
|
|
486
|
-
|
|
1061
|
+
...where === void 0 ? {} : { where }
|
|
487
1062
|
});
|
|
488
1063
|
}
|
|
489
1064
|
return;
|
|
490
1065
|
}
|
|
491
1066
|
const setValues = {};
|
|
492
1067
|
for (const childField of parentValues.keys()) setValues[childField] = null;
|
|
493
|
-
const meta = makePlanMeta(contract);
|
|
494
1068
|
if (!mutation.criteria || mutation.criteria.length === 0) {
|
|
495
1069
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
496
|
-
const
|
|
497
|
-
await scope.
|
|
498
|
-
meta,
|
|
499
|
-
kind: "scan-write",
|
|
500
|
-
storeName: relation.relatedStoreName,
|
|
1070
|
+
const parentJoinWhere = buildParentJoinWhere(parentValues);
|
|
1071
|
+
await writeMutationRows(scope, contract, relation.relatedModelName, {
|
|
501
1072
|
write: "put-merged",
|
|
502
1073
|
patch,
|
|
503
|
-
|
|
1074
|
+
where: parentJoinWhere
|
|
504
1075
|
});
|
|
505
1076
|
return;
|
|
506
1077
|
}
|
|
507
1078
|
for (const criterion of mutation.criteria) {
|
|
508
1079
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
const
|
|
512
|
-
await scope.
|
|
513
|
-
meta,
|
|
514
|
-
kind: "scan-write",
|
|
515
|
-
storeName: relation.relatedStoreName,
|
|
1080
|
+
const criterionWhere = shorthandToFilterExpr(criterion);
|
|
1081
|
+
const parentJoinWhere = buildParentJoinWhere(parentValues);
|
|
1082
|
+
const where = criterionWhere === void 0 ? parentJoinWhere : andExpr([parentJoinWhere, criterionWhere]);
|
|
1083
|
+
await writeMutationRows(scope, contract, relation.relatedModelName, {
|
|
516
1084
|
write: "put-merged",
|
|
517
1085
|
patch,
|
|
518
|
-
|
|
1086
|
+
where
|
|
519
1087
|
});
|
|
520
1088
|
}
|
|
521
1089
|
}
|
|
@@ -558,37 +1126,56 @@ function assertNoNestedCallbacks(modelName, data) {
|
|
|
558
1126
|
async function findRowByCriterion(scope, contract, modelName, criterion) {
|
|
559
1127
|
const expr = shorthandToFilterExpr(criterion);
|
|
560
1128
|
if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
|
|
561
|
-
|
|
562
|
-
return scanOneRow(scope, contract, modelName, filter);
|
|
1129
|
+
return (await readMutationRows(scope, contract, modelName, expr, 1))[0] ?? null;
|
|
563
1130
|
}
|
|
564
1131
|
async function findFirstByFilters(scope, contract, modelName, filters) {
|
|
565
1132
|
if (filters.length === 0) return null;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
1133
|
+
return (await readMutationRows(scope, contract, modelName, andExpr(filters), 1))[0] ?? null;
|
|
1134
|
+
}
|
|
1135
|
+
/** Execute lowered atomic operations without leaving the transaction's microtask chain. */
|
|
1136
|
+
async function executeMutationPlan(scope, plan) {
|
|
1137
|
+
const plans = plan.kind === "batch" ? plan.ops : [plan];
|
|
1138
|
+
const rows = [];
|
|
1139
|
+
for (const op of plans) rows.push(...await scope.execute(op));
|
|
1140
|
+
return rows;
|
|
1141
|
+
}
|
|
1142
|
+
/** Read mutation preimages through the planner, then reapply the original filter. */
|
|
1143
|
+
async function readMutationRows(scope, contract, modelName, where, take) {
|
|
1144
|
+
const catalog = buildCatalog(contract, getStoreName(contract, modelName));
|
|
1145
|
+
const request = {
|
|
1146
|
+
...where === void 0 ? {} : { where },
|
|
1147
|
+
...take === void 0 ? {} : { take }
|
|
569
1148
|
};
|
|
570
|
-
const
|
|
571
|
-
return
|
|
1149
|
+
const lowered = lowerRows(catalog, planQuery(catalog, request), makePlanMeta(contract), request);
|
|
1150
|
+
return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
|
|
572
1151
|
}
|
|
573
|
-
async function
|
|
574
|
-
const
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
1152
|
+
async function writeMutationRows(scope, contract, modelName, request) {
|
|
1153
|
+
const catalog = buildCatalog(contract, getStoreName(contract, modelName));
|
|
1154
|
+
const meta = makePlanMeta(contract);
|
|
1155
|
+
const lowered = lowerWrite(catalog, planQuery(catalog, request), meta, request);
|
|
1156
|
+
if (lowered.kind === "scan") return executeMutationPlan(scope, lowered.idbPlan);
|
|
1157
|
+
const oldRows = lowered.rows.finish(await executeMutationPlan(scope, lowered.rows.idbPlan));
|
|
1158
|
+
const results = [];
|
|
1159
|
+
for (const row of oldRows) {
|
|
1160
|
+
const key = extractKeyFromRow(row, catalog.primaryKey.keyPath);
|
|
1161
|
+
const rows = await scope.execute({
|
|
1162
|
+
meta,
|
|
1163
|
+
storeName: catalog.storeName,
|
|
1164
|
+
...request.write === "delete" ? {
|
|
1165
|
+
kind: "delete",
|
|
1166
|
+
key
|
|
1167
|
+
} : {
|
|
1168
|
+
kind: "update",
|
|
1169
|
+
key,
|
|
1170
|
+
patch: request.patch ?? {}
|
|
1171
|
+
}
|
|
1172
|
+
});
|
|
1173
|
+
results.push(...rows);
|
|
1174
|
+
}
|
|
1175
|
+
return results;
|
|
588
1176
|
}
|
|
589
|
-
function
|
|
590
|
-
|
|
591
|
-
return (row) => pairs.every(([childField, parentValue]) => fieldValuesEqual(row[childField], parentValue));
|
|
1177
|
+
function buildParentJoinWhere(parentValues) {
|
|
1178
|
+
return andExpr([...parentValues].map(([field, value]) => fieldFilter(field, "eq", value)));
|
|
592
1179
|
}
|
|
593
1180
|
function getStoredAction(contract, modelName, relationName, kind) {
|
|
594
1181
|
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
|
|
@@ -650,68 +1237,32 @@ function isChildEnforcementRelation(contract, modelName, def) {
|
|
|
650
1237
|
return false;
|
|
651
1238
|
}
|
|
652
1239
|
/**
|
|
653
|
-
*
|
|
654
|
-
*
|
|
655
|
-
* primary key (so the answer is a pure key lookup that needs no row value)
|
|
656
|
-
* and `value` is a legal IDB key. `null` otherwise, and the caller keeps its
|
|
657
|
-
* value-materializing `cursor-scan`.
|
|
658
|
-
*
|
|
659
|
-
* Deliberately `null` for a compound primary key even if `field` is one of its
|
|
660
|
-
* members (one member can't pin the whole key), and for any non-key field
|
|
661
|
-
* (that needs an index-resolution step — Phase 10.5's shared primitive, not
|
|
662
|
-
* this helper's job). `IDBKeyRange.only` throws `DataError` on invalid keys
|
|
663
|
-
* (`null`, `NaN`, booleans, …), hence the guard.
|
|
664
|
-
*/
|
|
665
|
-
function pkEqualityRange(contract, modelName, field, value) {
|
|
666
|
-
if (typeof IDBKeyRange === "undefined") return null;
|
|
667
|
-
const keyPath = getKeyPath(contract, modelName);
|
|
668
|
-
if (typeof keyPath !== "string" || keyPath !== field) return null;
|
|
669
|
-
if (!isValidIdbKey(value)) return null;
|
|
670
|
-
return IDBKeyRange.only(value);
|
|
671
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Resolves to the first primary key in `storeName` within `range`, or
|
|
674
|
-
* `undefined` — one `getKey` request, no row deserialized.
|
|
1240
|
+
* `true` if any child row matches the relation against `parentRow`'s values —
|
|
1241
|
+
* the existence check behind `restrict`. A null value references no child.
|
|
675
1242
|
*/
|
|
676
|
-
async function
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
storeName,
|
|
681
|
-
range,
|
|
682
|
-
take: 1
|
|
683
|
-
}))[0]?.["key"];
|
|
1243
|
+
async function childExists(scope, contract, meta, def, parentRow) {
|
|
1244
|
+
const values = def.localFields.map((field) => parentRow[field]);
|
|
1245
|
+
if (values.some((value) => value === null || value === void 0)) return false;
|
|
1246
|
+
return rowExists(scope, contract, meta, def.relatedStoreName, def.targetFields, values);
|
|
684
1247
|
}
|
|
685
1248
|
/**
|
|
686
|
-
* `
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
* value-materializing `cursor-scan` (`take: 1`) otherwise.
|
|
1249
|
+
* Does `storeName` have a row whose `fields` equal `values`, pairwise?
|
|
1250
|
+
* Compares values the way IndexedDB compares keys. `additionalFilter` narrows
|
|
1251
|
+
* the match in memory; it makes the check read rows instead of keys.
|
|
690
1252
|
*/
|
|
691
|
-
async function
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
return (await scope.execute({
|
|
697
|
-
meta,
|
|
698
|
-
kind: "cursor-scan",
|
|
699
|
-
storeName: def.relatedStoreName,
|
|
700
|
-
filter: buildChildFilterFromRow(def, parentRow),
|
|
701
|
-
take: 1
|
|
702
|
-
})).length > 0;
|
|
1253
|
+
async function rowExists(scope, contract, meta, storeName, fields, values, additionalFilter) {
|
|
1254
|
+
const where = andExpr(fields.map((field, i) => fieldFilter(field, "eq", values[i])));
|
|
1255
|
+
const catalog = buildCatalog(contract, storeName);
|
|
1256
|
+
const lowered = lowerExists(catalog, planQuery(catalog, { where }), meta, { where }, additionalFilter);
|
|
1257
|
+
return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
|
|
703
1258
|
}
|
|
704
1259
|
/**
|
|
705
1260
|
* Builds a filter matching a relation's children against one specific parent
|
|
706
1261
|
* row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
|
|
707
1262
|
* and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
|
|
708
1263
|
*/
|
|
709
|
-
function
|
|
710
|
-
|
|
711
|
-
childField: def.targetFields[i],
|
|
712
|
-
parentValue: row[lf]
|
|
713
|
-
}));
|
|
714
|
-
return (child) => pairs.every(({ childField, parentValue }) => fieldValuesEqual(child[childField], parentValue));
|
|
1264
|
+
function buildChildWhereFromRow(def, row) {
|
|
1265
|
+
return andExpr(def.targetFields.map((field, i) => fieldFilter(field, "eq", row[def.localFields[i]])));
|
|
715
1266
|
}
|
|
716
1267
|
/** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
|
|
717
1268
|
function getFieldDefault(contract, modelName, fieldName) {
|
|
@@ -842,7 +1393,7 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
842
1393
|
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
843
1394
|
if (changedFields.length === 0) continue;
|
|
844
1395
|
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
845
|
-
const
|
|
1396
|
+
const childWhere = buildChildWhereFromRow(def, oldRow);
|
|
846
1397
|
if (action === "restrict") {
|
|
847
1398
|
if (await childExists(scope, contract, meta, def, oldRow)) throw new Error(`Cannot update ${modelName} '${keyToken(extractKeyFromRow(oldRow, keyPath))}': changing field(s) ${changedFields.join(", ")} would orphan child records on relation '${def.relationName}'. Update or remove those children first, or declare onUpdate: Cascade, SetNull or SetDefault on the relation.`);
|
|
848
1399
|
continue;
|
|
@@ -854,46 +1405,32 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
854
1405
|
const tf = def.targetFields[i];
|
|
855
1406
|
if (changedFields.includes(lf)) childPatch[tf] = patch[lf];
|
|
856
1407
|
}
|
|
857
|
-
const children = await scope.
|
|
858
|
-
meta,
|
|
859
|
-
kind: "cursor-scan",
|
|
860
|
-
storeName: def.relatedStoreName,
|
|
861
|
-
filter: childFilter
|
|
862
|
-
});
|
|
1408
|
+
const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
|
|
863
1409
|
for (const child of children) await applyReferentialActionsForRowOnUpdate(scope, contract, def.relatedModelName, child, childPatch, visited);
|
|
864
|
-
await scope.
|
|
865
|
-
meta,
|
|
866
|
-
kind: "scan-write",
|
|
867
|
-
storeName: def.relatedStoreName,
|
|
1410
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
868
1411
|
write: "put-merged",
|
|
869
1412
|
patch: childPatch,
|
|
870
|
-
|
|
1413
|
+
where: childWhere
|
|
871
1414
|
});
|
|
872
1415
|
continue;
|
|
873
1416
|
}
|
|
874
1417
|
if (action === "setNull") {
|
|
875
1418
|
const childPatch = {};
|
|
876
1419
|
for (const targetField of def.targetFields) childPatch[targetField] = null;
|
|
877
|
-
await scope.
|
|
878
|
-
meta,
|
|
879
|
-
kind: "scan-write",
|
|
880
|
-
storeName: def.relatedStoreName,
|
|
1420
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
881
1421
|
write: "put-merged",
|
|
882
1422
|
patch: childPatch,
|
|
883
|
-
|
|
1423
|
+
where: childWhere
|
|
884
1424
|
});
|
|
885
1425
|
continue;
|
|
886
1426
|
}
|
|
887
1427
|
if (action === "setDefault") {
|
|
888
1428
|
const childPatch = buildSetDefaultPatch(contract, def);
|
|
889
1429
|
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
|
|
890
|
-
await scope.
|
|
891
|
-
meta,
|
|
892
|
-
kind: "scan-write",
|
|
893
|
-
storeName: def.relatedStoreName,
|
|
1430
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
894
1431
|
write: "put-merged",
|
|
895
1432
|
patch: childPatch,
|
|
896
|
-
|
|
1433
|
+
where: childWhere
|
|
897
1434
|
});
|
|
898
1435
|
continue;
|
|
899
1436
|
}
|
|
@@ -955,40 +1492,13 @@ function fkCheckNeedsExistingRow(contract, modelName, data) {
|
|
|
955
1492
|
/**
|
|
956
1493
|
* Does `parentModel` have a row whose `fields` equal `values`, pairwise?
|
|
957
1494
|
*
|
|
958
|
-
*
|
|
959
|
-
*
|
|
960
|
-
* the way IndexedDB compares keys. `excludeKey` leaves out one parent row,
|
|
961
|
-
* for checks that run while that row is being deleted or changed.
|
|
1495
|
+
* `excludeKey` leaves out one parent row, for checks that run while that row
|
|
1496
|
+
* is being deleted or changed.
|
|
962
1497
|
*/
|
|
963
1498
|
async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
|
|
964
1499
|
const storeName = getStoreName(contract, parentModel);
|
|
965
1500
|
const keyPath = getKeyPath(contract, parentModel);
|
|
966
|
-
|
|
967
|
-
if (range !== null) {
|
|
968
|
-
const foundKey = await firstKeyInRange(scope, meta, storeName, range);
|
|
969
|
-
return foundKey !== void 0 && (excludeKey === void 0 || !keyEquals(foundKey, excludeKey));
|
|
970
|
-
}
|
|
971
|
-
const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals(extractKeyFromRow(row, keyPath), excludeKey));
|
|
972
|
-
return (await scope.execute({
|
|
973
|
-
meta,
|
|
974
|
-
kind: "cursor-scan",
|
|
975
|
-
storeName,
|
|
976
|
-
filter,
|
|
977
|
-
take: 1
|
|
978
|
-
})).length > 0;
|
|
979
|
-
}
|
|
980
|
-
/**
|
|
981
|
-
* An `IDBKeyRange` for "the row whose primary key is `values`", when `fields`
|
|
982
|
-
* are exactly the key's fields (in any order) and every value is a valid
|
|
983
|
-
* IndexedDB key. `null` otherwise, and the caller scans instead.
|
|
984
|
-
*/
|
|
985
|
-
function primaryKeyRange(keyPath, fields, values) {
|
|
986
|
-
if (typeof IDBKeyRange === "undefined") return null;
|
|
987
|
-
const keyFields = keyPathFields(keyPath);
|
|
988
|
-
if (keyFields.length !== fields.length || !keyFields.every((f) => fields.includes(f))) return null;
|
|
989
|
-
const ordered = keyFields.map((f) => values[fields.indexOf(f)]);
|
|
990
|
-
if (!ordered.every((v) => isValidIdbKey(v))) return null;
|
|
991
|
-
return IDBKeyRange.only(typeof keyPath === "string" ? ordered[0] : ordered);
|
|
1501
|
+
return rowExists(scope, contract, meta, storeName, fields, values, excludeKey === void 0 ? void 0 : (row) => !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
|
|
992
1502
|
}
|
|
993
1503
|
/** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
|
|
994
1504
|
function describeTuple(fields, values) {
|
|
@@ -1060,27 +1570,14 @@ async function executeScalarUpdates(options, take) {
|
|
|
1060
1570
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
1061
1571
|
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
1062
1572
|
const meta = makePlanMeta(contract);
|
|
1063
|
-
const
|
|
1064
|
-
|
|
1065
|
-
exprs: filters
|
|
1066
|
-
};
|
|
1067
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1068
|
-
if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
|
|
1069
|
-
meta,
|
|
1070
|
-
kind: "scan-write",
|
|
1071
|
-
storeName,
|
|
1573
|
+
const where = filters.length === 0 ? void 0 : andExpr(filters);
|
|
1574
|
+
if (!enforcesOnUpdate && !needsRowForFks) return writeMutationRows(scope, contract, modelName, {
|
|
1072
1575
|
write: "put-merged",
|
|
1073
1576
|
patch,
|
|
1074
|
-
...take
|
|
1075
|
-
...
|
|
1076
|
-
});
|
|
1077
|
-
const oldRows = await scope.execute({
|
|
1078
|
-
meta,
|
|
1079
|
-
kind: "cursor-scan",
|
|
1080
|
-
storeName,
|
|
1081
|
-
...take !== void 0 ? { take } : {},
|
|
1082
|
-
...filter !== void 0 ? { filter } : {}
|
|
1577
|
+
...take === void 0 ? {} : { take },
|
|
1578
|
+
...where === void 0 ? {} : { where }
|
|
1083
1579
|
});
|
|
1580
|
+
const oldRows = await readMutationRows(scope, contract, modelName, where, take);
|
|
1084
1581
|
const keyPath = getKeyPath(contract, modelName);
|
|
1085
1582
|
const results = [];
|
|
1086
1583
|
for (const oldRow of oldRows) {
|
|
@@ -1162,19 +1659,14 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1162
1659
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1163
1660
|
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1164
1661
|
const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1165
|
-
const
|
|
1662
|
+
const childWhere = buildChildWhereFromRow(def, row);
|
|
1166
1663
|
if (action === "restrict") {
|
|
1167
1664
|
if (await childExists(scope, contract, meta, def, row)) throw new Error(`Cannot delete ${modelName} '${keyToken(extractKeyFromRow(row, keyPath))}': child records exist on relation '${def.relationName}'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.`);
|
|
1168
1665
|
continue;
|
|
1169
1666
|
}
|
|
1170
1667
|
if (action === "cascade") {
|
|
1171
1668
|
const childKeyPath = getKeyPath(contract, def.relatedModelName);
|
|
1172
|
-
const children = await scope.
|
|
1173
|
-
meta,
|
|
1174
|
-
kind: "cursor-scan",
|
|
1175
|
-
storeName: def.relatedStoreName,
|
|
1176
|
-
filter: childFilter
|
|
1177
|
-
});
|
|
1669
|
+
const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
|
|
1178
1670
|
for (const child of children) {
|
|
1179
1671
|
await applyReferentialActionsForRow(scope, contract, def.relatedModelName, child, visited);
|
|
1180
1672
|
await scope.execute({
|
|
@@ -1189,26 +1681,20 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1189
1681
|
if (action === "setNull") {
|
|
1190
1682
|
const patch = {};
|
|
1191
1683
|
for (const targetField of def.targetFields) patch[targetField] = null;
|
|
1192
|
-
await scope.
|
|
1193
|
-
meta,
|
|
1194
|
-
kind: "scan-write",
|
|
1195
|
-
storeName: def.relatedStoreName,
|
|
1684
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
1196
1685
|
write: "put-merged",
|
|
1197
1686
|
patch,
|
|
1198
|
-
|
|
1687
|
+
where: childWhere
|
|
1199
1688
|
});
|
|
1200
1689
|
continue;
|
|
1201
1690
|
}
|
|
1202
1691
|
if (action === "setDefault") {
|
|
1203
1692
|
const patch = buildSetDefaultPatch(contract, def);
|
|
1204
1693
|
await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
|
|
1205
|
-
await scope.
|
|
1206
|
-
meta,
|
|
1207
|
-
kind: "scan-write",
|
|
1208
|
-
storeName: def.relatedStoreName,
|
|
1694
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
1209
1695
|
write: "put-merged",
|
|
1210
1696
|
patch,
|
|
1211
|
-
|
|
1697
|
+
where: childWhere
|
|
1212
1698
|
});
|
|
1213
1699
|
continue;
|
|
1214
1700
|
}
|
|
@@ -1219,12 +1705,11 @@ async function executeDeleteWithReferentialActions(options) {
|
|
|
1219
1705
|
await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1220
1706
|
const storeName = getStoreName(contract, modelName);
|
|
1221
1707
|
const meta = makePlanMeta(contract);
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
}))[0];
|
|
1708
|
+
const keyPath = getKeyPath(contract, modelName);
|
|
1709
|
+
const fields = keyPathFields(keyPath);
|
|
1710
|
+
const values = typeof keyPath === "string" ? [key] : key;
|
|
1711
|
+
const where = andExpr(fields.map((field, i) => fieldFilter(field, "eq", values[i])));
|
|
1712
|
+
const row = (await readMutationRows(scope, contract, modelName, where, 1))[0];
|
|
1228
1713
|
if (!row) return [];
|
|
1229
1714
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1230
1715
|
await scope.execute({
|
|
@@ -1237,17 +1722,16 @@ async function executeDeleteWithReferentialActions(options) {
|
|
|
1237
1722
|
});
|
|
1238
1723
|
}
|
|
1239
1724
|
async function executeDeleteAllWithReferentialActions(options) {
|
|
1240
|
-
const { executor, contract, modelName,
|
|
1725
|
+
const { executor, contract, modelName, where } = options;
|
|
1241
1726
|
return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1242
1727
|
const storeName = getStoreName(contract, modelName);
|
|
1243
1728
|
const meta = makePlanMeta(contract);
|
|
1244
1729
|
const keyPath = getKeyPath(contract, modelName);
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
storeName,
|
|
1249
|
-
...filter !== void 0 ? { filter } : {}
|
|
1730
|
+
if (!hasEnforceableChildRelations(contract, modelName)) return writeMutationRows(scope, contract, modelName, {
|
|
1731
|
+
write: "delete",
|
|
1732
|
+
...where === void 0 ? {} : { where }
|
|
1250
1733
|
});
|
|
1734
|
+
const rows = await readMutationRows(scope, contract, modelName, where);
|
|
1251
1735
|
for (const row of rows) {
|
|
1252
1736
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1253
1737
|
const key = extractKeyFromRow(row, keyPath);
|
|
@@ -1262,6 +1746,6 @@ async function executeDeleteAllWithReferentialActions(options) {
|
|
|
1262
1746
|
});
|
|
1263
1747
|
}
|
|
1264
1748
|
//#endregion
|
|
1265
|
-
export {
|
|
1749
|
+
export { combineFilterExprs as A, planQuery as C, createMutationDefaultsCache as D, applyUpdateDefaults as E, getStoreName as F, keyEquals$1 as I, keyToken as L, fieldValueToken as M, getKeyPath as N, assertEnumValues as O, getRelation as P, lowerRows as S, applyCreateDefaults as T, readMutationRows as _, collectScalarFkStoreNames as a, withMutationScope as b, executeDeleteWithReferentialActions as c, executeScalarCreateAllWithFkValidation as d, executeScalarCreateWithFkValidation as f, hasScalarFkFields as g, hasNestedMutationCallbacks as h, collectOnUpdateEnforcementStoreNames as i, extractKeyFromRow as j, buildRowComparator as k, executeNestedCreateMutation as l, hasEnforceableChildRelations as m, applyReferentialActionsForRowOnUpdate as n, executeBulkUpdateWithFkValidation as o, executeScalarUpdateWithFkValidation as p, collectDeleteStoreNames as r, executeDeleteAllWithReferentialActions as s, applyReferentialActionsForRow as t, executeNestedUpdateMutation as u, requireTransactionExecutor as v, buildCatalog as w, lowerCount as x, validateScalarFks as y };
|
|
1266
1750
|
|
|
1267
|
-
//# sourceMappingURL=mutation-executor-
|
|
1751
|
+
//# sourceMappingURL=mutation-executor-inLFb0TE.mjs.map
|