@prisma-idb/client-idb 0.9.1 → 0.10.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.
@@ -0,0 +1,1256 @@
1
+ import { A as isValidIdbKey, C as assertEnumValues, D as getKeyPath, E as fieldValueToken, M as keyToken, O as getRelation, S as createMutationDefaultsCache, T as extractKeyFromRow, _ 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, k as getStoreName, 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 buildFieldToIndexMap, x as applyUpdateDefaults, y as withMutationScope } from "./mutation-executor-CD5n_kKT.mjs";
2
+ import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
3
+ import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
+ import { domainModelsAtDefaultNamespace } from "@prisma/orm-framework/contract/types";
5
+ import { compareFieldValues } from "@prisma-idb/target-idb/runtime";
6
+ import { keyPathFields } from "@prisma-idb/target-idb/pack";
7
+ //#region src/core/model-accessor.ts
8
+ /**
9
+ * Proxy-based typed accessor handed to `where(fn)` callbacks.
10
+ *
11
+ * Mirrors `createModelAccessor()` from
12
+ * Prisma ORM's SQL client, but without
13
+ * the codec-trait gating — IDB stores native JS values, so every operator
14
+ * is available on every field. The runtime accessor object is a Proxy
15
+ * that materialises an `IdbFieldAccessor` on demand for whichever field
16
+ * name is read; the type-level surface narrows that to the model's
17
+ * actual field set so callbacks get autocomplete.
18
+ */
19
+ /**
20
+ * Build an {@link IdbFieldAccessor} for a single field. The accessor is
21
+ * shared via the proxy below, but each accessor instance is bound to its
22
+ * own field name so the produced AST nodes carry the right field.
23
+ */
24
+ function createFieldAccessor(field) {
25
+ return {
26
+ eq: (value) => fieldFilter(field, "eq", value),
27
+ neq: (value) => fieldFilter(field, "neq", value),
28
+ gt: (value) => fieldFilter(field, "gt", value),
29
+ lt: (value) => fieldFilter(field, "lt", value),
30
+ gte: (value) => fieldFilter(field, "gte", value),
31
+ lte: (value) => fieldFilter(field, "lte", value),
32
+ in: (values) => fieldFilter(field, "in", values),
33
+ notIn: (values) => fieldFilter(field, "notIn", values),
34
+ contains: (sub) => fieldFilter(field, "contains", sub),
35
+ startsWith: (sub) => fieldFilter(field, "startsWith", sub),
36
+ endsWith: (sub) => fieldFilter(field, "endsWith", sub),
37
+ isNull: () => nullCheckExpr(field, true),
38
+ isNotNull: () => nullCheckExpr(field, false)
39
+ };
40
+ }
41
+ /**
42
+ * Create the typed model accessor used by `where(fn)` callbacks.
43
+ *
44
+ * Implemented as a Proxy keyed on field names — every read returns an
45
+ * {@link IdbFieldAccessor} bound to that field. The accessor cache means
46
+ * `m.name.eq(...)` and `m.name.startsWith(...)` reuse the same per-field
47
+ * accessor object across the same `where()` invocation.
48
+ *
49
+ * The type parameter is structural — no contract is consulted at
50
+ * runtime, because TS already gates the visible field set at compile
51
+ * time. (Misspelled fields surface as accessor calls on names that
52
+ * don't exist in stored rows, which is consistent with how the
53
+ * shorthand form behaves today.)
54
+ */
55
+ function createModelAccessor() {
56
+ const cache = /* @__PURE__ */ new Map();
57
+ return new Proxy({}, { get(_target, prop) {
58
+ if (typeof prop !== "string") return void 0;
59
+ let acc = cache.get(prop);
60
+ if (acc === void 0) {
61
+ acc = createFieldAccessor(prop);
62
+ cache.set(prop, acc);
63
+ }
64
+ return acc;
65
+ } });
66
+ }
67
+ //#endregion
68
+ //#region src/core/store-state.ts
69
+ /** Build an {@link IdbIncludeScalar} marker for a `count()` reducer. */
70
+ function createIncludeScalar(state) {
71
+ return {
72
+ kind: "includeScalar",
73
+ fn: "count",
74
+ state
75
+ };
76
+ }
77
+ /** Type guard: is `value` an {@link IdbIncludeScalar} marker? */
78
+ function isIncludeScalar(value) {
79
+ if (typeof value !== "object" || value === null) return false;
80
+ const candidate = value;
81
+ return candidate.kind === "includeScalar" && candidate.fn === "count";
82
+ }
83
+ /** Create a fresh, empty accessor state (no filters, no ordering, no includes). */
84
+ function emptyAccessorState() {
85
+ return {
86
+ filters: [],
87
+ includes: {}
88
+ };
89
+ }
90
+ /** Return a shallow-merged copy of `state` with the given overrides applied. */
91
+ function mergeAccessorState(state, overrides) {
92
+ return {
93
+ ...state,
94
+ ...overrides
95
+ };
96
+ }
97
+ //#endregion
98
+ //#region src/core/query-shaping.ts
99
+ /**
100
+ * Combine accumulated filter expressions with AND.
101
+ *
102
+ * Returns `undefined` when no filter is present so callers can skip building a
103
+ * row-filter closure. Shared by {@link IdbStoreAccessorImpl} (top-level scans)
104
+ * and the relation loader (refined `include()` child scans).
105
+ */
106
+ function combineFilterExprs(filters) {
107
+ if (filters.length === 0) return void 0;
108
+ if (filters.length === 1) return filters[0];
109
+ return andExpr(filters);
110
+ }
111
+ /** Recursively flatten nested AND nodes into a single array of children. */
112
+ function flattenAnd(filter) {
113
+ if (filter.kind !== "and") return [filter];
114
+ return filter.exprs.flatMap(flattenAnd);
115
+ }
116
+ /**
117
+ * `true` when `field` is a *single-field* primary key that a lone `eq`
118
+ * condition can directly point-range-query (`store.get`/`openCursor(range)`
119
+ * against the store's own keyPath, no named index required).
120
+ *
121
+ * Deliberately `false` for a compound (array) `keyPath`, even if `field`
122
+ * happens to be one of its member fields — a single `eq` condition can only
123
+ * pin one member, not the whole compound key, so it can't drive a PK
124
+ * point-range scan on its own. Accelerating that case needs *all* member
125
+ * fields ANDed together, which is a cost-based multi-field planner decision
126
+ * (Phase 10 §3.2), not this peephole optimizer's job.
127
+ */
128
+ function isPrimaryKeyField(field, keyPath) {
129
+ return typeof keyPath === "string" && field === keyPath;
130
+ }
131
+ /**
132
+ * Scan the combined filter expression for the first `eq` field condition whose
133
+ * field either has a matching IDB index (`fieldToIndexName[field]` is set) or
134
+ * *is* the store's own primary key (`keyPath`) — a store's keyPath is always
135
+ * point-range-queryable directly, no named index required.
136
+ *
137
+ * When found, the indexed condition is peeled off and returned as an
138
+ * {@link IndexEqualityHint}; any remaining conditions are preserved in
139
+ * `remainingFilter`. Returns `null` when no usable index can be found.
140
+ *
141
+ * Nested AND nodes are flattened before scanning, so deeply-composed filters
142
+ * are handled correctly. Only `eq` conditions are considered — range operators
143
+ * (`gt`, `lt`, etc.) could also use an index range but that optimization is
144
+ * not yet implemented.
145
+ */
146
+ function extractIndexEqualityHint(filter, fieldToIndexName, keyPath) {
147
+ if (filter === void 0) return null;
148
+ if (filter.kind === "field" && filter.op === "eq") {
149
+ const indexName = fieldToIndexName[filter.field];
150
+ if ((indexName !== void 0 || isPrimaryKeyField(filter.field, keyPath)) && isValidIdbKey(filter.value)) return {
151
+ indexName,
152
+ value: filter.value,
153
+ remainingFilter: void 0
154
+ };
155
+ return null;
156
+ }
157
+ if (filter.kind === "and") {
158
+ const flat = flattenAnd(filter);
159
+ for (let i = 0; i < flat.length; i++) {
160
+ const expr = flat[i];
161
+ if (expr.kind === "field" && expr.op === "eq") {
162
+ const indexName = fieldToIndexName[expr.field];
163
+ if ((indexName !== void 0 || isPrimaryKeyField(expr.field, keyPath)) && isValidIdbKey(expr.value)) {
164
+ const rest = flat.filter((_, j) => j !== i);
165
+ return {
166
+ indexName,
167
+ value: expr.value,
168
+ remainingFilter: combineFilterExprs(rest)
169
+ };
170
+ }
171
+ }
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ /**
177
+ * Check whether every branch of an OR node is a single `field eq value`
178
+ * condition whose field has a matching index. Returns an {@link IndexOrHint}
179
+ * when the whole OR can be satisfied by N point-range cursor scans, `null`
180
+ * otherwise.
181
+ */
182
+ function tryExtractOrBranches(orNode, fieldToIndexName, keyPath, remainingFilter) {
183
+ if (orNode.exprs.length === 0) return null;
184
+ const branches = [];
185
+ for (const expr of orNode.exprs) {
186
+ if (expr.kind !== "field" || expr.op !== "eq") return null;
187
+ const indexName = fieldToIndexName[expr.field];
188
+ if (indexName === void 0 && !isPrimaryKeyField(expr.field, keyPath) || !isValidIdbKey(expr.value)) return null;
189
+ branches.push({
190
+ indexName,
191
+ value: expr.value
192
+ });
193
+ }
194
+ return {
195
+ branches,
196
+ remainingFilter
197
+ };
198
+ }
199
+ /**
200
+ * Attempt to extract an {@link IndexOrHint} from `filter`.
201
+ *
202
+ * Handles two shapes:
203
+ * - Top-level `or` where every branch is `field eq value` on an indexed field.
204
+ * - Top-level `and` (with nested ANDs flattened) that contains exactly one `or`
205
+ * child fitting the above pattern; the remaining AND children become
206
+ * `remainingFilter` applied in-memory after the union.
207
+ *
208
+ * Returns `null` when the filter cannot be accelerated via OR index scans.
209
+ */
210
+ function extractIndexOrHint(filter, fieldToIndexName, keyPath) {
211
+ if (filter === void 0) return null;
212
+ if (filter.kind === "or") return tryExtractOrBranches(filter, fieldToIndexName, keyPath, void 0);
213
+ if (filter.kind === "and") {
214
+ const flat = flattenAnd(filter);
215
+ const orIndices = [];
216
+ for (let i = 0; i < flat.length; i++) if (flat[i].kind === "or") orIndices.push(i);
217
+ if (orIndices.length !== 1) return null;
218
+ const orIdx = orIndices[0];
219
+ const orNode = flat[orIdx];
220
+ return tryExtractOrBranches(orNode, fieldToIndexName, keyPath, combineFilterExprs(flat.filter((_, j) => j !== orIdx)));
221
+ }
222
+ return null;
223
+ }
224
+ /**
225
+ * Build an in-memory comparator from an `orderBy` spec (field → direction).
226
+ *
227
+ * Returns `undefined` when there is nothing to sort by. Compares fields in
228
+ * declaration order, with {@link compareFieldValues}: the same order an
229
+ * IndexedDB index uses, so equal `Date`s tie and fall through to the next
230
+ * field, and `null`s sort last (first when descending).
231
+ */
232
+ function buildRowComparator(orderBy) {
233
+ if (orderBy === void 0) return void 0;
234
+ return (a, b) => {
235
+ for (const [field, dir] of Object.entries(orderBy)) {
236
+ const cmp = compareFieldValues(a[field], b[field]);
237
+ if (cmp === 0) continue;
238
+ return dir === "desc" ? -cmp : cmp;
239
+ }
240
+ return 0;
241
+ };
242
+ }
243
+ /**
244
+ * `true` when a `cursor-scan` plan's result *cardinality* is fully determined
245
+ * by its store/index + key range — i.e. there is no in-memory `filter` that
246
+ * would need each row's value. Only then can `count()` be answered natively
247
+ * (`store.count(range)` / `index.count(range)`) without deserializing rows.
248
+ *
249
+ * `comparator` (ORDER BY), `direction`, `skip` and `take` don't change *which*
250
+ * rows match, so they don't disqualify a plan — `skip`/`take` are applied to
251
+ * the native total afterwards via {@link clampCount}.
252
+ *
253
+ * Safe for index scans because the equality hint that builds `range` never
254
+ * selects a `multiEntry` (or compound) index — `buildFieldToIndexMap` excludes
255
+ * them — so one record maps to at most one index entry and entries == rows.
256
+ */
257
+ function isNativelyCountable(plan) {
258
+ return plan.filter === void 0;
259
+ }
260
+ /** Rewrites a natively-countable `cursor-scan` as the equivalent native `count` plan. */
261
+ function toCountPlan(plan) {
262
+ return {
263
+ meta: plan.meta,
264
+ kind: "count",
265
+ storeName: plan.storeName,
266
+ ...plan.indexName !== void 0 ? { indexName: plan.indexName } : {},
267
+ ...plan.range !== void 0 ? { range: plan.range } : {}
268
+ };
269
+ }
270
+ /**
271
+ * Applies `skip`/`take` to an *unpaginated* total. A paginated count is pure
272
+ * arithmetic on the unpaginated count — `max(0, total - skip)`, capped at
273
+ * `take` — so this is exact, not an approximation.
274
+ */
275
+ function clampCount(total, skip, take) {
276
+ let n = total;
277
+ if (skip !== void 0) n = Math.max(0, n - skip);
278
+ if (take !== void 0) n = Math.min(take, n);
279
+ return n;
280
+ }
281
+ //#endregion
282
+ //#region src/core/aggregate-builder.ts
283
+ /**
284
+ * Build the aggregate selector factory handed to `.aggregate(agg => …)` and
285
+ * `groupBy(...).aggregate(agg => …)`.
286
+ *
287
+ * Each method returns an {@link IdbAggregateSelector} marker; the actual
288
+ * reduction happens in {@link reduceAggregate} once the matching rows are
289
+ * materialised in memory. Mirrors `createAggregateBuilder` from the vendor
290
+ * `sql-orm-client/aggregate-builder.ts`, minus the field→column mapping (IDB
291
+ * stores native field names).
292
+ */
293
+ function createAggregateBuilder() {
294
+ return {
295
+ count: () => ({
296
+ kind: "aggregate",
297
+ fn: "count"
298
+ }),
299
+ sum: (field) => ({
300
+ kind: "aggregate",
301
+ fn: "sum",
302
+ field
303
+ }),
304
+ avg: (field) => ({
305
+ kind: "aggregate",
306
+ fn: "avg",
307
+ field
308
+ }),
309
+ min: (field) => ({
310
+ kind: "aggregate",
311
+ fn: "min",
312
+ field
313
+ }),
314
+ max: (field) => ({
315
+ kind: "aggregate",
316
+ fn: "max",
317
+ field
318
+ })
319
+ };
320
+ }
321
+ /** Type guard: is `value` an {@link IdbAggregateSelector}? */
322
+ function isAggregateSelector(value) {
323
+ if (typeof value !== "object" || value === null) return false;
324
+ const candidate = value;
325
+ return candidate.kind === "aggregate" && (candidate.fn === "count" || candidate.fn === "sum" || candidate.fn === "avg" || candidate.fn === "min" || candidate.fn === "max");
326
+ }
327
+ /**
328
+ * Reduce a set of materialised rows to a single aggregate value.
329
+ *
330
+ * - `count` → the number of rows (always a number, never null).
331
+ * - `sum`/`avg`/`min`/`max` → computed over the non-null numeric values of
332
+ * `field`; `null` when no row has a non-null value (matching Prisma's
333
+ * "aggregate of an empty set is null" and the vendor `coerceAggregateValue`
334
+ * null handling). String-encoded numbers are coerced via `Number()`.
335
+ */
336
+ function reduceAggregate(fn, field, rows) {
337
+ if (fn === "count") return rows.length;
338
+ if (field === void 0) return null;
339
+ const values = [];
340
+ for (const row of rows) {
341
+ const raw = row[field];
342
+ if (raw === null || raw === void 0) continue;
343
+ const n = typeof raw === "bigint" ? Number(raw) : Number(raw);
344
+ if (!Number.isNaN(n)) values.push(n);
345
+ }
346
+ if (values.length === 0) return null;
347
+ switch (fn) {
348
+ case "sum": return values.reduce((acc, n) => acc + n, 0);
349
+ case "avg": return values.reduce((acc, n) => acc + n, 0) / values.length;
350
+ case "min": return Math.min(...values);
351
+ case "max": return Math.max(...values);
352
+ }
353
+ }
354
+ /**
355
+ * Run every selector in an aggregate spec against `rows`, producing the
356
+ * result object keyed by the spec's aliases.
357
+ */
358
+ function computeAggregateSpec(spec, rows) {
359
+ const result = {};
360
+ for (const [alias, selector] of Object.entries(spec)) result[alias] = reduceAggregate(selector.fn, selector.field, rows);
361
+ return result;
362
+ }
363
+ /**
364
+ * Project an aggregate spec down to the plain `{ fn, field? }` shape carried by
365
+ * the query AST (`IdbAggregateAst.aggregates` / `IdbGroupByAst.aggregates`), so
366
+ * middleware can observe the requested aggregations.
367
+ */
368
+ function toAggregateRequests(spec) {
369
+ const out = {};
370
+ for (const [alias, selector] of Object.entries(spec)) out[alias] = selector.field !== void 0 ? {
371
+ fn: selector.fn,
372
+ field: selector.field
373
+ } : { fn: selector.fn };
374
+ return out;
375
+ }
376
+ /**
377
+ * Validate a user-supplied aggregate spec: it must be non-empty and every
378
+ * value must be a real selector. Throws a descriptive error otherwise, so a
379
+ * typo (e.g. returning a plain object) fails loudly rather than silently
380
+ * producing `NaN`/`undefined` results.
381
+ */
382
+ function assertValidAggregateSpec(spec, context) {
383
+ const entries = Object.entries(spec);
384
+ if (entries.length === 0) throw new Error(`${context} requires at least one aggregation selector`);
385
+ for (const [alias, selector] of entries) if (!isAggregateSelector(selector)) throw new Error(`${context} selector "${alias}" is invalid`);
386
+ }
387
+ //#endregion
388
+ //#region src/core/grouped-accessor.ts
389
+ /**
390
+ * Build a composite group key from type-tagged field tokens. Equal Date, binary,
391
+ * array and JSON object values share a group; null and undefined also share a group.
392
+ */
393
+ function groupKeyOf(by, row) {
394
+ return JSON.stringify(by.map((field) => groupToken(row[field])));
395
+ }
396
+ /** Keep `1n`, `1` and `"1"` apart while preserving structural grouping for JSON objects. */
397
+ function groupToken(value) {
398
+ if (value instanceof Date && Number.isNaN(value.getTime())) return ["date", "invalid"];
399
+ const token = fieldValueToken(value ?? null);
400
+ return [typeof token, typeof token === "object" ? token : String(token)];
401
+ }
402
+ /** Build a grouped accessor that reduces materialized rows from the source accessor. */
403
+ function createGroupedAccessor(init) {
404
+ return { async aggregate(fn) {
405
+ const spec = fn(createAggregateBuilder());
406
+ assertValidAggregateSpec(spec, "groupBy().aggregate()");
407
+ const ast = {
408
+ kind: "groupBy",
409
+ modelName: init.modelName,
410
+ by: init.by,
411
+ aggregates: toAggregateRequests(spec),
412
+ ...init.where !== void 0 ? { where: init.where } : {}
413
+ };
414
+ const rows = await init.materialize(ast);
415
+ const groups = /* @__PURE__ */ new Map();
416
+ for (const row of rows) {
417
+ const gk = groupKeyOf(init.by, row);
418
+ let group = groups.get(gk);
419
+ if (group === void 0) {
420
+ const key = {};
421
+ for (const field of init.by) key[field] = row[field];
422
+ group = {
423
+ key,
424
+ rows: []
425
+ };
426
+ groups.set(gk, group);
427
+ }
428
+ group.rows.push(row);
429
+ }
430
+ return Array.from(groups.values()).map((group) => ({
431
+ ...group.key,
432
+ ...computeAggregateSpec(spec, group.rows)
433
+ }));
434
+ } };
435
+ }
436
+ //#endregion
437
+ //#region src/core/relation-loader.ts
438
+ /**
439
+ * Batch-load a single named relation for all rows in `rows` and attach the
440
+ * result to each row under the `relName` key.
441
+ *
442
+ * The join matches every field of the relation, so compound foreign keys join
443
+ * on the whole tuple. It uses one key-range scan per distinct tuple when an
444
+ * index or the related primary key covers the target fields, and otherwise
445
+ * one full scan with an in-memory filter. Rows are then grouped in memory,
446
+ * which avoids N+1 queries.
447
+ *
448
+ * The `entry` carries any `include()` refinement:
449
+ *
450
+ * - `collection` — the refined `where` further filters the child scan;
451
+ * `orderBy` / `skip` / `take` are applied **per parent group** for `1:N`
452
+ * relations (each parent's children are independently sorted and paginated).
453
+ * - `scalar` — the relation field becomes the `count` of matching children
454
+ * (to-many only; `include()` rejects scalar refinements on to-one relations).
455
+ *
456
+ * @param relName - The relation key to load (e.g. `"posts"`, `"author"`).
457
+ * @param entry - How to materialise the relation (collection vs scalar + refinement state).
458
+ * @param rows - The parent rows to attach related data to.
459
+ * @param contract - The resolved IDB contract.
460
+ * @param modelName - The source model name (owner of the relation).
461
+ * @param executor - The query executor used to run the related-store scan.
462
+ */
463
+ async function loadRelation(relName, entry, rows, contract, modelName, executor, groupingKey) {
464
+ if (rows.length === 0) return rows;
465
+ const models = domainModelsAtDefaultNamespace(contract.domain);
466
+ const model = models[modelName];
467
+ if (model === void 0) return rows;
468
+ const rawRelation = model.relations[relName];
469
+ if (rawRelation === void 0) return rows;
470
+ if (!("on" in rawRelation)) return rows;
471
+ const relation = rawRelation;
472
+ const { cardinality, on } = relation;
473
+ const relatedModelName = relation.to.model;
474
+ const localFields = on.localFields;
475
+ const targetFields = on.targetFields;
476
+ if (localFields.length === 0 || localFields.length !== targetFields.length) return rows;
477
+ if (models[relatedModelName] === void 0) return rows;
478
+ const relatedStoreName = getStoreName(contract, relatedModelName);
479
+ const isScalar = entry.kind === "scalar";
480
+ const localTuples = /* @__PURE__ */ new Map();
481
+ for (const row of rows) {
482
+ const token = tupleToken(row, localFields);
483
+ if (token !== null) localTuples.set(token, localFields.map((f) => row[f]));
484
+ }
485
+ if (localTuples.size === 0) return rows.map((row) => ({
486
+ ...row,
487
+ [relName]: isScalar ? 0 : cardinality === "1:N" ? [] : null
488
+ }));
489
+ const refinedWhere = combineFilterExprs(entry.state.filters);
490
+ const planMeta = {
491
+ target: "idb",
492
+ storageHash: contract.storage.storageHash,
493
+ lane: "idb-orm",
494
+ annotations: { groupingKey }
495
+ };
496
+ const rangeSource = findRangeSource(contract, relatedStoreName, relatedModelName, targetFields);
497
+ let relatedRows;
498
+ if (rangeSource !== void 0) {
499
+ const refinedFilter = refinedWhere !== void 0 ? (row) => evaluateFilter(refinedWhere, row) : void 0;
500
+ const ranges = [];
501
+ for (const values of localTuples.values()) {
502
+ const ordered = keyPathFields(rangeSource.keyPath).map((f) => values[targetFields.indexOf(f)]);
503
+ if (!ordered.every((v) => isValidIdbKey(v))) continue;
504
+ ranges.push(IDBKeyRange.only(typeof rangeSource.keyPath === "string" ? ordered[0] : ordered));
505
+ }
506
+ relatedRows = (await Promise.all(ranges.map(async (range) => {
507
+ const plan = {
508
+ meta: planMeta,
509
+ idbPlan: {
510
+ meta: planMeta,
511
+ kind: "cursor-scan",
512
+ storeName: relatedStoreName,
513
+ ...rangeSource.indexName !== void 0 ? { indexName: rangeSource.indexName } : {},
514
+ range,
515
+ ...refinedFilter !== void 0 ? { filter: refinedFilter } : {}
516
+ }
517
+ };
518
+ const rows = [];
519
+ for await (const row of executor.query(plan)) rows.push(row);
520
+ return rows;
521
+ }))).flat();
522
+ } else {
523
+ relatedRows = [];
524
+ const filter = (row) => {
525
+ const token = tupleToken(row, targetFields);
526
+ return token !== null && localTuples.has(token) && (refinedWhere === void 0 || evaluateFilter(refinedWhere, row));
527
+ };
528
+ const plan = {
529
+ meta: planMeta,
530
+ idbPlan: {
531
+ meta: planMeta,
532
+ kind: "cursor-scan",
533
+ storeName: relatedStoreName,
534
+ filter
535
+ }
536
+ };
537
+ for await (const row of executor.query(plan)) relatedRows.push(row);
538
+ }
539
+ return attachRelatedRows(relName, entry, rows, relatedRows, relation);
540
+ }
541
+ /** Attach related rows by their join tuple, shaping each parent's collection independently. */
542
+ function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
543
+ const { cardinality, on: { localFields, targetFields } } = relation;
544
+ if (cardinality === "1:N") {
545
+ const grouped = /* @__PURE__ */ new Map();
546
+ for (const rrow of relatedRows) {
547
+ const gk = tupleToken(rrow, targetFields);
548
+ if (gk === null) continue;
549
+ const group = grouped.get(gk) ?? [];
550
+ group.push(rrow);
551
+ grouped.set(gk, group);
552
+ }
553
+ const groupFor = (row) => {
554
+ const token = tupleToken(row, localFields);
555
+ return token === null ? [] : grouped.get(token) ?? [];
556
+ };
557
+ if (entry.kind === "scalar") return rows.map((row) => ({
558
+ ...row,
559
+ [relName]: groupFor(row).length
560
+ }));
561
+ const comparator = buildRowComparator(entry.state.orderBy);
562
+ const skip = entry.state.skip ?? 0;
563
+ const take = entry.state.take;
564
+ return rows.map((row) => {
565
+ let group = groupFor(row);
566
+ if (comparator !== void 0) group = [...group].sort(comparator);
567
+ if (skip > 0 || take !== void 0) group = group.slice(skip, take !== void 0 ? skip + take : void 0);
568
+ return {
569
+ ...row,
570
+ [relName]: group
571
+ };
572
+ });
573
+ }
574
+ const indexed = /* @__PURE__ */ new Map();
575
+ for (const rrow of relatedRows) {
576
+ const token = tupleToken(rrow, targetFields);
577
+ if (token !== null) indexed.set(token, rrow);
578
+ }
579
+ return rows.map((row) => {
580
+ const token = tupleToken(row, localFields);
581
+ return {
582
+ ...row,
583
+ [relName]: (token === null ? void 0 : indexed.get(token)) ?? null
584
+ };
585
+ });
586
+ }
587
+ /**
588
+ * A `Map` key for the values of `fields` in `row`, or `null` when any of them
589
+ * is null or undefined. Equal `Date`s and binary values give equal tokens.
590
+ */
591
+ function tupleToken(row, fields) {
592
+ const parts = [];
593
+ for (const field of fields) {
594
+ const value = row[field];
595
+ if (value === null || value === void 0) return null;
596
+ const token = fieldValueToken(value);
597
+ parts.push([typeof token, String(token)]);
598
+ }
599
+ return JSON.stringify(parts);
600
+ }
601
+ /**
602
+ * Finds an index, or else the store's primary key, whose key path has exactly
603
+ * `fields` (in any order), so a lookup on those fields can use a key range.
604
+ * Returns `undefined` when there is none, or when `IDBKeyRange` isn't available.
605
+ */
606
+ function findRangeSource(contract, storeName, modelName, fields) {
607
+ if (typeof IDBKeyRange === "undefined") return void 0;
608
+ const coversFields = (keyPath) => {
609
+ const keyFields = keyPathFields(keyPath);
610
+ return keyFields.length === fields.length && keyFields.every((f) => fields.includes(f));
611
+ };
612
+ const indexes = contract.storage.stores[storeName]?.indexes ?? {};
613
+ for (const [indexName, indexDef] of Object.entries(indexes)) if (indexDef.multiEntry !== true && coversFields(indexDef.keyPath)) return {
614
+ indexName,
615
+ keyPath: indexDef.keyPath
616
+ };
617
+ const primaryKey = getKeyPath(contract, modelName);
618
+ return coversFields(primaryKey) ? { keyPath: primaryKey } : void 0;
619
+ }
620
+ //#endregion
621
+ //#region src/core/store-accessor.ts
622
+ /**
623
+ * Concrete immutable query builder.
624
+ *
625
+ * Internal details:
626
+ * - All state is in `#state` (filters, orderBy, skip, take, includes, selectedFields).
627
+ * - Builder methods clone via `#clone()` — O(1) copies since state is
628
+ * structurally shared.
629
+ * - `all()` materialises the main rows first, then batch-loads each included
630
+ * relation, then applies any `.select()` projection before yielding.
631
+ * - `#includeRefinementMode` flips `count()` from an async terminal to an
632
+ * {@link IdbIncludeScalar} marker so it can be used inside `include()`.
633
+ */
634
+ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
635
+ #contract;
636
+ #modelName;
637
+ #executor;
638
+ #storeName;
639
+ #state;
640
+ #newGroupingKey;
641
+ #includeRefinementMode;
642
+ constructor(contract, modelName, executor, state, newGroupingKey, includeRefinementMode = false) {
643
+ this.#contract = contract;
644
+ this.#modelName = modelName;
645
+ this.#executor = executor;
646
+ this.#storeName = getStoreName(contract, modelName);
647
+ this.#state = state ?? emptyAccessorState();
648
+ let _key = 0;
649
+ this.#newGroupingKey = newGroupingKey ?? (() => `idb-op-${++_key}`);
650
+ this.#includeRefinementMode = includeRefinementMode;
651
+ }
652
+ where(filter) {
653
+ const expr = typeof filter === "function" ? filter(createModelAccessor()) : shorthandToFilterExpr(filter);
654
+ if (expr === void 0) return this.#clone({});
655
+ return this.#clone({ filters: [...this.#state.filters, expr] });
656
+ }
657
+ orderBy(spec) {
658
+ return this.#clone({ orderBy: spec });
659
+ }
660
+ take(n) {
661
+ return this.#clone({ take: n });
662
+ }
663
+ skip(n) {
664
+ return this.#clone({ skip: n });
665
+ }
666
+ include(relation, refineFn) {
667
+ const entry = this.#resolveIncludeEntry(relation, refineFn);
668
+ const newState = mergeAccessorState(this.#state, { includes: {
669
+ ...this.#state.includes,
670
+ [relation]: entry
671
+ } });
672
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, newState, this.#newGroupingKey, this.#includeRefinementMode);
673
+ }
674
+ select(...fields) {
675
+ return this.#clone({ selectedFields: fields });
676
+ }
677
+ all() {
678
+ const groupingKey = this.#newGroupingKey();
679
+ const buildScanPlan = this.#buildScanPlan.bind(this);
680
+ const executeOrRows = this.#executeOrRows.bind(this);
681
+ const executorQuery = this.#executor.query.bind(this.#executor);
682
+ const applyIncludes = this.#applyIncludes.bind(this);
683
+ const projectRows = this.#projectRows.bind(this);
684
+ const combined = this.#combinedFilterExpr();
685
+ const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
686
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
687
+ const comparator = buildRowComparator(this.#state.orderBy);
688
+ const skip = this.#state.skip;
689
+ const take = this.#state.take;
690
+ return new AsyncIterableResult((async function* () {
691
+ let rows;
692
+ const orHint = fieldToIndexMap !== void 0 ? extractIndexOrHint(combined, fieldToIndexMap, keyPath) : null;
693
+ if (orHint !== null) {
694
+ rows = await executeOrRows(orHint, groupingKey, combined);
695
+ if (comparator !== void 0) rows.sort(comparator);
696
+ if (skip !== void 0) rows = rows.slice(skip);
697
+ if (take !== void 0) rows = rows.slice(0, take);
698
+ } else {
699
+ const scanPlan = buildScanPlan(groupingKey, fieldToIndexMap);
700
+ rows = [];
701
+ for await (const row of executorQuery(scanPlan)) rows.push(row);
702
+ }
703
+ const withIncludes = await applyIncludes(rows, groupingKey);
704
+ for (const row of projectRows(withIncludes)) yield row;
705
+ })());
706
+ }
707
+ async first() {
708
+ return this.take(1).all().first();
709
+ }
710
+ async aggregate(fn) {
711
+ const spec = fn(createAggregateBuilder());
712
+ assertValidAggregateSpec(spec, "aggregate()");
713
+ const combined = this.#combinedFilterExpr();
714
+ const ast = {
715
+ kind: "aggregate",
716
+ modelName: this.#modelName,
717
+ aggregates: toAggregateRequests(spec),
718
+ ...combined !== void 0 ? { where: combined } : {}
719
+ };
720
+ const groupingKey = this.#newGroupingKey();
721
+ if (Object.values(spec).every((selector) => selector.fn === "count")) {
722
+ const scanPlan = this.#buildScanPlan(groupingKey);
723
+ const total = await this.#executeNativeCount(scanPlan, ast);
724
+ if (total !== null) {
725
+ const result = {};
726
+ for (const alias of Object.keys(spec)) result[alias] = total;
727
+ return result;
728
+ }
729
+ }
730
+ return computeAggregateSpec(spec, await this.#materialize(groupingKey, ast));
731
+ }
732
+ groupBy(...fields) {
733
+ const combined = this.#combinedFilterExpr();
734
+ const materialize = (ast) => this.#materialize(this.#newGroupingKey(), ast);
735
+ return createGroupedAccessor({
736
+ modelName: this.#modelName,
737
+ by: fields,
738
+ where: combined,
739
+ materialize
740
+ });
741
+ }
742
+ async create(data) {
743
+ const record = data;
744
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, record)) return await executeNestedCreateMutation({
745
+ executor: requireTransactionExecutor(this.#executor),
746
+ contract: this.#contract,
747
+ modelName: this.#modelName,
748
+ data: record
749
+ });
750
+ const withDefaults = applyCreateDefaults(this.#contract.execution?.mutations.defaults, this.#storeName, record, createMutationDefaultsCache());
751
+ assertEnumValues(this.#contract, this.#modelName, withDefaults);
752
+ if (hasScalarFkFields(this.#contract, this.#modelName, withDefaults)) return await executeScalarCreateWithFkValidation({
753
+ executor: requireTransactionExecutor(this.#executor),
754
+ contract: this.#contract,
755
+ modelName: this.#modelName,
756
+ data: withDefaults
757
+ });
758
+ const groupingKey = this.#newGroupingKey();
759
+ const meta = this.#planMeta(groupingKey);
760
+ const plan = {
761
+ meta,
762
+ ast: {
763
+ kind: "create",
764
+ modelName: this.#modelName,
765
+ data: withDefaults
766
+ },
767
+ idbPlan: {
768
+ meta,
769
+ kind: "add",
770
+ storeName: this.#storeName,
771
+ record: withDefaults
772
+ }
773
+ };
774
+ for await (const row of this.#executor.query(plan)) return row;
775
+ return withDefaults;
776
+ }
777
+ async findUnique(key) {
778
+ const groupingKey = this.#newGroupingKey();
779
+ const meta = this.#planMeta(groupingKey);
780
+ const plan = {
781
+ meta,
782
+ ast: {
783
+ kind: "findUnique",
784
+ modelName: this.#modelName,
785
+ key
786
+ },
787
+ idbPlan: {
788
+ meta,
789
+ kind: "key-get",
790
+ storeName: this.#storeName,
791
+ key
792
+ }
793
+ };
794
+ for await (const row of this.#executor.query(plan)) return row;
795
+ return null;
796
+ }
797
+ async delete(key) {
798
+ if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
799
+ await executeDeleteWithReferentialActions({
800
+ executor: requireTransactionExecutor(this.#executor),
801
+ contract: this.#contract,
802
+ modelName: this.#modelName,
803
+ key
804
+ });
805
+ return;
806
+ }
807
+ const groupingKey = this.#newGroupingKey();
808
+ const meta = this.#planMeta(groupingKey);
809
+ const plan = {
810
+ meta,
811
+ ast: {
812
+ kind: "delete",
813
+ modelName: this.#modelName,
814
+ key
815
+ },
816
+ idbPlan: {
817
+ meta,
818
+ kind: "delete",
819
+ storeName: this.#storeName,
820
+ key
821
+ }
822
+ };
823
+ await this.#executor.query(plan).toArray();
824
+ }
825
+ async update(patch) {
826
+ const patchRecord = patch;
827
+ if (hasNestedMutationCallbacks(this.#contract, this.#modelName, patchRecord)) return await executeNestedUpdateMutation({
828
+ executor: requireTransactionExecutor(this.#executor),
829
+ contract: this.#contract,
830
+ modelName: this.#modelName,
831
+ filters: this.#state.filters,
832
+ data: patchRecord
833
+ });
834
+ return await executeScalarUpdateWithFkValidation({
835
+ executor: requireTransactionExecutor(this.#executor),
836
+ contract: this.#contract,
837
+ modelName: this.#modelName,
838
+ filters: this.#state.filters,
839
+ data: patchRecord
840
+ });
841
+ }
842
+ /**
843
+ * Always routes through the transaction scope (`executeBulkUpdateWithFkValidation`),
844
+ * unlike single-row `update()`'s plan-level fallback for the no-FK-field
845
+ * case. A bulk scan-write's affected row set isn't knowable until it
846
+ * actually runs — going through the transaction scope is what lets the
847
+ * sync interceptor observe the real affected rows and write one outbox
848
+ * event per row (see `SyncInterceptingTransactionScope#maybeTrack`'s
849
+ * `scan-write` case), atomically with the write itself, instead of one
850
+ * event for the whole batch with no way to recover which rows it covered.
851
+ */
852
+ updateAll(patch) {
853
+ const contract = this.#contract;
854
+ const modelName = this.#modelName;
855
+ const filters = this.#state.filters;
856
+ const patchRecord = patch;
857
+ const executor = requireTransactionExecutor(this.#executor);
858
+ return new AsyncIterableResult((async function* () {
859
+ const rows = await executeBulkUpdateWithFkValidation({
860
+ executor,
861
+ contract,
862
+ modelName,
863
+ filters,
864
+ data: patchRecord
865
+ });
866
+ for (const row of rows) yield row;
867
+ })());
868
+ }
869
+ async updateCount(patch) {
870
+ return (await this.updateAll(patch).toArray()).length;
871
+ }
872
+ async upsert(args) {
873
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
874
+ const whereExpr = shorthandToFilterExpr(args.where);
875
+ const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
876
+ const meta = this.#planMeta(this.#newGroupingKey());
877
+ const createRecord = args.create;
878
+ const patchRecord = args.update;
879
+ const storeName = this.#storeName;
880
+ const executionDefaults = this.#contract.execution?.mutations.defaults;
881
+ const exec = requireTransactionExecutor(this.#executor);
882
+ const effectivePatch = applyUpdateDefaults(executionDefaults, storeName, patchRecord, createMutationDefaultsCache());
883
+ const createWithDefaults = applyCreateDefaults(executionDefaults, storeName, createRecord, createMutationDefaultsCache());
884
+ assertEnumValues(this.#contract, this.#modelName, effectivePatch);
885
+ assertEnumValues(this.#contract, this.#modelName, createWithDefaults);
886
+ const { storeNames: onUpdateStoreNames } = collectOnUpdateEnforcementStoreNames(this.#contract, this.#modelName, effectivePatch);
887
+ const storeNames = [.../* @__PURE__ */ new Set([
888
+ storeName,
889
+ ...onUpdateStoreNames,
890
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, createWithDefaults),
891
+ ...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
892
+ ])];
893
+ return withMutationScope(exec, storeNames, async (scope) => {
894
+ const existing = (await scope.execute({
895
+ meta,
896
+ kind: "cursor-scan",
897
+ storeName,
898
+ filter: matches,
899
+ take: 1
900
+ }))[0];
901
+ if (existing === void 0) {
902
+ await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
903
+ return (await scope.execute({
904
+ meta,
905
+ kind: "add",
906
+ storeName,
907
+ record: createWithDefaults
908
+ }))[0] ?? createWithDefaults;
909
+ }
910
+ await validateScalarFks(scope, this.#contract, this.#modelName, effectivePatch, existing);
911
+ const key = extractKeyFromRow(existing, keyPath);
912
+ await applyReferentialActionsForRowOnUpdate(scope, this.#contract, this.#modelName, existing, effectivePatch);
913
+ return (await scope.execute({
914
+ meta,
915
+ kind: "update",
916
+ storeName,
917
+ key,
918
+ patch: effectivePatch
919
+ }))[0] ?? existing;
920
+ });
921
+ }
922
+ createAll(data) {
923
+ const defaultsCache = createMutationDefaultsCache();
924
+ const executionDefaults = this.#contract.execution?.mutations.defaults;
925
+ const records = data.map((d) => applyCreateDefaults(executionDefaults, this.#storeName, d, defaultsCache));
926
+ if (records.some((row) => hasScalarFkFields(this.#contract, this.#modelName, row))) {
927
+ const executor = requireTransactionExecutor(this.#executor);
928
+ const contract = this.#contract;
929
+ const modelName = this.#modelName;
930
+ return new AsyncIterableResult((async function* () {
931
+ const rows = await executeScalarCreateAllWithFkValidation({
932
+ executor,
933
+ contract,
934
+ modelName,
935
+ data: records
936
+ });
937
+ for (const row of rows) yield row;
938
+ })());
939
+ }
940
+ const groupingKey = this.#newGroupingKey();
941
+ const meta = this.#planMeta(groupingKey);
942
+ const plan = {
943
+ meta,
944
+ ast: {
945
+ kind: "createAll",
946
+ modelName: this.#modelName,
947
+ data: records
948
+ },
949
+ idbPlan: {
950
+ meta,
951
+ kind: "batch",
952
+ storeNames: [this.#storeName],
953
+ ops: records.map((record) => ({
954
+ meta,
955
+ kind: "add",
956
+ storeName: this.#storeName,
957
+ record
958
+ }))
959
+ }
960
+ };
961
+ const executorQuery = this.#executor.query.bind(this.#executor);
962
+ const contract = this.#contract;
963
+ const modelName = this.#modelName;
964
+ return new AsyncIterableResult((async function* () {
965
+ for (const record of records) assertEnumValues(contract, modelName, record);
966
+ for await (const row of executorQuery(plan)) yield row;
967
+ })());
968
+ }
969
+ async createCount(data) {
970
+ return (await this.createAll(data).toArray()).length;
971
+ }
972
+ /**
973
+ * Always routes through the transaction scope (`executeDeleteAllWithReferentialActions`),
974
+ * not just when there are enforceable child relations to cascade — that
975
+ * function already handles the no-relations case correctly (an empty
976
+ * relation list is a no-op loop), and going through it unconditionally is
977
+ * what lets the sync interceptor's transaction-scope tracking write one
978
+ * outbox event per actually-deleted row instead of one lump event for
979
+ * the whole batch with no way to know which rows it covered. See
980
+ * `updateAll()`'s doc comment for the same reasoning.
981
+ */
982
+ deleteAll() {
983
+ const combined = this.#combinedFilterExpr();
984
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
985
+ const contract = this.#contract;
986
+ const modelName = this.#modelName;
987
+ const executor = requireTransactionExecutor(this.#executor);
988
+ return new AsyncIterableResult((async function* () {
989
+ const rows = await executeDeleteAllWithReferentialActions({
990
+ executor,
991
+ contract,
992
+ modelName,
993
+ ...filter !== void 0 ? { filter } : {}
994
+ });
995
+ for (const row of rows) yield row;
996
+ })());
997
+ }
998
+ async deleteCount() {
999
+ return (await this.deleteAll().toArray()).length;
1000
+ }
1001
+ count() {
1002
+ if (this.#includeRefinementMode) return createIncludeScalar(this.#state);
1003
+ return this.#countTerminal();
1004
+ }
1005
+ async #countTerminal() {
1006
+ const groupingKey = this.#newGroupingKey();
1007
+ const combined = this.#combinedFilterExpr();
1008
+ const fieldToIndexMap = typeof IDBKeyRange !== "undefined" ? buildFieldToIndexMap(this.#contract, this.#storeName) : void 0;
1009
+ if (fieldToIndexMap !== void 0) {
1010
+ const orHint = extractIndexOrHint(combined, fieldToIndexMap, getKeyPath(this.#contract, this.#modelName));
1011
+ if (orHint !== null) return clampCount((await this.#executeOrRows(orHint, groupingKey, combined)).length, this.#state.skip, this.#state.take);
1012
+ }
1013
+ const scanPlan = this.#buildScanPlan(groupingKey, fieldToIndexMap);
1014
+ const scanAst = scanPlan.ast;
1015
+ const ast = {
1016
+ kind: "count",
1017
+ modelName: this.#modelName,
1018
+ ...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
1019
+ };
1020
+ const nativeTotal = await this.#executeNativeCount(scanPlan, ast);
1021
+ if (nativeTotal !== null) return clampCount(nativeTotal, this.#state.skip, this.#state.take);
1022
+ const plan = {
1023
+ ...scanPlan,
1024
+ ast
1025
+ };
1026
+ let n = 0;
1027
+ for await (const _ of this.#executor.query(plan)) n++;
1028
+ return n;
1029
+ }
1030
+ /**
1031
+ * Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
1032
+ * optional refinement against a fresh refinement-mode child accessor, then
1033
+ * classify the result as a scalar count or a refined collection.
1034
+ */
1035
+ #resolveIncludeEntry(relation, refineFn) {
1036
+ if (refineFn === void 0) return {
1037
+ kind: "collection",
1038
+ state: emptyAccessorState()
1039
+ };
1040
+ const rel = getRelation(this.#contract, this.#modelName, relation);
1041
+ const relatedModelName = rel?.to.model ?? relation;
1042
+ const refined = refineFn(new IdbStoreAccessorImpl(this.#contract, relatedModelName, this.#executor, emptyAccessorState(), this.#newGroupingKey, true));
1043
+ if (isIncludeScalar(refined)) {
1044
+ if (rel !== void 0 && rel.cardinality !== "1:N") throw new Error(`include('${relation}'): count() is only supported for to-many (1:N) relations`);
1045
+ return {
1046
+ kind: "scalar",
1047
+ fn: refined.fn,
1048
+ state: refined.state
1049
+ };
1050
+ }
1051
+ if (refined instanceof IdbStoreAccessorImpl) return {
1052
+ kind: "collection",
1053
+ state: refined.#state
1054
+ };
1055
+ throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
1056
+ }
1057
+ /**
1058
+ * Runs `scanPlan` as a native `count` plan when its cardinality is fully
1059
+ * determined by a key range (no in-memory filter) — returning the
1060
+ * *unpaginated* total — or `null` when it can't (the caller then falls back
1061
+ * to materializing). `ast` is attached so middleware sees the caller's
1062
+ * intent (`count` / `aggregate`) regardless of the physical plan.
1063
+ */
1064
+ async #executeNativeCount(scanPlan, ast) {
1065
+ const body = scanPlan.idbPlan;
1066
+ if (body.kind !== "cursor-scan" || !isNativelyCountable(body)) return null;
1067
+ const nativePlan = {
1068
+ ...scanPlan,
1069
+ ast,
1070
+ idbPlan: toCountPlan(body)
1071
+ };
1072
+ let total = 0;
1073
+ for await (const row of this.#executor.query(nativePlan)) total = row.count;
1074
+ return total;
1075
+ }
1076
+ /**
1077
+ * Materialise all rows matching the accumulated filters with no pagination —
1078
+ * used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
1079
+ * scan plan so middleware can observe the aggregate intent.
1080
+ */
1081
+ async #materialize(groupingKey, ast) {
1082
+ const combined = this.#combinedFilterExpr();
1083
+ const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1084
+ const meta = this.#planMeta(groupingKey);
1085
+ const plan = {
1086
+ meta,
1087
+ ast,
1088
+ idbPlan: {
1089
+ meta,
1090
+ kind: "cursor-scan",
1091
+ storeName: this.#storeName,
1092
+ ...filter !== void 0 ? { filter } : {}
1093
+ }
1094
+ };
1095
+ const rows = [];
1096
+ for await (const row of this.#executor.query(plan)) rows.push(row);
1097
+ return rows;
1098
+ }
1099
+ #buildScanPlan(groupingKey, fieldToIndexMap) {
1100
+ const combined = this.#combinedFilterExpr();
1101
+ const comparator = buildRowComparator(this.#state.orderBy);
1102
+ const meta = this.#planMeta(groupingKey);
1103
+ const ast = {
1104
+ kind: "findMany",
1105
+ modelName: this.#modelName,
1106
+ ...combined !== void 0 ? { where: combined } : {},
1107
+ ...this.#state.orderBy !== void 0 ? { orderBy: this.#state.orderBy } : {},
1108
+ ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1109
+ ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1110
+ };
1111
+ let hint = null;
1112
+ if (typeof IDBKeyRange !== "undefined") hint = extractIndexEqualityHint(combined, fieldToIndexMap ?? buildFieldToIndexMap(this.#contract, this.#storeName), getKeyPath(this.#contract, this.#modelName));
1113
+ const remainingFilter = hint !== null ? hint.remainingFilter : combined;
1114
+ const filter = remainingFilter !== void 0 ? (row) => evaluateFilter(remainingFilter, row) : void 0;
1115
+ return {
1116
+ meta,
1117
+ ast,
1118
+ idbPlan: {
1119
+ meta,
1120
+ kind: "cursor-scan",
1121
+ storeName: this.#storeName,
1122
+ ...hint?.indexName !== void 0 ? { indexName: hint.indexName } : {},
1123
+ ...hint !== null ? { range: IDBKeyRange.only(hint.value) } : {},
1124
+ ...filter !== void 0 ? { filter } : {},
1125
+ ...comparator !== void 0 ? { comparator } : {},
1126
+ ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1127
+ ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1128
+ }
1129
+ };
1130
+ }
1131
+ /**
1132
+ * Execute one cursor-scan per OR branch, union the results, deduplicate by
1133
+ * primary key, and apply `hint.remainingFilter` in-memory. Skip/take are
1134
+ * NOT applied here — the caller slices the array after union so pagination
1135
+ * is correct across branches.
1136
+ */
1137
+ async #executeOrRows(hint, groupingKey, combined) {
1138
+ const meta = this.#planMeta(groupingKey);
1139
+ const storeName = this.#storeName;
1140
+ const keyPath = getKeyPath(this.#contract, this.#modelName);
1141
+ const seen = /* @__PURE__ */ new Set();
1142
+ const rows = [];
1143
+ const ast = {
1144
+ kind: "findMany",
1145
+ modelName: this.#modelName,
1146
+ ...combined !== void 0 ? { where: combined } : {}
1147
+ };
1148
+ const branchResults = await Promise.all(hint.branches.map(async (branch) => {
1149
+ const plan = {
1150
+ meta,
1151
+ ast,
1152
+ idbPlan: {
1153
+ meta,
1154
+ kind: "cursor-scan",
1155
+ storeName,
1156
+ ...branch.indexName !== void 0 ? { indexName: branch.indexName } : {},
1157
+ range: IDBKeyRange.only(branch.value)
1158
+ }
1159
+ };
1160
+ const branchRows = [];
1161
+ for await (const row of this.#executor.query(plan)) branchRows.push(row);
1162
+ return branchRows;
1163
+ }));
1164
+ for (const branchRows of branchResults) for (const row of branchRows) {
1165
+ const pk = keyToken(extractKeyFromRow(row, keyPath));
1166
+ if (!seen.has(pk)) {
1167
+ seen.add(pk);
1168
+ rows.push(row);
1169
+ }
1170
+ }
1171
+ if (hint.remainingFilter === void 0) return rows;
1172
+ const { remainingFilter } = hint;
1173
+ return rows.filter((row) => evaluateFilter(remainingFilter, row));
1174
+ }
1175
+ /**
1176
+ * Combine all accumulated filter expressions with AND.
1177
+ *
1178
+ * Returns `undefined` when no filter has been installed so the driver can
1179
+ * skip building a row filter closure (a small perf and readability win on
1180
+ * `.all()` paths). Delegates to the shared {@link combineFilterExprs}.
1181
+ */
1182
+ #combinedFilterExpr() {
1183
+ return combineFilterExprs(this.#state.filters);
1184
+ }
1185
+ async #applyIncludes(rows, groupingKey) {
1186
+ const relNames = Object.keys(this.#state.includes);
1187
+ if (relNames.length === 0) return rows;
1188
+ let result = rows;
1189
+ for (const relName of relNames) {
1190
+ const entry = this.#state.includes[relName];
1191
+ result = await loadRelation(relName, entry, result, this.#contract, this.#modelName, this.#executor, groupingKey);
1192
+ }
1193
+ return result;
1194
+ }
1195
+ /**
1196
+ * Apply a `.select()` projection (if any) to materialised rows. Keeps the
1197
+ * selected scalar fields plus every included relation key (which `include()`
1198
+ * attached during {@link #applyIncludes}); a no-op when nothing is selected.
1199
+ */
1200
+ #projectRows(rows) {
1201
+ const selected = this.#state.selectedFields;
1202
+ if (selected === void 0) return rows;
1203
+ const keep = [...selected, ...Object.keys(this.#state.includes)];
1204
+ return rows.map((row) => {
1205
+ const out = {};
1206
+ for (const field of keep) if (field in row) out[field] = row[field];
1207
+ return out;
1208
+ });
1209
+ }
1210
+ #planMeta(groupingKey) {
1211
+ return {
1212
+ target: "idb",
1213
+ storageHash: this.#contract.storage.storageHash,
1214
+ lane: "idb-orm",
1215
+ annotations: { groupingKey }
1216
+ };
1217
+ }
1218
+ #clone(overrides) {
1219
+ return new IdbStoreAccessorImpl(this.#contract, this.#modelName, this.#executor, mergeAccessorState(this.#state, overrides), this.#newGroupingKey, this.#includeRefinementMode);
1220
+ }
1221
+ };
1222
+ //#endregion
1223
+ //#region src/core/idb-orm.ts
1224
+ /**
1225
+ * Create a typed IDB ORM client from a contract and executor.
1226
+ *
1227
+ * The client exposes one `IdbStoreAccessor` per entry in `contract.roots`.
1228
+ * Only roots-declared stores are accessible at the top level — other stores
1229
+ * can be reached via `.include()` on any accessor.
1230
+ *
1231
+ * @example
1232
+ * ```ts
1233
+ * import { idbOrm } from "@prisma-idb/client-idb/orm";
1234
+ * import { createIdbRuntime } from "@prisma-idb/runtime-idb/runtime";
1235
+ * import contract from "./prisma/idb-contract";
1236
+ *
1237
+ * const runtime = createIdbRuntime({ adapter, driver });
1238
+ * const db = idbOrm({ contract, executor: runtime });
1239
+ *
1240
+ * // Typed query builder:
1241
+ * const alice = await db.users.where({ email: "alice@example.com" }).first();
1242
+ * const postsWithAuthor = await db.posts.include("author").all();
1243
+ * ```
1244
+ */
1245
+ function idbOrm(options) {
1246
+ const { contract, executor } = options;
1247
+ let _key = 0;
1248
+ const newGroupingKey = () => `idb-op-${++_key}`;
1249
+ const client = {};
1250
+ for (const [rootKey, ref] of Object.entries(contract.roots)) client[rootKey] = new IdbStoreAccessorImpl(contract, ref.model, executor, void 0, newGroupingKey);
1251
+ return client;
1252
+ }
1253
+ //#endregion
1254
+ export { idbOrm as t };
1255
+
1256
+ //# sourceMappingURL=idb-orm-DsfllvzD.mjs.map