@prisma-idb/client-idb 0.12.0 → 0.13.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 (35) hide show
  1. package/dist/client-auto.d.mts +2 -2
  2. package/dist/client-auto.d.mts.map +1 -1
  3. package/dist/client-auto.mjs +44 -3
  4. package/dist/client-auto.mjs.map +1 -1
  5. package/dist/client.d.mts +3 -2
  6. package/dist/client.mjs +3 -2
  7. package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
  8. package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
  9. package/dist/internal.d.mts +5 -4
  10. package/dist/internal.d.mts.map +1 -1
  11. package/dist/internal.mjs +2 -2
  12. package/dist/{managed-client-M3xMUk0n.mjs → managed-client-DMk8ERfX.mjs} +12 -5
  13. package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
  14. package/dist/{managed-client-DQlsxc4Z.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
  15. package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
  16. package/dist/{mutation-executor-inLFb0TE.mjs → mutation-executor-IuJmyWEG.mjs} +486 -105
  17. package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
  18. package/dist/{executor-CIEHpa1j.d.mts → mutation-scope-CE3GMR4X.d.mts} +66 -22
  19. package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
  20. package/dist/{idb-orm-B1Br1C0F.mjs → orm-transaction-BGx1eukw.mjs} +357 -61
  21. package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
  22. package/dist/{idb-orm-6OuwkeDM.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
  23. package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
  24. package/dist/orm.d.mts +13 -4
  25. package/dist/orm.d.mts.map +1 -1
  26. package/dist/orm.mjs +3 -3
  27. package/package.json +9 -9
  28. package/dist/executor-CIEHpa1j.d.mts.map +0 -1
  29. package/dist/idb-orm-6OuwkeDM.d.mts.map +0 -1
  30. package/dist/idb-orm-B1Br1C0F.mjs.map +0 -1
  31. package/dist/managed-client-DQlsxc4Z.d.mts.map +0 -1
  32. package/dist/managed-client-M3xMUk0n.mjs.map +0 -1
  33. package/dist/mutation-executor-inLFb0TE.mjs.map +0 -1
  34. package/dist/mutation-scope-DrxFhek3.d.mts +0 -33
  35. package/dist/mutation-scope-DrxFhek3.d.mts.map +0 -1
@@ -1,6 +1,8 @@
1
- import { andExpr, evaluateFilter, fieldFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
1
+ import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
2
2
  import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
3
- import { compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyEquals as keyEquals$1, keyToken } from "@prisma-idb/target-idb/runtime";
3
+ import { IdbExecuteError } from "@prisma-idb/driver-idb/runtime";
4
+ import { absentAsNull, compareDecimalValues, compareFieldValues, fieldValueToken, fieldValuesEqual, isValidIdbKey, keyEquals, keyEquals as keyEquals$1, keyToken, keyToken as keyToken$1, validateRecord } from "@prisma-idb/target-idb/runtime";
5
+ import { andExpr, assertValidDecimalFilter, evaluateFilter, fieldFilter, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
6
  import { generateId } from "@prisma/orm-framework/ids/runtime";
5
7
  import { keyPathFields } from "@prisma-idb/target-idb/pack";
6
8
  //#region src/core/types.ts
@@ -50,6 +52,301 @@ function getRelation(contract, modelName, relName) {
50
52
  return relation;
51
53
  }
52
54
  //#endregion
55
+ //#region src/core/planner/catalog.ts
56
+ const catalogs = /* @__PURE__ */ new WeakMap();
57
+ /**
58
+ * Build a catalog once per contract identity, store name and model name.
59
+ * Contracts are immutable.
60
+ *
61
+ * Several models can share one store, and each reads the rows differently.
62
+ * Pass the model that owns the request. Omit `modelName` for a store that no
63
+ * app model owns, such as an extension's own store: it has no fields to fill.
64
+ */
65
+ function buildCatalog(contract, storeName, modelName) {
66
+ const cacheKey = `${storeName}\0${modelName ?? ""}`;
67
+ const cached = catalogs.get(contract)?.get(cacheKey);
68
+ if (cached) return cached;
69
+ const store = contract.storage.stores[storeName];
70
+ if (!store) throw new Error(`Store "${storeName}" is not in the contract`);
71
+ const fields = {};
72
+ let normalize = (row) => row;
73
+ if (modelName !== void 0) {
74
+ const model = domainModelsAtDefaultNamespace(contract.domain)[modelName];
75
+ if (model === void 0) throw new Error(`Model "${modelName}" is not in the contract`);
76
+ if (getStoreName(contract, modelName) !== storeName) throw new Error(`Model "${modelName}" is not stored in "${storeName}"`);
77
+ normalize = absentAsNull(contract, modelName);
78
+ for (const [name, field] of Object.entries(model.fields)) fields[name] = Object.freeze({
79
+ codecId: field.type.kind === "scalar" ? field.type.codecId : void 0,
80
+ nullable: field.nullable,
81
+ collection: Boolean(field.many) || field.dict === true
82
+ });
83
+ }
84
+ const catalog = Object.freeze({
85
+ storeName,
86
+ primaryKey: source(store.keyPath, true, false),
87
+ indexes: Object.freeze(Object.entries(store.indexes ?? {}).map(([name, index]) => source(index.keyPath, index.unique, index.multiEntry ?? false, name))),
88
+ fields: Object.freeze(fields),
89
+ absentAsNull: normalize
90
+ });
91
+ let byStore = catalogs.get(contract);
92
+ if (!byStore) {
93
+ byStore = /* @__PURE__ */ new Map();
94
+ catalogs.set(contract, byStore);
95
+ }
96
+ byStore.set(cacheKey, catalog);
97
+ return catalog;
98
+ }
99
+ /** Freeze a key source and its field order for deterministic access selection. */
100
+ function source(keyPath, unique, multiEntry, indexName) {
101
+ const path = typeof keyPath === "string" ? keyPath : Object.freeze([...keyPath]);
102
+ return Object.freeze({
103
+ keyPath: path,
104
+ fields: Object.freeze(typeof path === "string" ? [path] : [...path]),
105
+ unique,
106
+ multiEntry,
107
+ ...indexName === void 0 ? {} : { indexName }
108
+ });
109
+ }
110
+ //#endregion
111
+ //#region src/core/decimal-identity.ts
112
+ /** A legacy store or sync metadata set contains several spellings of one Decimal key. */
113
+ var IdbDecimalIdentityConflictError = class extends Error {
114
+ code = "DECIMAL_IDENTITY_CONFLICT";
115
+ constructor(modelName) {
116
+ super(`Several stored keys identify the same Decimal value in model "${modelName}". Resolve the conflicting records before retrying.`);
117
+ this.name = "IdbDecimalIdentityConflictError";
118
+ }
119
+ };
120
+ /** Whether a primary key contains a contract-declared Decimal field. */
121
+ function hasDecimalPrimaryKey(contract, modelName) {
122
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
123
+ return hasDecimalFields(catalog, catalog.primaryKey);
124
+ }
125
+ /** Compare scalar or compound primary keys using Decimal codecs and native IDB equality elsewhere. */
126
+ function decimalKeysEqual(contract, modelName, left, right) {
127
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
128
+ const leftParts = typeof catalog.primaryKey.keyPath === "string" ? [left] : left;
129
+ const rightParts = typeof catalog.primaryKey.keyPath === "string" ? [right] : right;
130
+ if (!Array.isArray(leftParts) || !Array.isArray(rightParts) || leftParts.length !== catalog.primaryKey.fields.length || rightParts.length !== leftParts.length) return false;
131
+ return catalog.primaryKey.fields.every((field, i) => valuesEqual(catalog, field, leftParts[i], rightParts[i]));
132
+ }
133
+ /** Keep an existing Decimal primary-key spelling when the supplied value identifies the same row. */
134
+ function preserveDecimalKeySpelling(contract, modelName, record, storedKey) {
135
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
136
+ const parts = typeof catalog.primaryKey.keyPath === "string" ? [storedKey] : storedKey;
137
+ if (!Array.isArray(parts) || parts.length !== catalog.primaryKey.fields.length) return record;
138
+ const result = { ...record };
139
+ catalog.primaryKey.fields.forEach((field, i) => {
140
+ if (catalog.fields[field]?.codecId === "idb/decimal@1" && result[field] !== void 0 && valuesEqual(catalog, field, result[field], parts[i])) result[field] = parts[i];
141
+ });
142
+ return result;
143
+ }
144
+ /** Read the physical key first, then check Decimal aliases without choosing a legacy duplicate. */
145
+ async function resolveDecimalKey(scope, contract, modelName, key, snapshot) {
146
+ if (!isValidIdbKey(key)) return void 0;
147
+ const storeName = getStoreName(contract, modelName);
148
+ const decimal = hasDecimalPrimaryKey(contract, modelName);
149
+ if (decimal && !decimalKeysEqual(contract, modelName, key, key)) return void 0;
150
+ if (!snapshot) {
151
+ const [exact] = await scope.execute({
152
+ kind: "key-get",
153
+ storeName,
154
+ key
155
+ });
156
+ if (!decimal) return exact;
157
+ }
158
+ const matches = (snapshot ?? await scope.execute({
159
+ kind: "cursor-scan",
160
+ storeName
161
+ })).filter((row) => decimalKeysEqual(contract, modelName, extractKeyFromRow(row, buildCatalog(contract, storeName, modelName).primaryKey.keyPath), key));
162
+ if (matches.length > 1) throw new IdbDecimalIdentityConflictError(modelName);
163
+ return matches[0];
164
+ }
165
+ /** Whether this key source needs numeric equality instead of the native string index. */
166
+ function hasDecimalFields(catalog, source) {
167
+ return source.fields.some((field) => catalog.fields[field]?.codecId === "idb/decimal@1");
168
+ }
169
+ /** Compare one declared scalar without converting Decimal text through Number. */
170
+ function valuesEqual(catalog, field, left, right) {
171
+ return catalog.fields[field]?.codecId === "idb/decimal@1" ? typeof left === "string" && typeof right === "string" && compareDecimalValues(left, right) === 0 : isValidIdbKey(left) && isValidIdbKey(right) && keyEquals(left, right);
172
+ }
173
+ /** Numeric unique sources only; ordinary native indexes continue to enforce their own constraints. */
174
+ function decimalUniqueSources(catalog) {
175
+ return [catalog.primaryKey, ...catalog.indexes].filter((source) => source.unique && hasDecimalFields(catalog, source));
176
+ }
177
+ /** Match one index entry, excluding absent/null keys just as IndexedDB does. */
178
+ function sourceCollides(catalog, source, left, right) {
179
+ if (source.multiEntry && source.fields.length === 1) {
180
+ const field = source.fields[0];
181
+ const a = Array.isArray(left[field]) ? left[field] : [left[field]];
182
+ const b = Array.isArray(right[field]) ? right[field] : [right[field]];
183
+ return a.some((x) => b.some((y) => valuesEqual(catalog, field, x, y)));
184
+ }
185
+ return source.fields.every((field) => valuesEqual(catalog, field, left[field], right[field]));
186
+ }
187
+ /** Check new values against every other physical row inside the write transaction. */
188
+ async function assertUnique(scope, catalog, record, previousKey, kind, modelName, snapshot) {
189
+ const rows = snapshot ?? await scope.execute({
190
+ kind: "cursor-scan",
191
+ storeName: catalog.storeName
192
+ });
193
+ const previous = rows.find((row) => previousKey !== void 0 && keyEquals(extractKeyFromRow(row, catalog.primaryKey.keyPath), previousKey));
194
+ for (const source of decimalUniqueSources(catalog)) {
195
+ const matches = rows.filter((row) => sourceCollides(catalog, source, row, record));
196
+ if (matches.length > 1 || previous && rows.filter((row) => sourceCollides(catalog, source, row, previous)).length > 1) throw new IdbDecimalIdentityConflictError(modelName);
197
+ if (matches.some((row) => previousKey === void 0 || !keyEquals(extractKeyFromRow(row, catalog.primaryKey.keyPath), previousKey))) throw new IdbExecuteError({
198
+ code: kind === "add" ? "ADD_FAILED" : "PUT_FAILED",
199
+ planKind: kind,
200
+ storeName: catalog.storeName,
201
+ cause: new DOMException("Numerically equal Decimal key already exists", "ConstraintError")
202
+ }, "Unique constraint failed for a Decimal key");
203
+ }
204
+ }
205
+ const decimalModelCache = /* @__PURE__ */ new WeakMap();
206
+ /** Cache the contract models whose uniqueness needs more than native string indexes. */
207
+ function decimalModels(contract) {
208
+ const cached = decimalModelCache.get(contract);
209
+ if (cached) return cached;
210
+ const models = /* @__PURE__ */ new Map();
211
+ for (const modelName of Object.keys(domainModelsAtDefaultNamespace(contract.domain))) {
212
+ const store = getStoreName(contract, modelName);
213
+ if (decimalUniqueSources(buildCatalog(contract, store, modelName)).length > 0) models.set(store, modelName);
214
+ }
215
+ decimalModelCache.set(contract, models);
216
+ return models;
217
+ }
218
+ /** Wrap the transaction seam so nested, bulk and raw sync writes share the same identity rules. */
219
+ function withDecimalIdentityScope(scope, contract) {
220
+ const models = decimalModels(contract);
221
+ if (models.size === 0) return scope;
222
+ return {
223
+ commit: () => scope.commit(),
224
+ rollback: () => scope.rollback(),
225
+ async execute(plan) {
226
+ const modelName = models.get(plan.storeName);
227
+ if (modelName === void 0) return scope.execute(plan);
228
+ try {
229
+ return await executeDecimalWrite(scope, contract, modelName, plan);
230
+ } catch (error) {
231
+ scope.rollback();
232
+ throw error;
233
+ }
234
+ }
235
+ };
236
+ }
237
+ /** Resolve a keyed write or materialize a scan before enforcing its numeric constraints. */
238
+ async function executeDecimalWrite(scope, contract, modelName, plan, snapshot) {
239
+ const catalog = buildCatalog(contract, plan.storeName, modelName);
240
+ if (plan.kind === "scan-write") return executeDecimalScanWrite(scope, contract, modelName, plan);
241
+ if (plan.kind === "delete") {
242
+ if (!isValidIdbKey(plan.key)) return scope.execute(plan);
243
+ const row = await resolveDecimalKey(scope, contract, modelName, plan.key, snapshot);
244
+ return scope.execute({
245
+ ...plan,
246
+ key: row ? extractKeyFromRow(row, catalog.primaryKey.keyPath) : plan.key
247
+ });
248
+ }
249
+ if (plan.kind !== "add" && plan.kind !== "put" && plan.kind !== "update") return scope.execute(plan);
250
+ const key = plan.kind === "update" ? plan.key : extractKeyFromRow(plan.record, catalog.primaryKey.keyPath);
251
+ const previous = plan.kind === "add" ? void 0 : await resolveDecimalKey(scope, contract, modelName, key, snapshot);
252
+ const previousKey = previous && extractKeyFromRow(previous, catalog.primaryKey.keyPath);
253
+ const record = preserveDecimalKeySpelling(contract, modelName, plan.kind === "update" ? {
254
+ ...previous,
255
+ ...plan.patch
256
+ } : plan.record, previousKey);
257
+ await assertUnique(scope, catalog, record, previousKey, plan.kind, modelName, snapshot);
258
+ return scope.execute(plan.kind === "update" ? {
259
+ ...plan,
260
+ key: previousKey ?? plan.key,
261
+ patch: preserveDecimalKeySpelling(contract, modelName, plan.patch, previousKey)
262
+ } : {
263
+ ...plan,
264
+ record
265
+ });
266
+ }
267
+ /** Materialize preimages once so moving indexed fields cannot revisit a row. */
268
+ async function executeDecimalScanWrite(scope, contract, modelName, plan) {
269
+ const catalog = buildCatalog(contract, plan.storeName, modelName);
270
+ const { take, ...scan } = plan;
271
+ const rows = await scope.execute({
272
+ ...scan,
273
+ kind: "cursor-scan"
274
+ });
275
+ const seen = /* @__PURE__ */ new Set();
276
+ const matches = rows.filter((row) => {
277
+ const token = keyToken(extractKeyFromRow(row, catalog.primaryKey.keyPath));
278
+ if (seen.has(token)) return false;
279
+ seen.add(token);
280
+ return true;
281
+ }).slice(0, take);
282
+ if (matches.length === 0) return [];
283
+ const snapshot = !plan.indexName && !plan.range && !plan.filter ? [...rows] : await scope.execute({
284
+ kind: "cursor-scan",
285
+ storeName: plan.storeName
286
+ });
287
+ const result = [];
288
+ for (const row of matches) {
289
+ const key = extractKeyFromRow(row, catalog.primaryKey.keyPath);
290
+ const written = await executeDecimalWrite(scope, contract, modelName, plan.write === "delete" ? {
291
+ ...plan,
292
+ kind: "delete",
293
+ key
294
+ } : {
295
+ ...plan,
296
+ kind: "update",
297
+ key,
298
+ patch: plan.patch ?? {}
299
+ }, snapshot);
300
+ result.push(...written);
301
+ const index = snapshot.findIndex((stored) => keyEquals(extractKeyFromRow(stored, catalog.primaryKey.keyPath), key));
302
+ if (index >= 0) snapshot.splice(index, 1, ...plan.write === "delete" ? [] : written);
303
+ else if (plan.write !== "delete") snapshot.push(...written);
304
+ }
305
+ return result;
306
+ }
307
+ /** Decorate only Decimal-key mutations; all other queries retain their existing execution path. */
308
+ function withDecimalIdentityExecutor(contract, executor) {
309
+ const models = decimalModels(contract);
310
+ if (models.size === 0) return executor;
311
+ const inner = executor;
312
+ /** Only mutations of declared Decimal key sources need a transaction wrapper. */
313
+ const needsCheck = (plan) => {
314
+ if (![
315
+ "add",
316
+ "put",
317
+ "update",
318
+ "delete",
319
+ "scan-write"
320
+ ].includes(plan.kind)) return false;
321
+ return models.has(plan.storeName);
322
+ };
323
+ /** Keep validation and the write under the caller's transaction and sync interceptor. */
324
+ const transaction = async (stores, mode) => {
325
+ if (typeof inner.transaction !== "function") throw new Error("Decimal key mutations require a transaction-capable executor");
326
+ return withDecimalIdentityScope(await inner.transaction(stores, mode), contract);
327
+ };
328
+ return {
329
+ ...typeof inner.transaction === "function" ? { transaction } : {},
330
+ query(plan) {
331
+ const body = plan.idbPlan;
332
+ const ops = body.kind === "batch" ? body.ops : [body];
333
+ if (!ops.some(needsCheck)) return executor.query(plan);
334
+ return new AsyncIterableResult((async function* () {
335
+ const scope = await transaction(body.kind === "batch" ? [...body.storeNames] : [body.storeName], "readwrite");
336
+ try {
337
+ const rows = [];
338
+ for (const op of ops) rows.push(...await scope.execute(op));
339
+ await scope.commit();
340
+ for (const row of rows) yield row;
341
+ } catch (error) {
342
+ scope.rollback();
343
+ throw error;
344
+ }
345
+ })());
346
+ }
347
+ };
348
+ }
349
+ //#endregion
53
350
  //#region src/core/query-shaping.ts
54
351
  /**
55
352
  * Combine accumulated filter expressions with AND.
@@ -67,15 +364,19 @@ function combineFilterExprs(filters) {
67
364
  * Build an in-memory comparator from an `orderBy` spec (field → direction).
68
365
  *
69
366
  * Returns `undefined` when there is nothing to sort by. Compares fields in
70
- * declaration order, with {@link compareFieldValues}: the same order an
367
+ * declaration order. Decimal fields use exact numeric order; other fields
368
+ * use {@link compareFieldValues}: the same order an
71
369
  * IndexedDB index uses, so equal `Date`s tie and fall through to the next
72
370
  * field, and `null`s sort last (first when descending).
73
371
  */
74
- function buildRowComparator(orderBy) {
372
+ function buildRowComparator(orderBy, fields = {}) {
75
373
  if (orderBy === void 0) return void 0;
76
374
  return (a, b) => {
77
375
  for (const [field, dir] of Object.entries(orderBy)) {
78
- const cmp = compareFieldValues(a[field], b[field]);
376
+ const metadata = fields[field];
377
+ const decimal = metadata?.codecId === "idb/decimal@1" && !metadata.collection;
378
+ const present = a[field] != null && b[field] != null;
379
+ const cmp = decimal && present ? compareDecimalValues(a[field], b[field]) : compareFieldValues(a[field], b[field]);
79
380
  if (cmp === 0) continue;
80
381
  return dir === "desc" ? -cmp : cmp;
81
382
  }
@@ -85,6 +386,7 @@ function buildRowComparator(orderBy) {
85
386
  //#endregion
86
387
  //#region src/core/enum-validation.ts
87
388
  const rulesByContract = /* @__PURE__ */ new WeakMap();
389
+ /** Resolve an enum reference against its declared namespace before validating writes. */
88
390
  function resolveEnumRule(contract, field) {
89
391
  const ref = field.valueSet;
90
392
  if (ref === void 0 || ref.plane !== "domain" || ref.entityKind !== "enum") return void 0;
@@ -94,9 +396,11 @@ function resolveEnumRule(contract, field) {
94
396
  enumName: ref.entityName,
95
397
  values: new Set(contractEnum.members.map((member) => member.value)),
96
398
  nullable: field.nullable,
97
- many: field.many === true
399
+ many: Boolean(field.many),
400
+ elementNullable: typeof field.many === "object" && field.many.elementNullable === true
98
401
  };
99
402
  }
403
+ /** Cache enum rules by contract identity and model to avoid rebuilding them for each write. */
100
404
  function getModelEnumRules(contract, modelName) {
101
405
  let byModel = rulesByContract.get(contract);
102
406
  if (byModel === void 0) {
@@ -117,6 +421,7 @@ function getModelEnumRules(contract, modelName) {
117
421
  function describe(value) {
118
422
  return typeof value === "string" ? JSON.stringify(value) : `${String(value)} (${typeof value})`;
119
423
  }
424
+ /** Describe an invalid enum value together with the declared alternatives. */
120
425
  function invalidValueError(modelName, fieldName, rule, value) {
121
426
  const expected = [...rule.values].map((v) => JSON.stringify(v)).join(", ");
122
427
  return /* @__PURE__ */ new Error(`Invalid value ${describe(value)} for enum field "${modelName}.${fieldName}". Expected one of enum "${rule.enumName}": ${expected}.`);
@@ -125,7 +430,7 @@ function invalidValueError(modelName, fieldName, rule, value) {
125
430
  * Throws if `data` sets an enum field to a value its enum doesn't declare.
126
431
  * Fields left `undefined` are not checked. `null` is accepted only on an
127
432
  * optional (`Role?`) field; a list (`Role[]`) field must be an array whose
128
- * every element is a declared value.
433
+ * every element is a declared value, or `null` when the list allows nullable elements.
129
434
  */
130
435
  function assertEnumValues(contract, modelName, data) {
131
436
  const rules = getModelEnumRules(contract, modelName);
@@ -139,19 +444,49 @@ function assertEnumValues(contract, modelName, data) {
139
444
  }
140
445
  if (rule.many) {
141
446
  if (!Array.isArray(value)) throw new Error(`Enum list field "${modelName}.${fieldName}" expects an array of "${rule.enumName}" values, got ${describe(value)}.`);
142
- for (const element of value) if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
447
+ for (const element of value) {
448
+ if (element === null && rule.elementNullable) continue;
449
+ if (!rule.values.has(element)) throw invalidValueError(modelName, fieldName, rule, element);
450
+ }
143
451
  continue;
144
452
  }
145
453
  if (!rule.values.has(value)) throw invalidValueError(modelName, fieldName, rule, value);
146
454
  }
147
455
  }
148
456
  //#endregion
457
+ //#region src/core/record-validation.ts
458
+ /** A local ORM create or update failed the contract's native record validation. */
459
+ var IdbRecordValidationError = class extends Error {
460
+ modelName;
461
+ operation;
462
+ issues;
463
+ constructor(modelName, operation, issues) {
464
+ super(`Invalid ${operation} record for "${modelName}": ${issues.join("; ")}`);
465
+ this.modelName = modelName;
466
+ this.operation = operation;
467
+ this.issues = issues;
468
+ this.name = "IdbRecordValidationError";
469
+ }
470
+ };
471
+ /** Validate final write data, after defaults and relation scalar values are resolved. */
472
+ function assertValidRecord(contract, modelName, operation, data) {
473
+ const keyPath = getKeyPath(contract, modelName);
474
+ const store = contract.storage.stores[getStoreName(contract, modelName)];
475
+ const optionalFields = operation === "create" && store?.autoIncrement && typeof keyPath === "string" && !Object.hasOwn(data, keyPath) ? [keyPath] : [];
476
+ const result = validateRecord(contract, modelName, data, {
477
+ partial: operation === "update",
478
+ optionalFields
479
+ });
480
+ if (!result.ok) throw new IdbRecordValidationError(modelName, operation, result.issues);
481
+ }
482
+ //#endregion
149
483
  //#region src/core/mutation-defaults.ts
150
484
  function createMutationDefaultsCache() {
151
485
  return /* @__PURE__ */ new Map();
152
486
  }
153
487
  /** Generator ids whose value is shared across an entire top-level call. Every other id always regenerates. */
154
488
  const CACHEABLE_GENERATOR_IDS = /* @__PURE__ */ new Set(["timestampNow"]);
489
+ /** Resolve one declared default with the same generator IDs as the SQL execution layer. */
155
490
  function computeGeneratedValue(spec) {
156
491
  const id = spec.id;
157
492
  switch (id) {
@@ -166,6 +501,7 @@ function computeGeneratedValue(spec) {
166
501
  default: throw new Error(`Unknown mutation-default generator id "${String(id)}"`);
167
502
  }
168
503
  }
504
+ /** Share query-stable defaults across one mutation while generating fresh IDs for each field. */
169
505
  function resolveGeneratedValue(spec, cache) {
170
506
  const id = spec.id;
171
507
  if (!CACHEABLE_GENERATOR_IDS.has(id)) return computeGeneratedValue(spec);
@@ -184,14 +520,25 @@ function applyCreateDefaults(defaults, storeName, data, cache) {
184
520
  if (!defaults || defaults.length === 0) return data;
185
521
  let result = data;
186
522
  for (const d of defaults) {
187
- if (d.ref.table !== storeName || !d.onCreate) continue;
188
- if (result[d.ref.column] !== void 0) continue;
523
+ if (d.ref.entry !== storeName || !d.onCreate) continue;
524
+ if (result[d.ref.field] !== void 0) continue;
189
525
  if (result === data) result = { ...data };
190
- result[d.ref.column] = resolveGeneratedValue(d.onCreate, cache);
526
+ result[d.ref.field] = resolveGeneratedValue(d.onCreate, cache);
191
527
  }
192
528
  return result;
193
529
  }
194
530
  /**
531
+ * Completes a create record: applies the `onCreate` defaults, then stores
532
+ * `null` for every nullable field the record still lacks. IndexedDB has no
533
+ * column defaults, so this is what keeps stored rows complete while the create
534
+ * input leaves nullable fields optional, as Prisma's does. Call it after any
535
+ * relation values are resolved into `data`, and validate the result.
536
+ */
537
+ function completeCreateRecord(contract, modelName, data, cache) {
538
+ const withDefaults = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, cache);
539
+ return absentAsNull(contract, modelName)(withDefaults);
540
+ }
541
+ /**
195
542
  * Fills in every column with an `onUpdate` default that `patch` doesn't
196
543
  * already set. A patch with no defined values (empty, or every field
197
544
  * explicitly `undefined`) is returned unchanged — no write means no default
@@ -203,56 +550,14 @@ function applyUpdateDefaults(defaults, storeName, patch, cache) {
203
550
  if (!defaults || defaults.length === 0) return patch;
204
551
  let result = patch;
205
552
  for (const d of defaults) {
206
- if (d.ref.table !== storeName || !d.onUpdate) continue;
207
- if (result[d.ref.column] !== void 0) continue;
553
+ if (d.ref.entry !== storeName || !d.onUpdate) continue;
554
+ if (result[d.ref.field] !== void 0) continue;
208
555
  if (result === patch) result = { ...patch };
209
- result[d.ref.column] = resolveGeneratedValue(d.onUpdate, cache);
556
+ result[d.ref.field] = resolveGeneratedValue(d.onUpdate, cache);
210
557
  }
211
558
  return result;
212
559
  }
213
560
  //#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
561
  //#region src/core/planner/plan.ts
257
562
  const COST = {
258
563
  UNIQUE_POINT: 0,
@@ -269,11 +574,11 @@ const KEY_CODECS = /* @__PURE__ */ new Set([
269
574
  "idb/int32@1",
270
575
  "idb/double@1",
271
576
  "idb/date@1",
272
- "idb/decimal@1",
273
577
  "idb/bytes@1"
274
578
  ]);
275
579
  /** Choose the cheapest safe access without opening IndexedDB or probing cardinality. */
276
580
  function planQuery(catalog, request) {
581
+ if (request.where !== void 0) assertValidDecimalFilter(request.where, catalog.fields);
277
582
  const normalized = normalize(request.where);
278
583
  if (normalized.empty) return {
279
584
  access: { kind: "empty" },
@@ -312,7 +617,7 @@ function planQuery(catalog, request) {
312
617
  if (orders.length === 1 && request.take !== void 0) {
313
618
  const [field, order] = orders[0];
314
619
  if (selected) {
315
- if (singleFieldSource(selected.source, field)) direction = order === "asc" ? "next" : "prev";
620
+ if (catalog.fields[field]?.codecId !== "idb/decimal@1" && sourceOrdersField(selected.source, field, constraints)) direction = order === "asc" ? "next" : "prev";
316
621
  } else {
317
622
  const ordering = sources.find((source) => !source.multiEntry && singleFieldSource(source, field) && completeKeyField(catalog.fields[field]));
318
623
  if (ordering) {
@@ -347,6 +652,7 @@ function full(exact) {
347
652
  exact
348
653
  };
349
654
  }
655
+ /** Flatten supported conjunctions and same-field unions while retaining unsupported predicates. */
350
656
  function normalize(where) {
351
657
  const result = {
352
658
  atoms: [],
@@ -354,6 +660,7 @@ function normalize(where) {
354
660
  excluded: false,
355
661
  empty: false
356
662
  };
663
+ /** Collect usable atoms and mark filters that require the full-row path. */
357
664
  function visit(expr) {
358
665
  switch (expr.kind) {
359
666
  case "field":
@@ -404,10 +711,17 @@ function keyPartial(field) {
404
711
  function completeKeyField(field, constraint) {
405
712
  return keyTyped(field) && field?.nullable === false && (!keyPartial(field) || constraint?.rejectsNonKeys === true);
406
713
  }
714
+ /** A compound cursor orders a suffix only within one fixed preceding key. */
715
+ function sourceOrdersField(source, field, constraints) {
716
+ const position = source.fields.indexOf(field);
717
+ return position >= 0 && source.fields.slice(0, position).every((name) => constraints.get(name)?.points?.length === 1);
718
+ }
407
719
  function singleFieldSource(source, field) {
408
720
  return source.fields.length === 1 && source.fields[0] === field;
409
721
  }
722
+ /** Intersect one supported predicate with a field constraint without narrowing unsafe values. */
410
723
  function addAtom(constraint, atom, field) {
724
+ if (field?.codecId === "idb/decimal@1") return;
411
725
  if (atom.op === "eq" || atom.op === "in") {
412
726
  const values = atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
413
727
  if (!values.every(isValidIdbKey)) return;
@@ -451,11 +765,13 @@ function uniqueKeys(values) {
451
765
  const sorted = [...values].sort(compareFieldValues);
452
766
  return sorted.filter((value, i) => i === 0 || compareFieldValues(sorted[i - 1], value) !== 0);
453
767
  }
768
+ /** Keep the tighter endpoint when several comparisons constrain the same field. */
454
769
  function intersectBound(constraint, side, bound) {
455
770
  const previous = constraint[side];
456
771
  const comparison = previous === void 0 ? 0 : compareFieldValues(bound.value, previous.value);
457
772
  if (!previous || (side === "lower" ? comparison > 0 : comparison < 0) || comparison === 0 && bound.open) constraint[side] = bound;
458
773
  }
774
+ /** Check a point against both endpoints, including open-bound equality. */
459
775
  function insideBounds(value, constraint) {
460
776
  const lower = constraint.lower;
461
777
  const upper = constraint.upper;
@@ -463,6 +779,7 @@ function insideBounds(value, constraint) {
463
779
  if (upper && (compareFieldValues(value, upper.value) > 0 || upper.open && keyEquals(value, upper.value))) return false;
464
780
  return true;
465
781
  }
782
+ /** Discard points outside the bounds and detect ranges that cannot match. */
466
783
  function contradictory(constraint) {
467
784
  if (constraint.points) {
468
785
  constraint.points = constraint.points.filter((value) => insideBounds(value, constraint));
@@ -472,6 +789,7 @@ function contradictory(constraint) {
472
789
  const comparison = compareFieldValues(constraint.lower.value, constraint.upper.value);
473
790
  return comparison > 0 || comparison === 0 && (constraint.lower.open || constraint.upper.open);
474
791
  }
792
+ /** Build a safe source range only when its index contains every possible match. */
475
793
  function filteringCandidate(catalog, source, constraints, declaration) {
476
794
  if (source.multiEntry || source.fields.length === 0) return void 0;
477
795
  if (source.fields.some((name) => keyPartial(catalog.fields[name]) && !constraints.get(name)?.rejectsNonKeys)) return void 0;
@@ -513,6 +831,7 @@ function filteringCandidate(catalog, source, constraints, declaration) {
513
831
  declaration
514
832
  };
515
833
  }
834
+ /** Convert scalar endpoints into the matching driver range descriptor. */
516
835
  function scalarRange(constraint) {
517
836
  if (constraint.lower && constraint.upper) return {
518
837
  kind: "bound",
@@ -532,6 +851,7 @@ function scalarRange(constraint) {
532
851
  open: constraint.upper.open
533
852
  };
534
853
  }
854
+ /** Bound a compound prefix and suffix using IndexedDB key ordering. */
535
855
  function compoundRange(prefix, constraint, trailing) {
536
856
  const lower = constraint?.lower;
537
857
  const upper = constraint?.upper;
@@ -551,6 +871,7 @@ function compoundRange(prefix, constraint, trailing) {
551
871
  upperOpen: upper ? upper.open || trailing > 0 : true
552
872
  };
553
873
  }
874
+ /** Find the exclusive upper bound of a UTF-16 prefix, if one exists. */
554
875
  function stringSuccessor(prefix) {
555
876
  for (let i = prefix.length - 1; i >= 0; i--) {
556
877
  const code = prefix.charCodeAt(i);
@@ -560,6 +881,17 @@ function stringSuccessor(prefix) {
560
881
  //#endregion
561
882
  //#region src/core/planner/lower.ts
562
883
  /**
884
+ * The row filter shared by every lowered read and write. The driver runs it on stored rows, so it
885
+ * reads each row as the contract describes it first: a filter on `null` matches a row that
886
+ * lacks the field.
887
+ */
888
+ function rowFilter(catalog, request, additionalFilter) {
889
+ return (stored) => {
890
+ const row = catalog.absentAsNull(stored);
891
+ return (request.where === void 0 || evaluateFilter(request.where, row, catalog.fields)) && (additionalFilter === void 0 || additionalFilter(row));
892
+ };
893
+ }
894
+ /**
563
895
  * Lower row access without creating IDBKeyRange objects or awaiting work.
564
896
  * Run `finish` after the executor has materialized all returned rows.
565
897
  * Without `orderBy`, row order and pagination selection are unspecified.
@@ -567,8 +899,8 @@ function stringSuccessor(prefix) {
567
899
  * pagination boundaries. Add a unique ordering field for deterministic pages.
568
900
  */
569
901
  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;
902
+ const filter = request.where === void 0 && additionalFilter === void 0 ? void 0 : rowFilter(catalog, request, additionalFilter);
903
+ const comparator = logical.direction === void 0 ? buildRowComparator(request.orderBy, catalog.fields) : void 0;
572
904
  const base = {
573
905
  meta,
574
906
  storeName: catalog.storeName
@@ -592,7 +924,7 @@ function lowerRows(catalog, logical, meta, request, additionalFilter) {
592
924
  idbPlan = {
593
925
  ...base,
594
926
  kind: "cursor-scan",
595
- filter,
927
+ ...filter === void 0 ? {} : { filter },
596
928
  ...comparator === void 0 ? {} : { comparator },
597
929
  ...pagination
598
930
  };
@@ -618,7 +950,7 @@ function lowerRows(catalog, logical, meta, request, additionalFilter) {
618
950
  ...access,
619
951
  kind: "cursor-scan",
620
952
  direction: logical.direction,
621
- filter,
953
+ ...filter === void 0 ? {} : { filter },
622
954
  take: (request.skip ?? 0) + request.take
623
955
  };
624
956
  if (single && request.take !== void 0) {
@@ -626,7 +958,7 @@ function lowerRows(catalog, logical, meta, request, additionalFilter) {
626
958
  return {
627
959
  ...access,
628
960
  kind: "cursor-scan",
629
- filter,
961
+ ...filter === void 0 ? {} : { filter },
630
962
  ...logical.direction === void 0 ? {} : { direction: logical.direction },
631
963
  ...comparator === void 0 ? {} : { comparator },
632
964
  ...pagination
@@ -636,7 +968,7 @@ function lowerRows(catalog, logical, meta, request, additionalFilter) {
636
968
  ...access,
637
969
  kind: "cursor-scan",
638
970
  direction: "prev",
639
- filter
971
+ ...filter === void 0 ? {} : { filter }
640
972
  };
641
973
  return {
642
974
  ...access,
@@ -654,8 +986,10 @@ function lowerRows(catalog, logical, meta, request, additionalFilter) {
654
986
  }
655
987
  return {
656
988
  idbPlan,
989
+ /** Normalize returned rows and apply filtering and shaping not already handled by the driver. */
657
990
  finish(rows) {
658
- let result = rows.filter(filter);
991
+ let result = rows.map(catalog.absentAsNull);
992
+ if (filter !== void 0) result = result.filter(filter);
659
993
  if (!shapedByDriver) {
660
994
  if (comparator !== void 0) result.sort(comparator);
661
995
  const skip = request.skip ?? 0;
@@ -702,7 +1036,7 @@ function lowerExists(catalog, logical, meta, request, additionalFilter) {
702
1036
  }))),
703
1037
  finish: (rows) => rows.length > 0
704
1038
  };
705
- const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
1039
+ const filter = rowFilter(catalog, request, additionalFilter);
706
1040
  return {
707
1041
  idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
708
1042
  ...target,
@@ -728,7 +1062,7 @@ function lowerWrite(catalog, logical, meta, request, additionalFilter) {
728
1062
  kind: "collect",
729
1063
  rows: lowerRows(catalog, logical, meta, request, additionalFilter)
730
1064
  };
731
- const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
1065
+ const filter = rowFilter(catalog, request, additionalFilter);
732
1066
  return {
733
1067
  kind: "scan",
734
1068
  idbPlan: combine(catalog, meta, accessOps(catalog, meta, logical, (target) => ({
@@ -794,8 +1128,10 @@ async function withMutationScope(executor, storeNames, run) {
794
1128
  }
795
1129
  //#endregion
796
1130
  //#region src/core/relation-mutator.ts
1131
+ /** Build the relation descriptors consumed by the nested-write executor. */
797
1132
  function createRelationMutator() {
798
1133
  return {
1134
+ /** Normalize a single nested record or a list into one create descriptor. */
799
1135
  create(data) {
800
1136
  return {
801
1137
  kind: "create",
@@ -827,6 +1163,7 @@ function isRelationMutationCallback(value) {
827
1163
  }
828
1164
  //#endregion
829
1165
  //#region src/core/mutation-executor.ts
1166
+ /** Plan metadata for the raw plans the mutation executor issues itself, outside any accessor's grouping key. */
830
1167
  function makePlanMeta(contract) {
831
1168
  return {
832
1169
  target: "idb",
@@ -836,6 +1173,7 @@ function makePlanMeta(contract) {
836
1173
  };
837
1174
  }
838
1175
  const relationDefsCache = /* @__PURE__ */ new WeakMap();
1176
+ /** Cache reference-relation metadata by contract identity and model. */
839
1177
  function getRelationDefinitions(contract, modelName) {
840
1178
  let perContract = relationDefsCache.get(contract);
841
1179
  if (!perContract) {
@@ -886,6 +1224,7 @@ function requireTransactionExecutor(executor) {
886
1224
  if (typeof executor.transaction !== "function") throw new Error("This operation requires an executor with transaction support (nested relation writes, FK-validated create/update, and referential-action delete all need it). Use IdbRuntime (createIdbRuntime or createAutoMigratingIdbClient) instead of a plain IdbQueryExecutor.");
887
1225
  return executor;
888
1226
  }
1227
+ /** Create a parent and its nested related rows in one transaction. */
889
1228
  async function executeNestedCreateMutation(options) {
890
1229
  const { executor, contract, modelName, data } = options;
891
1230
  const record = data;
@@ -893,6 +1232,7 @@ async function executeNestedCreateMutation(options) {
893
1232
  const defaultsCache = createMutationDefaultsCache();
894
1233
  return withMutationScope(executor, storeNames, (scope) => createGraph(scope, contract, modelName, record, defaultsCache));
895
1234
  }
1235
+ /** Update the first filtered parent and its nested relations in one transaction. */
896
1236
  async function executeNestedUpdateMutation(options) {
897
1237
  const { executor, contract, modelName, filters, data } = options;
898
1238
  const record = data;
@@ -932,6 +1272,7 @@ function collectStoreNames(contract, modelName, data, kind) {
932
1272
  function fkParentStoreNames(contract, modelName) {
933
1273
  return getRelationDefinitions(contract, modelName).filter((def) => def.cardinality === "N:1").map((def) => def.relatedStoreName);
934
1274
  }
1275
+ /** Resolve parent-owned references before inserting the parent, then write its children. */
935
1276
  async function createGraph(scope, contract, modelName, input, defaultsCache) {
936
1277
  const parsed = parseMutationInput(contract, modelName, input);
937
1278
  const { parentOwned, childOwned } = partitionByOwnership(parsed.relationMutations);
@@ -947,6 +1288,7 @@ async function createGraph(scope, contract, modelName, input, defaultsCache) {
947
1288
  }
948
1289
  return parentRow;
949
1290
  }
1291
+ /** Read the parent before resolving relation mutations and validating its scalar patch. */
950
1292
  async function updateFirstGraph(scope, contract, modelName, filters, input, defaultsCache) {
951
1293
  const existingRow = await findFirstByFilters(scope, contract, modelName, filters);
952
1294
  if (!existingRow) return null;
@@ -961,6 +1303,7 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
961
1303
  const meta = makePlanMeta(contract);
962
1304
  const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, scalarData, defaultsCache);
963
1305
  assertEnumValues(contract, modelName, patch);
1306
+ assertValidRecord(contract, modelName, "update", patch);
964
1307
  await validateScalarFks(scope, contract, modelName, patch, existingRow);
965
1308
  await applyReferentialActionsForRowOnUpdate(scope, contract, modelName, existingRow, patch);
966
1309
  const updated = (await scope.execute({
@@ -970,11 +1313,12 @@ async function updateFirstGraph(scope, contract, modelName, filters, input, defa
970
1313
  key,
971
1314
  patch
972
1315
  }))[0];
973
- if (updated) parentRow = updated;
1316
+ if (updated) parentRow = absentAsNull(contract, modelName)(updated);
974
1317
  }
975
1318
  for (const item of childOwned) await applyChildOwnedMutation(scope, contract, modelName, parentRow, item.relation, item.mutation, defaultsCache);
976
1319
  return parentRow;
977
1320
  }
1321
+ /** Separate scalar values from validated relation callback results. */
978
1322
  function parseMutationInput(contract, modelName, input) {
979
1323
  const scalarData = {};
980
1324
  const relationDefs = new Map(getRelationDefinitions(contract, modelName).map((r) => [r.relationName, r]));
@@ -998,6 +1342,7 @@ function parseMutationInput(contract, modelName, input) {
998
1342
  relationMutations
999
1343
  };
1000
1344
  }
1345
+ /** Separate relations whose foreign keys belong to the parent from those owned by children. */
1001
1346
  function partitionByOwnership(mutations) {
1002
1347
  const parentOwned = [];
1003
1348
  const childOwned = [];
@@ -1014,6 +1359,7 @@ function partitionByOwnership(mutations) {
1014
1359
  childOwned
1015
1360
  };
1016
1361
  }
1362
+ /** Resolve a to-one mutation into scalar foreign-key values on the parent. */
1017
1363
  async function applyParentOwnedMutation(scope, contract, scalarData, relation, mutation, defaultsCache) {
1018
1364
  if (mutation.kind === "disconnect") {
1019
1365
  for (const localField of relation.localFields) scalarData[localField] = null;
@@ -1031,6 +1377,7 @@ async function applyParentOwnedMutation(scope, contract, scalarData, relation, m
1031
1377
  if (!relatedRow) throw new Error(`connect() nested mutation for relation "${relation.relationName}" did not find a matching row`);
1032
1378
  copyRelatedValuesToParent(relation, scalarData, relatedRow);
1033
1379
  }
1380
+ /** Copy each referenced value into the matching local foreign-key field. */
1034
1381
  function copyRelatedValuesToParent(relation, scalarData, relatedRow) {
1035
1382
  for (let i = 0; i < relation.localFields.length; i++) {
1036
1383
  const localField = relation.localFields[i];
@@ -1039,6 +1386,7 @@ function copyRelatedValuesToParent(relation, scalarData, relatedRow) {
1039
1386
  scalarData[localField] = relatedRow[targetField];
1040
1387
  }
1041
1388
  }
1389
+ /** Create, connect, or disconnect children using the persisted parent join values. */
1042
1390
  async function applyChildOwnedMutation(scope, contract, parentModelName, parentRow, relation, mutation, defaultsCache) {
1043
1391
  const parentValues = readParentColumnValues(parentModelName, relation, parentRow);
1044
1392
  if (mutation.kind === "create") {
@@ -1087,6 +1435,7 @@ async function applyChildOwnedMutation(scope, contract, parentModelName, parentR
1087
1435
  });
1088
1436
  }
1089
1437
  }
1438
+ /** Resolve parent join values before writing children; missing values cannot form a relation. */
1090
1439
  function readParentColumnValues(parentModelName, relation, parentRow) {
1091
1440
  const values = /* @__PURE__ */ new Map();
1092
1441
  for (let i = 0; i < relation.localFields.length; i++) {
@@ -1099,12 +1448,14 @@ function readParentColumnValues(parentModelName, relation, parentRow) {
1099
1448
  }
1100
1449
  return values;
1101
1450
  }
1451
+ /** Complete and validate a nested record, then insert it with foreign-key checks. */
1102
1452
  async function insertSingleRow(scope, contract, modelName, data, defaultsCache) {
1103
1453
  assertNoNestedCallbacks(modelName, data);
1104
1454
  const storeName = getStoreName(contract, modelName);
1105
1455
  const meta = makePlanMeta(contract);
1106
- const record = applyCreateDefaults(contract.execution?.mutations.defaults, storeName, data, defaultsCache);
1456
+ const record = completeCreateRecord(contract, modelName, data, defaultsCache);
1107
1457
  assertEnumValues(contract, modelName, record);
1458
+ assertValidRecord(contract, modelName, "create", record);
1108
1459
  await validateScalarFks(scope, contract, modelName, record);
1109
1460
  return (await scope.execute({
1110
1461
  meta,
@@ -1115,19 +1466,21 @@ async function insertSingleRow(scope, contract, modelName, data, defaultsCache)
1115
1466
  }
1116
1467
  /**
1117
1468
  * Recursive nesting (a relation callback inside an already-nested create) is
1118
- * not supported in Phase 6.4. Without this guard the callback function would be
1469
+ * not supported. Without this guard the callback function would be
1119
1470
  * handed to `store.put(...)`, where IDB's structured-clone throws an opaque
1120
1471
  * `DataCloneError` ("could not be cloned") that gives the developer no hint
1121
- * about the real cause. Surface a precise error instead. (PLAN Issue #22.)
1472
+ * about the real cause. Surface a precise error instead.
1122
1473
  */
1123
1474
  function assertNoNestedCallbacks(modelName, data) {
1124
1475
  for (const [field, value] of Object.entries(data)) if (typeof value === "function") throw new Error(`Recursive nested writes are not supported: field "${field}" on a nested "${modelName}" record is a relation callback. Only one level of relation nesting is supported — flatten the inner relation into a separate create/connect call.`);
1125
1476
  }
1477
+ /** Require a non-empty connect criterion and return its first matching row. */
1126
1478
  async function findRowByCriterion(scope, contract, modelName, criterion) {
1127
1479
  const expr = shorthandToFilterExpr(criterion);
1128
1480
  if (!expr) throw new Error(`Nested connect for model "${modelName}" requires a non-empty criterion`);
1129
1481
  return (await readMutationRows(scope, contract, modelName, expr, 1))[0] ?? null;
1130
1482
  }
1483
+ /** Find the first parent selected by the nested-update filters. */
1131
1484
  async function findFirstByFilters(scope, contract, modelName, filters) {
1132
1485
  if (filters.length === 0) return null;
1133
1486
  return (await readMutationRows(scope, contract, modelName, andExpr(filters), 1))[0] ?? null;
@@ -1141,7 +1494,7 @@ async function executeMutationPlan(scope, plan) {
1141
1494
  }
1142
1495
  /** Read mutation preimages through the planner, then reapply the original filter. */
1143
1496
  async function readMutationRows(scope, contract, modelName, where, take) {
1144
- const catalog = buildCatalog(contract, getStoreName(contract, modelName));
1497
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1145
1498
  const request = {
1146
1499
  ...where === void 0 ? {} : { where },
1147
1500
  ...take === void 0 ? {} : { take }
@@ -1149,8 +1502,10 @@ async function readMutationRows(scope, contract, modelName, where, take) {
1149
1502
  const lowered = lowerRows(catalog, planQuery(catalog, request), makePlanMeta(contract), request);
1150
1503
  return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
1151
1504
  }
1505
+ /** Write planned matches, collecting preimages first when changing the walked index key. */
1152
1506
  async function writeMutationRows(scope, contract, modelName, request) {
1153
- const catalog = buildCatalog(contract, getStoreName(contract, modelName));
1507
+ if (request.write !== "delete") assertValidRecord(contract, modelName, "update", request.patch ?? {});
1508
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1154
1509
  const meta = makePlanMeta(contract);
1155
1510
  const lowered = lowerWrite(catalog, planQuery(catalog, request), meta, request);
1156
1511
  if (lowered.kind === "scan") return executeMutationPlan(scope, lowered.idbPlan);
@@ -1177,6 +1532,7 @@ async function writeMutationRows(scope, contract, modelName, request) {
1177
1532
  function buildParentJoinWhere(parentValues) {
1178
1533
  return andExpr([...parentValues].map(([field, value]) => fieldFilter(field, "eq", value)));
1179
1534
  }
1535
+ /** Read an explicitly declared referential action from model storage metadata. */
1180
1536
  function getStoredAction(contract, modelName, relationName, kind) {
1181
1537
  return (domainModelsAtDefaultNamespace(contract.domain)[modelName]?.storage)?.relations?.[relationName]?.[kind];
1182
1538
  }
@@ -1203,6 +1559,7 @@ function sameFields(a, b) {
1203
1559
  function getReferentialActionForRelation(contract, modelName, def, kind) {
1204
1560
  return enforcedAction(findDeclaredAction(contract, modelName, def, kind));
1205
1561
  }
1562
+ /** Find an action on the enforcement side or on its matching inverse relation. */
1206
1563
  function findDeclaredAction(contract, modelName, def, kind) {
1207
1564
  const direct = getStoredAction(contract, modelName, def.relationName, kind);
1208
1565
  if (direct !== void 0) return direct;
@@ -1228,6 +1585,7 @@ function findDeclaredAction(contract, modelName, def, kind) {
1228
1585
  function enforcedAction(declared) {
1229
1586
  return declared === void 0 || declared === "noAction" ? "restrict" : declared;
1230
1587
  }
1588
+ /** Identify relations that need child checks when this model changes. */
1231
1589
  function isChildEnforcementRelation(contract, modelName, def) {
1232
1590
  if (def.cardinality === "1:N") return true;
1233
1591
  if (def.cardinality === "1:1") {
@@ -1243,16 +1601,16 @@ function isChildEnforcementRelation(contract, modelName, def) {
1243
1601
  async function childExists(scope, contract, meta, def, parentRow) {
1244
1602
  const values = def.localFields.map((field) => parentRow[field]);
1245
1603
  if (values.some((value) => value === null || value === void 0)) return false;
1246
- return rowExists(scope, contract, meta, def.relatedStoreName, def.targetFields, values);
1604
+ return rowExists(scope, contract, meta, def.relatedModelName, def.targetFields, values);
1247
1605
  }
1248
1606
  /**
1249
- * Does `storeName` have a row whose `fields` equal `values`, pairwise?
1607
+ * Does `modelName` have a row whose `fields` equal `values`, pairwise?
1250
1608
  * Compares values the way IndexedDB compares keys. `additionalFilter` narrows
1251
1609
  * the match in memory; it makes the check read rows instead of keys.
1252
1610
  */
1253
- async function rowExists(scope, contract, meta, storeName, fields, values, additionalFilter) {
1611
+ async function rowExists(scope, contract, meta, modelName, fields, values, additionalFilter) {
1254
1612
  const where = andExpr(fields.map((field, i) => fieldFilter(field, "eq", values[i])));
1255
- const catalog = buildCatalog(contract, storeName);
1613
+ const catalog = buildCatalog(contract, getStoreName(contract, modelName), modelName);
1256
1614
  const lowered = lowerExists(catalog, planQuery(catalog, { where }), meta, { where }, additionalFilter);
1257
1615
  return lowered.finish(await executeMutationPlan(scope, lowered.idbPlan));
1258
1616
  }
@@ -1336,26 +1694,15 @@ async function validateSetDefaultPatch(scope, contract, modelName, def, patch, e
1336
1694
  */
1337
1695
  function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1338
1696
  const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1339
- const visitedModels = /* @__PURE__ */ new Set();
1697
+ const visitedModels = /* @__PURE__ */ new Set([modelName]);
1340
1698
  let enforces = false;
1341
- function walkCascadeChain(mName) {
1342
- if (visitedModels.has(mName)) return;
1343
- visitedModels.add(mName);
1344
- for (const def of getRelationDefinitions(contract, mName)) {
1345
- if (!isChildEnforcementRelation(contract, mName, def)) continue;
1346
- const action = getReferentialActionForRelation(contract, mName, def, "onUpdate");
1347
- stores.add(def.relatedStoreName);
1348
- if (action === "cascade") walkCascadeChain(def.relatedModelName);
1349
- }
1350
- }
1351
- visitedModels.add(modelName);
1352
1699
  for (const def of getRelationDefinitions(contract, modelName)) {
1353
1700
  if (!isChildEnforcementRelation(contract, modelName, def)) continue;
1354
1701
  if (!def.localFields.some((f) => f in data)) continue;
1355
1702
  const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
1356
1703
  enforces = true;
1357
1704
  stores.add(def.relatedStoreName);
1358
- if (action === "cascade") walkCascadeChain(def.relatedModelName);
1705
+ if (action === "cascade") addOnUpdateCascadeStores(contract, def.relatedModelName, stores, visitedModels);
1359
1706
  }
1360
1707
  return {
1361
1708
  storeNames: [...stores],
@@ -1384,7 +1731,8 @@ function collectOnUpdateEnforcementStoreNames(contract, modelName, data) {
1384
1731
  */
1385
1732
  async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName, oldRow, patch, visited = /* @__PURE__ */ new Set()) {
1386
1733
  const keyPath = getKeyPath(contract, modelName);
1387
- const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(oldRow, keyPath))}`;
1734
+ patch = preserveDecimalKeySpelling(contract, modelName, patch, extractKeyFromRow(oldRow, keyPath));
1735
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken$1(extractKeyFromRow(oldRow, keyPath))}`;
1388
1736
  if (visited.has(rowKey)) return;
1389
1737
  visited.add(rowKey);
1390
1738
  const meta = makePlanMeta(contract);
@@ -1395,7 +1743,7 @@ async function applyReferentialActionsForRowOnUpdate(scope, contract, modelName,
1395
1743
  const action = getReferentialActionForRelation(contract, modelName, def, "onUpdate");
1396
1744
  const childWhere = buildChildWhereFromRow(def, oldRow);
1397
1745
  if (action === "restrict") {
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.`);
1746
+ if (await childExists(scope, contract, meta, def, oldRow)) throw new Error(`Cannot update ${modelName} '${keyToken$1(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.`);
1399
1747
  continue;
1400
1748
  }
1401
1749
  if (action === "cascade") {
@@ -1447,6 +1795,7 @@ function hasScalarFkFields(contract, modelName, data) {
1447
1795
  }
1448
1796
  return false;
1449
1797
  }
1798
+ /** Include parent stores for the non-null foreign keys supplied by this write. */
1450
1799
  function collectScalarFkStoreNames(contract, modelName, data) {
1451
1800
  const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1452
1801
  for (const def of getRelationDefinitions(contract, modelName)) {
@@ -1496,18 +1845,18 @@ function fkCheckNeedsExistingRow(contract, modelName, data) {
1496
1845
  * is being deleted or changed.
1497
1846
  */
1498
1847
  async function parentExists(scope, contract, meta, parentModel, fields, values, excludeKey) {
1499
- const storeName = getStoreName(contract, parentModel);
1500
1848
  const keyPath = getKeyPath(contract, parentModel);
1501
- return rowExists(scope, contract, meta, storeName, fields, values, excludeKey === void 0 ? void 0 : (row) => !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
1849
+ return rowExists(scope, contract, meta, parentModel, fields, values, excludeKey === void 0 ? void 0 : (row) => !keyEquals$1(extractKeyFromRow(row, keyPath), excludeKey));
1502
1850
  }
1503
1851
  /** `id='u1'`, or `orgId='a', id='u1'` for a compound key. */
1504
1852
  function describeTuple(fields, values) {
1505
1853
  return fields.map((f, i) => `${f}='${String(values[i])}'`).join(", ");
1506
1854
  }
1855
+ /** Create one completed record and check its foreign keys in the same transaction. */
1507
1856
  async function executeScalarCreateWithFkValidation(options) {
1508
1857
  const { executor, contract, modelName, data } = options;
1509
1858
  const defaultsCache = createMutationDefaultsCache();
1510
- const record = applyCreateDefaults(contract.execution?.mutations.defaults, getStoreName(contract, modelName), data, defaultsCache);
1859
+ const record = completeCreateRecord(contract, modelName, data, defaultsCache);
1511
1860
  return withMutationScope(executor, collectScalarFkStoreNames(contract, modelName, record), (scope) => insertSingleRow(scope, contract, modelName, record, defaultsCache));
1512
1861
  }
1513
1862
  /**
@@ -1518,8 +1867,7 @@ async function executeScalarCreateWithFkValidation(options) {
1518
1867
  async function executeScalarCreateAllWithFkValidation(options) {
1519
1868
  const { executor, contract, modelName, data } = options;
1520
1869
  const defaultsCache = createMutationDefaultsCache();
1521
- const storeName = getStoreName(contract, modelName);
1522
- const records = data.map((row) => applyCreateDefaults(contract.execution?.mutations.defaults, storeName, row, defaultsCache));
1870
+ const records = data.map((row) => completeCreateRecord(contract, modelName, row, defaultsCache));
1523
1871
  return withMutationScope(executor, [...new Set(records.flatMap((row) => collectScalarFkStoreNames(contract, modelName, row)))], async (scope) => {
1524
1872
  const inserted = [];
1525
1873
  for (const record of records) inserted.push(await insertSingleRow(scope, contract, modelName, record, defaultsCache));
@@ -1527,6 +1875,29 @@ async function executeScalarCreateAllWithFkValidation(options) {
1527
1875
  });
1528
1876
  }
1529
1877
  /**
1878
+ * Adds the stores an `onUpdate` cascade reaches from `modelName`: every child
1879
+ * relation's store, walking further through `cascade` edges only.
1880
+ */
1881
+ function addOnUpdateCascadeStores(contract, modelName, stores, visitedModels) {
1882
+ if (visitedModels.has(modelName)) return;
1883
+ visitedModels.add(modelName);
1884
+ for (const def of getRelationDefinitions(contract, modelName)) {
1885
+ if (!isChildEnforcementRelation(contract, modelName, def)) continue;
1886
+ stores.add(def.relatedStoreName);
1887
+ if (getReferentialActionForRelation(contract, modelName, def, "onUpdate") === "cascade") addOnUpdateCascadeStores(contract, def.relatedModelName, stores, visitedModels);
1888
+ }
1889
+ }
1890
+ /**
1891
+ * Every store an `onUpdate` enforcement on `modelName` could touch, whichever
1892
+ * fields the write sets. Use it to open a transaction before the write's
1893
+ * fields are known.
1894
+ */
1895
+ function collectOnUpdateClosureStoreNames(contract, modelName) {
1896
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1897
+ addOnUpdateCascadeStores(contract, modelName, stores, /* @__PURE__ */ new Set());
1898
+ return [...stores];
1899
+ }
1900
+ /**
1530
1901
  * Combines FK-existence store scope (this model's own N:1 relations) with
1531
1902
  * `onUpdate` enforcement store scope (this model's child relations) for a
1532
1903
  * single write. Returns the union alongside whether `onUpdate` enforcement
@@ -1565,9 +1936,10 @@ async function executeScalarUpdates(options, take) {
1565
1936
  const storeName = getStoreName(contract, modelName);
1566
1937
  const patch = applyUpdateDefaults(contract.execution?.mutations.defaults, storeName, data, createMutationDefaultsCache());
1567
1938
  assertEnumValues(contract, modelName, patch);
1939
+ assertValidRecord(contract, modelName, "update", patch);
1568
1940
  const { storeNames, enforcesOnUpdate } = collectUpdateStoreNames(contract, modelName, patch);
1569
1941
  const needsRowForFks = fkCheckNeedsExistingRow(contract, modelName, patch);
1570
- return withMutationScope(executor, storeNames, async (scope) => {
1942
+ return (await withMutationScope(executor, storeNames, async (scope) => {
1571
1943
  if (!needsRowForFks) await validateScalarFks(scope, contract, modelName, patch);
1572
1944
  const meta = makePlanMeta(contract);
1573
1945
  const where = filters.length === 0 ? void 0 : andExpr(filters);
@@ -1594,7 +1966,7 @@ async function executeScalarUpdates(options, take) {
1594
1966
  if (updated) results.push(updated);
1595
1967
  }
1596
1968
  return results;
1597
- });
1969
+ })).map(absentAsNull(contract, modelName));
1598
1970
  }
1599
1971
  /**
1600
1972
  * Returns true if the model has at least one child relation (1:N or parent-side
@@ -1604,6 +1976,12 @@ async function executeScalarUpdates(options, take) {
1604
1976
  function hasEnforceableChildRelations(contract, modelName) {
1605
1977
  return getRelationDefinitions(contract, modelName).some((def) => isChildEnforcementRelation(contract, modelName, def));
1606
1978
  }
1979
+ /** The store of `modelName` plus the store of every model it has a relation to, in either direction. */
1980
+ function collectRelatedStoreNames(contract, modelName) {
1981
+ const stores = /* @__PURE__ */ new Set([getStoreName(contract, modelName)]);
1982
+ for (const def of getRelationDefinitions(contract, modelName)) stores.add(def.relatedStoreName);
1983
+ return [...stores];
1984
+ }
1607
1985
  /**
1608
1986
  * Transitively walks the `onDelete` cascade graph from `modelName`, collecting
1609
1987
  * every store a recursive delete might touch. `cascade` edges are walked
@@ -1618,6 +1996,7 @@ function hasEnforceableChildRelations(contract, modelName) {
1618
1996
  function collectDeleteStoreNames(contract, modelName) {
1619
1997
  const stores = /* @__PURE__ */ new Set();
1620
1998
  const visitedModels = /* @__PURE__ */ new Set();
1999
+ /** Collect cascade stores once per model so cyclic relations cannot recurse indefinitely. */
1621
2000
  function walk(mName) {
1622
2001
  if (visitedModels.has(mName)) return;
1623
2002
  visitedModels.add(mName);
@@ -1652,7 +2031,7 @@ function collectDeleteStoreNames(contract, modelName) {
1652
2031
  */
1653
2032
  async function applyReferentialActionsForRow(scope, contract, modelName, row, visited = /* @__PURE__ */ new Set()) {
1654
2033
  const keyPath = getKeyPath(contract, modelName);
1655
- const rowKey = `${getStoreName(contract, modelName)}::${keyToken(extractKeyFromRow(row, keyPath))}`;
2034
+ const rowKey = `${getStoreName(contract, modelName)}::${keyToken$1(extractKeyFromRow(row, keyPath))}`;
1656
2035
  if (visited.has(rowKey)) return;
1657
2036
  visited.add(rowKey);
1658
2037
  const meta = makePlanMeta(contract);
@@ -1661,7 +2040,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
1661
2040
  const action = getReferentialActionForRelation(contract, modelName, def, "onDelete");
1662
2041
  const childWhere = buildChildWhereFromRow(def, row);
1663
2042
  if (action === "restrict") {
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.`);
2043
+ if (await childExists(scope, contract, meta, def, row)) throw new Error(`Cannot delete ${modelName} '${keyToken$1(extractKeyFromRow(row, keyPath))}': child records exist on relation '${def.relationName}'. Delete those children first, or declare onDelete: Cascade, SetNull or SetDefault on the relation.`);
1665
2044
  continue;
1666
2045
  }
1667
2046
  if (action === "cascade") {
@@ -1700,6 +2079,7 @@ async function applyReferentialActionsForRow(scope, contract, modelName, row, vi
1700
2079
  }
1701
2080
  }
1702
2081
  }
2082
+ /** Load a keyed row, apply its child actions, and delete it in one transaction. */
1703
2083
  async function executeDeleteWithReferentialActions(options) {
1704
2084
  const { executor, contract, modelName, key } = options;
1705
2085
  await withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
@@ -1721,16 +2101,17 @@ async function executeDeleteWithReferentialActions(options) {
1721
2101
  return [];
1722
2102
  });
1723
2103
  }
2104
+ /** Delete filtered rows with all child actions inside the same transaction. */
1724
2105
  async function executeDeleteAllWithReferentialActions(options) {
1725
2106
  const { executor, contract, modelName, where } = options;
1726
2107
  return withMutationScope(executor, collectDeleteStoreNames(contract, modelName), async (scope) => {
1727
2108
  const storeName = getStoreName(contract, modelName);
1728
2109
  const meta = makePlanMeta(contract);
1729
2110
  const keyPath = getKeyPath(contract, modelName);
1730
- if (!hasEnforceableChildRelations(contract, modelName)) return writeMutationRows(scope, contract, modelName, {
2111
+ if (!hasEnforceableChildRelations(contract, modelName)) return (await writeMutationRows(scope, contract, modelName, {
1731
2112
  write: "delete",
1732
2113
  ...where === void 0 ? {} : { where }
1733
- });
2114
+ })).map(absentAsNull(contract, modelName));
1734
2115
  const rows = await readMutationRows(scope, contract, modelName, where);
1735
2116
  for (const row of rows) {
1736
2117
  await applyReferentialActionsForRow(scope, contract, modelName, row);
@@ -1746,6 +2127,6 @@ async function executeDeleteAllWithReferentialActions(options) {
1746
2127
  });
1747
2128
  }
1748
2129
  //#endregion
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 };
2130
+ export { createMutationDefaultsCache as A, resolveDecimalKey as B, validateScalarFks as C, planQuery as D, lowerRows as E, combineFilterExprs as F, fieldValueToken as G, withDecimalIdentityScope as H, IdbDecimalIdentityConflictError as I, getStoreName as J, getKeyPath as K, decimalKeysEqual as L, assertValidRecord as M, assertEnumValues as N, applyUpdateDefaults as O, buildRowComparator as P, hasDecimalPrimaryKey as R, requireTransactionExecutor as S, lowerCount as T, buildCatalog as U, withDecimalIdentityExecutor as V, extractKeyFromRow as W, keyToken$1 as X, keyEquals$1 as Y, hasEnforceableChildRelations as _, collectOnUpdateEnforcementStoreNames as a, makePlanMeta as b, executeBulkUpdateWithFkValidation as c, executeMutationPlan as d, executeNestedCreateMutation as f, executeScalarUpdateWithFkValidation as g, executeScalarCreateWithFkValidation as h, collectOnUpdateClosureStoreNames as i, IdbRecordValidationError as j, completeCreateRecord as k, executeDeleteAllWithReferentialActions as l, executeScalarCreateAllWithFkValidation as m, applyReferentialActionsForRowOnUpdate as n, collectRelatedStoreNames as o, executeNestedUpdateMutation as p, getRelation as q, collectDeleteStoreNames as r, collectScalarFkStoreNames as s, applyReferentialActionsForRow as t, executeDeleteWithReferentialActions as u, hasNestedMutationCallbacks as v, withMutationScope as w, readMutationRows as x, hasScalarFkFields as y, preserveDecimalKeySpelling as z };
1750
2131
 
1751
- //# sourceMappingURL=mutation-executor-inLFb0TE.mjs.map
2132
+ //# sourceMappingURL=mutation-executor-IuJmyWEG.mjs.map