@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.
- 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-Bp-hdRg2.d.mts} +3 -3
- package/dist/{executor-CevtdBfz.d.mts.map → executor-Bp-hdRg2.d.mts.map} +1 -1
- package/dist/{idb-orm-gEKHfG4H.d.mts → idb-orm-9XN3c9ly.d.mts} +5 -5
- package/dist/{idb-orm-gEKHfG4H.d.mts.map → idb-orm-9XN3c9ly.d.mts.map} +1 -1
- package/dist/{idb-orm-DsfllvzD.mjs → idb-orm-BitJ-EMq.mjs} +527 -382
- package/dist/idb-orm-BitJ-EMq.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-CS78EjPu.d.mts} +3 -3
- package/dist/{managed-client-B_xGXdjz.d.mts.map → managed-client-CS78EjPu.d.mts.map} +1 -1
- package/dist/{managed-client-Cp01irrm.mjs → managed-client-CxlNmERd.mjs} +3 -3
- package/dist/{managed-client-Cp01irrm.mjs.map → managed-client-CxlNmERd.mjs.map} +1 -1
- package/dist/{mutation-executor-CD5n_kKT.mjs → mutation-executor-SJqTuSbb.mjs} +11 -82
- package/dist/mutation-executor-SJqTuSbb.mjs.map +1 -0
- package/dist/{mutation-scope-pOn0QzvV.d.mts → mutation-scope-1hS0ogGa.d.mts} +2 -2
- package/dist/{mutation-scope-pOn0QzvV.d.mts.map → mutation-scope-1hS0ogGa.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,8 @@
|
|
|
1
|
-
import {
|
|
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
|
|
444
|
-
*
|
|
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
|
|
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);
|
|
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
|
-
|
|
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);
|
|
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
|
-
|
|
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
|
|
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
|
-
}
|
|
1005
|
+
const ast = {
|
|
1006
|
+
kind: "findUnique",
|
|
1007
|
+
modelName: this.#modelName,
|
|
1008
|
+
key
|
|
793
1009
|
};
|
|
794
|
-
|
|
795
|
-
|
|
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
|
|
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
|
-
...
|
|
1245
|
+
...combined !== void 0 ? { where: combined } : {}
|
|
1019
1246
|
};
|
|
1020
|
-
const
|
|
1021
|
-
|
|
1022
|
-
const plan = {
|
|
1023
|
-
...scanPlan,
|
|
1247
|
+
const rows = await this.#executor.query({
|
|
1248
|
+
...scan.plan,
|
|
1024
1249
|
ast
|
|
1025
|
-
};
|
|
1026
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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));
|
|
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
|
|
1179
|
-
*
|
|
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-
|
|
1401
|
+
//# sourceMappingURL=idb-orm-BitJ-EMq.mjs.map
|