@prisma-idb/client-idb 0.11.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-Bp-hdRg2.d.mts → executor-CIEHpa1j.d.mts} +14 -7
- package/dist/{executor-Bp-hdRg2.d.mts.map → executor-CIEHpa1j.d.mts.map} +1 -1
- package/dist/{idb-orm-9XN3c9ly.d.mts → idb-orm-6OuwkeDM.d.mts} +2 -2
- package/dist/{idb-orm-9XN3c9ly.d.mts.map → idb-orm-6OuwkeDM.d.mts.map} +1 -1
- package/dist/{idb-orm-BitJ-EMq.mjs → idb-orm-B1Br1C0F.mjs} +18 -506
- package/dist/idb-orm-B1Br1C0F.mjs.map +1 -0
- package/dist/internal.d.mts +2 -2
- package/dist/internal.mjs +1 -1
- package/dist/{managed-client-CS78EjPu.d.mts → managed-client-DQlsxc4Z.d.mts} +3 -3
- package/dist/{managed-client-CS78EjPu.d.mts.map → managed-client-DQlsxc4Z.d.mts.map} +1 -1
- package/dist/{managed-client-CxlNmERd.mjs → managed-client-M3xMUk0n.mjs} +3 -3
- package/dist/{managed-client-CxlNmERd.mjs.map → managed-client-M3xMUk0n.mjs.map} +1 -1
- package/dist/{mutation-executor-SJqTuSbb.mjs → mutation-executor-inLFb0TE.mjs} +701 -146
- package/dist/mutation-executor-inLFb0TE.mjs.map +1 -0
- package/dist/{mutation-scope-1hS0ogGa.d.mts → mutation-scope-DrxFhek3.d.mts} +2 -2
- package/dist/{mutation-scope-1hS0ogGa.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 +6 -6
- package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
- package/dist/mutation-executor-SJqTuSbb.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, keyEquals as keyEquals$1, 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
|
|
@@ -50,6 +50,39 @@ function getRelation(contract, modelName, relName) {
|
|
|
50
50
|
return relation;
|
|
51
51
|
}
|
|
52
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
|
|
53
86
|
//#region src/core/enum-validation.ts
|
|
54
87
|
const rulesByContract = /* @__PURE__ */ new WeakMap();
|
|
55
88
|
function resolveEnumRule(contract, field) {
|
|
@@ -178,6 +211,565 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
|
|
|
178
211
|
return result;
|
|
179
212
|
}
|
|
180
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
|
|
181
773
|
//#region src/core/mutation-scope.ts
|
|
182
774
|
/**
|
|
183
775
|
* Run a callback inside a single multi-store IDB readwrite transaction.
|
|
@@ -462,47 +1054,36 @@ async function applyChildOwnedMutation(scope, contract, parentModelName, parentR
|
|
|
462
1054
|
const setValues = {};
|
|
463
1055
|
for (const [childField, parentValue] of parentValues.entries()) setValues[childField] = parentValue;
|
|
464
1056
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
await scope.execute({
|
|
468
|
-
meta,
|
|
469
|
-
kind: "scan-write",
|
|
470
|
-
storeName: relation.relatedStoreName,
|
|
1057
|
+
const where = shorthandToFilterExpr(criterion);
|
|
1058
|
+
await writeMutationRows(scope, contract, relation.relatedModelName, {
|
|
471
1059
|
write: "put-merged",
|
|
472
1060
|
patch,
|
|
473
|
-
|
|
1061
|
+
...where === void 0 ? {} : { where }
|
|
474
1062
|
});
|
|
475
1063
|
}
|
|
476
1064
|
return;
|
|
477
1065
|
}
|
|
478
1066
|
const setValues = {};
|
|
479
1067
|
for (const childField of parentValues.keys()) setValues[childField] = null;
|
|
480
|
-
const meta = makePlanMeta(contract);
|
|
481
1068
|
if (!mutation.criteria || mutation.criteria.length === 0) {
|
|
482
1069
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
483
|
-
const
|
|
484
|
-
await scope.
|
|
485
|
-
meta,
|
|
486
|
-
kind: "scan-write",
|
|
487
|
-
storeName: relation.relatedStoreName,
|
|
1070
|
+
const parentJoinWhere = buildParentJoinWhere(parentValues);
|
|
1071
|
+
await writeMutationRows(scope, contract, relation.relatedModelName, {
|
|
488
1072
|
write: "put-merged",
|
|
489
1073
|
patch,
|
|
490
|
-
|
|
1074
|
+
where: parentJoinWhere
|
|
491
1075
|
});
|
|
492
1076
|
return;
|
|
493
1077
|
}
|
|
494
1078
|
for (const criterion of mutation.criteria) {
|
|
495
1079
|
const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, relation.relatedStoreName, setValues, defaultsCache);
|
|
496
|
-
const
|
|
497
|
-
const
|
|
498
|
-
const
|
|
499
|
-
await scope.
|
|
500
|
-
meta,
|
|
501
|
-
kind: "scan-write",
|
|
502
|
-
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, {
|
|
503
1084
|
write: "put-merged",
|
|
504
1085
|
patch,
|
|
505
|
-
|
|
1086
|
+
where
|
|
506
1087
|
});
|
|
507
1088
|
}
|
|
508
1089
|
}
|
|
@@ -545,37 +1126,56 @@ function assertNoNestedCallbacks(modelName, data) {
|
|
|
545
1126
|
async function findRowByCriterion(scope, contract, modelName, criterion) {
|
|
546
1127
|
const expr = shorthandToFilterExpr(criterion);
|
|
547
1128
|
if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
|
|
548
|
-
|
|
549
|
-
return scanOneRow(scope, contract, modelName, filter);
|
|
1129
|
+
return (await readMutationRows(scope, contract, modelName, expr, 1))[0] ?? null;
|
|
550
1130
|
}
|
|
551
1131
|
async function findFirstByFilters(scope, contract, modelName, filters) {
|
|
552
1132
|
if (filters.length === 0) return null;
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
take:
|
|
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 }
|
|
568
1148
|
};
|
|
569
|
-
|
|
1149
|
+
const lowered = lowerRows(catalog, planQuery(catalog, request), makePlanMeta(contract), request);
|
|
1150
|
+
return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
|
|
570
1151
|
}
|
|
571
|
-
function
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
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;
|
|
575
1176
|
}
|
|
576
|
-
function
|
|
577
|
-
|
|
578
|
-
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)));
|
|
579
1179
|
}
|
|
580
1180
|
function getStoredAction(contract, modelName, relationName, kind) {
|
|
581
1181
|
return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
|
|
@@ -638,28 +1238,31 @@ function isChildEnforcementRelation(contract, modelName, def) {
|
|
|
638
1238
|
}
|
|
639
1239
|
/**
|
|
640
1240
|
* `true` if any child row matches the relation against `parentRow`'s values —
|
|
641
|
-
* the existence check behind `restrict`.
|
|
1241
|
+
* the existence check behind `restrict`. A null value references no child.
|
|
642
1242
|
*/
|
|
643
|
-
async function childExists(scope, meta, def, parentRow) {
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
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.
|
|
1252
|
+
*/
|
|
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));
|
|
651
1258
|
}
|
|
652
1259
|
/**
|
|
653
1260
|
* Builds a filter matching a relation's children against one specific parent
|
|
654
1261
|
* row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
|
|
655
1262
|
* and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
|
|
656
1263
|
*/
|
|
657
|
-
function
|
|
658
|
-
|
|
659
|
-
childField: def.targetFields[i],
|
|
660
|
-
parentValue: row[lf]
|
|
661
|
-
}));
|
|
662
|
-
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]])));
|
|
663
1266
|
}
|
|
664
1267
|
/** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
|
|
665
1268
|
function getFieldDefault(contract, modelName, fieldName) {
|
|
@@ -790,9 +1393,9 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
790
1393
|
const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
|
|
791
1394
|
if (changedFields.length === 0) continue;
|
|
792
1395
|
const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
|
|
793
|
-
const
|
|
1396
|
+
const childWhere = buildChildWhereFromRow(def, oldRow);
|
|
794
1397
|
if (action === "restrict") {
|
|
795
|
-
if (await childExists(scope, 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.`);
|
|
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.`);
|
|
796
1399
|
continue;
|
|
797
1400
|
}
|
|
798
1401
|
if (action === "cascade") {
|
|
@@ -802,46 +1405,32 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
|
|
|
802
1405
|
const tf = def.targetFields[i];
|
|
803
1406
|
if (changedFields.includes(lf)) childPatch[tf] = patch[lf];
|
|
804
1407
|
}
|
|
805
|
-
const children = await scope.
|
|
806
|
-
meta,
|
|
807
|
-
kind: "cursor-scan",
|
|
808
|
-
storeName: def.relatedStoreName,
|
|
809
|
-
filter: childFilter
|
|
810
|
-
});
|
|
1408
|
+
const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
|
|
811
1409
|
for (const child of children) await applyReferentialActionsForRowOnUpdate(scope, contract, def.relatedModelName, child, childPatch, visited);
|
|
812
|
-
await scope.
|
|
813
|
-
meta,
|
|
814
|
-
kind: "scan-write",
|
|
815
|
-
storeName: def.relatedStoreName,
|
|
1410
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
816
1411
|
write: "put-merged",
|
|
817
1412
|
patch: childPatch,
|
|
818
|
-
|
|
1413
|
+
where: childWhere
|
|
819
1414
|
});
|
|
820
1415
|
continue;
|
|
821
1416
|
}
|
|
822
1417
|
if (action === "setNull") {
|
|
823
1418
|
const childPatch = {};
|
|
824
1419
|
for (const targetField of def.targetFields) childPatch[targetField] = null;
|
|
825
|
-
await scope.
|
|
826
|
-
meta,
|
|
827
|
-
kind: "scan-write",
|
|
828
|
-
storeName: def.relatedStoreName,
|
|
1420
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
829
1421
|
write: "put-merged",
|
|
830
1422
|
patch: childPatch,
|
|
831
|
-
|
|
1423
|
+
where: childWhere
|
|
832
1424
|
});
|
|
833
1425
|
continue;
|
|
834
1426
|
}
|
|
835
1427
|
if (action === "setDefault") {
|
|
836
1428
|
const childPatch = buildSetDefaultPatch(contract, def);
|
|
837
1429
|
await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
|
|
838
|
-
await scope.
|
|
839
|
-
meta,
|
|
840
|
-
kind: "scan-write",
|
|
841
|
-
storeName: def.relatedStoreName,
|
|
1430
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
842
1431
|
write: "put-merged",
|
|
843
1432
|
patch: childPatch,
|
|
844
|
-
|
|
1433
|
+
where: childWhere
|
|
845
1434
|
});
|
|
846
1435
|
continue;
|
|
847
1436
|
}
|
|
@@ -903,21 +1492,13 @@ function fkCheckNeedsExistingRow(contract, modelName, data) {
|
|
|
903
1492
|
/**
|
|
904
1493
|
* Does `parentModel` have a row whose `fields` equal `values`, pairwise?
|
|
905
1494
|
*
|
|
906
|
-
* Scans the parent store, comparing values the way IndexedDB compares keys.
|
|
907
1495
|
* `excludeKey` leaves out one parent row, for checks that run while that row
|
|
908
1496
|
* is being deleted or changed.
|
|
909
1497
|
*/
|
|
910
1498
|
async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
|
|
911
1499
|
const storeName = getStoreName(contract, parentModel);
|
|
912
1500
|
const keyPath = getKeyPath(contract, parentModel);
|
|
913
|
-
|
|
914
|
-
return (await scope.execute({
|
|
915
|
-
meta,
|
|
916
|
-
kind: "cursor-scan",
|
|
917
|
-
storeName,
|
|
918
|
-
filter,
|
|
919
|
-
take: 1
|
|
920
|
-
})).length > 0;
|
|
1501
|
+
return rowExists(scope, contract, meta, storeName, fields, values, excludeKey === void 0 ? void 0 : (row) => !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
|
|
921
1502
|
}
|
|
922
1503
|
/** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
|
|
923
1504
|
function describeTuple(fields, values) {
|
|
@@ -989,27 +1570,14 @@ async function executeScalarUpdates(options, take) {
|
|
|
989
1570
|
return withMutationScope(executor, storeNames, async (scope) => {
|
|
990
1571
|
if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
|
|
991
1572
|
const meta = makePlanMeta(contract);
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
exprs: filters
|
|
995
|
-
};
|
|
996
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
997
|
-
if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
|
|
998
|
-
meta,
|
|
999
|
-
kind: "scan-write",
|
|
1000
|
-
storeName,
|
|
1573
|
+
const where = filters.length === 0 ? void 0 : andExpr(filters);
|
|
1574
|
+
if (!enforcesOnUpdate && !needsRowForFks) return writeMutationRows(scope, contract, modelName, {
|
|
1001
1575
|
write: "put-merged",
|
|
1002
1576
|
patch,
|
|
1003
|
-
...take
|
|
1004
|
-
...
|
|
1005
|
-
});
|
|
1006
|
-
const oldRows = await scope.execute({
|
|
1007
|
-
meta,
|
|
1008
|
-
kind: "cursor-scan",
|
|
1009
|
-
storeName,
|
|
1010
|
-
...take !== void 0 ? { take } : {},
|
|
1011
|
-
...filter !== void 0 ? { filter } : {}
|
|
1577
|
+
...take === void 0 ? {} : { take },
|
|
1578
|
+
...where === void 0 ? {} : { where }
|
|
1012
1579
|
});
|
|
1580
|
+
const oldRows = await readMutationRows(scope, contract, modelName, where, take);
|
|
1013
1581
|
const keyPath = getKeyPath(contract, modelName);
|
|
1014
1582
|
const results = [];
|
|
1015
1583
|
for (const oldRow of oldRows) {
|
|
@@ -1091,19 +1659,14 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1091
1659
|
for (const def of getRelationDefinitions(contract, modelName)) {
|
|
1092
1660
|
if (!isChildEnforcementRelation(contract, modelName, def)) continue;
|
|
1093
1661
|
const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
|
|
1094
|
-
const
|
|
1662
|
+
const childWhere = buildChildWhereFromRow(def, row);
|
|
1095
1663
|
if (action === "restrict") {
|
|
1096
|
-
if (await childExists(scope, 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.`);
|
|
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.`);
|
|
1097
1665
|
continue;
|
|
1098
1666
|
}
|
|
1099
1667
|
if (action === "cascade") {
|
|
1100
1668
|
const childKeyPath = getKeyPath(contract, def.relatedModelName);
|
|
1101
|
-
const children = await scope.
|
|
1102
|
-
meta,
|
|
1103
|
-
kind: "cursor-scan",
|
|
1104
|
-
storeName: def.relatedStoreName,
|
|
1105
|
-
filter: childFilter
|
|
1106
|
-
});
|
|
1669
|
+
const children = await readMutationRows(scope, contract, def.relatedModelName, childWhere);
|
|
1107
1670
|
for (const child of children) {
|
|
1108
1671
|
await applyReferentialActionsForRow(scope, contract, def.relatedModelName, child, visited);
|
|
1109
1672
|
await scope.execute({
|
|
@@ -1118,26 +1681,20 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
|
|
|
1118
1681
|
if (action === "setNull") {
|
|
1119
1682
|
const patch = {};
|
|
1120
1683
|
for (const targetField of def.targetFields) patch[targetField] = null;
|
|
1121
|
-
await scope.
|
|
1122
|
-
meta,
|
|
1123
|
-
kind: "scan-write",
|
|
1124
|
-
storeName: def.relatedStoreName,
|
|
1684
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
1125
1685
|
write: "put-merged",
|
|
1126
1686
|
patch,
|
|
1127
|
-
|
|
1687
|
+
where: childWhere
|
|
1128
1688
|
});
|
|
1129
1689
|
continue;
|
|
1130
1690
|
}
|
|
1131
1691
|
if (action === "setDefault") {
|
|
1132
1692
|
const patch = buildSetDefaultPatch(contract, def);
|
|
1133
1693
|
await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
|
|
1134
|
-
await scope.
|
|
1135
|
-
meta,
|
|
1136
|
-
kind: "scan-write",
|
|
1137
|
-
storeName: def.relatedStoreName,
|
|
1694
|
+
await writeMutationRows(scope, contract, def.relatedModelName, {
|
|
1138
1695
|
write: "put-merged",
|
|
1139
1696
|
patch,
|
|
1140
|
-
|
|
1697
|
+
where: childWhere
|
|
1141
1698
|
});
|
|
1142
1699
|
continue;
|
|
1143
1700
|
}
|
|
@@ -1148,12 +1705,11 @@ async function executeDeleteWithReferentialActions(options) {
|
|
|
1148
1705
|
await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1149
1706
|
const storeName = getStoreName(contract, modelName);
|
|
1150
1707
|
const meta = makePlanMeta(contract);
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
}))[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];
|
|
1157
1713
|
if (!row) return [];
|
|
1158
1714
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1159
1715
|
await scope.execute({
|
|
@@ -1166,17 +1722,16 @@ async function executeDeleteWithReferentialActions(options) {
|
|
|
1166
1722
|
});
|
|
1167
1723
|
}
|
|
1168
1724
|
async function executeDeleteAllWithReferentialActions(options) {
|
|
1169
|
-
const { executor, contract, modelName,
|
|
1725
|
+
const { executor, contract, modelName, where } = options;
|
|
1170
1726
|
return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
|
|
1171
1727
|
const storeName = getStoreName(contract, modelName);
|
|
1172
1728
|
const meta = makePlanMeta(contract);
|
|
1173
1729
|
const keyPath = getKeyPath(contract, modelName);
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
storeName,
|
|
1178
|
-
...filter !== void 0 ? { filter } : {}
|
|
1730
|
+
if (!hasEnforceableChildRelations(contract, modelName)) return writeMutationRows(scope, contract, modelName, {
|
|
1731
|
+
write: "delete",
|
|
1732
|
+
...where === void 0 ? {} : { where }
|
|
1179
1733
|
});
|
|
1734
|
+
const rows = await readMutationRows(scope, contract, modelName, where);
|
|
1180
1735
|
for (const row of rows) {
|
|
1181
1736
|
await applyReferentialActionsForRow(scope, contract, modelName, row);
|
|
1182
1737
|
const key = extractKeyFromRow(row, keyPath);
|
|
@@ -1191,6 +1746,6 @@ async function executeDeleteAllWithReferentialActions(options) {
|
|
|
1191
1746
|
});
|
|
1192
1747
|
}
|
|
1193
1748
|
//#endregion
|
|
1194
|
-
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 };
|
|
1195
1750
|
|
|
1196
|
-
//# sourceMappingURL=mutation-executor-
|
|
1751
|
+
//# sourceMappingURL=mutation-executor-inLFb0TE.mjs.map
|