@prisma-idb/client-idb 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/dist/client-auto.d.mts +2 -2
  2. package/dist/client-auto.mjs +1 -1
  3. package/dist/client.d.mts +1 -1
  4. package/dist/client.mjs +1 -1
  5. package/dist/{executor-CevtdBfz.d.mts → executor-Bp-hdRg2.d.mts} +3 -3
  6. package/dist/{executor-CevtdBfz.d.mts.map → executor-Bp-hdRg2.d.mts.map} +1 -1
  7. package/dist/{idb-orm-gEKHfG4H.d.mts → idb-orm-9XN3c9ly.d.mts} +5 -5
  8. package/dist/{idb-orm-gEKHfG4H.d.mts.map → idb-orm-9XN3c9ly.d.mts.map} +1 -1
  9. package/dist/{idb-orm-DsfllvzD.mjs → idb-orm-BitJ-EMq.mjs} +527 -382
  10. package/dist/idb-orm-BitJ-EMq.mjs.map +1 -0
  11. package/dist/internal.d.mts +2 -2
  12. package/dist/internal.d.mts.map +1 -1
  13. package/dist/internal.mjs +1 -1
  14. package/dist/{managed-client-B_xGXdjz.d.mts → managed-client-CS78EjPu.d.mts} +3 -3
  15. package/dist/{managed-client-B_xGXdjz.d.mts.map → managed-client-CS78EjPu.d.mts.map} +1 -1
  16. package/dist/{managed-client-Cp01irrm.mjs → managed-client-CxlNmERd.mjs} +3 -3
  17. package/dist/{managed-client-Cp01irrm.mjs.map → managed-client-CxlNmERd.mjs.map} +1 -1
  18. package/dist/{mutation-executor-CD5n_kKT.mjs → mutation-executor-SJqTuSbb.mjs} +11 -82
  19. package/dist/mutation-executor-SJqTuSbb.mjs.map +1 -0
  20. package/dist/{mutation-scope-pOn0QzvV.d.mts → mutation-scope-1hS0ogGa.d.mts} +2 -2
  21. package/dist/{mutation-scope-pOn0QzvV.d.mts.map → mutation-scope-1hS0ogGa.d.mts.map} +1 -1
  22. package/dist/orm.d.mts +3 -3
  23. package/dist/orm.mjs +2 -2
  24. package/package.json +7 -6
  25. package/dist/idb-orm-DsfllvzD.mjs.map +0 -1
  26. package/dist/mutation-executor-CD5n_kKT.mjs.map +0 -1
@@ -1,9 +1,8 @@
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";
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";
2
2
  import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
3
3
  import { andExpr, evaluateFilter, fieldFilter, nullCheckExpr, shorthandToFilterExpr } from "@prisma-idb/adapter-idb/runtime";
4
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";
5
+ import { compareFieldValues, isValidIdbKey, keyEquals } from "@prisma-idb/target-idb/runtime";
7
6
  //#region src/core/model-accessor.ts
8
7
  /**
9
8
  * Proxy-based typed accessor handed to `where(fn)` callbacks.
@@ -108,119 +107,6 @@ function combineFilterExprs(filters) {
108
107
  if (filters.length === 1) return filters[0];
109
108
  return andExpr(filters);
110
109
  }
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
110
  /**
225
111
  * Build an in-memory comparator from an `orderBy` spec (field → direction).
226
112
  *
@@ -240,44 +126,6 @@ function buildRowComparator(orderBy) {
240
126
  return 0;
241
127
  };
242
128
  }
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
129
  //#endregion
282
130
  //#region src/core/aggregate-builder.ts
283
131
  /**
@@ -434,16 +282,469 @@ function createGroupedAccessor(init) {
434
282
  } };
435
283
  }
436
284
  //#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
+ }
630
+ }
631
+ //#endregion
632
+ //#region src/core/planner/lower.ts
633
+ /**
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.
639
+ */
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
+ };
738
+ }
739
+ //#endregion
437
740
  //#region src/core/relation-loader.ts
438
741
  /**
439
742
  * Batch-load a single named relation for all rows in `rows` and attach the
440
743
  * result to each row under the `relName` key.
441
744
  *
442
745
  * 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.
746
+ * on the whole tuple. It plans one batch lookup for all parents, then groups
747
+ * the rows in memory, which avoids N+1 queries.
447
748
  *
448
749
  * The `entry` carries any `include()` refinement:
449
750
  *
@@ -493,50 +794,19 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
493
794
  lane: "idb-orm",
494
795
  annotations: { groupingKey }
495
796
  };
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);
797
+ const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
798
+ const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
799
+ const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
800
+ const catalog = buildCatalog(contract, relatedStoreName);
801
+ const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
802
+ const token = tupleToken(row, targetFields);
803
+ return token !== null && localTuples.has(token);
804
+ });
805
+ const plan = {
806
+ meta: planMeta,
807
+ idbPlan: lowered.idbPlan
808
+ };
809
+ return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
540
810
  }
541
811
  /** Attach related rows by their join tuple, shaping each parent's collection independently. */
542
812
  function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
@@ -598,25 +868,6 @@ function tupleToken(row, fields) {
598
868
  }
599
869
  return JSON.stringify(parts);
600
870
  }
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
871
  //#endregion
621
872
  //#region src/core/store-accessor.ts
622
873
  /**
@@ -677,30 +928,14 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
677
928
  all() {
678
929
  const groupingKey = this.#newGroupingKey();
679
930
  const buildScanPlan = this.#buildScanPlan.bind(this);
680
- const executeOrRows = this.#executeOrRows.bind(this);
681
931
  const executorQuery = this.#executor.query.bind(this.#executor);
682
932
  const applyIncludes = this.#applyIncludes.bind(this);
683
933
  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
934
  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);
935
+ const scan = buildScanPlan(groupingKey);
936
+ const rows = [];
937
+ for await (const row of executorQuery(scan.plan)) rows.push(row);
938
+ const withIncludes = await applyIncludes(scan.finish(rows), groupingKey);
704
939
  for (const row of projectRows(withIncludes)) yield row;
705
940
  })());
706
941
  }
@@ -717,17 +952,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
717
952
  aggregates: toAggregateRequests(spec),
718
953
  ...combined !== void 0 ? { where: combined } : {}
719
954
  };
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));
955
+ return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
731
956
  }
732
957
  groupBy(...fields) {
733
958
  const combined = this.#combinedFilterExpr();
@@ -777,22 +1002,31 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
777
1002
  async findUnique(key) {
778
1003
  const groupingKey = this.#newGroupingKey();
779
1004
  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
- }
1005
+ const ast = {
1006
+ kind: "findUnique",
1007
+ modelName: this.#modelName,
1008
+ key
793
1009
  };
794
- for await (const row of this.#executor.query(plan)) return row;
795
- return null;
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({
1025
+ meta,
1026
+ ast,
1027
+ idbPlan: lowered.idbPlan
1028
+ }).toArray();
1029
+ return lowered.finish(rows)[0] ?? null;
796
1030
  }
797
1031
  async delete(key) {
798
1032
  if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
@@ -1003,29 +1237,18 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1003
1237
  return this.#countTerminal();
1004
1238
  }
1005
1239
  async #countTerminal() {
1006
- const groupingKey = this.#newGroupingKey();
1240
+ const scan = this.#buildScanPlan(this.#newGroupingKey());
1007
1241
  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
1242
  const ast = {
1016
1243
  kind: "count",
1017
1244
  modelName: this.#modelName,
1018
- ...scanAst?.kind === "findMany" && scanAst.where !== void 0 ? { where: scanAst.where } : {}
1245
+ ...combined !== void 0 ? { where: combined } : {}
1019
1246
  };
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,
1247
+ const rows = await this.#executor.query({
1248
+ ...scan.plan,
1024
1249
  ast
1025
- };
1026
- let n = 0;
1027
- for await (const _ of this.#executor.query(plan)) n++;
1028
- return n;
1250
+ }).toArray();
1251
+ return scan.finish(rows).length;
1029
1252
  }
1030
1253
  /**
1031
1254
  * Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
@@ -1055,50 +1278,27 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1055
1278
  throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
1056
1279
  }
1057
1280
  /**
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
1281
  * Materialise all rows matching the accumulated filters with no pagination —
1078
1282
  * used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
1079
- * scan plan so middleware can observe the aggregate intent.
1283
+ * row plan so middleware can observe the aggregate intent.
1080
1284
  */
1081
1285
  async #materialize(groupingKey, ast) {
1082
1286
  const combined = this.#combinedFilterExpr();
1083
- const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
1084
1287
  const meta = this.#planMeta(groupingKey);
1085
- const plan = {
1288
+ const lowered = this.#lowerRows(meta, { ...combined === void 0 ? {} : { where: combined } });
1289
+ const rows = await this.#executor.query({
1086
1290
  meta,
1087
1291
  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;
1292
+ idbPlan: lowered.idbPlan
1293
+ }).toArray();
1294
+ return lowered.finish(rows);
1295
+ }
1296
+ #lowerRows(meta, request) {
1297
+ const catalog = buildCatalog(this.#contract, this.#storeName);
1298
+ return lowerRows(catalog, planQuery(catalog, request), meta, request);
1098
1299
  }
1099
- #buildScanPlan(groupingKey, fieldToIndexMap) {
1300
+ #buildScanPlan(groupingKey) {
1100
1301
  const combined = this.#combinedFilterExpr();
1101
- const comparator = buildRowComparator(this.#state.orderBy);
1102
1302
  const meta = this.#planMeta(groupingKey);
1103
1303
  const ast = {
1104
1304
  kind: "findMany",
@@ -1108,76 +1308,21 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
1108
1308
  ...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
1109
1309
  ...this.#state.take !== void 0 ? { take: this.#state.take } : {}
1110
1310
  };
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;
1311
+ const lowered = this.#lowerRows(meta, ast);
1115
1312
  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 = {
1313
+ plan: {
1150
1314
  meta,
1151
1315
  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));
1316
+ idbPlan: lowered.idbPlan
1317
+ },
1318
+ finish: lowered.finish
1319
+ };
1174
1320
  }
1175
1321
  /**
1176
1322
  * Combine all accumulated filter expressions with AND.
1177
1323
  *
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}.
1324
+ * Returns `undefined` when no filter has been installed so the planner sees
1325
+ * an unfiltered request. Delegates to the shared {@link combineFilterExprs}.
1181
1326
  */
1182
1327
  #combinedFilterExpr() {
1183
1328
  return combineFilterExprs(this.#state.filters);
@@ -1253,4 +1398,4 @@ function idbOrm(options) {
1253
1398
  //#endregion
1254
1399
  export { idbOrm as t };
1255
1400
 
1256
- //# sourceMappingURL=idb-orm-DsfllvzD.mjs.map
1401
+ //# sourceMappingURL=idb-orm-BitJ-EMq.mjs.map