@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.
Files changed (26) 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-Bp-hdRg2.d.mts → executor-CIEHpa1j.d.mts} +14 -7
  7. package/dist/{executor-Bp-hdRg2.d.mts.map → executor-CIEHpa1j.d.mts.map} +1 -1
  8. package/dist/{idb-orm-9XN3c9ly.d.mts → idb-orm-6OuwkeDM.d.mts} +2 -2
  9. package/dist/{idb-orm-9XN3c9ly.d.mts.map → idb-orm-6OuwkeDM.d.mts.map} +1 -1
  10. package/dist/{idb-orm-BitJ-EMq.mjs → idb-orm-B1Br1C0F.mjs} +18 -506
  11. package/dist/idb-orm-B1Br1C0F.mjs.map +1 -0
  12. package/dist/internal.d.mts +2 -2
  13. package/dist/internal.mjs +1 -1
  14. package/dist/{managed-client-CS78EjPu.d.mts → managed-client-DQlsxc4Z.d.mts} +3 -3
  15. package/dist/{managed-client-CS78EjPu.d.mts.map → managed-client-DQlsxc4Z.d.mts.map} +1 -1
  16. package/dist/{managed-client-CxlNmERd.mjs → managed-client-M3xMUk0n.mjs} +3 -3
  17. package/dist/{managed-client-CxlNmERd.mjs.map → managed-client-M3xMUk0n.mjs.map} +1 -1
  18. package/dist/{mutation-executor-SJqTuSbb.mjs → mutation-executor-inLFb0TE.mjs} +701 -146
  19. package/dist/mutation-executor-inLFb0TE.mjs.map +1 -0
  20. package/dist/{mutation-scope-1hS0ogGa.d.mts → mutation-scope-DrxFhek3.d.mts} +2 -2
  21. package/dist/{mutation-scope-1hS0ogGa.d.mts.map → mutation-scope-DrxFhek3.d.mts.map} +1 -1
  22. package/dist/orm.d.mts +3 -3
  23. package/dist/orm.mjs +2 -2
  24. package/package.json +6 -6
  25. package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
  26. 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 filter = buildCriterionFilter(criterion);
466
- const meta = makePlanMeta(contract);
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
- filter
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 parentJoinFilter = buildParentJoinFilter(parentValues);
484
- await scope.execute({
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
- filter: parentJoinFilter
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 criterionFilter = buildCriterionFilter(criterion);
497
- const parentJoinFilter = buildParentJoinFilter(parentValues);
498
- const combinedFilter = (row) => parentJoinFilter(row) && criterionFilter(row);
499
- await scope.execute({
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
- filter: combinedFilter
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
- const filter = (row) => evaluateFilter(expr, row);
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
- const combined = filters.length === 1 ? filters[0] : {
554
- kind: "and",
555
- exprs: filters
556
- };
557
- const filter = (row) => evaluateFilter(combined, row);
558
- return scanOneRow(scope, contract, modelName, filter);
559
- }
560
- async function scanOneRow(scope, contract, modelName, filter) {
561
- const storeName = getStoreName(contract, modelName);
562
- const plan = {
563
- meta: makePlanMeta(contract),
564
- kind: "cursor-scan",
565
- storeName,
566
- filter,
567
- take: 1
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
- return (await scope.execute(plan))[0] ?? null;
1149
+ const lowered = lowerRows(catalog, planQuery(catalog, request), makePlanMeta(contract), request);
1150
+ return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
570
1151
  }
571
- function buildCriterionFilter(criterion) {
572
- const expr = shorthandToFilterExpr(criterion);
573
- if (!expr) return () => true;
574
- 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;
575
1176
  }
576
- function buildParentJoinFilter(parentValues) {
577
- const pairs = [...parentValues.entries()];
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
- return (await scope.execute({
645
- meta,
646
- kind: "cursor-scan",
647
- storeName: def.relatedStoreName,
648
- filter: buildChildFilterFromRow(def, parentRow),
649
- take: 1
650
- })).length > 0;
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 buildChildFilterFromRow(def, row) {
658
- const pairs = def.localFields.map((lf, i) => ({
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 childFilter = buildChildFilterFromRow(def, oldRow);
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.execute({
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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
- const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
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 combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
993
- kind: "and",
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 !== void 0 ? { take } : {},
1004
- ...filter !== void 0 ? { filter } : {}
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 childFilter = buildChildFilterFromRow(def, row);
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.execute({
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.execute({
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
- filter: childFilter
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.execute({
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
- filter: childFilter
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 row = (await scope.execute({
1152
- meta,
1153
- kind: "key-get",
1154
- storeName,
1155
- key
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, filter } = options;
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
- const rows = await scope.execute({
1175
- meta,
1176
- kind: "cursor-scan",
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 { keyToken as A, assertEnumValues as C, getRelation as D, getKeyPath as E, getStoreName as O, createMutationDefaultsCache as S, fieldValueToken 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$1 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, extractKeyFromRow 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 };
1195
1750
 
1196
- //# sourceMappingURL=mutation-executor-SJqTuSbb.mjs.map
1751
+ //# sourceMappingURL=mutation-executor-inLFb0TE.mjs.map