@prisma-idb/client-idb 0.11.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 (37) hide show
  1. package/README.md +6 -0
  2. package/dist/client-auto.d.mts +2 -2
  3. package/dist/client-auto.d.mts.map +1 -1
  4. package/dist/client-auto.mjs +44 -3
  5. package/dist/client-auto.mjs.map +1 -1
  6. package/dist/client.d.mts +3 -2
  7. package/dist/client.mjs +3 -2
  8. package/dist/decimal-identity-D_1J9lPN.d.mts +21 -0
  9. package/dist/decimal-identity-D_1J9lPN.d.mts.map +1 -0
  10. package/dist/internal.d.mts +5 -4
  11. package/dist/internal.d.mts.map +1 -1
  12. package/dist/internal.mjs +2 -2
  13. package/dist/{managed-client-CxlNmERd.mjs → managed-client-DMk8ERfX.mjs} +12 -5
  14. package/dist/managed-client-DMk8ERfX.mjs.map +1 -0
  15. package/dist/{managed-client-CS78EjPu.d.mts → managed-client-eksFK4UF.d.mts} +32 -3
  16. package/dist/managed-client-eksFK4UF.d.mts.map +1 -0
  17. package/dist/mutation-executor-IuJmyWEG.mjs +2132 -0
  18. package/dist/mutation-executor-IuJmyWEG.mjs.map +1 -0
  19. package/dist/{executor-Bp-hdRg2.d.mts → mutation-scope-CE3GMR4X.d.mts} +79 -28
  20. package/dist/mutation-scope-CE3GMR4X.d.mts.map +1 -0
  21. package/dist/{idb-orm-BitJ-EMq.mjs → orm-transaction-BGx1eukw.mjs} +366 -558
  22. package/dist/orm-transaction-BGx1eukw.mjs.map +1 -0
  23. package/dist/{idb-orm-9XN3c9ly.d.mts → orm-transaction-B_ZJ92uY.d.mts} +40 -7
  24. package/dist/orm-transaction-B_ZJ92uY.d.mts.map +1 -0
  25. package/dist/orm.d.mts +13 -4
  26. package/dist/orm.d.mts.map +1 -1
  27. package/dist/orm.mjs +3 -3
  28. package/package.json +9 -9
  29. package/dist/executor-Bp-hdRg2.d.mts.map +0 -1
  30. package/dist/idb-orm-9XN3c9ly.d.mts.map +0 -1
  31. package/dist/idb-orm-BitJ-EMq.mjs.map +0 -1
  32. package/dist/managed-client-CS78EjPu.d.mts.map +0 -1
  33. package/dist/managed-client-CxlNmERd.mjs.map +0 -1
  34. package/dist/mutation-executor-SJqTuSbb.mjs +0 -1196
  35. package/dist/mutation-executor-SJqTuSbb.mjs.map +0 -1
  36. package/dist/mutation-scope-1hS0ogGa.d.mts +0 -33
  37. package/dist/mutation-scope-1hS0ogGa.d.mts.map +0 -1
@@ -1,8 +1,9 @@
1
- import { C as assertEnumValues, D as getRelation, E as getKeyPath, O as getStoreName, S as createMutationDefaultsCache, T as fieldValueToken, _ as requireTransactionExecutor, a as collectScalarFkStoreNames, b as applyCreateDefaults, c as executeDeleteWithReferentialActions, d as executeScalarCreateAllWithFkValidation, f as executeScalarCreateWithFkValidation, g as hasScalarFkFields, h as hasNestedMutationCallbacks, i as collectOnUpdateEnforcementStoreNames, l as executeNestedCreateMutation, m as hasEnforceableChildRelations, n as applyReferentialActionsForRowOnUpdate, o as executeBulkUpdateWithFkValidation, p as executeScalarUpdateWithFkValidation, s as executeDeleteAllWithReferentialActions, u as executeNestedUpdateMutation, v as validateScalarFks, w as extractKeyFromRow, x as applyUpdateDefaults, y as withMutationScope } from "./mutation-executor-SJqTuSbb.mjs";
1
+ import { A as createMutationDefaultsCache, B as resolveDecimalKey, C as validateScalarFks, D as planQuery, E as lowerRows, F as combineFilterExprs, G as fieldValueToken, J as getStoreName, K as getKeyPath, M as assertValidRecord, N as assertEnumValues, O as applyUpdateDefaults, P as buildRowComparator, S as requireTransactionExecutor, T as lowerCount, U as buildCatalog, V as withDecimalIdentityExecutor, W as extractKeyFromRow, _ as hasEnforceableChildRelations, a as collectOnUpdateEnforcementStoreNames, b as makePlanMeta, c as executeBulkUpdateWithFkValidation, d as executeMutationPlan, f as executeNestedCreateMutation, g as executeScalarUpdateWithFkValidation, h as executeScalarCreateWithFkValidation, i as collectOnUpdateClosureStoreNames, k as completeCreateRecord, l as executeDeleteAllWithReferentialActions, m as executeScalarCreateAllWithFkValidation, n as applyReferentialActionsForRowOnUpdate, o as collectRelatedStoreNames, p as executeNestedUpdateMutation, q as getRelation, r as collectDeleteStoreNames, s as collectScalarFkStoreNames, u as executeDeleteWithReferentialActions, v as hasNestedMutationCallbacks, w as withMutationScope, x as readMutationRows, y as hasScalarFkFields } from "./mutation-executor-IuJmyWEG.mjs";
2
2
  import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
3
- import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
3
  import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
5
- import { compareFieldValues, isValidIdbKey, keyEquals } from "@prisma-idb/target-idb/runtime";
4
+ import { IdbExecuteError } from "@prisma-idb/driver-idb/runtime";
5
+ import { absentAsNull, isValidIdbKey } from "@prisma-idb/target-idb/runtime";
6
+ import { andExpr, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
6
7
  //#region src/core/model-accessor.ts
7
8
  /**
8
9
  * Proxy-based typed accessor handed to `where(fn)` callbacks.
@@ -53,7 +54,9 @@ function createFieldAccessor(field) {
53
54
  */
54
55
  function createModelAccessor() {
55
56
  const cache = /* @__PURE__ */ new Map();
56
- return new Proxy({}, { get(_target, prop) {
57
+ return new Proxy({}, {
58
+ /** Reuse the field accessor within one filter callback so repeated reads share its operator surface. */
59
+ get(_target, prop) {
57
60
  if (typeof prop !== "string") return void 0;
58
61
  let acc = cache.get(prop);
59
62
  if (acc === void 0) {
@@ -94,39 +97,6 @@ function mergeAccessorState(state, overrides) {
94
97
  };
95
98
  }
96
99
  //#endregion
97
- //#region src/core/query-shaping.ts
98
- /**
99
- * Combine accumulated filter expressions with AND.
100
- *
101
- * Returns `undefined` when no filter is present so callers can skip building a
102
- * row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
103
- * and the relation loader (refined `include()` child scans).
104
- */
105
- function combineFilterExprs(filters) {
106
- if (filters.length === 0) return void 0;
107
- if (filters.length === 1) return filters[0];
108
- return andExpr(filters);
109
- }
110
- /**
111
- * Build an in-memory comparator from an `orderBy` spec (field → direction).
112
- *
113
- * Returns `undefined` when there is nothing to sort by. Compares fields in
114
- * declaration order, with {@link compareFieldValues}: the same order an
115
- * IndexedDB index uses, so equal `Date`s tie and fall through to the next
116
- * field, and `null`s sort last (first when descending).
117
- */
118
- function buildRowComparator(orderBy) {
119
- if (orderBy === void 0) return void 0;
120
- return (a, b) => {
121
- for (const [field, dir] of Object.entries(orderBy)) {
122
- const cmp = compareFieldValues(a[field], b[field]);
123
- if (cmp === 0) continue;
124
- return dir === "desc" ? -cmp : cmp;
125
- }
126
- return 0;
127
- };
128
- }
129
- //#endregion
130
100
  //#region src/core/aggregate-builder.ts
131
101
  /**
132
102
  * Build the aggregate selector factory handed to `.aggregate(agg => …)` and
@@ -179,33 +149,43 @@ function isAggregateSelector(value) {
179
149
  * - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
180
150
  * `field`; `null` when no row has a non-null value (matching Prisma's
181
151
  * "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
182
- * null handling). String-encoded numbers are coerced via `Number()`.
152
+ * null handling). Values must match the declared number codec.
183
153
  */
184
- function reduceAggregate(fn, field, rows) {
154
+ function reduceAggregate(fn, field, rows, codecId) {
185
155
  if (fn === "count") return rows.length;
186
156
  if (field === void 0) return null;
187
157
  const values = [];
188
158
  for (const row of rows) {
189
159
  const raw = row[field];
190
160
  if (raw === null || raw === void 0) continue;
191
- const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
192
- if (!Number.isNaN(n)) values.push(n);
161
+ if (typeof raw !== "number" || !Number.isFinite(raw)) throw new TypeError(`${fn}("${field}") requires finite number values`);
162
+ if (codecId === "idb/int32@1" && (!Number.isInteger(raw) || raw < -2147483648 || raw > 2147483647)) throw new TypeError(`${fn}("${field}") requires Int values in the signed 32-bit range`);
163
+ values.push(raw);
193
164
  }
194
165
  if (values.length === 0) return null;
195
166
  switch (fn) {
196
- case "sum": return values.reduce((acc, n) => acc + n, 0);
197
- case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
198
- case "min": return Math.min(...values);
199
- case "max": return Math.max(...values);
167
+ case "sum": return sumValues(values, field, codecId === "idb/int32@1");
168
+ case "avg": return sumValues(values, field, codecId === "idb/int32@1") / values.length;
169
+ case "min": return values.reduce((acc, n) => Math.min(acc, n));
170
+ case "max": return values.reduce((acc, n) => Math.max(acc, n));
171
+ }
172
+ }
173
+ /** Reject overflow before a rounded or nonfinite intermediate total can reach the result. */
174
+ function sumValues(values, field, integer) {
175
+ let total = 0;
176
+ for (const value of values) {
177
+ total += value;
178
+ if (!Number.isFinite(total) || integer && !Number.isSafeInteger(total)) throw new RangeError(`Aggregate total for "${field}" exceeds the ${integer ? "safe integer" : "finite number"} range`);
200
179
  }
180
+ return total;
201
181
  }
202
182
  /**
203
183
  * Run every selector in an aggregate spec against `rows`, producing the
204
184
  * result object keyed by the spec's aliases.
205
185
  */
206
- function computeAggregateSpec(spec, rows) {
186
+ function computeAggregateSpec(spec, rows, fields = {}) {
207
187
  const result = {};
208
- for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
188
+ for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows, fields[selector.field ?? ""]?.codecId);
209
189
  return result;
210
190
  }
211
191
  /**
@@ -223,14 +203,20 @@ function toAggregateRequests(spec) {
223
203
  }
224
204
  /**
225
205
  * Validate a user-supplied aggregate spec: it must be non-empty and every
226
- * value must be a real selector. Throws a descriptive error otherwise, so a
206
+ * value must be a real selector over a declared scalar number field (except
207
+ * count, which has no field). Throws a descriptive error otherwise, so a
227
208
  * typo (e.g. returning a plain object) fails loudly rather than silently
228
209
  * producing `NaN`/`undefined` results.
229
210
  */
230
- function assertValidAggregateSpec(spec, context) {
211
+ function assertValidAggregateSpec(spec, context, fields) {
231
212
  const entries = Object.entries(spec);
232
213
  if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
233
- for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
214
+ for (const [alias, selector] of entries) {
215
+ if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
216
+ if (selector.fn === "count") continue;
217
+ const field = typeof selector.field === "string" ? fields[selector.field] : void 0;
218
+ if (!field || field.collection || field.codecId !== "idb/int32@1" && field.codecId !== "idb/double@1") throw new TypeError(`${context} selector "${alias}" requires a declared number field (Int or Float)`);
219
+ }
234
220
  }
235
221
  //#endregion
236
222
  //#region src/core/grouped-accessor.ts
@@ -249,9 +235,11 @@ function groupToken(value) {
249
235
  }
250
236
  /** Build a grouped accessor that reduces materialized rows from the source accessor. */
251
237
  function createGroupedAccessor(init) {
252
- return { async aggregate(fn) {
238
+ return {
239
+ /** Materialize matching rows, partition them by group key, and reduce each group. */
240
+ async aggregate(fn) {
253
241
  const spec = fn(createAggregateBuilder());
254
- assertValidAggregateSpec(spec, "groupBy().aggregate()");
242
+ assertValidAggregateSpec(spec, "groupBy().aggregate()", init.fields);
255
243
  const ast = {
256
244
  kind: "groupBy",
257
245
  modelName: init.modelName,
@@ -277,464 +265,28 @@ function createGroupedAccessor(init) {
277
265
  }
278
266
  return Array.from(groups.values()).map((group) => ({
279
267
  ...group.key,
280
- ...computeAggregateSpec(spec, group.rows)
268
+ ...computeAggregateSpec(spec, group.rows, init.fields)
281
269
  }));
282
270
  } };
283
271
  }
284
272
  //#endregion
285
- //#region src/core/planner/catalog.ts
286
- const catalogs = /* @__PURE__ */ new WeakMap();
287
- /** Build a catalog once per contract identity and store name. Contracts are immutable. */
288
- function buildCatalog(contract, storeName) {
289
- const cached = catalogs.get(contract)?.get(storeName);
290
- if (cached) return cached;
291
- const store = contract.storage.stores[storeName];
292
- if (!store) throw new Error(`Store "${storeName}" is not in the contract`);
293
- const fields = {};
294
- for (const model of Object.values(domainModelsAtDefaultNamespace(contract.domain))) {
295
- if (model.storage.storeName !== storeName) continue;
296
- for (const [name, field] of Object.entries(model.fields)) fields[name] = Object.freeze({
297
- codecId: field.type.kind === "scalar" ? field.type.codecId : void 0,
298
- nullable: field.nullable,
299
- collection: field.many === true || field.dict === true
300
- });
301
- }
302
- const catalog = Object.freeze({
303
- storeName,
304
- primaryKey: source(store.keyPath, true, false),
305
- indexes: Object.freeze(Object.entries(store.indexes ?? {}).map(([name, index]) => source(index.keyPath, index.unique, index.multiEntry ?? false, name))),
306
- fields: Object.freeze(fields)
307
- });
308
- let byStore = catalogs.get(contract);
309
- if (!byStore) {
310
- byStore = /* @__PURE__ */ new Map();
311
- catalogs.set(contract, byStore);
312
- }
313
- byStore.set(storeName, catalog);
314
- return catalog;
315
- }
316
- function source(keyPath, unique, multiEntry, indexName) {
317
- const path = typeof keyPath === "string" ? keyPath : Object.freeze([...keyPath]);
318
- return Object.freeze({
319
- keyPath: path,
320
- fields: Object.freeze(typeof path === "string" ? [path] : [...path]),
321
- unique,
322
- multiEntry,
323
- ...indexName === void 0 ? {} : { indexName }
324
- });
325
- }
326
- //#endregion
327
- //#region src/core/planner/plan.ts
328
- const COST = {
329
- UNIQUE_POINT: 0,
330
- PRIMARY_POINT: 1,
331
- INDEX_POINT: 2,
332
- PREFIX_POINT: 3,
333
- TWO_SIDED_RANGE: 4,
334
- ONE_SIDED_RANGE: 5,
335
- ORDER_ONLY_SCAN: 6
336
- };
337
- const KEY_PARTIAL_CODECS = /* @__PURE__ */ new Set(["idb/double@1", "idb/date@1"]);
338
- const KEY_CODECS = /* @__PURE__ */ new Set([
339
- "idb/string@1",
340
- "idb/int32@1",
341
- "idb/double@1",
342
- "idb/date@1",
343
- "idb/decimal@1",
344
- "idb/bytes@1"
345
- ]);
346
- /** Choose the cheapest safe access without opening IndexedDB or probing cardinality. */
347
- function planQuery(catalog, request) {
348
- const normalized = normalize(request.where);
349
- if (normalized.empty) return {
350
- access: { kind: "empty" },
351
- exact: true
352
- };
353
- const orders = Object.entries(request.orderBy ?? {});
354
- if (normalized.excluded || request.hasRelationFilter || orders.length > 1) return full(false);
355
- const constraints = /* @__PURE__ */ new Map();
356
- for (const atom of normalized.atoms) {
357
- let constraint = constraints.get(atom.field);
358
- if (!constraint) {
359
- constraint = { consumed: 0 };
360
- constraints.set(atom.field, constraint);
361
- }
362
- addAtom(constraint, atom, catalog.fields[atom.field]);
363
- }
364
- for (const [name, constraint] of constraints) {
365
- if (keyPartial(catalog.fields[name]) && !constraint.rejectsNonKeys) {
366
- delete constraint.lower;
367
- delete constraint.upper;
368
- constraint.consumed = 0;
369
- }
370
- if (contradictory(constraint)) return {
371
- access: { kind: "empty" },
372
- exact: true
373
- };
374
- }
375
- const sources = [catalog.primaryKey, ...catalog.indexes];
376
- const candidates = sources.flatMap((source, declaration) => {
377
- const candidate = filteringCandidate(catalog, source, constraints, declaration);
378
- return candidate ? [candidate] : [];
379
- });
380
- candidates.sort((a, b) => a.cost - b.cost || a.ranges.length - b.ranges.length || Number(a.source.indexName !== void 0) - Number(b.source.indexName !== void 0) || a.declaration - b.declaration);
381
- let selected = candidates[0];
382
- let direction;
383
- if (orders.length === 1 && request.take !== void 0) {
384
- const [field, order] = orders[0];
385
- if (selected) {
386
- if (singleFieldSource(selected.source, field)) direction = order === "asc" ? "next" : "prev";
387
- } else {
388
- const ordering = sources.find((source) => !source.multiEntry && singleFieldSource(source, field) && completeKeyField(catalog.fields[field]));
389
- if (ordering) {
390
- selected = {
391
- source: ordering,
392
- ranges: [void 0],
393
- cost: COST.ORDER_ONLY_SCAN,
394
- consumed: 0,
395
- declaration: sources.indexOf(ordering)
396
- };
397
- direction = order === "asc" ? "next" : "prev";
398
- }
399
- }
400
- }
401
- if (!selected) {
402
- const partialOrder = orders.length === 1 && request.take !== void 0 && keyPartial(catalog.fields[orders[0][0]]);
403
- return full(normalized.atoms.length + normalized.residual === 0 && !partialOrder);
404
- }
405
- return {
406
- access: {
407
- kind: "ranges",
408
- source: selected.source,
409
- ranges: selected.ranges
410
- },
411
- exact: selected.consumed === normalized.atoms.length && normalized.residual === 0,
412
- ...direction === void 0 ? {} : { direction }
413
- };
414
- }
415
- function full(exact) {
416
- return {
417
- access: { kind: "full" },
418
- exact
419
- };
420
- }
421
- function normalize(where) {
422
- const result = {
423
- atoms: [],
424
- residual: 0,
425
- excluded: false,
426
- empty: false
427
- };
428
- function visit(expr) {
429
- switch (expr.kind) {
430
- case "field":
431
- if (expr.op === "in" && (!Array.isArray(expr.value) || expr.value.length === 0)) result.empty = true;
432
- result.atoms.push(expr);
433
- break;
434
- case "and":
435
- expr.exprs.forEach(visit);
436
- break;
437
- case "or": {
438
- if (expr.exprs.length === 0) {
439
- result.empty = true;
440
- break;
441
- }
442
- const branches = expr.exprs;
443
- const first = branches[0];
444
- if (first.kind !== "field" || !branches.every((branch) => branch.kind === "field" && branch.field === first.field && (branch.op === "eq" || branch.op === "in"))) {
445
- result.excluded = true;
446
- break;
447
- }
448
- const values = branches.flatMap((branch) => {
449
- const atom = branch;
450
- return atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
451
- });
452
- visit({
453
- kind: "field",
454
- field: first.field,
455
- op: "in",
456
- value: values
457
- });
458
- break;
459
- }
460
- case "not":
461
- result.excluded = true;
462
- break;
463
- case "null-check": result.residual++;
464
- }
465
- }
466
- if (where) visit(where);
467
- return result;
468
- }
469
- function keyTyped(field) {
470
- return field !== void 0 && !field.collection && KEY_CODECS.has(field.codecId ?? "");
471
- }
472
- function keyPartial(field) {
473
- return KEY_PARTIAL_CODECS.has(field?.codecId ?? "");
474
- }
475
- function completeKeyField(field, constraint) {
476
- return keyTyped(field) && field?.nullable === false && (!keyPartial(field) || constraint?.rejectsNonKeys === true);
477
- }
478
- function singleFieldSource(source, field) {
479
- return source.fields.length === 1 && source.fields[0] === field;
480
- }
481
- function addAtom(constraint, atom, field) {
482
- if (atom.op === "eq" || atom.op === "in") {
483
- const values = atom.op === "eq" ? [atom.value] : Array.isArray(atom.value) ? atom.value : [];
484
- if (!values.every(isValidIdbKey)) return;
485
- const points = uniqueKeys(values);
486
- constraint.points = constraint.points === void 0 ? points : constraint.points.filter((point) => points.some((value) => keyEquals(point, value)));
487
- constraint.rejectsNonKeys = true;
488
- constraint.consumed++;
489
- return;
490
- }
491
- if (!keyTyped(field)) return;
492
- if (atom.op === "startsWith") {
493
- if (field?.codecId !== "idb/string@1" || typeof atom.value !== "string") return;
494
- intersectBound(constraint, "lower", {
495
- value: atom.value,
496
- open: false
497
- });
498
- const upper = stringSuccessor(atom.value);
499
- if (upper !== void 0) intersectBound(constraint, "upper", {
500
- value: upper,
501
- open: true
502
- });
503
- constraint.startsWith = true;
504
- constraint.consumed++;
505
- } else if ([
506
- "gt",
507
- "gte",
508
- "lt",
509
- "lte"
510
- ].includes(atom.op) && isValidIdbKey(atom.value)) {
511
- const side = atom.op === "gt" || atom.op === "gte" ? "lower" : "upper";
512
- const strict = atom.op === "gt" || atom.op === "lt";
513
- intersectBound(constraint, side, {
514
- value: atom.value,
515
- open: strict
516
- });
517
- if (strict) constraint.rejectsNonKeys = true;
518
- constraint.consumed++;
519
- }
520
- }
521
- function uniqueKeys(values) {
522
- const sorted = [...values].sort(compareFieldValues);
523
- return sorted.filter((value, i) => i === 0 || compareFieldValues(sorted[i - 1], value) !== 0);
524
- }
525
- function intersectBound(constraint, side, bound) {
526
- const previous = constraint[side];
527
- const comparison = previous === void 0 ? 0 : compareFieldValues(bound.value, previous.value);
528
- if (!previous || (side === "lower" ? comparison > 0 : comparison < 0) || comparison === 0 && bound.open) constraint[side] = bound;
529
- }
530
- function insideBounds(value, constraint) {
531
- const lower = constraint.lower;
532
- const upper = constraint.upper;
533
- if (lower && (compareFieldValues(value, lower.value) < 0 || lower.open && keyEquals(value, lower.value))) return false;
534
- if (upper && (compareFieldValues(value, upper.value) > 0 || upper.open && keyEquals(value, upper.value))) return false;
535
- return true;
536
- }
537
- function contradictory(constraint) {
538
- if (constraint.points) {
539
- constraint.points = constraint.points.filter((value) => insideBounds(value, constraint));
540
- return constraint.points.length === 0;
541
- }
542
- if (!constraint.lower || !constraint.upper) return false;
543
- const comparison = compareFieldValues(constraint.lower.value, constraint.upper.value);
544
- return comparison > 0 || comparison === 0 && (constraint.lower.open || constraint.upper.open);
545
- }
546
- function filteringCandidate(catalog, source, constraints, declaration) {
547
- if (source.multiEntry || source.fields.length === 0) return void 0;
548
- if (source.fields.some((name) => keyPartial(catalog.fields[name]) && !constraints.get(name)?.rejectsNonKeys)) return void 0;
549
- let prefixes = [[]];
550
- let consumed = 0;
551
- let position = 0;
552
- while (position < source.fields.length) {
553
- const constraint = constraints.get(source.fields[position]);
554
- if (constraint?.points === void 0) break;
555
- if (prefixes.length * constraint.points.length > 1024) return void 0;
556
- prefixes = prefixes.flatMap((prefix) => constraint.points.map((point) => [...prefix, point]));
557
- consumed += constraint.consumed;
558
- position++;
559
- }
560
- const constraint = constraints.get(source.fields[position] ?? "");
561
- const hasRange = constraint !== void 0 && (constraint.lower !== void 0 || constraint.upper !== void 0);
562
- if (position === 0 && !hasRange) return void 0;
563
- const used = position + Number(hasRange);
564
- if (!source.fields.slice(used).every((field) => completeKeyField(catalog.fields[field], constraints.get(field)))) return void 0;
565
- const compound = typeof source.keyPath !== "string";
566
- if (position === source.fields.length) return {
567
- source,
568
- ranges: prefixes.map((prefix) => ({
569
- kind: "only",
570
- key: compound ? prefix : prefix[0]
571
- })),
572
- cost: source.indexName !== void 0 && source.unique ? COST.UNIQUE_POINT : source.indexName === void 0 ? COST.PRIMARY_POINT : COST.INDEX_POINT,
573
- consumed,
574
- declaration
575
- };
576
- if (hasRange) consumed += constraint.consumed;
577
- const ranges = prefixes.map((prefix) => compound ? compoundRange(prefix, constraint, source.fields.length - used) : scalarRange(constraint));
578
- if (ranges.some((range) => range.kind === "bound" && (compareFieldValues(range.lower, range.upper) > 0 || keyEquals(range.lower, range.upper) && (range.lowerOpen || range.upperOpen)))) return void 0;
579
- return {
580
- source,
581
- ranges,
582
- cost: hasRange ? constraint.lower && constraint.upper && !constraint.startsWith ? COST.TWO_SIDED_RANGE : COST.ONE_SIDED_RANGE : COST.PREFIX_POINT,
583
- consumed,
584
- declaration
585
- };
586
- }
587
- function scalarRange(constraint) {
588
- if (constraint.lower && constraint.upper) return {
589
- kind: "bound",
590
- lower: constraint.lower.value,
591
- upper: constraint.upper.value,
592
- lowerOpen: constraint.lower.open,
593
- upperOpen: constraint.upper.open
594
- };
595
- if (constraint.lower) return {
596
- kind: "lower",
597
- key: constraint.lower.value,
598
- open: constraint.lower.open
599
- };
600
- return {
601
- kind: "upper",
602
- key: constraint.upper.value,
603
- open: constraint.upper.open
604
- };
605
- }
606
- function compoundRange(prefix, constraint, trailing) {
607
- const lower = constraint?.lower;
608
- const upper = constraint?.upper;
609
- return {
610
- kind: "bound",
611
- lower: lower ? [
612
- ...prefix,
613
- lower.value,
614
- ...lower.open && trailing > 0 ? [[]] : []
615
- ] : prefix.length ? prefix : [-Infinity],
616
- upper: upper ? [
617
- ...prefix,
618
- upper.value,
619
- ...!upper.open && trailing > 0 ? [[]] : []
620
- ] : [...prefix, []],
621
- lowerOpen: lower?.open ?? false,
622
- upperOpen: upper ? upper.open || trailing > 0 : true
623
- };
624
- }
625
- function stringSuccessor(prefix) {
626
- for (let i = prefix.length - 1; i >= 0; i--) {
627
- const code = prefix.charCodeAt(i);
628
- if (code < 65535) return prefix.slice(0, i) + String.fromCharCode(code + 1);
629
- }
273
+ //#region src/core/unique-lookup.ts
274
+ /**
275
+ * Copy the own enumerable properties of an upsert `where` once. Validation and
276
+ * filter parsing then see the same values even when a property is a getter.
277
+ * Non-object inputs pass through so `assertUniqueLookup` can reject them.
278
+ */
279
+ function snapshotLookup(where) {
280
+ return typeof where === "object" && where !== null && !Array.isArray(where) ? Object.fromEntries(Object.entries(where)) : where;
630
281
  }
631
- //#endregion
632
- //#region src/core/planner/lower.ts
633
282
  /**
634
- * Lower row access without creating IDBKeyRange objects or awaiting work.
635
- * Run `finish` after the executor has materialized all returned rows.
636
- * Without `orderBy`, row order and pagination selection are unspecified.
637
- * Rows tied on every ordering field may appear in any order, including at
638
- * pagination boundaries. Add a unique ordering field for deterministic pages.
283
+ * Require equality on a complete native unique key before upsert can mutate.
284
+ * Null and other non-key values are absent from IndexedDB indexes, so even a
285
+ * declared unique index cannot make those filters identify at most one row.
639
286
  */
640
- function lowerRows(catalog, logical, meta, request, additionalFilter) {
641
- const filter = (row) => (request.where === void 0 || evaluateFilter(request.where, row)) && (additionalFilter === void 0 || additionalFilter(row));
642
- const comparator = logical.direction === void 0 ? buildRowComparator(request.orderBy) : void 0;
643
- const base = {
644
- meta,
645
- storeName: catalog.storeName
646
- };
647
- const pagination = {
648
- ...request.skip === void 0 ? {} : { skip: request.skip },
649
- ...request.take === void 0 ? {} : { take: request.take }
650
- };
651
- let shapedByDriver = false;
652
- let idbPlan;
653
- switch (logical.access.kind) {
654
- case "empty":
655
- idbPlan = {
656
- meta,
657
- kind: "batch",
658
- storeNames: [catalog.storeName],
659
- ops: []
660
- };
661
- break;
662
- case "full":
663
- idbPlan = {
664
- ...base,
665
- kind: "cursor-scan",
666
- filter,
667
- ...comparator === void 0 ? {} : { comparator },
668
- ...pagination
669
- };
670
- shapedByDriver = true;
671
- break;
672
- case "ranges": {
673
- const { source } = logical.access;
674
- const ranges = [...logical.access.ranges];
675
- if (logical.direction === "prev") ranges.reverse();
676
- const single = ranges.length === 1;
677
- const ops = ranges.map((range) => {
678
- if (range?.kind === "only" && source.indexName === void 0) return {
679
- ...base,
680
- kind: "key-get",
681
- key: range.key
682
- };
683
- const access = {
684
- ...base,
685
- ...source.indexName === void 0 ? {} : { indexName: source.indexName },
686
- ...range === void 0 ? {} : { range }
687
- };
688
- if (!single && logical.direction !== void 0 && request.take !== void 0) return {
689
- ...access,
690
- kind: "cursor-scan",
691
- direction: logical.direction,
692
- filter,
693
- take: (request.skip ?? 0) + request.take
694
- };
695
- if (single && request.take !== void 0) {
696
- shapedByDriver = true;
697
- return {
698
- ...access,
699
- kind: "cursor-scan",
700
- filter,
701
- ...logical.direction === void 0 ? {} : { direction: logical.direction },
702
- ...comparator === void 0 ? {} : { comparator },
703
- ...pagination
704
- };
705
- }
706
- if (logical.direction === "prev") return {
707
- ...access,
708
- kind: "cursor-scan",
709
- direction: "prev",
710
- filter
711
- };
712
- return {
713
- ...access,
714
- kind: "get-all"
715
- };
716
- });
717
- idbPlan = single ? ops[0] : {
718
- meta,
719
- kind: "batch",
720
- storeNames: [catalog.storeName],
721
- ops
722
- };
723
- break;
724
- }
725
- }
726
- return {
727
- idbPlan,
728
- finish(rows) {
729
- let result = rows.filter(filter);
730
- if (!shapedByDriver) {
731
- if (comparator !== void 0) result.sort(comparator);
732
- const skip = request.skip ?? 0;
733
- result = result.slice(skip, request.take === void 0 ? void 0 : skip + request.take);
734
- }
735
- return result;
736
- }
737
- };
287
+ function assertUniqueLookup(catalog, where) {
288
+ const sources = [catalog.primaryKey, ...catalog.indexes];
289
+ if (!(typeof where === "object" && where !== null && !Array.isArray(where) && sources.some((source) => source.unique && !source.multiEntry && source.fields.length > 0 && source.fields.every((field) => Object.prototype.propertyIsEnumerable.call(where, field) && isValidIdbKey(where[field]))))) throw new Error(`Invalid upsert where for "${catalog.storeName}": provide a complete primary key or unique index with non-null IndexedDB key values`);
738
290
  }
739
291
  //#endregion
740
292
  //#region src/core/relation-loader.ts
@@ -797,7 +349,7 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
797
349
  const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
798
350
  const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
799
351
  const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
800
- const catalog = buildCatalog(contract, relatedStoreName);
352
+ const catalog = buildCatalog(contract, relatedStoreName, relatedModelName);
801
353
  const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
802
354
  const token = tupleToken(row, targetFields);
803
355
  return token !== null && localTuples.has(token);
@@ -806,10 +358,10 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
806
358
  meta: planMeta,
807
359
  idbPlan: lowered.idbPlan
808
360
  };
809
- return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
361
+ return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation, catalog.fields);
810
362
  }
811
363
  /** Attach related rows by their join tuple, shaping each parent's collection independently. */
812
- function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
364
+ function attachRelatedRows(relName, entry, rows, relatedRows, relation, fields) {
813
365
  const { cardinality, on: { localFields, targetFields } } = relation;
814
366
  if (cardinality === "1:N") {
815
367
  const grouped = /* @__PURE__ */ new Map();
@@ -828,7 +380,7 @@ function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
828
380
  ...row,
829
381
  [relName]: groupFor(row).length
830
382
  }));
831
- const comparator = buildRowComparator(entry.state.orderBy);
383
+ const comparator = buildRowComparator(entry.state.orderBy, fields);
832
384
  const skip = entry.state.skip ?? 0;
833
385
  const take = entry.state.take;
834
386
  return rows.map((row) => {
@@ -900,6 +452,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
900
452
  this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
901
453
  this.#includeRefinementMode = includeRefinementMode;
902
454
  }
455
+ /** Add a callback or shorthand predicate to a new accessor without mutating this one. */
903
456
  where(filter) {
904
457
  const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
905
458
  if (expr === void 0) return this.#clone({});
@@ -914,6 +467,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
914
467
  skip(n) {
915
468
  return this.#clone({ skip: n });
916
469
  }
470
+ /** Add a collection or count refinement while preserving this accessor state. */
917
471
  include(relation, refineFn) {
918
472
  const entry = this.#resolveIncludeEntry(relation, refineFn);
919
473
  const newState = mergeAccessorState(this.#state, { includes: {
@@ -922,9 +476,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
922
476
  } });
923
477
  return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
924
478
  }
479
+ /** Narrow scalar output fields while retaining included relations. */
925
480
  select(...fields) {
926
481
  return this.#clone({ selectedFields: fields });
927
482
  }
483
+ /** Read matching rows, load includes, and project fields before yielding. */
928
484
  all() {
929
485
  const groupingKey = this.#newGroupingKey();
930
486
  const buildScanPlan = this.#buildScanPlan.bind(this);
@@ -942,9 +498,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
942
498
  async first() {
943
499
  return this.take(1).all().first();
944
500
  }
501
+ /** Reduce all filtered rows; query pagination does not constrain aggregates. */
945
502
  async aggregate(fn) {
946
503
  const spec = fn(createAggregateBuilder());
947
- assertValidAggregateSpec(spec, "aggregate()");
504
+ const { fields } = buildCatalog(this.#contract, this.#storeName, this.#modelName);
505
+ assertValidAggregateSpec(spec, "aggregate()", fields);
948
506
  const combined = this.#combinedFilterExpr();
949
507
  const ast = {
950
508
  kind: "aggregate",
@@ -952,18 +510,21 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
952
510
  aggregates: toAggregateRequests(spec),
953
511
  ...combined !== void 0 ? { where: combined } : {}
954
512
  };
955
- return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
513
+ return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast), fields);
956
514
  }
515
+ /** Carry this filter into a grouped accessor whose materialization is owned here. */
957
516
  groupBy(...fields) {
958
517
  const combined = this.#combinedFilterExpr();
959
518
  const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
960
519
  return createGroupedAccessor({
961
520
  modelName: this.#modelName,
962
521
  by: fields,
522
+ fields: buildCatalog(this.#contract, this.#storeName, this.#modelName).fields,
963
523
  where: combined,
964
524
  materialize
965
525
  });
966
526
  }
527
+ /** Complete and validate one record, using a transaction when relations need checks. */
967
528
  async create(data) {
968
529
  const record = data;
969
530
  if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
@@ -972,8 +533,9 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
972
533
  modelName: this.#modelName,
973
534
  data: record
974
535
  });
975
- const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
536
+ const withDefaults = completeCreateRecord(this.#contract, this.#modelName, record, createMutationDefaultsCache());
976
537
  assertEnumValues(this.#contract, this.#modelName, withDefaults);
538
+ assertValidRecord(this.#contract, this.#modelName, "create", withDefaults);
977
539
  if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
978
540
  executor: requireTransactionExecutor(this.#executor),
979
541
  contract: this.#contract,
@@ -996,38 +558,29 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
996
558
  record: withDefaults
997
559
  }
998
560
  };
999
- for await (const row of this.#executor.query(plan)) return row;
1000
- return withDefaults;
561
+ const [row] = await this.#executor.query(plan).toArray();
562
+ return row ?? withDefaults;
1001
563
  }
564
+ /** Resolve a primary key numerically for Decimal fields while retaining its stored spelling. */
1002
565
  async findUnique(key) {
1003
- const groupingKey = this.#newGroupingKey();
1004
- const meta = this.#planMeta(groupingKey);
566
+ if (!isValidIdbKey(key)) return null;
567
+ const meta = this.#planMeta(this.#newGroupingKey());
1005
568
  const ast = {
1006
569
  kind: "findUnique",
1007
570
  modelName: this.#modelName,
1008
571
  key
1009
572
  };
1010
- const keyPath = getKeyPath(this.#contract, this.#modelName);
1011
- const fields = typeof keyPath === "string" ? [keyPath] : keyPath;
1012
- const values = typeof keyPath === "string" ? [key] : key;
1013
- const where = combineFilterExprs(fields.map((field, i) => ({
1014
- kind: "field",
1015
- field,
1016
- op: "eq",
1017
- value: values[i]
1018
- })));
1019
- const request = {
1020
- ...where === void 0 ? {} : { where },
1021
- take: 1
1022
- };
1023
- const lowered = this.#lowerRows(meta, request);
1024
- const rows = await this.#executor.query({
573
+ const row = await resolveDecimalKey({ execute: (body) => this.#executor.query({
1025
574
  meta,
1026
575
  ast,
1027
- idbPlan: lowered.idbPlan
1028
- }).toArray();
1029
- return lowered.finish(rows)[0] ?? null;
576
+ idbPlan: {
577
+ ...body,
578
+ meta
579
+ }
580
+ }).toArray() }, this.#contract, this.#modelName, key);
581
+ return row === void 0 ? null : absentAsNull(this.#contract, this.#modelName)(row);
1030
582
  }
583
+ /** Delete a keyed row with child actions when its relations require them. */
1031
584
  async delete(key) {
1032
585
  if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
1033
586
  await executeDeleteWithReferentialActions({
@@ -1056,6 +609,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1056
609
  };
1057
610
  await this.#executor.query(plan).toArray();
1058
611
  }
612
+ /** Update the first matching row with scalar or nested relation validation. */
1059
613
  async update(patch) {
1060
614
  const patchRecord = patch;
1061
615
  if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
@@ -1103,10 +657,12 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1103
657
  async updateCount(patch) {
1104
658
  return (await this.updateAll(patch).toArray()).length;
1105
659
  }
660
+ /** Select and apply the create or update arm inside one transaction. */
1106
661
  async upsert(args) {
662
+ const where = snapshotLookup(args.where);
663
+ assertUniqueLookup(buildCatalog(this.#contract, this.#storeName, this.#modelName), where);
1107
664
  const keyPath = getKeyPath(this.#contract, this.#modelName);
1108
- const whereExpr = shorthandToFilterExpr(args.where);
1109
- const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
665
+ const whereExpr = shorthandToFilterExpr(where);
1110
666
  const meta = this.#planMeta(this.#newGroupingKey());
1111
667
  const createRecord = args.create;
1112
668
  const patchRecord = args.update;
@@ -1114,9 +670,11 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1114
670
  const executionDefaults = this.#contract.execution?.mutations.defaults;
1115
671
  const exec = requireTransactionExecutor(this.#executor);
1116
672
  const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
1117
- const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
673
+ const createWithDefaults = completeCreateRecord(this.#contract, this.#modelName, createRecord, createMutationDefaultsCache());
1118
674
  assertEnumValues(this.#contract, this.#modelName, effectivePatch);
1119
675
  assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
676
+ assertValidRecord(this.#contract, this.#modelName, "update", effectivePatch);
677
+ assertValidRecord(this.#contract, this.#modelName, "create", createWithDefaults);
1120
678
  const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
1121
679
  const storeNames = [.../* @__PURE__ */ new Set([
1122
680
  storeName,
@@ -1125,13 +683,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1125
683
  ...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
1126
684
  ])];
1127
685
  return withMutationScope(exec, storeNames, async (scope) => {
1128
- const existing = (await scope.execute({
1129
- meta,
1130
- kind: "cursor-scan",
1131
- storeName,
1132
- filter: matches,
1133
- take: 1
1134
- }))[0];
686
+ const existing = (await readMutationRows(scope, this.#contract, this.#modelName, whereExpr, 1))[0];
1135
687
  if (existing === void 0) {
1136
688
  await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
1137
689
  return (await scope.execute({
@@ -1144,19 +696,20 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1144
696
  await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
1145
697
  const key = extractKeyFromRow(existing, keyPath);
1146
698
  await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
1147
- return (await scope.execute({
699
+ const rows = await scope.execute({
1148
700
  meta,
1149
701
  kind: "update",
1150
702
  storeName,
1151
703
  key,
1152
704
  patch: effectivePatch
1153
- }))[0] ?? existing;
705
+ });
706
+ return absentAsNull(this.#contract, this.#modelName)(rows[0] ?? existing);
1154
707
  });
1155
708
  }
709
+ /** Complete a batch with shared query defaults and insert it atomically. */
1156
710
  createAll(data) {
1157
711
  const defaultsCache = createMutationDefaultsCache();
1158
- const executionDefaults = this.#contract.execution?.mutations.defaults;
1159
- const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
712
+ const records = data.map((d) => completeCreateRecord(this.#contract, this.#modelName, d, defaultsCache));
1160
713
  if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
1161
714
  const executor = requireTransactionExecutor(this.#executor);
1162
715
  const contract = this.#contract;
@@ -1196,7 +749,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1196
749
  const contract = this.#contract;
1197
750
  const modelName = this.#modelName;
1198
751
  return new AsyncIterableResult((async function* () {
1199
- for (const record of records) assertEnumValues(contract, modelName, record);
752
+ for (const record of records) {
753
+ assertEnumValues(contract, modelName, record);
754
+ assertValidRecord(contract, modelName, "create", record);
755
+ }
1200
756
  for await (const row of executorQuery(plan)) yield row;
1201
757
  })());
1202
758
  }
@@ -1215,7 +771,6 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1215
771
  */
1216
772
  deleteAll() {
1217
773
  const combined = this.#combinedFilterExpr();
1218
- const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1219
774
  const contract = this.#contract;
1220
775
  const modelName = this.#modelName;
1221
776
  const executor = requireTransactionExecutor(this.#executor);
@@ -1224,7 +779,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1224
779
  executor,
1225
780
  contract,
1226
781
  modelName,
1227
- ...filter !== void 0 ? { filter } : {}
782
+ ...combined === void 0 ? {} : { where: combined }
1228
783
  });
1229
784
  for (const row of rows) yield row;
1230
785
  })());
@@ -1232,23 +787,34 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1232
787
  async deleteCount() {
1233
788
  return (await this.deleteAll().toArray()).length;
1234
789
  }
790
+ /** Return an include-count descriptor during refinement, or execute a top-level count. */
1235
791
  count() {
1236
792
  if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
1237
793
  return this.#countTerminal();
1238
794
  }
795
+ /** Count this filtered page through the planner, without applying select or include. */
1239
796
  async #countTerminal() {
1240
- const scan = this.#buildScanPlan(this.#newGroupingKey());
1241
797
  const combined = this.#combinedFilterExpr();
798
+ const { skip, take } = this.#state;
799
+ const request = {
800
+ ...combined === void 0 ? {} : { where: combined },
801
+ ...skip === void 0 ? {} : { skip },
802
+ ...take === void 0 ? {} : { take }
803
+ };
804
+ const meta = this.#planMeta(this.#newGroupingKey());
805
+ const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
806
+ const lowered = lowerCount(catalog, planQuery(catalog, request), meta, request);
1242
807
  const ast = {
1243
808
  kind: "count",
1244
809
  modelName: this.#modelName,
1245
810
  ...combined !== void 0 ? { where: combined } : {}
1246
811
  };
1247
812
  const rows = await this.#executor.query({
1248
- ...scan.plan,
1249
- ast
813
+ meta,
814
+ ast,
815
+ idbPlan: lowered.idbPlan
1250
816
  }).toArray();
1251
- return scan.finish(rows).length;
817
+ return lowered.finish(rows);
1252
818
  }
1253
819
  /**
1254
820
  * Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
@@ -1294,9 +860,10 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1294
860
  return lowered.finish(rows);
1295
861
  }
1296
862
  #lowerRows(meta, request) {
1297
- const catalog = buildCatalog(this.#contract, this.#storeName);
863
+ const catalog = buildCatalog(this.#contract, this.#storeName, this.#modelName);
1298
864
  return lowerRows(catalog, planQuery(catalog, request), meta, request);
1299
865
  }
866
+ /** Pair the query intent with its planned driver operation and result shaping. */
1300
867
  #buildScanPlan(groupingKey) {
1301
868
  const combined = this.#combinedFilterExpr();
1302
869
  const meta = this.#planMeta(groupingKey);
@@ -1327,6 +894,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1327
894
  #combinedFilterExpr() {
1328
895
  return combineFilterExprs(this.#state.filters);
1329
896
  }
897
+ /** Batch-load each requested relation using the unprojected parent rows. */
1330
898
  async #applyIncludes(rows, groupingKey) {
1331
899
  const relNames = Object.keys(this.#state.includes);
1332
900
  if (relNames.length === 0) return rows;
@@ -1352,6 +920,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1352
920
  return out;
1353
921
  });
1354
922
  }
923
+ /** Attach the contract hash and grouping key shared by this operation. */
1355
924
  #planMeta(groupingKey) {
1356
925
  return {
1357
926
  target: "idb",
@@ -1360,6 +929,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1360
929
  annotations: { groupingKey }
1361
930
  };
1362
931
  }
932
+ /** Preserve shared runtime wiring while replacing only the accessor state. */
1363
933
  #clone(overrides) {
1364
934
  return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
1365
935
  }
@@ -1379,7 +949,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1379
949
  * import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
1380
950
  * import contract from "./prisma/idb-contract";
1381
951
  *
1382
- * const runtime = createIdbRuntime({ adapter, driver });
952
+ * const runtime = createIdbRuntime({ adapter, driver, contract });
1383
953
  * const db = idbOrm({ contract, executor: runtime });
1384
954
  *
1385
955
  * // Typed query builder:
@@ -1388,7 +958,8 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1388
958
  * ```
1389
959
  */
1390
960
  function idbOrm(options) {
1391
- const { contract, executor } = options;
961
+ const { contract } = options;
962
+ const executor = withDecimalIdentityExecutor(contract, options.executor);
1392
963
  let _key = 0;
1393
964
  const newGroupingKey = () => `idb-op-${++_key}`;
1394
965
  const client = {};
@@ -1396,6 +967,243 @@ function idbOrm(options) {
1396
967
  return client;
1397
968
  }
1398
969
  //#endregion
1399
- export { idbOrm as t };
970
+ //#region src/core/open-transaction.ts
971
+ /**
972
+ * An executor bound to one already-open IDB transaction.
973
+ *
974
+ * `db.transaction()` hands its callback ORM accessors built on this executor.
975
+ * Every query the accessors issue runs on the open transaction instead of
976
+ * opening a new one. Operations that normally open their own mutation scope
977
+ * (FK-checked writes, cascade deletes) get a view of the same transaction:
978
+ * `commit()` on the view does nothing, so an inner operation cannot commit the
979
+ * outer transaction, and `rollback()` aborts it.
980
+ *
981
+ * The executor also records how the transaction ended, so the runner can tell
982
+ * a rolled-back transaction from one that IndexedDB auto-committed early
983
+ * (ADR 005).
984
+ */
985
+ function isTransactionInactive(error) {
986
+ return error instanceof IdbExecuteError && error.code === "TRANSACTION_INACTIVE";
987
+ }
988
+ /** Bind ORM operations to one scope and record aborts or early commits. */
989
+ var OpenTransaction = class {
990
+ #scope;
991
+ #storeNames;
992
+ #meta;
993
+ #state = "open";
994
+ #autoCommitCause;
995
+ #ended = false;
996
+ constructor(scope, storeNames, meta) {
997
+ this.#scope = scope;
998
+ this.#storeNames = new Set(storeNames);
999
+ this.#meta = meta;
1000
+ }
1001
+ /** Stop accepting operations. Called once the callback has settled. */
1002
+ end() {
1003
+ this.#ended = true;
1004
+ }
1005
+ /** The error of the first operation that found the transaction already finished, if any. */
1006
+ get autoCommitCause() {
1007
+ return this.#autoCommitCause;
1008
+ }
1009
+ /**
1010
+ * Whether IndexedDB has already finished this transaction on its own, so a
1011
+ * rollback can no longer undo its writes. Probes the transaction when no
1012
+ * operation has failed yet, and records the probe error as the cause, so call
1013
+ * it before rolling back or committing.
1014
+ */
1015
+ async hasAutoCommitted() {
1016
+ if (this.#state !== "open") return this.#state === "auto-committed";
1017
+ const [storeName] = this.#storeNames;
1018
+ try {
1019
+ await this.#scope.execute({
1020
+ meta: this.#meta,
1021
+ kind: "key-get",
1022
+ storeName,
1023
+ key: ""
1024
+ });
1025
+ return false;
1026
+ } catch (error) {
1027
+ if (!isTransactionInactive(error)) return false;
1028
+ this.#state = "auto-committed";
1029
+ this.#autoCommitCause = error;
1030
+ return true;
1031
+ }
1032
+ }
1033
+ /** Expose rows from the existing scope through the ORM query-result interface. */
1034
+ query(plan) {
1035
+ const run = executeMutationPlan.bind(null, this);
1036
+ return new AsyncIterableResult((async function* () {
1037
+ for (const row of await run(plan.idbPlan)) yield row;
1038
+ })());
1039
+ }
1040
+ /** Reuse the outer scope after verifying that the requested stores were declared. */
1041
+ async transaction(storeNames) {
1042
+ this.#assertNotEnded();
1043
+ try {
1044
+ this.#assertStoresListed(storeNames);
1045
+ } catch (error) {
1046
+ this.#recordFailure(error);
1047
+ throw error;
1048
+ }
1049
+ return this;
1050
+ }
1051
+ /** Run one operation in the scope and abort the transaction when it fails. */
1052
+ async execute(plan) {
1053
+ this.#assertNotEnded();
1054
+ try {
1055
+ this.#assertStoresListed([plan.storeName]);
1056
+ return await this.#scope.execute(plan);
1057
+ } catch (error) {
1058
+ this.#recordFailure(error);
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ async commit() {}
1063
+ rollback() {
1064
+ this.#recordFailure(void 0);
1065
+ }
1066
+ /**
1067
+ * Wrap an ORM accessor so that any call that fails aborts the transaction.
1068
+ *
1069
+ * Some failures, such as enum validation, happen before an operation reaches
1070
+ * this executor. Without this wrapper the callback could catch them and
1071
+ * commit the writes made before. The wrapper covers errors thrown
1072
+ * synchronously, rejected promises, and rejections of a result that is
1073
+ * awaited directly (`AsyncIterableResult`). Objects the accessor returns,
1074
+ * such as `where()` builders, are wrapped too.
1075
+ */
1076
+ abortOnFailure(target) {
1077
+ return new Proxy(target, { get: (object, property) => {
1078
+ const value = Reflect.get(object, property, object);
1079
+ if (typeof value !== "function") return value;
1080
+ if (property === "then") return this.#observeThen(object, value);
1081
+ return (...args) => {
1082
+ const result = this.#recordThrown(() => value.apply(object, args));
1083
+ if (result instanceof Promise) {
1084
+ result.catch((error) => this.#recordFailure(error));
1085
+ return result;
1086
+ }
1087
+ return typeof result === "object" && result !== null ? this.abortOnFailure(result) : result;
1088
+ };
1089
+ } });
1090
+ }
1091
+ /**
1092
+ * `await result` passes its own rejection handler to `then()`, which turns the
1093
+ * rejection into a fulfilled promise. Record the failure before that handler runs.
1094
+ */
1095
+ #observeThen(object, then) {
1096
+ return (onFulfilled, onRejected) => then.call(object, onFulfilled, (error) => {
1097
+ this.#recordFailure(error);
1098
+ if (typeof onRejected !== "function") throw error;
1099
+ return onRejected(error);
1100
+ });
1101
+ }
1102
+ /** Record synchronous accessor failures before propagating them to the callback. */
1103
+ #recordThrown(call) {
1104
+ try {
1105
+ return call();
1106
+ } catch (error) {
1107
+ this.#recordFailure(error);
1108
+ throw error;
1109
+ }
1110
+ }
1111
+ #assertNotEnded() {
1112
+ if (this.#ended) throw new Error("This transaction has ended. Do not use `tx` after the db.transaction() callback finishes.");
1113
+ }
1114
+ /** Reject undeclared stores before an operation can escape the transaction scope. */
1115
+ #assertStoresListed(storeNames) {
1116
+ const missing = storeNames.filter((name) => !this.#storeNames.has(name));
1117
+ if (missing.length > 0) throw new Error(`Stores ${missing.map((name) => `"${name}"`).join(", ")} are not part of this transaction. List the models that own them in the first argument of db.transaction().`);
1118
+ }
1119
+ /** Any failed operation aborts the whole transaction, even if the callback catches the error. */
1120
+ #recordFailure(error) {
1121
+ if (this.#state !== "open") return;
1122
+ if (isTransactionInactive(error)) {
1123
+ this.#state = "auto-committed";
1124
+ this.#autoCommitCause = error;
1125
+ return;
1126
+ }
1127
+ this.#state = "aborted";
1128
+ this.#scope.rollback();
1129
+ }
1130
+ };
1131
+ //#endregion
1132
+ //#region src/core/orm-transaction.ts
1133
+ /**
1134
+ * Thrown when IndexedDB finished the transaction before the callback did, so
1135
+ * writes made before that point were committed and could not be rolled back.
1136
+ *
1137
+ * IndexedDB commits a transaction as soon as it has no pending request. That
1138
+ * happens when the callback awaits something other than a `tx` operation, such
1139
+ * as a timer or a network call (ADR 005). `cause` holds the original error.
1140
+ */
1141
+ var IdbTransactionCommittedEarlyError = class extends Error {
1142
+ constructor(cause) {
1143
+ super("The transaction was committed before the db.transaction() callback finished, so writes made earlier in the callback were saved and not rolled back. Await only `tx` operations inside the callback; do not await timers or network calls.", { cause });
1144
+ this.name = "IdbTransactionCommittedEarlyError";
1145
+ }
1146
+ };
1147
+ /**
1148
+ * Run `fn` inside one readwrite transaction over the models behind `rootKeys`.
1149
+ *
1150
+ * Besides the listed models, the transaction opens the stores their operations
1151
+ * need: parents for foreign-key checks, children for referential actions, and
1152
+ * related models for `include`. An operation that needs any other store fails
1153
+ * with an error that names it.
1154
+ */
1155
+ async function runOrmTransaction(options, rootKeys, fn) {
1156
+ const { contract, executor } = options;
1157
+ if (rootKeys.length === 0) throw new Error("db.transaction() needs at least one model to open the transaction on.");
1158
+ const modelNames = rootKeys.map((key) => {
1159
+ const root = contract.roots[key];
1160
+ if (root === void 0) throw new Error(`db.transaction(): "${key}" is not a root of the contract.`);
1161
+ return root.model;
1162
+ });
1163
+ const storeNames = [...new Set(modelNames.flatMap((model) => storeNamesNeededBy(contract, model)))];
1164
+ const scope = await executor.transaction(storeNames, "readwrite");
1165
+ const open = new OpenTransaction(scope, storeNames, makePlanMeta(contract));
1166
+ let result;
1167
+ try {
1168
+ result = await fn(pickRoots(idbOrm({
1169
+ contract,
1170
+ executor: open
1171
+ }), rootKeys, open));
1172
+ } catch (error) {
1173
+ open.end();
1174
+ if (await open.hasAutoCommitted()) await failCommittedEarly(scope, error);
1175
+ scope.rollback();
1176
+ throw error;
1177
+ }
1178
+ open.end();
1179
+ if (await open.hasAutoCommitted()) await failCommittedEarly(scope, open.autoCommitCause);
1180
+ await scope.commit();
1181
+ return result;
1182
+ }
1183
+ /**
1184
+ * Reports a transaction that IndexedDB committed before the callback ended. The writes
1185
+ * persisted, so the scope is committed rather than rolled back: a tracked scope only
1186
+ * announces its writes (for example, sync outbox notifications) when it commits.
1187
+ */
1188
+ async function failCommittedEarly(scope, cause) {
1189
+ await scope.commit().catch(() => {});
1190
+ throw new IdbTransactionCommittedEarlyError(cause);
1191
+ }
1192
+ /** Collect stores used by relation reads, child actions, and cascading updates. */
1193
+ function storeNamesNeededBy(contract, modelName) {
1194
+ return [
1195
+ ...collectRelatedStoreNames(contract, modelName),
1196
+ ...collectDeleteStoreNames(contract, modelName),
1197
+ ...collectOnUpdateClosureStoreNames(contract, modelName)
1198
+ ];
1199
+ }
1200
+ /** Expose only the requested roots and abort the scope on any accessor failure. */
1201
+ function pickRoots(orm, rootKeys, open) {
1202
+ const picked = {};
1203
+ for (const key of rootKeys) picked[key] = open.abortOnFailure(orm[key]);
1204
+ return picked;
1205
+ }
1206
+ //#endregion
1207
+ export { runOrmTransaction as n, idbOrm as r, IdbTransactionCommittedEarlyError as t };
1400
1208
 
1401
- //# sourceMappingURL=idb-orm-BitJ-EMq.mjs.map
1209
+ //# sourceMappingURL=orm-transaction-BGx1eukw.mjs.map