@prisma-idb/client-idb 0.6.1 → 0.8.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.
@@ -1,7 +1,9 @@
1
1
  import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
2
2
  import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
3
3
  import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
4
+ import { compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyToken } from "@prisma-idb/target-idb/runtime";
4
5
  import { generateId } from "@prisma/orm-framework/ids/runtime";
6
+ import { keyPathFields } from "@prisma-idb/target-idb/pack";
5
7
  //#region src/core/types.ts
6
8
  /**
7
9
  * Extract the `storeName` from a model's storage metadata at runtime.
@@ -12,22 +14,30 @@ function getStoreName(contract, modelName) {
12
14
  }
13
15
  /**
14
16
  * Extract the `keyPath` from a model's storage metadata at runtime.
15
- * Falls back to `"id"` (the invariant key name for all syncable IDB models).
17
+ * A single field name for the common case; an ordered array of field names
18
+ * for a compound primary key.
19
+ *
20
+ * @throws if the model is unknown or its storage has no `keyPath` — a
21
+ * validated contract always has one, so this only fires on a malformed or
22
+ * hand-built contract, where guessing a key name would silently read/write
23
+ * the wrong key.
16
24
  */
17
- function getKeyPath$1(contract, modelName) {
18
- return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath ?? "id";
25
+ function getKeyPath(contract, modelName) {
26
+ const keyPath = (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath;
27
+ if (keyPath === void 0) throw new Error(`Model "${modelName}" has no storage.keyPath in the contract.`);
28
+ return keyPath;
19
29
  }
20
30
  /**
21
- * Find the IDB index name for `fieldName` on the given object store, searching
22
- * `contract.storage.stores[storeName].indexes` by `keyPath` equality.
23
- *
24
- * Returns the index name (e.g. `"byEmail"`) when a single-field index whose
25
- * `keyPath` equals `fieldName` exists, or `undefined` otherwise.
26
- * Multi-entry and composite indexes (`keyPath` is an array) are skipped —
27
- * equality semantics on those are unsupported.
31
+ * Extracts a row's primary key as an {@link IDBValidKey} — the scalar value
32
+ * at `row[keyPath]` for a single-field key, or an ordered array of the
33
+ * compound key's member field values (matching the array form IDB itself
34
+ * expects for a compound `keyPath`). The one construction site every
35
+ * key-get/put/delete/update call should route through, so compound-key field
36
+ * order stays consistent everywhere.
28
37
  */
29
- function getIndexForField(contract, storeName, fieldName) {
30
- return buildFieldToIndexMap(contract, storeName)[fieldName];
38
+ function extractKeyFromRow(row, keyPath) {
39
+ if (typeof keyPath === "string") return row[keyPath];
40
+ return keyPath.map((field) => row[field]);
31
41
  }
32
42
  /**
33
43
  * Build a field → indexName lookup for every single-field, non-multi-entry
@@ -43,27 +53,6 @@ function buildFieldToIndexMap(contract, storeName) {
43
53
  return result;
44
54
  }
45
55
  /**
46
- * Returns `true` when `value` is a valid {@link IDBValidKey} — i.e. a value
47
- * that can be passed to `IDBKeyRange.only()` without throwing a DataError.
48
- * Booleans, `NaN`, `BigInt`, plain objects, and `null`/`undefined` are not
49
- * valid IDB keys (only number/string/Date/binary/Array are, per the
50
- * IndexedDB spec). Arrays are validated recursively — an array containing
51
- * any invalid element (e.g. a nested boolean) is itself not a valid key.
52
- *
53
- * Shared by the relation loader (filtering FK values before building
54
- * `IDBKeyRange.only()` plans) and query-shaping (gating `eq` conditions for
55
- * index/PK point-range acceleration).
56
- */
57
- function isValidIdbKey(value) {
58
- if (typeof value === "number") return !Number.isNaN(value);
59
- if (typeof value === "string") return true;
60
- if (value instanceof Date) return true;
61
- if (value instanceof ArrayBuffer) return true;
62
- if (ArrayBuffer.isView(value)) return true;
63
- if (Array.isArray(value)) return value.every((v) => isValidIdbKey(v));
64
- return false;
65
- }
66
- /**
67
56
  * Resolve a model's named relation to a {@link ContractReferenceRelation} at
68
57
  * runtime, or `undefined` when the relation is absent or an embedded relation
69
58
  * (no `on` join block). Used by `include()` to find the related model name and
@@ -201,6 +190,21 @@ function isIndexableEqValue(value) {
201
190
  return isValidIdbKey(value);
202
191
  }
203
192
  /**
193
+ * `true` when `field` is a *single-field* primary key that a lone `eq`
194
+ * condition can directly point-range-query (`store.get`/`openCursor(range)`
195
+ * against the store's own keyPath, no named index required).
196
+ *
197
+ * Deliberately `false` for a compound (array) `keyPath`, even if `field`
198
+ * happens to be one of its member fields — a single `eq` condition can only
199
+ * pin one member, not the whole compound key, so it can't drive a PK
200
+ * point-range scan on its own. Accelerating that case needs *all* member
201
+ * fields ANDed together, which is a cost-based multi-field planner decision
202
+ * (Phase 10 §3.2), not this peephole optimizer's job.
203
+ */
204
+ function isPrimaryKeyField(field, keyPath) {
205
+ return typeof keyPath === "string" && field === keyPath;
206
+ }
207
+ /**
204
208
  * Scan the combined filter expression for the first `eq` field condition whose
205
209
  * field either has a matching IDB index (`fieldToIndexName[field]` is set) or
206
210
  * *is* the store's own primary key (`keyPath`) — a store's keyPath is always
@@ -219,7 +223,7 @@ function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
219
223
  if (filter === void 0) return null;
220
224
  if (filter.kind === "field" && filter.op === "eq") {
221
225
  const indexName = fieldToIndexName[filter.field];
222
- if ((indexName !== void 0 || filter.field === keyPath) && isIndexableEqValue(filter.value)) return {
226
+ if ((indexName !== void 0 || isPrimaryKeyField(filter.field, keyPath)) && isIndexableEqValue(filter.value)) return {
223
227
  indexName,
224
228
  value: filter.value,
225
229
  remainingFilter: void 0
@@ -232,7 +236,7 @@ function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
232
236
  const expr = flat[i];
233
237
  if (expr.kind === "field" && expr.op === "eq") {
234
238
  const indexName = fieldToIndexName[expr.field];
235
- if ((indexName !== void 0 || expr.field === keyPath) && isIndexableEqValue(expr.value)) {
239
+ if ((indexName !== void 0 || isPrimaryKeyField(expr.field, keyPath)) && isIndexableEqValue(expr.value)) {
236
240
  const rest = flat.filter((_, j) => j !== i);
237
241
  return {
238
242
  indexName,
@@ -257,7 +261,7 @@ function tryExtractOrBranches(orNode, fieldToIndexName, keyPath, remainingFilter
257
261
  for (const expr of orNode.exprs) {
258
262
  if (expr.kind !== "field" || expr.op !== "eq") return null;
259
263
  const indexName = fieldToIndexName[expr.field];
260
- if (indexName === void 0 && expr.field !== keyPath || !isIndexableEqValue(expr.value)) return null;
264
+ if (indexName === void 0 && !isPrimaryKeyField(expr.field, keyPath) || !isIndexableEqValue(expr.value)) return null;
261
265
  branches.push({
262
266
  indexName,
263
267
  value: expr.value
@@ -297,22 +301,59 @@ function extractIndexOrHint(filter, fieldToIndexName, keyPath) {
297
301
  * Build an in-memory comparator from an `orderBy` spec (field → direction).
298
302
  *
299
303
  * Returns `undefined` when there is nothing to sort by. Compares fields in
300
- * declaration order; values are primitives (strings, numbers, dates) in
301
- * practice, so JS relational comparison is sufficient.
304
+ * declaration order, with {@link compareFieldValues}: the same order an
305
+ * IndexedDB index uses, so equal `Date`s tie and fall through to the next
306
+ * field, and `null`s sort last (first when descending).
302
307
  */
303
308
  function buildRowComparator(orderBy) {
304
309
  if (orderBy === void 0) return void 0;
305
310
  return (a, b) => {
306
311
  for (const [field, dir] of Object.entries(orderBy)) {
307
- const av = a[field];
308
- const bv = b[field];
309
- if (av === bv) continue;
310
- const cmp = av < bv ? -1 : 1;
312
+ const cmp = compareFieldValues(a[field], b[field]);
313
+ if (cmp === 0) continue;
311
314
  return dir === "desc" ? -cmp : cmp;
312
315
  }
313
316
  return 0;
314
317
  };
315
318
  }
319
+ /**
320
+ * `true` when a `cursor-scan` plan's result *cardinality* is fully determined
321
+ * by its store/index + key range — i.e. there is no in-memory `filter` that
322
+ * would need each row's value. Only then can `count()` be answered natively
323
+ * (`store.count(range)` / `index.count(range)`) without deserializing rows.
324
+ *
325
+ * `comparator` (ORDER BY), `direction`, `skip` and `take` don't change *which*
326
+ * rows match, so they don't disqualify a plan — `skip`/`take` are applied to
327
+ * the native total afterwards via {@link clampCount}.
328
+ *
329
+ * Safe for index scans because the equality hint that builds `range` never
330
+ * selects a `multiEntry` (or compound) index — `buildFieldToIndexMap` excludes
331
+ * them — so one record maps to at most one index entry and entries == rows.
332
+ */
333
+ function isNativelyCountable(plan) {
334
+ return plan.filter === void 0;
335
+ }
336
+ /** Rewrites a natively-countable `cursor-scan` as the equivalent native `count` plan. */
337
+ function toCountPlan(plan) {
338
+ return {
339
+ meta: plan.meta,
340
+ kind: "count",
341
+ storeName: plan.storeName,
342
+ ...plan.indexName !== void 0 ? { indexName: plan.indexName } : {},
343
+ ...plan.range !== void 0 ? { range: plan.range } : {}
344
+ };
345
+ }
346
+ /**
347
+ * Applies `skip`/`take` to an *unpaginated* total. A paginated count is pure
348
+ * arithmetic on the unpaginated count — `max(0, total - skip)`, capped at
349
+ * `take` — so this is exact, not an approximation.
350
+ */
351
+ function clampCount(total, skip, take) {
352
+ let n = total;
353
+ if (skip !== void 0) n = Math.max(0, n - skip);
354
+ if (take !== void 0) n = Math.min(take, n);
355
+ return n;
356
+ }
316
357
  //#endregion
317
358
  //#region src/core/aggregate-builder.ts
318
359
  /**
@@ -463,6 +504,69 @@ function createGroupedAccessor(init) {
463
504
  } };
464
505
  }
465
506
  //#endregion
507
+ //#region src/core/enum-validation.ts
508
+ const rulesByContract = /* @__PURE__ */ new WeakMap();
509
+ function resolveEnumRule(contract, field) {
510
+ const ref = field.valueSet;
511
+ if (ref === void 0 || ref.plane !== "domain" || ref.entityKind !== "enum") return void 0;
512
+ const contractEnum = contract.domain.namespaces[ref.namespaceId]?.enum?.[ref.entityName];
513
+ if (contractEnum === void 0) throw new Error(`Enum "${ref.entityName}" referenced by the contract is not declared in its domain.`);
514
+ return {
515
+ enumName: ref.entityName,
516
+ values: new Set(contractEnum.members.map((member) => member.value)),
517
+ nullable: field.nullable,
518
+ many: field.many === true
519
+ };
520
+ }
521
+ function getModelEnumRules(contract, modelName) {
522
+ let byModel = rulesByContract.get(contract);
523
+ if (byModel === void 0) {
524
+ byModel = /* @__PURE__ */ new Map();
525
+ rulesByContract.set(contract, byModel);
526
+ }
527
+ const cached = byModel.get(modelName);
528
+ if (cached !== void 0) return cached;
529
+ const rules = /* @__PURE__ */ new Map();
530
+ const fields = domainModelsAtDefaultNamespace(contract.domain)[modelName]?.fields ?? {};
531
+ for (const [fieldName, field] of Object.entries(fields)) {
532
+ const rule = resolveEnumRule(contract, field);
533
+ if (rule !== void 0) rules.set(fieldName, rule);
534
+ }
535
+ byModel.set(modelName, rules);
536
+ return rules;
537
+ }
538
+ function describe(value) {
539
+ return typeof value === "string" ? JSON.stringify(value) : `${String(value)} (${typeof value})`;
540
+ }
541
+ function invalidValueError(modelName, fieldName, rule, value) {
542
+ const expected = [...rule.values].map((v) => JSON.stringify(v)).join(", ");
543
+ return /* @__PURE__ */ new Error(`Invalid value ${describe(value)} for enum field "${modelName}.${fieldName}". Expected one of enum "${rule.enumName}": ${expected}.`);
544
+ }
545
+ /**
546
+ * Throws if `data` sets an enum field to a value its enum doesn't declare.
547
+ * Fields left `undefined` are not checked. `null` is accepted only on an
548
+ * optional (`Role?`) field; a list (`Role[]`) field must be an array whose
549
+ * every element is a declared value.
550
+ */
551
+ function assertEnumValues(contract, modelName, data) {
552
+ const rules = getModelEnumRules(contract, modelName);
553
+ if (rules.size === 0) return;
554
+ for (const [fieldName, rule] of rules) {
555
+ const value = data[fieldName];
556
+ if (value === void 0) continue;
557
+ if (value === null) {
558
+ if (rule.nullable) continue;
559
+ throw new Error(`Enum field "${modelName}.${fieldName}" is required and cannot be null.`);
560
+ }
561
+ if (rule.many) {
562
+ if (!Array.isArray(value)) throw new Error(`Enum list field "${modelName}.${fieldName}" expects an array of "${rule.enumName}" values, got ${describe(value)}.`);
563
+ for (const element of value) if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
564
+ continue;
565
+ }
566
+ if (!rule.values.has(value)) throw invalidValueError(modelName, fieldName, rule, value);
567
+ }
568
+ }
569
+ //#endregion
466
570
  //#region src/core/mutation-defaults.ts
467
571
  function createMutationDefaultsCache() {
468
572
  return /* @__PURE__ */ new Map();
@@ -533,8 +637,11 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
533
637
  * Batch-load a single named relation for all rows in `rows` and attach the
534
638
  * result to each row under the `relName` key.
535
639
  *
536
- * The join is done with one cursor scan over the related store (with an
537
- * in-memory filter), then grouped/indexed in memory — avoiding N+1 queries.
640
+ * The join matches every field of the relation, so compound foreign keys join
641
+ * on the whole tuple. It uses one key-range scan per distinct tuple when an
642
+ * index or the related primary key covers the target fields, and otherwise
643
+ * one full scan with an in-memory filter. Rows are then grouped in memory,
644
+ * which avoids N+1 queries.
538
645
  *
539
646
  * The `entry` carries any `include()` refinement:
540
647
  *
@@ -562,23 +669,22 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
562
669
  const relation = rawRelation;
563
670
  const { cardinality, on } = relation;
564
671
  const relatedModelName = relation.to.model;
565
- const localField = on.localFields[0];
566
- const foreignField = on.targetFields[0];
567
- if (localField === void 0 || foreignField === void 0) return rows;
672
+ const localFields = on.localFields;
673
+ const targetFields = on.targetFields;
674
+ if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
568
675
  const relatedModel = models[relatedModelName];
569
676
  if (relatedModel === void 0) return rows;
570
677
  const relatedStoreName = typeof relatedModel.storage === "object" && relatedModel.storage !== null && "storeName" in relatedModel.storage ? String(relatedModel.storage["storeName"]) : relatedModelName;
571
678
  const isScalar = entry.kind === "scalar";
572
- const localValues = /* @__PURE__ */ new Set();
679
+ const localTuples = /* @__PURE__ */ new Map();
573
680
  for (const row of rows) {
574
- const v = row[localField];
575
- if (v !== void 0 && v !== null) localValues.add(v);
681
+ const token = tupleToken(row, localFields);
682
+ if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
576
683
  }
577
- if (localValues.size === 0) return rows.map((row) => ({
684
+ if (localTuples.size === 0) return rows.map((row) => ({
578
685
  ...row,
579
686
  [relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
580
687
  }));
581
- const capturedForeignField = foreignField;
582
688
  const refinedWhere = combineFilterExprs(entry.state.filters);
583
689
  const planMeta = {
584
690
  target: "idb",
@@ -586,21 +692,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
586
692
  lane: "idb-orm",
587
693
  annotations: { groupingKey }
588
694
  };
589
- const fkIndexName = typeof IDBKeyRange !== "undefined" ? getIndexForField(contract, relatedStoreName, capturedForeignField) : void 0;
590
- const targetsRelatedPk = fkIndexName === void 0 && typeof IDBKeyRange !== "undefined" && capturedForeignField === getKeyPath$1(contract, relatedModelName);
695
+ const rangeSource = findRangeSource(contract, relatedStoreName, relatedModelName, targetFields);
591
696
  let relatedRows;
592
- if (fkIndexName !== void 0 || targetsRelatedPk) {
697
+ if (rangeSource !== void 0) {
593
698
  const refinedFilter = refinedWhere !== void 0 ? (row) => evaluateFilter(refinedWhere, row) : void 0;
594
- const validValues = Array.from(localValues).filter(isValidIdbKey);
595
- relatedRows = (await Promise.all(validValues.map(async (value) => {
699
+ const ranges = [];
700
+ for (const values of localTuples.values()) {
701
+ const ordered = keyPathFields(rangeSource.keyPath).map((f) => values[targetFields.indexOf(f)]);
702
+ if (!ordered.every((v) => isValidIdbKey(v))) continue;
703
+ ranges.push(IDBKeyRange.only(typeof rangeSource.keyPath === "string" ? ordered[0] : ordered));
704
+ }
705
+ relatedRows = (await Promise.all(ranges.map(async (range) => {
596
706
  const plan = {
597
707
  meta: planMeta,
598
708
  idbPlan: {
599
709
  meta: planMeta,
600
710
  kind: "cursor-scan",
601
711
  storeName: relatedStoreName,
602
- ...fkIndexName !== void 0 ? { indexName: fkIndexName } : {},
603
- range: IDBKeyRange.only(value),
712
+ ...rangeSource.indexName !== void 0 ? { indexName: rangeSource.indexName } : {},
713
+ range,
604
714
  ...refinedFilter !== void 0 ? { filter: refinedFilter } : {}
605
715
  }
606
716
  };
@@ -610,7 +720,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
610
720
  }))).flat();
611
721
  } else {
612
722
  relatedRows = [];
613
- const filter = (row) => localValues.has(row[capturedForeignField]) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
723
+ const filter = (row) => {
724
+ const token = tupleToken(row, targetFields);
725
+ return token !== null && localTuples.has(token) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
726
+ };
614
727
  const plan = {
615
728
  meta: planMeta,
616
729
  idbPlan: {
@@ -625,20 +738,25 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
625
738
  if (cardinality === "1:N") {
626
739
  const grouped = /* @__PURE__ */ new Map();
627
740
  for (const rrow of relatedRows) {
628
- const gk = rrow[capturedForeignField];
741
+ const gk = tupleToken(rrow, targetFields);
742
+ if (gk === null) continue;
629
743
  const group = grouped.get(gk) ?? [];
630
744
  group.push(rrow);
631
745
  grouped.set(gk, group);
632
746
  }
747
+ const groupFor = (row) => {
748
+ const token = tupleToken(row, localFields);
749
+ return token === null ? [] : grouped.get(token) ?? [];
750
+ };
633
751
  if (isScalar) return rows.map((row) => ({
634
752
  ...row,
635
- [relName]: (grouped.get(row[localField]) ?? []).length
753
+ [relName]: groupFor(row).length
636
754
  }));
637
755
  const comparator = buildRowComparator(entry.state.orderBy);
638
756
  const skip = entry.state.skip ?? 0;
639
757
  const take = entry.state.take;
640
758
  return rows.map((row) => {
641
- let group = grouped.get(row[localField]) ?? [];
759
+ let group = groupFor(row);
642
760
  if (comparator !== void 0) group = [...group].sort(comparator);
643
761
  if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
644
762
  return {
@@ -648,11 +766,50 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
648
766
  });
649
767
  }
650
768
  const indexed = /* @__PURE__ */ new Map();
651
- for (const rrow of relatedRows) indexed.set(rrow[capturedForeignField], rrow);
652
- return rows.map((row) => ({
653
- ...row,
654
- [relName]: indexed.get(row[localField]) ?? null
655
- }));
769
+ for (const rrow of relatedRows) {
770
+ const token = tupleToken(rrow, targetFields);
771
+ if (token !== null) indexed.set(token, rrow);
772
+ }
773
+ return rows.map((row) => {
774
+ const token = tupleToken(row, localFields);
775
+ return {
776
+ ...row,
777
+ [relName]: (token === null ? void 0 : indexed.get(token)) ?? null
778
+ };
779
+ });
780
+ }
781
+ /**
782
+ * A `Map` key for the values of `fields` in `row`, or `null` when any of them
783
+ * is null or undefined. Equal `Date`s and binary values give equal tokens.
784
+ */
785
+ function tupleToken(row, fields) {
786
+ const parts = [];
787
+ for (const field of fields) {
788
+ const value = row[field];
789
+ if (value === null || value === void 0) return null;
790
+ const token = fieldValueToken(value);
791
+ parts.push([typeof token, String(token)]);
792
+ }
793
+ return JSON.stringify(parts);
794
+ }
795
+ /**
796
+ * Finds an index, or else the store's primary key, whose key path has exactly
797
+ * `fields` (in any order), so a lookup on those fields can use a key range.
798
+ * Returns `undefined` when there is none, or when `IDBKeyRange` isn't available.
799
+ */
800
+ function findRangeSource(contract, storeName, modelName, fields) {
801
+ if (typeof IDBKeyRange === "undefined") return void 0;
802
+ const coversFields = (keyPath) => {
803
+ const keyFields = keyPathFields(keyPath);
804
+ return keyFields.length === fields.length && keyFields.every((f) => fields.includes(f));
805
+ };
806
+ const indexes = contract.storage.stores[storeName]?.indexes ?? {};
807
+ for (const [indexName, indexDef] of Object.entries(indexes)) if (indexDef.multiEntry !== true && coversFields(indexDef.keyPath)) return {
808
+ indexName,
809
+ keyPath: indexDef.keyPath
810
+ };
811
+ const primaryKey = getKeyPath(contract, modelName);
812
+ return coversFields(primaryKey) ? { keyPath: primaryKey } : void 0;
656
813
  }
657
814
  //#endregion
658
815
  //#region src/core/mutation-scope.ts
@@ -774,22 +931,49 @@ function requireTransactionExecutor(executor) {
774
931
  async function executeNestedCreateMutation(options) {
775
932
  const { executor, contract, modelName, data } = options;
776
933
  const record = data;
777
- const storeNames = collectStoreNames(contract, modelName, record);
934
+ const storeNames = collectStoreNames(contract, modelName, record, "create");
778
935
  const defaultsCache = createMutationDefaultsCache();
779
936
  return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
780
937
  }
781
938
  async function executeNestedUpdateMutation(options) {
782
939
  const { executor, contract, modelName, filters, data } = options;
783
940
  const record = data;
784
- const storeNames = collectStoreNames(contract, modelName, record);
941
+ const storeNames = collectStoreNames(contract, modelName, record, "update");
785
942
  const defaultsCache = createMutationDefaultsCache();
786
943
  return withMutationScope(executor, storeNames, (scope) => updateFirstGraph(scope, contract, modelName, filters, record, defaultsCache));
787
944
  }
788
- function collectStoreNames(contract, modelName, data) {
789
- const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
790
- for (const def of getRelationDefinitions(contract, modelName)) if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) stores.add(def.relatedStoreName);
945
+ /**
946
+ * Every store a nested write may touch: the model's own store, each related
947
+ * store a relation callback writes to, and the parent stores that foreign-key
948
+ * checks read, for the model and for each related model. An update also
949
+ * declares the stores its `onUpdate` referential actions may touch.
950
+ *
951
+ * Parent stores are declared for every foreign key, not only the ones `data`
952
+ * sets, because a nested write can set foreign keys through `connect()` or
953
+ * through a default.
954
+ */
955
+ function collectStoreNames(contract, modelName, data, kind) {
956
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName), ...fkParentStoreNames(contract, modelName)]);
957
+ const scalarData = {};
958
+ const relationNames = /* @__PURE__ */ new Set();
959
+ for (const def of getRelationDefinitions(contract, modelName)) {
960
+ relationNames.add(def.relationName);
961
+ if (def.relationName in data && isRelationMutationCallback(data[def.relationName])) {
962
+ stores.add(def.relatedStoreName);
963
+ for (const store of fkParentStoreNames(contract, def.relatedModelName)) stores.add(store);
964
+ }
965
+ }
966
+ if (kind === "update") {
967
+ for (const [field, value] of Object.entries(data)) if (!relationNames.has(field)) scalarData[field] = value;
968
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), scalarData, createMutationDefaultsCache());
969
+ for (const store of collectOnUpdateEnforcementStoreNames(contract, modelName, patch).storeNames) stores.add(store);
970
+ }
791
971
  return [...stores];
792
972
  }
973
+ /** The stores of every parent `modelName` has a foreign key to. */
974
+ function fkParentStoreNames(contract, modelName) {
975
+ return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
976
+ }
793
977
  async function createGraph(scope, contract, modelName, input, defaultsCache) {
794
978
  const parsed = parseMutationInput(contract, modelName, input);
795
979
  const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
@@ -815,9 +999,12 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
815
999
  let parentRow = existingRow;
816
1000
  if (Object.keys(scalarData).length > 0) {
817
1001
  const storeName = getStoreName(contract, modelName);
818
- const key = existingRow[getKeyPath(contract, modelName)];
1002
+ const key = extractKeyFromRow(existingRow, getKeyPath(contract, modelName));
819
1003
  const meta = makePlanMeta(contract);
820
1004
  const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
1005
+ assertEnumValues(contract, modelName, patch);
1006
+ await validateScalarFks(scope, contract, modelName, patch, existingRow);
1007
+ await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
821
1008
  const updated = (await scope.execute({
822
1009
  meta,
823
1010
  kind: "update",
@@ -970,6 +1157,8 @@ async function insertSingleRow(scope, contract, modelName, data, defaultsCache)
970
1157
  const storeName = getStoreName(contract, modelName);
971
1158
  const meta = makePlanMeta(contract);
972
1159
  const record = applyCreateDefaults(contract.execution?.mutations.defaults, storeName, data, defaultsCache);
1160
+ assertEnumValues(contract, modelName, record);
1161
+ await validateScalarFks(scope, contract, modelName, record);
973
1162
  return (await scope.execute({
974
1163
  meta,
975
1164
  kind: "add",
@@ -1020,10 +1209,7 @@ function buildCriterionFilter(criterion) {
1020
1209
  }
1021
1210
  function buildParentJoinFilter(parentValues) {
1022
1211
  const pairs = [...parentValues.entries()];
1023
- return (row) => pairs.every(([childField, parentValue]) => row[childField] === parentValue);
1024
- }
1025
- function getKeyPath(contract, modelName) {
1026
- return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.keyPath ?? "id";
1212
+ return (row) => pairs.every(([childField, parentValue]) => fieldValuesEqual(row[childField], parentValue));
1027
1213
  }
1028
1214
  function getStoredAction(contract, modelName, relationName, kind) {
1029
1215
  return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
@@ -1048,7 +1234,10 @@ function sameFields(a, b) {
1048
1234
  * action kind on both sides so this fallback never has to arbitrate a real
1049
1235
  * conflict.
1050
1236
  */
1051
- function getReferentialActionForRelation(contract, modelName, def, kind, defaultAction) {
1237
+ function getReferentialActionForRelation(contract, modelName, def, kind) {
1238
+ return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
1239
+ }
1240
+ function findDeclaredAction(contract, modelName, def, kind) {
1052
1241
  const direct = getStoredAction(contract, modelName, def.relationName, kind);
1053
1242
  if (direct !== void 0) return direct;
1054
1243
  for (const inverse of getRelationDefinitions(contract, def.relatedModelName)) {
@@ -1058,29 +1247,88 @@ function getReferentialActionForRelation(contract, modelName, def, kind, default
1058
1247
  const inverseAction = getStoredAction(contract, def.relatedModelName, inverse.relationName, kind);
1059
1248
  if (inverseAction !== void 0) return inverseAction;
1060
1249
  }
1061
- return defaultAction;
1062
- }
1063
- function getOnDeleteForDeleteRelation(contract, modelName, def) {
1064
- return getReferentialActionForRelation(contract, modelName, def, "onDelete", "restrict");
1065
1250
  }
1066
1251
  /**
1067
- * `onUpdate`'s default (when neither side declares one) is `cascade`, unlike
1068
- * `onDelete`'s `restrict` default — this matches Prisma's own documented
1069
- * behavior: propagating a changed referenced value to children is normally
1070
- * safe, unlike deleting the row those children point to.
1252
+ * Applies the default and resolves `noAction`, matching what Postgres does
1253
+ * with the same schema, so a synced app behaves the same on both sides:
1254
+ *
1255
+ * - An undeclared action defaults to `restrict`, for `onDelete` and
1256
+ * `onUpdate` alike. Prisma 8 emits no `ON DELETE`/`ON UPDATE` clause for an
1257
+ * undeclared action, and the database default is `NO ACTION`.
1258
+ * - `noAction` behaves like `restrict`. In SQL, `NO ACTION` also rejects the
1259
+ * change; it only defers the check to the end of the statement, which makes
1260
+ * no difference here.
1071
1261
  */
1262
+ function enforcedAction(declared) {
1263
+ return declared === void 0 || declared === "noAction" ? "restrict" : declared;
1264
+ }
1265
+ function getOnDeleteForDeleteRelation(contract, modelName, def) {
1266
+ return getReferentialActionForRelation(contract, modelName, def, "onDelete");
1267
+ }
1072
1268
  function getOnUpdateForRelation(contract, modelName, def) {
1073
- return getReferentialActionForRelation(contract, modelName, def, "onUpdate", "cascade");
1269
+ return getReferentialActionForRelation(contract, modelName, def, "onUpdate");
1074
1270
  }
1075
1271
  function isDeleteEnforcementRelation(contract, modelName, def) {
1076
1272
  if (def.cardinality === "1:N") return true;
1077
1273
  if (def.cardinality === "1:1") {
1078
1274
  const keyPath = getKeyPath(contract, modelName);
1079
- return def.localFields.length > 0 && def.localFields[0] === keyPath;
1275
+ return sameFields(def.localFields, keyPathFields(keyPath));
1080
1276
  }
1081
1277
  return false;
1082
1278
  }
1083
1279
  /**
1280
+ * An `IDBKeyRange` for the lookup "does `modelName` have a row whose `field`
1281
+ * equals `value`?" — but ONLY when `field` is that model's own single-field
1282
+ * primary key (so the answer is a pure key lookup that needs no row value)
1283
+ * and `value` is a legal IDB key. `null` otherwise, and the caller keeps its
1284
+ * value-materializing `cursor-scan`.
1285
+ *
1286
+ * Deliberately `null` for a compound primary key even if `field` is one of its
1287
+ * members (one member can't pin the whole key), and for any non-key field
1288
+ * (that needs an index-resolution step — Phase 10.5's shared primitive, not
1289
+ * this helper's job). `IDBKeyRange.only` throws `DataError` on invalid keys
1290
+ * (`null`, `NaN`, booleans, …), hence the guard.
1291
+ */
1292
+ function pkEqualityRange(contract, modelName, field, value) {
1293
+ if (typeof IDBKeyRange === "undefined") return null;
1294
+ const keyPath = getKeyPath(contract, modelName);
1295
+ if (typeof keyPath !== "string" || keyPath !== field) return null;
1296
+ if (!isValidIdbKey(value)) return null;
1297
+ return IDBKeyRange.only(value);
1298
+ }
1299
+ /**
1300
+ * Resolves to the first primary key in `storeName` within `range`, or
1301
+ * `undefined` — one `getKey` request, no row deserialized.
1302
+ */
1303
+ async function firstKeyInRange(scope, meta, storeName, range) {
1304
+ return (await scope.execute({
1305
+ meta,
1306
+ kind: "keys",
1307
+ storeName,
1308
+ range,
1309
+ take: 1
1310
+ }))[0]?.["key"];
1311
+ }
1312
+ /**
1313
+ * `true` if any child row matches the relation against `parentRow`'s values —
1314
+ * the existence check behind `restrict`. Key-only when the relation's single
1315
+ * child field is the child model's own primary key (a shared-PK 1:1); a
1316
+ * value-materializing `cursor-scan` (`take: 1`) otherwise.
1317
+ */
1318
+ async function childExists(scope, contract, meta, def, parentRow) {
1319
+ if (def.targetFields.length === 1) {
1320
+ const range = pkEqualityRange(contract, def.relatedModelName, def.targetFields[0], parentRow[def.localFields[0]]);
1321
+ if (range !== null) return await firstKeyInRange(scope, meta, def.relatedStoreName, range) !== void 0;
1322
+ }
1323
+ return (await scope.execute({
1324
+ meta,
1325
+ kind: "cursor-scan",
1326
+ storeName: def.relatedStoreName,
1327
+ filter: buildChildFilterFromRow(def, parentRow),
1328
+ take: 1
1329
+ })).length > 0;
1330
+ }
1331
+ /**
1084
1332
  * Builds a filter matching a relation's children against one specific parent
1085
1333
  * row's values. Shared by `onDelete` cascade (`applyReferentialActionsForRow`)
1086
1334
  * and `onUpdate` cascade (`applyReferentialActionsForRowOnUpdate`).
@@ -1090,7 +1338,7 @@ function buildChildFilterFromRow(def, row) {
1090
1338
  childField: def.targetFields[i],
1091
1339
  parentValue: row[lf]
1092
1340
  }));
1093
- return (child) => pairs.every(({ childField, parentValue }) => child[childField] === parentValue);
1341
+ return (child) => pairs.every(({ childField, parentValue }) => fieldValuesEqual(child[childField], parentValue));
1094
1342
  }
1095
1343
  /** Reads a field's literal `@default(...)` value from `IdbModelStorage.fieldDefaults`, if declared. */
1096
1344
  function getFieldDefault(contract, modelName, fieldName) {
@@ -1127,10 +1375,8 @@ function buildSetDefaultPatch(contract, def) {
1127
1375
  * part of the transaction (it's the model literally being written), so no
1128
1376
  * store-list changes are needed to call this.
1129
1377
  *
1130
- * Refuses (throws) rather than validates incorrectly for compound (multi-
1131
- * field) relations, for the same reason `validateScalarFks` does: each
1132
- * field's default is resolved independently, so there's no guarantee the
1133
- * combination corresponds to a single real row.
1378
+ * A compound relation is checked as one tuple: a single parent row must
1379
+ * match every field's default.
1134
1380
  *
1135
1381
  * `excludeKey`, when given, excludes the parent row currently being
1136
1382
  * deleted/updated from the existence scan. That row is still physically
@@ -1142,21 +1388,9 @@ function buildSetDefaultPatch(contract, def) {
1142
1388
  * default references a real row, when after the transaction nothing will.
1143
1389
  */
1144
1390
  async function validateSetDefaultPatch(scope, contract, modelName, def, patch, excludeKey) {
1145
- if (def.localFields.length > 1) throw new Error(`setDefault referential action on relation '${def.relationName}' cannot validate a compound (multi-field) default: each field's default is resolved independently, so there is no guarantee the combination corresponds to a single real row.`);
1146
- const meta = makePlanMeta(contract);
1147
- const parentStoreName = getStoreName(contract, modelName);
1148
- const parentKeyPath = getKeyPath(contract, modelName);
1149
- const localField = def.localFields[0];
1150
- const targetField = def.targetFields[0];
1151
- const value = patch[targetField];
1152
- const filter = (row) => row[localField] === value && (excludeKey === void 0 || row[parentKeyPath] !== excludeKey);
1153
- if ((await scope.execute({
1154
- meta,
1155
- kind: "cursor-scan",
1156
- storeName: parentStoreName,
1157
- filter,
1158
- take: 1
1159
- })).length === 0) throw new Error(`setDefault referential action on relation '${def.relationName}' would set '${def.relatedModelName}.${targetField}' to '${String(value)}', but no ${modelName} with ${localField}='${String(value)}' exists — the declared default does not reference a real row.`);
1391
+ const values = def.targetFields.map((f) => patch[f]);
1392
+ if (values.some((v) => v === null || v === void 0)) return;
1393
+ if (!await parentExists(scope, contract, makePlanMeta(contract), modelName, def.localFields, values, excludeKey)) throw new Error(`setDefault referential action on relation '${def.relationName}' would set ${def.relatedModelName} to ${describeTuple(def.targetFields, values)}, but no ${modelName} with ${describeTuple(def.localFields, values)} exists — the declared default does not reference a real row.`);
1160
1394
  }
1161
1395
  /**
1162
1396
  * Collects every store an `onUpdate`-enforced write to `modelName` might
@@ -1186,7 +1420,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1186
1420
  for (const def of getRelationDefinitions(contract, mName)) {
1187
1421
  if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
1188
1422
  const action = getOnUpdateForRelation(contract, mName, def);
1189
- if (action === "noAction") continue;
1190
1423
  stores.add(def.relatedStoreName);
1191
1424
  if (action === "cascade") walkCascadeChain(def.relatedModelName);
1192
1425
  }
@@ -1196,7 +1429,6 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1196
1429
  if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
1197
1430
  if (!def.localFields.some((f) => f in data)) continue;
1198
1431
  const action = getOnUpdateForRelation(contract, modelName, def);
1199
- if (action === "noAction") continue;
1200
1432
  enforces = true;
1201
1433
  stores.add(def.relatedStoreName);
1202
1434
  if (action === "cascade") walkCascadeChain(def.relatedModelName);
@@ -1231,25 +1463,18 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1231
1463
  */
1232
1464
  async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
1233
1465
  const keyPath = getKeyPath(contract, modelName);
1234
- const rowKey = `${getStoreName(contract, modelName)}::${String(oldRow[keyPath])}`;
1466
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(oldRow, keyPath))}`;
1235
1467
  if (visited.has(rowKey)) return;
1236
1468
  visited.add(rowKey);
1237
1469
  const meta = makePlanMeta(contract);
1238
1470
  for (const def of getRelationDefinitions(contract, modelName)) {
1239
1471
  if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
1240
- const changedFields = def.localFields.filter((f) => f in patch && patch[f] !== oldRow[f]);
1472
+ const changedFields = def.localFields.filter((f) => f in patch && !fieldValuesEqual(patch[f], oldRow[f]));
1241
1473
  if (changedFields.length === 0) continue;
1242
1474
  const action = getOnUpdateForRelation(contract, modelName, def);
1243
- if (action === "noAction") continue;
1244
1475
  const childFilter = buildChildFilterFromRow(def, oldRow);
1245
1476
  if (action === "restrict") {
1246
- if ((await scope.execute({
1247
- meta,
1248
- kind: "cursor-scan",
1249
- storeName: def.relatedStoreName,
1250
- filter: childFilter,
1251
- take: 1
1252
- })).length > 0) throw new Error(`Cannot update ${modelName} '${String(oldRow[keyPath])}': changing field(s) ${changedFields.join(", ")} would orphan child records on relation '${def.relationName}'. Use onUpdate: 'cascade', 'setNull', 'setDefault', or 'noAction'.`);
1477
+ 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.`);
1253
1478
  continue;
1254
1479
  }
1255
1480
  if (action === "cascade") {
@@ -1291,7 +1516,7 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
1291
1516
  }
1292
1517
  if (action === "setDefault") {
1293
1518
  const childPatch = buildSetDefaultPatch(contract, def);
1294
- await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, oldRow[keyPath]);
1519
+ await validateSetDefaultPatch(scope, contract, modelName, def, childPatch, extractKeyFromRow(oldRow, keyPath));
1295
1520
  await scope.execute({
1296
1521
  meta,
1297
1522
  kind: "scan-write",
@@ -1323,31 +1548,102 @@ function collectScalarFkStoreNames(contract, modelName, data) {
1323
1548
  }
1324
1549
  return [...stores];
1325
1550
  }
1326
- async function validateScalarFks(scope, contract, modelName, data) {
1551
+ /**
1552
+ * Checks that every foreign key `data` sets points at an existing parent row,
1553
+ * inside the write's transaction.
1554
+ *
1555
+ * A compound foreign key is checked as one tuple: a single parent row must
1556
+ * match every field. Checking the fields one at a time could pass with each
1557
+ * value taken from a different parent. When `data` sets only some fields of a
1558
+ * compound key, the rest come from `existingRow`, the row being updated.
1559
+ * Callers must pass it in that case; see {@link fkCheckNeedsExistingRow}.
1560
+ *
1561
+ * A key with any `null` field isn't checked, like SQL's default `MATCH SIMPLE`.
1562
+ */
1563
+ async function validateScalarFks(scope, contract, modelName, data, existingRow) {
1327
1564
  const meta = makePlanMeta(contract);
1328
1565
  for (const def of getRelationDefinitions(contract, modelName)) {
1329
1566
  if (def.cardinality !== "N:1") continue;
1330
- if (!def.localFields.some((f) => f in data && data[f] !== null && data[f] !== void 0)) continue;
1331
- if (def.localFields.length > 1) throw new Error(`FK violation on relation '${def.relationName}': compound (multi-field) scalar FK validation is not supported`);
1332
- const localField = def.localFields[0];
1333
- const targetField = def.targetFields[0];
1334
- const value = data[localField];
1335
- const filter = (row) => row[targetField] === value;
1336
- const plan = {
1337
- meta,
1338
- kind: "cursor-scan",
1339
- storeName: def.relatedStoreName,
1340
- filter,
1341
- take: 1
1342
- };
1343
- if ((await scope.execute(plan)).length === 0) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${targetField}='${String(value)}'`);
1567
+ if (!def.localFields.some((f) => f in data)) continue;
1568
+ const values = def.localFields.map((f) => f in data ? data[f] : existingRow?.[f]);
1569
+ if (values.some((v) => v === null || v === void 0)) continue;
1570
+ if (!await parentExists(scope, contract, meta, def.relatedModelName, def.targetFields, values)) throw new Error(`FK violation on relation '${def.relationName}': no ${def.relatedModelName} with ${describeTuple(def.targetFields, values)}`);
1344
1571
  }
1345
1572
  }
1573
+ /**
1574
+ * `true` when `data` sets some, but not all, fields of a compound foreign
1575
+ * key. Checking it then needs the rest of the key from the row being
1576
+ * updated, so the update has to read each row before writing it.
1577
+ */
1578
+ function fkCheckNeedsExistingRow(contract, modelName, data) {
1579
+ return getRelationDefinitions(contract, modelName).some((def) => {
1580
+ if (def.cardinality !== "N:1") return false;
1581
+ const set = def.localFields.filter((f) => f in data).length;
1582
+ return set > 0 && set < def.localFields.length;
1583
+ });
1584
+ }
1585
+ /**
1586
+ * Does `parentModel` have a row whose `fields` equal `values`, pairwise?
1587
+ *
1588
+ * When `fields` are exactly the parent's primary key (in any order), this is
1589
+ * one key-only lookup. Otherwise it scans the parent store, comparing values
1590
+ * the way IndexedDB compares keys. `excludeKey` leaves out one parent row,
1591
+ * for checks that run while that row is being deleted or changed.
1592
+ */
1593
+ async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
1594
+ const storeName = getStoreName(contract, parentModel);
1595
+ const keyPath = getKeyPath(contract, parentModel);
1596
+ const range = primaryKeyRange(keyPath, fields, values);
1597
+ if (range !== null) {
1598
+ const foundKey = await firstKeyInRange(scope, meta, storeName, range);
1599
+ return foundKey !== void 0 && (excludeKey === void 0 || !keyEquals(foundKey, excludeKey));
1600
+ }
1601
+ const filter = (row) => fields.every((f, i) => fieldValuesEqual(row[f], values[i])) && (excludeKey === void 0 || !keyEquals(extractKeyFromRow(row, keyPath), excludeKey));
1602
+ return (await scope.execute({
1603
+ meta,
1604
+ kind: "cursor-scan",
1605
+ storeName,
1606
+ filter,
1607
+ take: 1
1608
+ })).length > 0;
1609
+ }
1610
+ /**
1611
+ * An `IDBKeyRange` for "the row whose primary key is `values`", when `fields`
1612
+ * are exactly the key's fields (in any order) and every value is a valid
1613
+ * IndexedDB key. `null` otherwise, and the caller scans instead.
1614
+ */
1615
+ function primaryKeyRange(keyPath, fields, values) {
1616
+ if (typeof IDBKeyRange === "undefined") return null;
1617
+ const keyFields = keyPathFields(keyPath);
1618
+ if (keyFields.length !== fields.length || !keyFields.every((f) => fields.includes(f))) return null;
1619
+ const ordered = keyFields.map((f) => values[fields.indexOf(f)]);
1620
+ if (!ordered.every((v) => isValidIdbKey(v))) return null;
1621
+ return IDBKeyRange.only(typeof keyPath === "string" ? ordered[0] : ordered);
1622
+ }
1623
+ /** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
1624
+ function describeTuple(fields, values) {
1625
+ return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
1626
+ }
1346
1627
  async function executeScalarCreateWithFkValidation(options) {
1347
1628
  const { executor, contract, modelName, data } = options;
1348
- return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, data), async (scope) => {
1349
- await validateScalarFks(scope, contract, modelName, data);
1350
- return insertSingleRow(scope, contract, modelName, data, createMutationDefaultsCache());
1629
+ const defaultsCache = createMutationDefaultsCache();
1630
+ const record = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, defaultsCache);
1631
+ return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
1632
+ }
1633
+ /**
1634
+ * `createAll()` for rows that set foreign keys: checks every row's foreign
1635
+ * keys and inserts it, all in one transaction, so one bad row writes nothing.
1636
+ * The batch shares one defaults cache, like the plain `createAll()` path.
1637
+ */
1638
+ async function executeScalarCreateAllWithFkValidation(options) {
1639
+ const { executor, contract, modelName, data } = options;
1640
+ const defaultsCache = createMutationDefaultsCache();
1641
+ const storeName = getStoreName(contract, modelName);
1642
+ const records = data.map((row) => applyCreateDefaults(contract.execution?.mutations.defaults, storeName, row, defaultsCache));
1643
+ return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
1644
+ const inserted = [];
1645
+ for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
1646
+ return inserted;
1351
1647
  });
1352
1648
  }
1353
1649
  /**
@@ -1368,18 +1664,20 @@ function collectUpdateStoreNames(contract, modelName, data) {
1368
1664
  }
1369
1665
  async function executeScalarUpdateWithFkValidation(options) {
1370
1666
  const { executor, contract, modelName, filters, data } = options;
1371
- const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, data);
1667
+ const storeName = getStoreName(contract, modelName);
1668
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1669
+ assertEnumValues(contract, modelName, patch);
1670
+ const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
1671
+ const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
1372
1672
  return withMutationScope(executor, storeNames, async (scope) => {
1373
- await validateScalarFks(scope, contract, modelName, data);
1374
- const storeName = getStoreName(contract, modelName);
1673
+ if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
1375
1674
  const meta = makePlanMeta(contract);
1376
1675
  const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
1377
1676
  kind: "and",
1378
1677
  exprs: filters
1379
1678
  };
1380
1679
  const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1381
- const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1382
- if (!enforcesOnUpdate) return (await scope.execute({
1680
+ if (!enforcesOnUpdate && !needsRowForFks) return (await scope.execute({
1383
1681
  meta,
1384
1682
  kind: "scan-write",
1385
1683
  storeName,
@@ -1396,8 +1694,9 @@ async function executeScalarUpdateWithFkValidation(options) {
1396
1694
  ...filter !== void 0 ? { filter } : {}
1397
1695
  }))[0];
1398
1696
  if (!oldRow) return null;
1399
- await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
1400
- const key = oldRow[getKeyPath(contract, modelName)];
1697
+ if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
1698
+ if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
1699
+ const key = extractKeyFromRow(oldRow, getKeyPath(contract, modelName));
1401
1700
  return (await scope.execute({
1402
1701
  meta,
1403
1702
  kind: "update",
@@ -1426,18 +1725,20 @@ async function executeScalarUpdateWithFkValidation(options) {
1426
1725
  */
1427
1726
  async function executeBulkUpdateWithFkValidation(options) {
1428
1727
  const { executor, contract, modelName, filters, data } = options;
1429
- const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, data);
1728
+ const storeName = getStoreName(contract, modelName);
1729
+ const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1730
+ assertEnumValues(contract, modelName, patch);
1731
+ const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
1732
+ const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
1430
1733
  return withMutationScope(executor, storeNames, async (scope) => {
1431
- await validateScalarFks(scope, contract, modelName, data);
1432
- const storeName = getStoreName(contract, modelName);
1734
+ if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
1433
1735
  const meta = makePlanMeta(contract);
1434
1736
  const combined = filters.length === 0 ? void 0 : filters.length === 1 ? filters[0] : {
1435
1737
  kind: "and",
1436
1738
  exprs: filters
1437
1739
  };
1438
1740
  const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1439
- const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1440
- if (!enforcesOnUpdate) return scope.execute({
1741
+ if (!enforcesOnUpdate && !needsRowForFks) return scope.execute({
1441
1742
  meta,
1442
1743
  kind: "scan-write",
1443
1744
  storeName,
@@ -1454,8 +1755,9 @@ async function executeBulkUpdateWithFkValidation(options) {
1454
1755
  const keyPath = getKeyPath(contract, modelName);
1455
1756
  const results = [];
1456
1757
  for (const oldRow of oldRows) {
1457
- await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
1458
- const key = oldRow[keyPath];
1758
+ if (needsRowForFks) await validateScalarFks(scope, contract, modelName, patch, oldRow);
1759
+ if (enforcesOnUpdate) await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch);
1760
+ const key = extractKeyFromRow(oldRow, keyPath);
1459
1761
  const updated = (await scope.execute({
1460
1762
  meta,
1461
1763
  kind: "update",
@@ -1470,16 +1772,11 @@ async function executeBulkUpdateWithFkValidation(options) {
1470
1772
  }
1471
1773
  /**
1472
1774
  * Returns true if the model has at least one child relation (1:N or parent-side
1473
- * 1:1) whose `onDelete` action requires enforcement (anything except `noAction`).
1474
- * Since the default is `restrict`, any model with 1:N/1:1 relations that do not
1475
- * explicitly set `noAction` returns true.
1775
+ * 1:1). Every such relation has an `onDelete` action to enforce: `noAction`
1776
+ * behaves like `restrict`, which is also the default.
1476
1777
  */
1477
1778
  function hasEnforceableChildRelations(contract, modelName) {
1478
- for (const def of getRelationDefinitions(contract, modelName)) {
1479
- if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
1480
- if (getOnDeleteForDeleteRelation(contract, modelName, def) !== "noAction") return true;
1481
- }
1482
- return false;
1779
+ return getRelationDefinitions(contract, modelName).some((def) => isDeleteEnforcementRelation(contract, modelName, def));
1483
1780
  }
1484
1781
  /**
1485
1782
  * Transitively walks the `onDelete` cascade graph from `modelName`, collecting
@@ -1502,7 +1799,6 @@ function collectDeleteStoreNames(contract, modelName) {
1502
1799
  for (const def of getRelationDefinitions(contract, mName)) {
1503
1800
  if (!isDeleteEnforcementRelation(contract, mName, def)) continue;
1504
1801
  const action = getOnDeleteForDeleteRelation(contract, mName, def);
1505
- if (action === "noAction") continue;
1506
1802
  stores.add(def.relatedStoreName);
1507
1803
  if (action === "cascade") walk(def.relatedModelName);
1508
1804
  }
@@ -1530,23 +1826,16 @@ function collectDeleteStoreNames(contract, modelName) {
1530
1826
  */
1531
1827
  async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
1532
1828
  const keyPath = getKeyPath(contract, modelName);
1533
- const rowKey = `${getStoreName(contract, modelName)}::${String(row[keyPath])}`;
1829
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(row, keyPath))}`;
1534
1830
  if (visited.has(rowKey)) return;
1535
1831
  visited.add(rowKey);
1536
1832
  const meta = makePlanMeta(contract);
1537
1833
  for (const def of getRelationDefinitions(contract, modelName)) {
1538
1834
  if (!isDeleteEnforcementRelation(contract, modelName, def)) continue;
1539
1835
  const action = getOnDeleteForDeleteRelation(contract, modelName, def);
1540
- if (action === "noAction") continue;
1541
1836
  const childFilter = buildChildFilterFromRow(def, row);
1542
1837
  if (action === "restrict") {
1543
- if ((await scope.execute({
1544
- meta,
1545
- kind: "cursor-scan",
1546
- storeName: def.relatedStoreName,
1547
- filter: childFilter,
1548
- take: 1
1549
- })).length > 0) throw new Error(`Cannot delete ${modelName} '${String(row[keyPath])}': child records exist on relation '${def.relationName}'. Use onDelete: 'cascade', 'setNull', or 'noAction'.`);
1838
+ 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.`);
1550
1839
  continue;
1551
1840
  }
1552
1841
  if (action === "cascade") {
@@ -1563,7 +1852,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
1563
1852
  meta,
1564
1853
  kind: "delete",
1565
1854
  storeName: def.relatedStoreName,
1566
- key: child[childKeyPath]
1855
+ key: extractKeyFromRow(child, childKeyPath)
1567
1856
  });
1568
1857
  }
1569
1858
  continue;
@@ -1583,7 +1872,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
1583
1872
  }
1584
1873
  if (action === "setDefault") {
1585
1874
  const patch = buildSetDefaultPatch(contract, def);
1586
- await validateSetDefaultPatch(scope, contract, modelName, def, patch, row[keyPath]);
1875
+ await validateSetDefaultPatch(scope, contract, modelName, def, patch, extractKeyFromRow(row, keyPath));
1587
1876
  await scope.execute({
1588
1877
  meta,
1589
1878
  kind: "scan-write",
@@ -1632,7 +1921,7 @@ async function executeDeleteAllWithReferentialActions(options) {
1632
1921
  });
1633
1922
  for (const row of rows) {
1634
1923
  await applyReferentialActionsForRow(scope, contract, modelName, row);
1635
- const key = row[keyPath];
1924
+ const key = extractKeyFromRow(row, keyPath);
1636
1925
  await scope.execute({
1637
1926
  meta,
1638
1927
  kind: "delete",
@@ -1709,7 +1998,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1709
1998
  const projectRows = this.#projectRows.bind(this);
1710
1999
  const combined = this.#combinedFilterExpr();
1711
2000
  const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
1712
- const keyPath = getKeyPath$1(this.#contract, this.#modelName);
2001
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
1713
2002
  const comparator = buildRowComparator(this.#state.orderBy);
1714
2003
  const skip = this.#state.skip;
1715
2004
  const take = this.#state.take;
@@ -1743,7 +2032,17 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1743
2032
  aggregates: toAggregateRequests(spec),
1744
2033
  ...combined !== void 0 ? { where: combined } : {}
1745
2034
  };
1746
- return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
2035
+ const groupingKey = this.#newGroupingKey();
2036
+ if (Object.values(spec).every((selector) => selector.fn === "count")) {
2037
+ const scanPlan = this.#buildScanPlan(groupingKey);
2038
+ const total = await this.#executeNativeCount(scanPlan, ast);
2039
+ if (total !== null) {
2040
+ const result = {};
2041
+ for (const alias of Object.keys(spec)) result[alias] = total;
2042
+ return result;
2043
+ }
2044
+ }
2045
+ return computeAggregateSpec(spec, await this.#materialize(groupingKey, ast));
1747
2046
  }
1748
2047
  groupBy(...fields) {
1749
2048
  const combined = this.#combinedFilterExpr();
@@ -1763,15 +2062,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1763
2062
  modelName: this.#modelName,
1764
2063
  data: record
1765
2064
  });
1766
- if (hasScalarFkFields(this.#contract, this.#modelName, record)) return await executeScalarCreateWithFkValidation({
2065
+ const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
2066
+ assertEnumValues(this.#contract, this.#modelName, withDefaults);
2067
+ if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
1767
2068
  executor: requireTransactionExecutor(this.#executor),
1768
2069
  contract: this.#contract,
1769
2070
  modelName: this.#modelName,
1770
- data: record
2071
+ data: withDefaults
1771
2072
  });
1772
2073
  const groupingKey = this.#newGroupingKey();
1773
2074
  const meta = this.#planMeta(groupingKey);
1774
- const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
1775
2075
  const plan = {
1776
2076
  meta,
1777
2077
  ast: {
@@ -1885,7 +2185,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1885
2185
  return (await this.updateAll(patch).toArray()).length;
1886
2186
  }
1887
2187
  async upsert(args) {
1888
- const keyPath = getKeyPath$1(this.#contract, this.#modelName);
2188
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
1889
2189
  const whereExpr = shorthandToFilterExpr(args.where);
1890
2190
  const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
1891
2191
  const meta = this.#planMeta(this.#newGroupingKey());
@@ -1895,8 +2195,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1895
2195
  const executionDefaults = this.#contract.execution?.mutations.defaults;
1896
2196
  const exec = requireTransactionExecutor(this.#executor);
1897
2197
  const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
2198
+ const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
2199
+ assertEnumValues(this.#contract, this.#modelName, effectivePatch);
2200
+ assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
1898
2201
  const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
1899
- return withMutationScope(exec, [.../* @__PURE__ */ new Set([storeName, ...onUpdateStoreNames])], async (scope) => {
2202
+ return withMutationScope(exec, [.../* @__PURE__ */ new Set([
2203
+ storeName,
2204
+ ...onUpdateStoreNames,
2205
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, createWithDefaults),
2206
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
2207
+ ])], async (scope) => {
1900
2208
  const existing = (await scope.execute({
1901
2209
  meta,
1902
2210
  kind: "cursor-scan",
@@ -1905,15 +2213,16 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1905
2213
  take: 1
1906
2214
  }))[0];
1907
2215
  if (existing === void 0) {
1908
- const record = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
2216
+ await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
1909
2217
  return (await scope.execute({
1910
2218
  meta,
1911
2219
  kind: "add",
1912
2220
  storeName,
1913
- record
1914
- }))[0] ?? record;
2221
+ record: createWithDefaults
2222
+ }))[0] ?? createWithDefaults;
1915
2223
  }
1916
- const key = existing[keyPath];
2224
+ await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
2225
+ const key = extractKeyFromRow(existing, keyPath);
1917
2226
  await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
1918
2227
  return (await scope.execute({
1919
2228
  meta,
@@ -1925,11 +2234,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1925
2234
  });
1926
2235
  }
1927
2236
  createAll(data) {
1928
- const groupingKey = this.#newGroupingKey();
1929
- const meta = this.#planMeta(groupingKey);
1930
2237
  const defaultsCache = createMutationDefaultsCache();
1931
2238
  const executionDefaults = this.#contract.execution?.mutations.defaults;
1932
2239
  const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
2240
+ if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
2241
+ const executor = requireTransactionExecutor(this.#executor);
2242
+ const contract = this.#contract;
2243
+ const modelName = this.#modelName;
2244
+ return new AsyncIterableResult((async function* () {
2245
+ const rows = await executeScalarCreateAllWithFkValidation({
2246
+ executor,
2247
+ contract,
2248
+ modelName,
2249
+ data: records
2250
+ });
2251
+ for (const row of rows) yield row;
2252
+ })());
2253
+ }
2254
+ const groupingKey = this.#newGroupingKey();
2255
+ const meta = this.#planMeta(groupingKey);
1933
2256
  const plan = {
1934
2257
  meta,
1935
2258
  ast: {
@@ -1950,7 +2273,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1950
2273
  }
1951
2274
  };
1952
2275
  const executorQuery = this.#executor.query.bind(this.#executor);
2276
+ const contract = this.#contract;
2277
+ const modelName = this.#modelName;
1953
2278
  return new AsyncIterableResult((async function* () {
2279
+ for (const record of records) assertEnumValues(contract, modelName, record);
1954
2280
  for await (const row of executorQuery(plan)) yield row;
1955
2281
  })());
1956
2282
  }
@@ -1995,13 +2321,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1995
2321
  const combined = this.#combinedFilterExpr();
1996
2322
  const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
1997
2323
  if (fieldToIndexMap !== void 0) {
1998
- const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath$1(this.#contract, this.#modelName));
1999
- if (orHint !== null) {
2000
- let n = (await this.#executeOrRows(orHint, groupingKey, combined)).length;
2001
- if (this.#state.skip !== void 0) n = Math.max(0, n - this.#state.skip);
2002
- if (this.#state.take !== void 0) n = Math.min(this.#state.take, n);
2003
- return n;
2004
- }
2324
+ const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath(this.#contract, this.#modelName));
2325
+ if (orHint !== null) return clampCount((await this.#executeOrRows(orHint, groupingKey, combined)).length, this.#state.skip, this.#state.take);
2005
2326
  }
2006
2327
  const scanPlan = this.#buildScanPlan(groupingKey, fieldToIndexMap);
2007
2328
  const scanAst = scanPlan.ast;
@@ -2010,6 +2331,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
2010
2331
  modelName: this.#modelName,
2011
2332
  ...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
2012
2333
  };
2334
+ const nativeTotal = await this.#executeNativeCount(scanPlan, ast);
2335
+ if (nativeTotal !== null) return clampCount(nativeTotal, this.#state.skip, this.#state.take);
2013
2336
  const plan = {
2014
2337
  ...scanPlan,
2015
2338
  ast
@@ -2046,6 +2369,25 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
2046
2369
  throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
2047
2370
  }
2048
2371
  /**
2372
+ * Runs `scanPlan` as a native `count` plan when its cardinality is fully
2373
+ * determined by a key range (no in-memory filter) — returning the
2374
+ * *unpaginated* total — or `null` when it can't (the caller then falls back
2375
+ * to materializing). `ast` is attached so middleware sees the caller's
2376
+ * intent (`count` / `aggregate`) regardless of the physical plan.
2377
+ */
2378
+ async #executeNativeCount(scanPlan, ast) {
2379
+ const body = scanPlan.idbPlan;
2380
+ if (body.kind !== "cursor-scan" || !isNativelyCountable(body)) return null;
2381
+ const nativePlan = {
2382
+ ...scanPlan,
2383
+ ast,
2384
+ idbPlan: toCountPlan(body)
2385
+ };
2386
+ let total = 0;
2387
+ for await (const row of this.#executor.query(nativePlan)) total = row.count;
2388
+ return total;
2389
+ }
2390
+ /**
2049
2391
  * Materialise all rows matching the accumulated filters with no pagination —
2050
2392
  * used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
2051
2393
  * scan plan so middleware can observe the aggregate intent.
@@ -2081,7 +2423,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
2081
2423
  ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
2082
2424
  };
2083
2425
  if (typeof IDBKeyRange !== "undefined") {
2084
- const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath$1(this.#contract, this.#modelName));
2426
+ const hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath(this.#contract, this.#modelName));
2085
2427
  if (hint !== null) {
2086
2428
  const { indexName, value, remainingFilter } = hint;
2087
2429
  const filter = remainingFilter !== void 0 ? (row) => evaluateFilter(remainingFilter, row) : void 0;
@@ -2126,7 +2468,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
2126
2468
  async #executeOrRows(hint, groupingKey, combined) {
2127
2469
  const meta = this.#planMeta(groupingKey);
2128
2470
  const storeName = this.#storeName;
2129
- const keyPath = getKeyPath$1(this.#contract, this.#modelName);
2471
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
2130
2472
  const seen = /* @__PURE__ */ new Set();
2131
2473
  const rows = [];
2132
2474
  const ast = {
@@ -2151,7 +2493,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
2151
2493
  return branchRows;
2152
2494
  }));
2153
2495
  for (const branchRows of branchResults) for (const row of branchRows) {
2154
- const pk = row[keyPath];
2496
+ const pk = keyToken(extractKeyFromRow(row, keyPath));
2155
2497
  if (!seen.has(pk)) {
2156
2498
  seen.add(pk);
2157
2499
  rows.push(row);
@@ -2240,6 +2582,6 @@ function idbOrm(options) {
2240
2582
  return client;
2241
2583
  }
2242
2584
  //#endregion
2243
- export { createRelationMutator as a, withMutationScope as c, hasNestedMutationCallbacks as i, getKeyPath$1 as l, applyReferentialActionsForRow as n, isRelationMutationCallback as o, collectDeleteStoreNames as r, isRelationMutationDescriptor as s, idbOrm as t, getStoreName as u };
2585
+ export { createRelationMutator as a, withMutationScope as c, getStoreName as d, keyEquals as f, hasNestedMutationCallbacks as i, extractKeyFromRow as l, applyReferentialActionsForRow as n, isRelationMutationCallback as o, keyToken as p, collectDeleteStoreNames as r, isRelationMutationDescriptor as s, idbOrm as t, getKeyPath as u };
2244
2586
 
2245
- //# sourceMappingURL=idb-orm-Cupnnwum.mjs.map
2587
+ //# sourceMappingURL=idb-orm-C8yXJp6R.mjs.map