@prisma-idb/client-idb 0.10.0 → 0.12.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.
- package/README.md +6 -0
- package/dist/client-auto.d.mts +2 -2
- package/dist/client-auto.mjs +1 -1
- package/dist/client.d.mts +1 -1
- package/dist/client.mjs +1 -1
- package/dist/{executor-CevtdBfz.d.mts → executor-CIEHpa1j.d.mts} +16 -9
- package/dist/{executor-CevtdBfz.d.mts.map → executor-CIEHpa1j.d.mts.map} +1 -1
- package/dist/{idb-orm-gEKHfG4H.d.mts → idb-orm-6OuwkeDM.d.mts} +5 -5
- package/dist/{idb-orm-gEKHfG4H.d.mts.map → idb-orm-6OuwkeDM.d.mts.map} +1 -1
- package/dist/{idb-orm-DsfllvzD.mjs → idb-orm-B1Br1C0F.mjs} +84 -427
- package/dist/idb-orm-B1Br1C0F.mjs.map +1 -0
- package/dist/internal.d.mts +2 -2
- package/dist/internal.d.mts.map +1 -1
- package/dist/internal.mjs +1 -1
- package/dist/{managed-client-B_xGXdjz.d.mts → managed-client-DQlsxc4Z.d.mts} +3 -3
- package/dist/{managed-client-B_xGXdjz.d.mts.map → managed-client-DQlsxc4Z.d.mts.map} +1 -1
- package/dist/{managed-client-Cp01irrm.mjs → managed-client-M3xMUk0n.mjs} +3 -3
- package/dist/{managed-client-Cp01irrm.mjs.map → managed-client-M3xMUk0n.mjs.map} +1 -1
- package/dist/{mutation-executor-CD5n_kKT.mjs → mutation-executor-inLFb0TE.mjs} +699 -215
- package/dist/mutation-executor-inLFb0TE.mjs.map +1 -0
- package/dist/{mutation-scope-pOn0QzvV.d.mts → mutation-scope-DrxFhek3.d.mts} +2 -2
- package/dist/{mutation-scope-pOn0QzvV.d.mts.map → mutation-scope-DrxFhek3.d.mts.map} +1 -1
- package/dist/orm.d.mts +3 -3
- package/dist/orm.mjs +2 -2
- package/package.json +7 -6
- package/dist/idb-orm-DsfllvzD.mjs.map +0 -1
- package/dist/mutation-executor-CD5n_kKT.mjs.map +0 -1
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as combineFilterExprs, C as planQuery, D as createMutationDefaultsCache, E as applyUpdateDefaults, F as getStoreName, M as fieldValueToken, N as getKeyPath, O as assertEnumValues, P as getRelation, S as lowerRows, T as applyCreateDefaults, _ as readMutationRows, a as collectScalarFkStoreNames, b as withMutationScope, c as executeDeleteWithReferentialActions, d as executeScalarCreateAllWithFkValidation, f as executeScalarCreateWithFkValidation, g as hasScalarFkFields, h as hasNestedMutationCallbacks, i as collectOnUpdateEnforcementStoreNames, j as extractKeyFromRow, k as buildRowComparator, l as executeNestedCreateMutation, m as hasEnforceableChildRelations, n as applyReferentialActionsForRowOnUpdate, o as executeBulkUpdateWithFkValidation, p as executeScalarUpdateWithFkValidation, s as executeDeleteAllWithReferentialActions, u as executeNestedUpdateMutation, v as requireTransactionExecutor, w as buildCatalog, x as lowerCount, y as validateScalarFks } from "./mutation-executor-inLFb0TE.mjs";
|
|
2
2
|
import { AsyncIterableResult } from "@prisma/orm-framework/components/runtime";
|
|
3
|
-
import { andExpr,
|
|
3
|
+
import { andExpr, 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";
|
|
7
5
|
//#region src/core/model-accessor.ts
|
|
8
6
|
/**
|
|
9
7
|
* Proxy-based typed accessor handed to `where(fn)` callbacks.
|
|
@@ -95,190 +93,6 @@ function mergeAccessorState(state, overrides) {
|
|
|
95
93
|
};
|
|
96
94
|
}
|
|
97
95
|
//#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
96
|
//#region src/core/aggregate-builder.ts
|
|
283
97
|
/**
|
|
284
98
|
* Build the aggregate selector factory handed to `.aggregate(agg => …)` and
|
|
@@ -440,10 +254,8 @@ function createGroupedAccessor(init) {
|
|
|
440
254
|
* result to each row under the `relName` key.
|
|
441
255
|
*
|
|
442
256
|
* The join matches every field of the relation, so compound foreign keys join
|
|
443
|
-
* on the whole tuple. It
|
|
444
|
-
*
|
|
445
|
-
* one full scan with an in-memory filter. Rows are then grouped in memory,
|
|
446
|
-
* which avoids N+1 queries.
|
|
257
|
+
* on the whole tuple. It plans one batch lookup for all parents, then groups
|
|
258
|
+
* the rows in memory, which avoids N+1 queries.
|
|
447
259
|
*
|
|
448
260
|
* The `entry` carries any `include()` refinement:
|
|
449
261
|
*
|
|
@@ -493,50 +305,19 @@ async function loadRelation(relName, entry, rows, contract, modelName, executor,
|
|
|
493
305
|
lane: "idb-orm",
|
|
494
306
|
annotations: { groupingKey }
|
|
495
307
|
};
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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);
|
|
308
|
+
const joinWhere = andExpr(targetFields.map((field, i) => fieldFilter(field, "in", [...localTuples.values()].map((tuple) => tuple[i]))));
|
|
309
|
+
const where = refinedWhere === void 0 ? joinWhere : andExpr([joinWhere, refinedWhere]);
|
|
310
|
+
const request = { ...refinedWhere === void 0 ? {} : { where: refinedWhere } };
|
|
311
|
+
const catalog = buildCatalog(contract, relatedStoreName);
|
|
312
|
+
const lowered = lowerRows(catalog, planQuery(catalog, { where }), planMeta, request, (row) => {
|
|
313
|
+
const token = tupleToken(row, targetFields);
|
|
314
|
+
return token !== null && localTuples.has(token);
|
|
315
|
+
});
|
|
316
|
+
const plan = {
|
|
317
|
+
meta: planMeta,
|
|
318
|
+
idbPlan: lowered.idbPlan
|
|
319
|
+
};
|
|
320
|
+
return attachRelatedRows(relName, entry, rows, lowered.finish(await executor.query(plan).toArray()), relation);
|
|
540
321
|
}
|
|
541
322
|
/** Attach related rows by their join tuple, shaping each parent's collection independently. */
|
|
542
323
|
function attachRelatedRows(relName, entry, rows, relatedRows, relation) {
|
|
@@ -598,25 +379,6 @@ function tupleToken(row, fields) {
|
|
|
598
379
|
}
|
|
599
380
|
return JSON.stringify(parts);
|
|
600
381
|
}
|
|
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
382
|
//#endregion
|
|
621
383
|
//#region src/core/store-accessor.ts
|
|
622
384
|
/**
|
|
@@ -677,30 +439,14 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
677
439
|
all() {
|
|
678
440
|
const groupingKey = this.#newGroupingKey();
|
|
679
441
|
const buildScanPlan = this.#buildScanPlan.bind(this);
|
|
680
|
-
const executeOrRows = this.#executeOrRows.bind(this);
|
|
681
442
|
const executorQuery = this.#executor.query.bind(this.#executor);
|
|
682
443
|
const applyIncludes = this.#applyIncludes.bind(this);
|
|
683
444
|
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
445
|
return new AsyncIterableResult((async function* () {
|
|
691
|
-
|
|
692
|
-
const
|
|
693
|
-
|
|
694
|
-
|
|
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);
|
|
446
|
+
const scan = buildScanPlan(groupingKey);
|
|
447
|
+
const rows = [];
|
|
448
|
+
for await (const row of executorQuery(scan.plan)) rows.push(row);
|
|
449
|
+
const withIncludes = await applyIncludes(scan.finish(rows), groupingKey);
|
|
704
450
|
for (const row of projectRows(withIncludes)) yield row;
|
|
705
451
|
})());
|
|
706
452
|
}
|
|
@@ -717,17 +463,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
717
463
|
aggregates: toAggregateRequests(spec),
|
|
718
464
|
...combined !== void 0 ? { where: combined } : {}
|
|
719
465
|
};
|
|
720
|
-
|
|
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));
|
|
466
|
+
return computeAggregateSpec(spec, await this.#materialize(this.#newGroupingKey(), ast));
|
|
731
467
|
}
|
|
732
468
|
groupBy(...fields) {
|
|
733
469
|
const combined = this.#combinedFilterExpr();
|
|
@@ -777,22 +513,31 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
777
513
|
async findUnique(key) {
|
|
778
514
|
const groupingKey = this.#newGroupingKey();
|
|
779
515
|
const meta = this.#planMeta(groupingKey);
|
|
780
|
-
const
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
modelName: this.#modelName,
|
|
785
|
-
key
|
|
786
|
-
},
|
|
787
|
-
idbPlan: {
|
|
788
|
-
meta,
|
|
789
|
-
kind: "key-get",
|
|
790
|
-
storeName: this.#storeName,
|
|
791
|
-
key
|
|
792
|
-
}
|
|
516
|
+
const ast = {
|
|
517
|
+
kind: "findUnique",
|
|
518
|
+
modelName: this.#modelName,
|
|
519
|
+
key
|
|
793
520
|
};
|
|
794
|
-
|
|
795
|
-
|
|
521
|
+
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
522
|
+
const fields = typeof keyPath === "string" ? [keyPath] : keyPath;
|
|
523
|
+
const values = typeof keyPath === "string" ? [key] : key;
|
|
524
|
+
const where = combineFilterExprs(fields.map((field, i) => ({
|
|
525
|
+
kind: "field",
|
|
526
|
+
field,
|
|
527
|
+
op: "eq",
|
|
528
|
+
value: values[i]
|
|
529
|
+
})));
|
|
530
|
+
const request = {
|
|
531
|
+
...where === void 0 ? {} : { where },
|
|
532
|
+
take: 1
|
|
533
|
+
};
|
|
534
|
+
const lowered = this.#lowerRows(meta, request);
|
|
535
|
+
const rows = await this.#executor.query({
|
|
536
|
+
meta,
|
|
537
|
+
ast,
|
|
538
|
+
idbPlan: lowered.idbPlan
|
|
539
|
+
}).toArray();
|
|
540
|
+
return lowered.finish(rows)[0] ?? null;
|
|
796
541
|
}
|
|
797
542
|
async delete(key) {
|
|
798
543
|
if (hasEnforceableChildRelations(this.#contract, this.#modelName)) {
|
|
@@ -872,7 +617,6 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
872
617
|
async upsert(args) {
|
|
873
618
|
const keyPath = getKeyPath(this.#contract, this.#modelName);
|
|
874
619
|
const whereExpr = shorthandToFilterExpr(args.where);
|
|
875
|
-
const matches = (row) => whereExpr === void 0 || evaluateFilter(whereExpr, row);
|
|
876
620
|
const meta = this.#planMeta(this.#newGroupingKey());
|
|
877
621
|
const createRecord = args.create;
|
|
878
622
|
const patchRecord = args.update;
|
|
@@ -891,13 +635,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
891
635
|
...collectScalarFkStoreNames(this.#contract, this.#modelName, effectivePatch)
|
|
892
636
|
])];
|
|
893
637
|
return withMutationScope(exec, storeNames, async (scope) => {
|
|
894
|
-
const existing = (await scope
|
|
895
|
-
meta,
|
|
896
|
-
kind: "cursor-scan",
|
|
897
|
-
storeName,
|
|
898
|
-
filter: matches,
|
|
899
|
-
take: 1
|
|
900
|
-
}))[0];
|
|
638
|
+
const existing = (await readMutationRows(scope, this.#contract, this.#modelName, whereExpr, 1))[0];
|
|
901
639
|
if (existing === void 0) {
|
|
902
640
|
await validateScalarFks(scope, this.#contract, this.#modelName, createWithDefaults);
|
|
903
641
|
return (await scope.execute({
|
|
@@ -981,7 +719,6 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
981
719
|
*/
|
|
982
720
|
deleteAll() {
|
|
983
721
|
const combined = this.#combinedFilterExpr();
|
|
984
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
985
722
|
const contract = this.#contract;
|
|
986
723
|
const modelName = this.#modelName;
|
|
987
724
|
const executor = requireTransactionExecutor(this.#executor);
|
|
@@ -990,7 +727,7 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
990
727
|
executor,
|
|
991
728
|
contract,
|
|
992
729
|
modelName,
|
|
993
|
-
...
|
|
730
|
+
...combined === void 0 ? {} : { where: combined }
|
|
994
731
|
});
|
|
995
732
|
for (const row of rows) yield row;
|
|
996
733
|
})());
|
|
@@ -1003,29 +740,27 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1003
740
|
return this.#countTerminal();
|
|
1004
741
|
}
|
|
1005
742
|
async #countTerminal() {
|
|
1006
|
-
const groupingKey = this.#newGroupingKey();
|
|
1007
743
|
const combined = this.#combinedFilterExpr();
|
|
1008
|
-
const
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
const
|
|
744
|
+
const { skip, take } = this.#state;
|
|
745
|
+
const request = {
|
|
746
|
+
...combined === void 0 ? {} : { where: combined },
|
|
747
|
+
...skip === void 0 ? {} : { skip },
|
|
748
|
+
...take === void 0 ? {} : { take }
|
|
749
|
+
};
|
|
750
|
+
const meta = this.#planMeta(this.#newGroupingKey());
|
|
751
|
+
const catalog = buildCatalog(this.#contract, this.#storeName);
|
|
752
|
+
const lowered = lowerCount(catalog, planQuery(catalog, request), meta, request);
|
|
1015
753
|
const ast = {
|
|
1016
754
|
kind: "count",
|
|
1017
755
|
modelName: this.#modelName,
|
|
1018
|
-
...
|
|
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
|
|
756
|
+
...combined !== void 0 ? { where: combined } : {}
|
|
1025
757
|
};
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
758
|
+
const rows = await this.#executor.query({
|
|
759
|
+
meta,
|
|
760
|
+
ast,
|
|
761
|
+
idbPlan: lowered.idbPlan
|
|
762
|
+
}).toArray();
|
|
763
|
+
return lowered.finish(rows);
|
|
1029
764
|
}
|
|
1030
765
|
/**
|
|
1031
766
|
* Resolve an `include()` argument pair into an {@link IncludeEntry}: run the
|
|
@@ -1055,50 +790,27 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1055
790
|
throw new Error(`include('${relation}') refinement must return the collection (for where/orderBy/take/skip) or a count() selector`);
|
|
1056
791
|
}
|
|
1057
792
|
/**
|
|
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
793
|
* Materialise all rows matching the accumulated filters with no pagination —
|
|
1078
794
|
* used by `aggregate()` / `groupBy()`. The supplied `ast` is attached to the
|
|
1079
|
-
*
|
|
795
|
+
* row plan so middleware can observe the aggregate intent.
|
|
1080
796
|
*/
|
|
1081
797
|
async #materialize(groupingKey, ast) {
|
|
1082
798
|
const combined = this.#combinedFilterExpr();
|
|
1083
|
-
const filter = combined !== void 0 ? (row) => evaluateFilter(combined, row) : void 0;
|
|
1084
799
|
const meta = this.#planMeta(groupingKey);
|
|
1085
|
-
const
|
|
800
|
+
const lowered = this.#lowerRows(meta, { ...combined === void 0 ? {} : { where: combined } });
|
|
801
|
+
const rows = await this.#executor.query({
|
|
1086
802
|
meta,
|
|
1087
803
|
ast,
|
|
1088
|
-
idbPlan:
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
const rows = [];
|
|
1096
|
-
for await (const row of this.#executor.query(plan)) rows.push(row);
|
|
1097
|
-
return rows;
|
|
804
|
+
idbPlan: lowered.idbPlan
|
|
805
|
+
}).toArray();
|
|
806
|
+
return lowered.finish(rows);
|
|
807
|
+
}
|
|
808
|
+
#lowerRows(meta, request) {
|
|
809
|
+
const catalog = buildCatalog(this.#contract, this.#storeName);
|
|
810
|
+
return lowerRows(catalog, planQuery(catalog, request), meta, request);
|
|
1098
811
|
}
|
|
1099
|
-
#buildScanPlan(groupingKey
|
|
812
|
+
#buildScanPlan(groupingKey) {
|
|
1100
813
|
const combined = this.#combinedFilterExpr();
|
|
1101
|
-
const comparator = buildRowComparator(this.#state.orderBy);
|
|
1102
814
|
const meta = this.#planMeta(groupingKey);
|
|
1103
815
|
const ast = {
|
|
1104
816
|
kind: "findMany",
|
|
@@ -1108,76 +820,21 @@ var IdbStoreAccessorImpl = class IdbStoreAccessorImpl {
|
|
|
1108
820
|
...this.#state.skip !== void 0 ? { skip: this.#state.skip } : {},
|
|
1109
821
|
...this.#state.take !== void 0 ? { take: this.#state.take } : {}
|
|
1110
822
|
};
|
|
1111
|
-
|
|
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;
|
|
823
|
+
const lowered = this.#lowerRows(meta, ast);
|
|
1115
824
|
return {
|
|
1116
|
-
|
|
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 = {
|
|
825
|
+
plan: {
|
|
1150
826
|
meta,
|
|
1151
827
|
ast,
|
|
1152
|
-
idbPlan:
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
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));
|
|
828
|
+
idbPlan: lowered.idbPlan
|
|
829
|
+
},
|
|
830
|
+
finish: lowered.finish
|
|
831
|
+
};
|
|
1174
832
|
}
|
|
1175
833
|
/**
|
|
1176
834
|
* Combine all accumulated filter expressions with AND.
|
|
1177
835
|
*
|
|
1178
|
-
* Returns `undefined` when no filter has been installed so the
|
|
1179
|
-
*
|
|
1180
|
-
* `.all()` paths). Delegates to the shared {@link combineFilterExprs}.
|
|
836
|
+
* Returns `undefined` when no filter has been installed so the planner sees
|
|
837
|
+
* an unfiltered request. Delegates to the shared {@link combineFilterExprs}.
|
|
1181
838
|
*/
|
|
1182
839
|
#combinedFilterExpr() {
|
|
1183
840
|
return combineFilterExprs(this.#state.filters);
|
|
@@ -1253,4 +910,4 @@ function idbOrm(options) {
|
|
|
1253
910
|
//#endregion
|
|
1254
911
|
export { idbOrm as t };
|
|
1255
912
|
|
|
1256
|
-
//# sourceMappingURL=idb-orm-
|
|
913
|
+
//# sourceMappingURL=idb-orm-B1Br1C0F.mjs.map
|