@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.
Files changed (27) hide show
  1. package/README.md +6 -0
  2. package/dist/client-auto.d.mts +2 -2
  3. package/dist/client-auto.mjs +1 -1
  4. package/dist/client.d.mts +1 -1
  5. package/dist/client.mjs +1 -1
  6. package/dist/{executor-CevtdBfz.d.mts → executor-CIEHpa1j.d.mts} +16 -9
  7. package/dist/{executor-CevtdBfz.d.mts.map → executor-CIEHpa1j.d.mts.map} +1 -1
  8. package/dist/{idb-orm-gEKHfG4H.d.mts → idb-orm-6OuwkeDM.d.mts} +5 -5
  9. package/dist/{idb-orm-gEKHfG4H.d.mts.map → idb-orm-6OuwkeDM.d.mts.map} +1 -1
  10. package/dist/{idb-orm-DsfllvzD.mjs → idb-orm-B1Br1C0F.mjs} +84 -427
  11. package/dist/idb-orm-B1Br1C0F.mjs.map +1 -0
  12. package/dist/internal.d.mts +2 -2
  13. package/dist/internal.d.mts.map +1 -1
  14. package/dist/internal.mjs +1 -1
  15. package/dist/{managed-client-B_xGXdjz.d.mts → managed-client-DQlsxc4Z.d.mts} +3 -3
  16. package/dist/{managed-client-B_xGXdjz.d.mts.map → managed-client-DQlsxc4Z.d.mts.map} +1 -1
  17. package/dist/{managed-client-Cp01irrm.mjs → managed-client-M3xMUk0n.mjs} +3 -3
  18. package/dist/{managed-client-Cp01irrm.mjs.map → managed-client-M3xMUk0n.mjs.map} +1 -1
  19. package/dist/{mutation-executor-CD5n_kKT.mjs → mutation-executor-inLFb0TE.mjs} +699 -215
  20. package/dist/mutation-executor-inLFb0TE.mjs.map +1 -0
  21. package/dist/{mutation-scope-pOn0QzvV.d.mts → mutation-scope-DrxFhek3.d.mts} +2 -2
  22. package/dist/{mutation-scope-pOn0QzvV.d.mts.map → mutation-scope-DrxFhek3.d.mts.map} +1 -1
  23. package/dist/orm.d.mts +3 -3
  24. package/dist/orm.mjs +2 -2
  25. package/package.json +7 -6
  26. package/dist/idb-orm-DsfllvzD.mjs.map +0 -1
  27. 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 filter = buildCriterionFilter(criterion);
479
- const meta = makePlanMeta(contract);
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
- filter
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 parentJoinFilter = buildParentJoinFilter(parentValues);
497
- await scope.execute({
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
- filter: parentJoinFilter
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 criterionFilter = buildCriterionFilter(criterion);
510
- const parentJoinFilter = buildParentJoinFilter(parentValues);
511
- const combinedFilter = (row) => parentJoinFilter(row) && criterionFilter(row);
512
- await scope.execute({
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
- filter: combinedFilter
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
- const filter = (row) => evaluateFilter(expr, row);
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
- const combined = filters.length === 1 ? filters[0] : {
567
- kind: "and",
568
- exprs: filters
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 filter = (row) => evaluateFilter(combined, row);
571
- return scanOneRow(scope, contract, modelName, filter);
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 scanOneRow(scope, contract, modelName, filter) {
574
- const storeName = getStoreName(contract, modelName);
575
- const plan = {
576
- meta: makePlanMeta(contract),
577
- kind: "cursor-scan",
578
- storeName,
579
- filter,
580
- take: 1
581
- };
582
- return (await scope.execute(plan))[0] ?? null;
583
- }
584
- function buildCriterionFilter(criterion) {
585
- const expr = shorthandToFilterExpr(criterion);
586
- if (!expr) return () => true;
587
- return (row) => evaluateFilter(expr, row);
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 buildParentJoinFilter(parentValues) {
590
- const pairs = [...parentValues.entries()];
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
- * An `IDBKeyRange` for the lookup "does `modelName` have a row whose `field`
654
- * equals `value`?" — but ONLY when `field` is that model's own single-field
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 firstKeyInRange(scope, meta, storeName, range) {
677
- return (await scope.execute({
678
- meta,
679
- kind: "keys",
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
- * `true` if any child row matches the relation against `parentRow`'s values —
687
- * the existence check behind `restrict`. Key-only when the relation's single
688
- * child field is the child model's own primary key (a shared-PK 1:1); a
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 childExists(scope, contract, meta, def, parentRow) {
692
- if (def.targetFields.length === 1) {
693
- const range = pkEqualityRange(contract, def.relatedModelName, def.targetFields[0], parentRow[def.localFields[0]]);
694
- if (range !== null) return await firstKeyInRange(scope, meta, def.relatedStoreName, range) !== void 0;
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 buildChildFilterFromRow(def, row) {
710
- const pairs = def.localFields.map((lf, i) => ({
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 childFilter = buildChildFilterFromRow(def, oldRow);
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.execute({
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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
- * When `fields` are exactly the parent's primary key (in any order), this is
959
- * one key-only lookup. Otherwise it scans the parent store, comparing values
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
- const range = primaryKeyRange(keyPath, fields, values);
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 combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
1064
- kind: "and",
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 !== void 0 ? { take } : {},
1075
- ...filter !== void 0 ? { filter } : {}
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 childFilter = buildChildFilterFromRow(def, row);
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.execute({
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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 row = (await scope.execute({
1223
- meta,
1224
- kind: "key-get",
1225
- storeName,
1226
- key
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, filter } = options;
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
- const rows = await scope.execute({
1246
- meta,
1247
- kind: "cursor-scan",
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 { isValidIdbKey as A, assertEnumValues as C, getKeyPath as D, fieldValueToken as E, keyToken as M, getRelation as O, createMutationDefaultsCache as S, extractKeyFromRow as T, requireTransactionExecutor as _, collectScalarFkStoreNames as a, applyCreateDefaults as b, executeDeleteWithReferentialActions as c, executeScalarCreateAllWithFkValidation as d, executeScalarCreateWithFkValidation as f, hasScalarFkFields as g, hasNestedMutationCallbacks as h, collectOnUpdateEnforcementStoreNames as i, keyEquals as j, getStoreName 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, validateScalarFks as v, buildFieldToIndexMap as w, applyUpdateDefaults as x, withMutationScope as y };
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-CD5n_kKT.mjs.map
1751
+ //# sourceMappingURL=mutation-executor-inLFb0TE.mjs.map