kitcn 0.17.5 → 0.19.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/aggregate/index.d.ts +1 -1
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/index.d.ts +12 -12
- package/dist/auth/index.js +1 -1
- package/dist/{generated-contract-disabled-CHR4NLoa.d.ts → generated-contract-disabled-LOAfma7_.d.ts} +29 -29
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +595 -143
- package/dist/{query-context-C90vNlc9.js → query-context-BzihIpnM.js} +230 -103
- package/dist/{where-clause-compiler-DVO0oBe9.d.ts → where-clause-compiler-CmBz9CWA.d.ts} +48 -9
- package/package.json +1 -1
- package/skills/kitcn/references/features/orm.md +9 -2
package/dist/orm/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { A as vectorIndex, B as ConvexColumnBuilder, C as RlsPolicy, D as rankIndex, E as index, F as arrayOf, I as custom, L as json, M as createSystemFields, N as integer, O as searchIndex, P as id, R as objectOf, S as TablePolymorphic, T as aggregateIndex, V as entityKind, _ as OrmSchemaRelations, a as deletion, b as TableDeleteConfig, c as Columns, d as OrmSchemaDefinition, f as OrmSchemaExtensionRelations, g as OrmSchemaOptions, h as OrmSchemaExtensions, i as convexTable, j as text, k as uniqueIndex, l as EnableRLS, m as OrmSchemaExtensionTriggers, o as discriminator, p as OrmSchemaExtensionTables, s as Brand, t as DirectAggregate, u as OrmContext, v as OrmSchemaTriggers, w as rlsPolicy, x as TableName, y as RlsPolicies, z as unionOf } from "../runtime-DmVSOe24.js";
|
|
2
2
|
import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-C7LelqTN.js";
|
|
3
|
-
import { A as like, B as or, C as gt, D as isFieldReference, E as inArray, F as not, I as notBetween, L as notIlike, M as lte, N as matchLikePattern, O as isNotNull, P as ne, R as notInArray, S as filterValuesEqual, T as ilike, V as startsWith, _ as contains, a as indexKeyWithinBounds, b as fieldRef, c as streamIndexRange, d as and, f as arrayContained, g as column, h as between, i as getIndexFields, j as lt, k as isNull, l as isUnsetToken, m as arrayOverlaps, n as EmptyStream, o as mergedStream, p as arrayContains, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken, v as endsWith, w as gte, x as filterValueInList, y as eq, z as notLike } from "../query-context-
|
|
3
|
+
import { A as like, B as or, C as gt, D as isFieldReference, E as inArray, F as not, I as notBetween, L as notIlike, M as lte, N as matchLikePattern, O as isNotNull, P as ne, R as notInArray, S as filterValuesEqual, T as ilike, V as startsWith, _ as contains, a as indexKeyWithinBounds, b as fieldRef, c as streamIndexRange, d as and, f as arrayContained, g as column, h as between, i as getIndexFields, j as lt, k as isNull, l as isUnsetToken, m as arrayOverlaps, n as EmptyStream, o as mergedStream, p as arrayContains, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken, v as endsWith, w as gte, x as filterValueInList, y as eq, z as notLike } from "../query-context-BzihIpnM.js";
|
|
4
4
|
import { compareValues, v } from "convex/values";
|
|
5
5
|
import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric } from "convex/server";
|
|
6
6
|
|
|
@@ -290,6 +290,19 @@ function check(name, expression) {
|
|
|
290
290
|
return new ConvexCheckBuilder(name, expression);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
//#endregion
|
|
294
|
+
//#region src/orm/timestamp-mode.ts
|
|
295
|
+
const PUBLIC_CREATED_AT_FIELD = "createdAt";
|
|
296
|
+
const INTERNAL_CREATION_TIME_FIELD = "_creationTime";
|
|
297
|
+
const CREATED_AT_MIGRATION_MESSAGE = "`_creationTime` is no longer public. Use `createdAt` instead.";
|
|
298
|
+
const hasUserCreatedAtColumn = (table) => {
|
|
299
|
+
if (!table || typeof table !== "object") return false;
|
|
300
|
+
const columns = table[Columns];
|
|
301
|
+
if (!columns || typeof columns !== "object") return false;
|
|
302
|
+
return Object.hasOwn(columns, PUBLIC_CREATED_AT_FIELD);
|
|
303
|
+
};
|
|
304
|
+
const usesSystemCreatedAtAlias = (_table) => true;
|
|
305
|
+
|
|
293
306
|
//#endregion
|
|
294
307
|
//#region src/orm/index-utils.ts
|
|
295
308
|
function getIndexes(table) {
|
|
@@ -319,37 +332,53 @@ function findVectorIndexByName(table, indexName) {
|
|
|
319
332
|
return getVectorIndexes(table).find((index) => index.name === indexName) ?? null;
|
|
320
333
|
}
|
|
321
334
|
function findIndexForColumns(indexes, columns) {
|
|
322
|
-
for (const index of indexes)
|
|
323
|
-
if (index.fields.length < columns.length) continue;
|
|
324
|
-
let matches = true;
|
|
325
|
-
for (let i = 0; i < columns.length; i++) if (index.fields[i] !== columns[i]) {
|
|
326
|
-
matches = false;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
if (matches) return index.name;
|
|
330
|
-
}
|
|
335
|
+
for (const index of indexes) if (hasColumnPrefix(index, columns)) return index.name;
|
|
331
336
|
return null;
|
|
332
337
|
}
|
|
333
|
-
|
|
334
|
-
|
|
338
|
+
const hasColumnPrefix = (index, columns) => {
|
|
339
|
+
if (index.fields.length < columns.length) return false;
|
|
340
|
+
for (let i = 0; i < columns.length; i += 1) if (index.fields[i] !== columns[i]) return false;
|
|
341
|
+
return true;
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Answers "does scanning this index already produce the requested order?".
|
|
345
|
+
*
|
|
346
|
+
* Convex walks an index in full key order, so once a leading run of fields is
|
|
347
|
+
* pinned to a single value by `eq`, the remainder of the scan is already sorted
|
|
348
|
+
* by the next index field — and by `_creationTime` once every declared field is
|
|
349
|
+
* pinned, because Convex appends it as the implicit trailing key. When that
|
|
350
|
+
* field is the one the caller asked to sort by, `.order(dir).take(n)` returns
|
|
351
|
+
* the exact page and nothing has to be collected and sorted afterwards.
|
|
352
|
+
*
|
|
353
|
+
* This is the single owner of that decision. The top-level query planner and
|
|
354
|
+
* the relation loader both call it, so a bound one of them can push into the
|
|
355
|
+
* index can never be one the other silently drops.
|
|
356
|
+
*
|
|
357
|
+
* Returns the direction to hand to `.order()`, or null when the sort must run
|
|
358
|
+
* after the fetch.
|
|
359
|
+
*/
|
|
360
|
+
function resolveIndexOrderPushdown(params) {
|
|
361
|
+
const { indexFields, pinnedEqCount, orderSpecs } = params;
|
|
362
|
+
if (!indexFields || orderSpecs.length !== 1) return null;
|
|
363
|
+
const primary = orderSpecs[0];
|
|
364
|
+
const eqCount = Math.min(Math.max(pinnedEqCount, 0), indexFields.length);
|
|
365
|
+
for (let i = 0; i < eqCount; i += 1) if (indexFields[i] === primary.field) return primary.direction;
|
|
366
|
+
const nativeField = eqCount >= indexFields.length ? INTERNAL_CREATION_TIME_FIELD : indexFields[eqCount];
|
|
367
|
+
return primary.field === nativeField ? primary.direction : null;
|
|
368
|
+
}
|
|
369
|
+
function findRelationIndex(table, columns, relationName, targetTableName, strict = true, allowFullScan = false, orderSpecs = []) {
|
|
370
|
+
const indexes = getIndexes(table);
|
|
371
|
+
const fallbackIndex = findIndexForColumns(indexes, columns);
|
|
372
|
+
const index = (orderSpecs.length === 0 ? void 0 : indexes.find((candidate) => hasColumnPrefix(candidate, columns) && resolveIndexOrderPushdown({
|
|
373
|
+
indexFields: candidate.fields,
|
|
374
|
+
pinnedEqCount: columns.length,
|
|
375
|
+
orderSpecs
|
|
376
|
+
}) !== null))?.name ?? fallbackIndex;
|
|
335
377
|
if (!index && !allowFullScan) throw new Error(`Relation ${relationName} requires index on '${targetTableName}(${columns.join(", ")})'. Set allowFullScan: true to override.`);
|
|
336
378
|
if (!index && strict) console.warn(`Relation ${relationName} running without index (allowFullScan: true).`);
|
|
337
379
|
return index;
|
|
338
380
|
}
|
|
339
381
|
|
|
340
|
-
//#endregion
|
|
341
|
-
//#region src/orm/timestamp-mode.ts
|
|
342
|
-
const PUBLIC_CREATED_AT_FIELD = "createdAt";
|
|
343
|
-
const INTERNAL_CREATION_TIME_FIELD = "_creationTime";
|
|
344
|
-
const CREATED_AT_MIGRATION_MESSAGE = "`_creationTime` is no longer public. Use `createdAt` instead.";
|
|
345
|
-
const hasUserCreatedAtColumn = (table) => {
|
|
346
|
-
if (!table || typeof table !== "object") return false;
|
|
347
|
-
const columns = table[Columns];
|
|
348
|
-
if (!columns || typeof columns !== "object") return false;
|
|
349
|
-
return Object.hasOwn(columns, PUBLIC_CREATED_AT_FIELD);
|
|
350
|
-
};
|
|
351
|
-
const usesSystemCreatedAtAlias = (_table) => true;
|
|
352
|
-
|
|
353
382
|
//#endregion
|
|
354
383
|
//#region src/orm/write-fanout.ts
|
|
355
384
|
/**
|
|
@@ -3897,7 +3926,24 @@ async function filterSelectRows(options) {
|
|
|
3897
3926
|
|
|
3898
3927
|
//#endregion
|
|
3899
3928
|
//#region src/orm/where-clause-compiler.ts
|
|
3929
|
+
/**
|
|
3930
|
+
* Enough to clear the 25-point gap between an exact match and a prefix match,
|
|
3931
|
+
* so an index that supplies the order outranks a narrower one that does not.
|
|
3932
|
+
*/
|
|
3933
|
+
const INDEX_ORDER_BONUS = 30;
|
|
3934
|
+
/**
|
|
3935
|
+
* Widest `in` list still worth turning into an index union when it is the only
|
|
3936
|
+
* indexable term. Each value becomes its own index range, so past this width
|
|
3937
|
+
* the fan-out costs more than the single scan it would replace.
|
|
3938
|
+
*/
|
|
3939
|
+
const MAX_PROMOTED_PROBES = 64;
|
|
3900
3940
|
var WhereClauseCompiler = class {
|
|
3941
|
+
/**
|
|
3942
|
+
* Requested sort fields for the compile in flight. Index choice has to see
|
|
3943
|
+
* them: an index whose next key after the pinned prefix is the sort field
|
|
3944
|
+
* lets Convex serve the order from the scan, which no narrower index can do.
|
|
3945
|
+
*/
|
|
3946
|
+
orderFields = [];
|
|
3901
3947
|
constructor(_tableName, availableIndexes) {
|
|
3902
3948
|
this.availableIndexes = availableIndexes;
|
|
3903
3949
|
}
|
|
@@ -3905,9 +3951,11 @@ var WhereClauseCompiler = class {
|
|
|
3905
3951
|
* Compile a filter expression to Convex query structure
|
|
3906
3952
|
*
|
|
3907
3953
|
* @param expression - Filter expression tree
|
|
3954
|
+
* @param options.orderFields - Resolved orderBy fields, most significant first
|
|
3908
3955
|
* @returns Compilation result with index and filters
|
|
3909
3956
|
*/
|
|
3910
|
-
compile(expression) {
|
|
3957
|
+
compile(expression, options) {
|
|
3958
|
+
this.orderFields = options?.orderFields ?? [];
|
|
3911
3959
|
if (!expression) return {
|
|
3912
3960
|
strategy: "none",
|
|
3913
3961
|
selectedIndex: null,
|
|
@@ -3919,6 +3967,10 @@ var WhereClauseCompiler = class {
|
|
|
3919
3967
|
if (specialCase) return specialCase;
|
|
3920
3968
|
const referencedFields = this.extractFieldReferences(expression);
|
|
3921
3969
|
const selectedIndex = this.selectIndex(referencedFields);
|
|
3970
|
+
if (!selectedIndex) {
|
|
3971
|
+
const promoted = this.tryCompileAndInArray(expression);
|
|
3972
|
+
if (promoted) return promoted;
|
|
3973
|
+
}
|
|
3922
3974
|
const { indexFilters, postFilters } = this.splitFilters(expression, selectedIndex);
|
|
3923
3975
|
return {
|
|
3924
3976
|
strategy: this.resolveStrategy(selectedIndex, indexFilters),
|
|
@@ -3937,6 +3989,39 @@ var WhereClauseCompiler = class {
|
|
|
3937
3989
|
if (expression.type === "logical") return this.tryCompileOrSpecialCase(expression);
|
|
3938
3990
|
return null;
|
|
3939
3991
|
}
|
|
3992
|
+
/**
|
|
3993
|
+
* `where: { status: { in: [...] }, name: { contains: 'x' } }` compiles to an
|
|
3994
|
+
* AND, and `extractFieldReferences` treats `inArray` as unindexable, so index
|
|
3995
|
+
* selection sees nothing and the plan degrades to a full table scan — even
|
|
3996
|
+
* though the very same `in` compiles to an index union on its own.
|
|
3997
|
+
*
|
|
3998
|
+
* Only fires when no index was selected at all, so a working `eq`-anchored
|
|
3999
|
+
* plan is never traded for a probe fan-out. Every other term of the AND stays
|
|
4000
|
+
* in `postFilters`, which the executor enforces per probe and again in
|
|
4001
|
+
* JavaScript.
|
|
4002
|
+
*/
|
|
4003
|
+
tryCompileAndInArray(expression) {
|
|
4004
|
+
if (expression.type !== "logical" || expression.operator !== "and") return null;
|
|
4005
|
+
const terms = [];
|
|
4006
|
+
const flatten = (expr) => {
|
|
4007
|
+
if (expr.type === "logical" && expr.operator === "and") {
|
|
4008
|
+
for (const operand of expr.operands) flatten(operand);
|
|
4009
|
+
return;
|
|
4010
|
+
}
|
|
4011
|
+
terms.push(expr);
|
|
4012
|
+
};
|
|
4013
|
+
flatten(expression);
|
|
4014
|
+
for (const term of terms) {
|
|
4015
|
+
if (term.type !== "binary" || term.operator !== "inArray") continue;
|
|
4016
|
+
const probePlan = this.tryCompileInArray(term);
|
|
4017
|
+
if (!probePlan || probePlan.probeFilters.length > MAX_PROMOTED_PROBES) continue;
|
|
4018
|
+
return {
|
|
4019
|
+
...probePlan,
|
|
4020
|
+
postFilters: [expression]
|
|
4021
|
+
};
|
|
4022
|
+
}
|
|
4023
|
+
return null;
|
|
4024
|
+
}
|
|
3940
4025
|
resolveStrategy(selectedIndex, indexFilters) {
|
|
3941
4026
|
if (!selectedIndex || indexFilters.length === 0) return "none";
|
|
3942
4027
|
return indexFilters.some((filter) => filter.type === "binary" && (filter.operator === "gt" || filter.operator === "gte" || filter.operator === "lt" || filter.operator === "lte")) ? "rangeIndex" : "singleIndex";
|
|
@@ -4118,7 +4203,13 @@ var WhereClauseCompiler = class {
|
|
|
4118
4203
|
};
|
|
4119
4204
|
}
|
|
4120
4205
|
findLeadingIndex(fieldName) {
|
|
4121
|
-
|
|
4206
|
+
const candidates = this.availableIndexes.filter((index) => index.indexFields[0] === fieldName).sort((a, b) => a.indexFields.length - b.indexFields.length);
|
|
4207
|
+
if (this.orderFields.length > 0) {
|
|
4208
|
+
const orderField = this.orderFields[0];
|
|
4209
|
+
const serving = candidates.find((index) => index.indexFields.length > 1 && index.indexFields[1] === orderField);
|
|
4210
|
+
if (serving) return serving;
|
|
4211
|
+
}
|
|
4212
|
+
return candidates[0] ?? null;
|
|
4122
4213
|
}
|
|
4123
4214
|
getLikePrefix(pattern) {
|
|
4124
4215
|
if (!pattern || pattern.startsWith("%") || pattern.includes("_")) return null;
|
|
@@ -4276,7 +4367,7 @@ var WhereClauseCompiler = class {
|
|
|
4276
4367
|
const prefixCount = this.getPrefixMatchCount(indexFields, referencedFields);
|
|
4277
4368
|
if (prefixCount > 0) return {
|
|
4278
4369
|
index,
|
|
4279
|
-
score: 75 + prefixCount,
|
|
4370
|
+
score: 75 + prefixCount + this.indexOrderBonus(indexFields, prefixCount),
|
|
4280
4371
|
matchType: "prefix",
|
|
4281
4372
|
matchedFields: indexFields.slice(0, prefixCount)
|
|
4282
4373
|
};
|
|
@@ -4290,6 +4381,15 @@ var WhereClauseCompiler = class {
|
|
|
4290
4381
|
return null;
|
|
4291
4382
|
}
|
|
4292
4383
|
/**
|
|
4384
|
+
* Reward an index that also supplies the requested order. Large enough to
|
|
4385
|
+
* clear the 25-point exact-over-prefix premium, so `(orgId, createdAt)` beats
|
|
4386
|
+
* `(orgId)` for `where { orgId } orderBy { createdAt }`.
|
|
4387
|
+
*/
|
|
4388
|
+
indexOrderBonus(indexFields, pinnedLength) {
|
|
4389
|
+
if (this.orderFields.length === 0 || pinnedLength >= indexFields.length) return 0;
|
|
4390
|
+
return indexFields[pinnedLength] === this.orderFields[0] ? INDEX_ORDER_BONUS : 0;
|
|
4391
|
+
}
|
|
4392
|
+
/**
|
|
4293
4393
|
* Check if referenced fields exactly match index fields in order
|
|
4294
4394
|
*/
|
|
4295
4395
|
isExactMatch(indexFields, referencedFields) {
|
|
@@ -4390,7 +4490,7 @@ var WhereClauseCompiler = class {
|
|
|
4390
4490
|
takeRange(fieldName);
|
|
4391
4491
|
break;
|
|
4392
4492
|
}
|
|
4393
|
-
for (const binary of
|
|
4493
|
+
for (const filters of binariesByField.values()) for (const binary of filters) if (!consumed.has(binary)) postFilters.push(binary);
|
|
4394
4494
|
return {
|
|
4395
4495
|
indexFilters,
|
|
4396
4496
|
postFilters
|
|
@@ -4419,6 +4519,13 @@ const POST_FETCH_ONLY_OPERATORS = new Set([
|
|
|
4419
4519
|
"arrayOverlaps"
|
|
4420
4520
|
]);
|
|
4421
4521
|
const DEFAULT_RELATION_FAN_OUT_MAX_KEYS = 1e3;
|
|
4522
|
+
/**
|
|
4523
|
+
* How many child rows the bounded relation stream filters at a time. Batching
|
|
4524
|
+
* lets the sub-relation loader de-duplicate foreign keys and run its reads
|
|
4525
|
+
* concurrently; the cost is reading at most `chunk - 1` rows past the one that
|
|
4526
|
+
* satisfies the limit.
|
|
4527
|
+
*/
|
|
4528
|
+
const RELATION_FILTER_STREAM_CHUNK = 32;
|
|
4422
4529
|
const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS = 4096;
|
|
4423
4530
|
const DEFAULT_AGGREGATE_WORK_BUDGET = 16384;
|
|
4424
4531
|
const PUBLIC_ID_FIELD = "id";
|
|
@@ -4429,6 +4536,11 @@ const RELATION_COUNT_ERROR = {
|
|
|
4429
4536
|
FILTER_UNSUPPORTED: "RELATION_COUNT_FILTER_UNSUPPORTED"
|
|
4430
4537
|
};
|
|
4431
4538
|
/**
|
|
4539
|
+
* Physical-table-name lookup, keyed on schema identity. The schema is a
|
|
4540
|
+
* module-level immutable, so the index outlives any single request.
|
|
4541
|
+
*/
|
|
4542
|
+
const tableConfigByDbNameCache = /* @__PURE__ */ new WeakMap();
|
|
4543
|
+
/**
|
|
4432
4544
|
* Replays an already-read run of `[doc | null, indexKey]` entries.
|
|
4433
4545
|
*
|
|
4434
4546
|
* `narrow()` filters the buffer instead of re-issuing the read, so a stream
|
|
@@ -4473,6 +4585,42 @@ var BufferedQueryStream = class BufferedQueryStream extends QueryStream {
|
|
|
4473
4585
|
return this.equalityIndexFilter;
|
|
4474
4586
|
}
|
|
4475
4587
|
};
|
|
4588
|
+
const ID_LIST_POSITION_FIELD = "__kitcn_id_list_position";
|
|
4589
|
+
/**
|
|
4590
|
+
* Reads an `id` / `id in [...]` where one document at a time, in the order the
|
|
4591
|
+
* ids were given.
|
|
4592
|
+
*
|
|
4593
|
+
* The index key is the position in the de-duplicated id list, so a cursor names
|
|
4594
|
+
* a position and `narrow` drops entries without reading them. A page reads only
|
|
4595
|
+
* the listed positions it visits, not every id in the list on every page.
|
|
4596
|
+
*/
|
|
4597
|
+
var LazyIdListQueryStream = class LazyIdListQueryStream extends QueryStream {
|
|
4598
|
+
constructor(readId, entries, order) {
|
|
4599
|
+
super();
|
|
4600
|
+
this.readId = readId;
|
|
4601
|
+
this.entries = entries;
|
|
4602
|
+
this.order = order;
|
|
4603
|
+
}
|
|
4604
|
+
iterWithKeys() {
|
|
4605
|
+
const entries = this.entries;
|
|
4606
|
+
const readId = this.readId;
|
|
4607
|
+
return { async *[Symbol.asyncIterator]() {
|
|
4608
|
+
for (const [position, id] of entries) yield [await readId(id), [position]];
|
|
4609
|
+
} };
|
|
4610
|
+
}
|
|
4611
|
+
narrow(indexBounds) {
|
|
4612
|
+
return new LazyIdListQueryStream(this.readId, this.entries.filter(([position]) => indexKeyWithinBounds([position], indexBounds)), this.order);
|
|
4613
|
+
}
|
|
4614
|
+
getOrder() {
|
|
4615
|
+
return this.order;
|
|
4616
|
+
}
|
|
4617
|
+
getIndexFields() {
|
|
4618
|
+
return [ID_LIST_POSITION_FIELD];
|
|
4619
|
+
}
|
|
4620
|
+
getEqualityIndexFilter() {
|
|
4621
|
+
return [];
|
|
4622
|
+
}
|
|
4623
|
+
};
|
|
4476
4624
|
const PIPELINE_LIMIT_ORDINAL_FIELD = "__kitcn_limit_ordinal";
|
|
4477
4625
|
/** Cap a stream at its first `limit` matching documents without eager reads. */
|
|
4478
4626
|
var LimitedMatchesQueryStream = class LimitedMatchesQueryStream extends QueryStream {
|
|
@@ -4565,6 +4713,33 @@ var GelRankQuery = class {
|
|
|
4565
4713
|
return await readRankRandom(this.db, plan);
|
|
4566
4714
|
}
|
|
4567
4715
|
};
|
|
4716
|
+
const CONFIGURED_INDEX_RANGE_OPERATORS = new Set([
|
|
4717
|
+
"eq",
|
|
4718
|
+
"gt",
|
|
4719
|
+
"gte",
|
|
4720
|
+
"lt",
|
|
4721
|
+
"lte"
|
|
4722
|
+
]);
|
|
4723
|
+
const observeConfiguredIndexRange = (builder, operations) => new Proxy(builder, { get(target, property) {
|
|
4724
|
+
const value = Reflect.get(target, property, target);
|
|
4725
|
+
if (typeof property === "string" && CONFIGURED_INDEX_RANGE_OPERATORS.has(property) && typeof value === "function") return (field, ...args) => {
|
|
4726
|
+
operations.push({
|
|
4727
|
+
field,
|
|
4728
|
+
operator: property
|
|
4729
|
+
});
|
|
4730
|
+
return observeConfiguredIndexRange(Reflect.apply(value, target, [field, ...args]), operations);
|
|
4731
|
+
};
|
|
4732
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
4733
|
+
} });
|
|
4734
|
+
const countConfiguredIndexEqPrefix = (indexFields, operations) => {
|
|
4735
|
+
if (!indexFields) return 0;
|
|
4736
|
+
let count = 0;
|
|
4737
|
+
for (const operation of operations) {
|
|
4738
|
+
if (operation.operator !== "eq" || operation.field !== indexFields[count]) break;
|
|
4739
|
+
count += 1;
|
|
4740
|
+
}
|
|
4741
|
+
return count;
|
|
4742
|
+
};
|
|
4568
4743
|
/**
|
|
4569
4744
|
* Relational query builder with promise-based execution
|
|
4570
4745
|
*
|
|
@@ -4754,8 +4929,19 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4754
4929
|
}
|
|
4755
4930
|
return 0;
|
|
4756
4931
|
}
|
|
4932
|
+
/**
|
|
4933
|
+
* Physical table name to relational config. Called once per row on the
|
|
4934
|
+
* relation-count path, so the linear schema scan is indexed once per schema
|
|
4935
|
+
* object rather than repeated. The schema is fixed for the process lifetime.
|
|
4936
|
+
*/
|
|
4757
4937
|
_getTableConfigByDbName(dbName) {
|
|
4758
|
-
|
|
4938
|
+
let byDbName = tableConfigByDbNameCache.get(this.schema);
|
|
4939
|
+
if (!byDbName) {
|
|
4940
|
+
byDbName = /* @__PURE__ */ new Map();
|
|
4941
|
+
for (const table of Object.values(this.schema)) if (table?.name && !byDbName.has(table.name)) byDbName.set(table.name, table);
|
|
4942
|
+
tableConfigByDbNameCache.set(this.schema, byDbName);
|
|
4943
|
+
}
|
|
4944
|
+
return byDbName.get(dbName);
|
|
4759
4945
|
}
|
|
4760
4946
|
_matchLike(value, pattern, caseInsensitive) {
|
|
4761
4947
|
return matchLikePattern(value, pattern, caseInsensitive);
|
|
@@ -5637,9 +5823,12 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
5637
5823
|
* The where-clause compiler is built from declared indexes only, and `_id` is
|
|
5638
5824
|
* never one of them, so an `id` filter can never be index-selected: it lands
|
|
5639
5825
|
* in the post-filters and the stream walks the creation-time index until it
|
|
5640
|
-
* happens on the row. `db.get()` reads exactly the rows asked for
|
|
5641
|
-
*
|
|
5642
|
-
*
|
|
5826
|
+
* happens on the row. `db.get()` reads exactly the rows asked for.
|
|
5827
|
+
*
|
|
5828
|
+
* Rows come back in the order the ids were given, one read at a time. Missing
|
|
5829
|
+
* or policy-filtered ids still cost a read, but a page does not reread the
|
|
5830
|
+
* complete list. An `orderBy` on creation time is the exception: an id carries
|
|
5831
|
+
* no creation time, so every id must be read before the first row is placed.
|
|
5643
5832
|
*
|
|
5644
5833
|
* Returns null when something else already owns the read: a pinned index, an
|
|
5645
5834
|
* index the compiler did select, a `where(predicate)`, or an `orderBy` that
|
|
@@ -5651,13 +5840,26 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
5651
5840
|
const primaryOrder = queryConfig.order?.[0];
|
|
5652
5841
|
if (primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD) return null;
|
|
5653
5842
|
const ids = idLookup.kind === "in" ? Array.from(new Map(idLookup.ids.map((id) => [String(id), id])).values()) : [idLookup.id];
|
|
5654
|
-
const
|
|
5655
|
-
|
|
5656
|
-
|
|
5843
|
+
const readId = (id) => this._getById(this.tableConfig.name, id);
|
|
5844
|
+
if (!primaryOrder) {
|
|
5845
|
+
const entries = ids.map((id, position) => [position, id]);
|
|
5846
|
+
if (order === "desc") entries.reverse();
|
|
5847
|
+
return new LazyIdListQueryStream(readId, entries, order);
|
|
5848
|
+
}
|
|
5849
|
+
const rows = (await this._mapWithConcurrency(ids, readId)).filter((row) => !!row);
|
|
5657
5850
|
rows.sort((a, b) => compareValues(a[INTERNAL_CREATION_TIME_FIELD], b[INTERNAL_CREATION_TIME_FIELD]) || compareValues(a[INTERNAL_ID_FIELD], b[INTERNAL_ID_FIELD]));
|
|
5658
5851
|
if (order === "desc") rows.reverse();
|
|
5659
5852
|
return new BufferedQueryStream(rows.map((row) => [row, [row[INTERNAL_CREATION_TIME_FIELD], row[INTERNAL_ID_FIELD]]]), order, [INTERNAL_CREATION_TIME_FIELD, INTERNAL_ID_FIELD], []);
|
|
5660
5853
|
}
|
|
5854
|
+
/**
|
|
5855
|
+
* The declared index a stream read can walk to emit `field` in order.
|
|
5856
|
+
*
|
|
5857
|
+
* A stream orders by the index it scans, so only an index that leads with
|
|
5858
|
+
* the field produces that order.
|
|
5859
|
+
*/
|
|
5860
|
+
_findStreamOrderIndex(field) {
|
|
5861
|
+
return getIndexes(this.tableConfig.table).find((index) => index.fields[0] === field);
|
|
5862
|
+
}
|
|
5661
5863
|
_buildBasePipelineStream(queryConfig, wherePredicate, configuredIndex) {
|
|
5662
5864
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
5663
5865
|
let streamQuery = stream(this.db, schemaDefinition).query(this.tableConfig.name);
|
|
@@ -5669,8 +5871,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
5669
5871
|
return indexQuery;
|
|
5670
5872
|
});
|
|
5671
5873
|
else if (configuredIndex?.name) streamQuery = streamQuery.withIndex(configuredIndex.name, configuredIndex.range ? configuredIndex.range : (q) => q);
|
|
5672
|
-
else if (primaryOrder && primaryOrder.field !==
|
|
5673
|
-
const orderIndex =
|
|
5874
|
+
else if (primaryOrder && primaryOrder.field !== INTERNAL_CREATION_TIME_FIELD) {
|
|
5875
|
+
const orderIndex = this._findStreamOrderIndex(primaryOrder.field);
|
|
5674
5876
|
if (orderIndex) streamQuery = streamQuery.withIndex(orderIndex.name, (q) => q);
|
|
5675
5877
|
}
|
|
5676
5878
|
streamQuery = streamQuery.order(primaryOrderDirection);
|
|
@@ -6828,7 +7030,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
6828
7030
|
streamQuery = mergedStream(streams, pipeline.interleaveBy.map((field) => this._normalizePublicFieldName(field)));
|
|
6829
7031
|
}
|
|
6830
7032
|
} else {
|
|
6831
|
-
|
|
7033
|
+
const orderedIdList = isCursorPaginated && maxScan !== void 0 && idLookup?.kind === "in" ? queryConfig.order?.[0] : void 0;
|
|
7034
|
+
if (orderedIdList) {
|
|
7035
|
+
if (orderedIdList.field === INTERNAL_CREATION_TIME_FIELD) throw new Error("An id IN pipeline cannot combine orderBy on createdAt with maxScan, because ordering an id list by creation time requires reading every id in the list. Drop maxScan, or drop orderBy to page in id-list order at one read per row.");
|
|
7036
|
+
if (queryConfig.order?.length !== 1 || configuredIndex?.name || queryConfig.index || !this._findStreamOrderIndex(orderedIdList.field)) throw new Error(`An id IN pipeline cannot combine orderBy on ${this._toPublicFilterFieldName(orderedIdList.field)} with maxScan, because the scan cannot produce that order. It needs a single orderBy field that an index leads with, and no index pinned by withIndex().`);
|
|
7037
|
+
}
|
|
6832
7038
|
streamQuery = await this._buildIdLookupStream({
|
|
6833
7039
|
configuredIndex,
|
|
6834
7040
|
idLookup,
|
|
@@ -6959,14 +7165,28 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
6959
7165
|
return indexQuery;
|
|
6960
7166
|
});
|
|
6961
7167
|
if (primaryOrder) {
|
|
6962
|
-
const
|
|
6963
|
-
|
|
7168
|
+
const pushdownDirection = resolveIndexOrderPushdown({
|
|
7169
|
+
indexFields: indexConfig.fields,
|
|
7170
|
+
pinnedEqCount: this._indexEqPrefixCount(queryConfig),
|
|
7171
|
+
orderSpecs: [primaryOrder]
|
|
7172
|
+
});
|
|
7173
|
+
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
6964
7174
|
else needsPostFetchSortForPrimary = true;
|
|
6965
7175
|
}
|
|
6966
7176
|
} else if (configuredIndex?.name) {
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
7177
|
+
const configuredIndexFields = getIndexes(this.tableConfig.table).find((idx) => idx.name === configuredIndex.name)?.fields;
|
|
7178
|
+
const rangeOperations = [];
|
|
7179
|
+
const configuredRange = configuredIndex.range;
|
|
7180
|
+
query = query.withIndex(configuredIndex.name, configuredRange ? (q) => configuredRange(observeConfiguredIndexRange(q, rangeOperations)) : (q) => q);
|
|
7181
|
+
if (primaryOrder) {
|
|
7182
|
+
const pushdownDirection = resolveIndexOrderPushdown({
|
|
7183
|
+
indexFields: configuredIndexFields,
|
|
7184
|
+
pinnedEqCount: countConfiguredIndexEqPrefix(configuredIndexFields, rangeOperations),
|
|
7185
|
+
orderSpecs: [primaryOrder]
|
|
7186
|
+
});
|
|
7187
|
+
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
7188
|
+
else needsPostFetchSortForPrimary = true;
|
|
7189
|
+
}
|
|
6970
7190
|
} else if (queryConfig.order && primaryOrder) {
|
|
6971
7191
|
const orderField = primaryOrder.field;
|
|
6972
7192
|
if (orderField === "_creationTime") query = query.order(primaryOrder.direction);
|
|
@@ -7033,32 +7253,42 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7033
7253
|
return this._returnSelectedRows(selectedRows);
|
|
7034
7254
|
}
|
|
7035
7255
|
if (queryConfig.strategy === "multiProbe" && queryConfig.index && !isCursorPaginated) {
|
|
7256
|
+
const probeOffset = config.offset ?? 0;
|
|
7257
|
+
if (typeof probeOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
7258
|
+
const probeLimit = this._resolveNonPaginatedLimit(config);
|
|
7259
|
+
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
7260
|
+
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
7261
|
+
const probeHasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
7262
|
+
const probeOrderDirection = primaryOrder ? resolveIndexOrderPushdown({
|
|
7263
|
+
indexFields: queryConfig.index.fields,
|
|
7264
|
+
pinnedEqCount: this._indexEqPrefixCount(queryConfig),
|
|
7265
|
+
orderSpecs: [primaryOrder]
|
|
7266
|
+
}) : null;
|
|
7267
|
+
const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null && !hasSecondaryOrders) ? probeOffset + probeLimit : void 0;
|
|
7036
7268
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
7037
7269
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
7038
7270
|
let indexQuery = q;
|
|
7039
7271
|
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
7040
7272
|
return indexQuery;
|
|
7041
7273
|
});
|
|
7042
|
-
if (
|
|
7274
|
+
if (probeBound !== void 0 && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
|
|
7275
|
+
if (convexProbeFilters.length > 0) probeQuery = probeQuery.filter((q) => {
|
|
7043
7276
|
let result = null;
|
|
7044
|
-
for (const filter of
|
|
7277
|
+
for (const filter of convexProbeFilters) {
|
|
7045
7278
|
const expr = this._toConvexExpression(filter)(q);
|
|
7046
7279
|
result = result ? q.and(result, expr) : expr;
|
|
7047
7280
|
}
|
|
7048
7281
|
return result ?? q;
|
|
7049
7282
|
});
|
|
7050
|
-
return await probeQuery.collect();
|
|
7283
|
+
return probeBound === void 0 ? await probeQuery.collect() : await probeQuery.take(probeBound);
|
|
7051
7284
|
}));
|
|
7052
7285
|
let rows = Array.from(new Map(probeRows.flat().map((row) => [String(row._id), row])).values());
|
|
7053
7286
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
7054
7287
|
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
7055
7288
|
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with);
|
|
7056
7289
|
if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
|
|
7057
|
-
|
|
7058
|
-
if (
|
|
7059
|
-
const limit = this._resolveNonPaginatedLimit(config);
|
|
7060
|
-
if (offset > 0) rows = rows.slice(offset);
|
|
7061
|
-
if (limit !== void 0) rows = rows.slice(0, limit);
|
|
7290
|
+
if (probeOffset > 0) rows = rows.slice(probeOffset);
|
|
7291
|
+
if (probeLimit !== void 0) rows = rows.slice(0, probeLimit);
|
|
7062
7292
|
const selectedRows = await this._finalizeRows(rows);
|
|
7063
7293
|
return this._returnSelectedRows(selectedRows);
|
|
7064
7294
|
}
|
|
@@ -7195,6 +7425,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7195
7425
|
}
|
|
7196
7426
|
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
7197
7427
|
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
7428
|
+
const hasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
7198
7429
|
if (convexPostFilters.length > 0) query = query.filter((q) => {
|
|
7199
7430
|
let result = null;
|
|
7200
7431
|
for (const filter of convexPostFilters) {
|
|
@@ -7207,7 +7438,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7207
7438
|
if (typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
7208
7439
|
const limit = this._resolveNonPaginatedLimit(config);
|
|
7209
7440
|
const paginateAfterPostFetchSort = usePostFetchSort && postFetchOrders.length > 0;
|
|
7210
|
-
const sizeAfterPostFilter = hasResidualPostFilter && !paginateAfterPostFetchSort;
|
|
7441
|
+
const sizeAfterPostFilter = (hasResidualPostFilter || hasPostFetchMembership) && !paginateAfterPostFetchSort;
|
|
7211
7442
|
const residualLimitStream = sizeAfterPostFilter && limit !== void 0 ? this._buildResidualFilterStream({
|
|
7212
7443
|
queryConfig,
|
|
7213
7444
|
configuredIndex,
|
|
@@ -7218,7 +7449,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7218
7449
|
}) : null;
|
|
7219
7450
|
let rows;
|
|
7220
7451
|
if (residualLimitStream) rows = await residualLimitStream.take(offset > 0 ? offset + limit : limit);
|
|
7221
|
-
else if (limit === void 0 || paginateAfterPostFetchSort || hasResidualPostFilter) rows = await query.collect();
|
|
7452
|
+
else if (limit === void 0 || paginateAfterPostFetchSort || hasResidualPostFilter || hasPostFetchMembership) rows = await query.collect();
|
|
7222
7453
|
else rows = await query.take(offset > 0 ? offset + limit : limit);
|
|
7223
7454
|
if (!(paginateAfterPostFetchSort || sizeAfterPostFilter) && offset > 0) rows = rows.slice(offset);
|
|
7224
7455
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
@@ -7249,9 +7480,17 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7249
7480
|
indexFields: index.fields
|
|
7250
7481
|
}));
|
|
7251
7482
|
const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
|
|
7483
|
+
let orderSpecs = [];
|
|
7484
|
+
if (config.orderBy) {
|
|
7485
|
+
const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
|
|
7486
|
+
asc,
|
|
7487
|
+
desc
|
|
7488
|
+
}) : config.orderBy;
|
|
7489
|
+
orderSpecs = this._orderBySpecs(orderByValue);
|
|
7490
|
+
}
|
|
7252
7491
|
let whereExpression = whereExpressionOverride;
|
|
7253
7492
|
if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
|
|
7254
|
-
const planned = compiler.compile(whereExpression);
|
|
7493
|
+
const planned = compiler.compile(whereExpression, { orderFields: orderSpecs.map((spec) => spec.field) });
|
|
7255
7494
|
const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
|
|
7256
7495
|
const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
|
|
7257
7496
|
strategy: "none",
|
|
@@ -7268,16 +7507,10 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7268
7507
|
};
|
|
7269
7508
|
if (compiled.selectedIndex && (compiled.indexFilters.length > 0 || compiled.probeFilters.length > 0)) result.index = {
|
|
7270
7509
|
name: compiled.selectedIndex.indexName,
|
|
7510
|
+
fields: compiled.selectedIndex.indexFields,
|
|
7271
7511
|
filters: compiled.indexFilters
|
|
7272
7512
|
};
|
|
7273
|
-
if (
|
|
7274
|
-
const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
|
|
7275
|
-
asc,
|
|
7276
|
-
desc
|
|
7277
|
-
}) : config.orderBy;
|
|
7278
|
-
const orderSpecs = this._orderBySpecs(orderByValue);
|
|
7279
|
-
if (orderSpecs.length > 0) result.order = orderSpecs;
|
|
7280
|
-
}
|
|
7513
|
+
if (orderSpecs.length > 0) result.order = orderSpecs;
|
|
7281
7514
|
return result;
|
|
7282
7515
|
}
|
|
7283
7516
|
_buildRelationKey(row, fields) {
|
|
@@ -7286,6 +7519,30 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7286
7519
|
if (values.some((value) => value === null || value === void 0)) return null;
|
|
7287
7520
|
return JSON.stringify(values);
|
|
7288
7521
|
}
|
|
7522
|
+
/**
|
|
7523
|
+
* How many leading fields of the scanned index are pinned to a single value.
|
|
7524
|
+
*
|
|
7525
|
+
* `splitFilters` emits index filters in index-key order — a run of `eq`, then
|
|
7526
|
+
* at most one range on the first unpinned field — so the leading `eq` run is
|
|
7527
|
+
* the prefix Convex holds constant. A multi-probe plan carries no index
|
|
7528
|
+
* filters; each probe supplies its own bound instead, and the union is only
|
|
7529
|
+
* as pinned as its least pinned probe.
|
|
7530
|
+
*/
|
|
7531
|
+
_indexEqPrefixCount(queryConfig) {
|
|
7532
|
+
const countEqPrefix = (filters) => {
|
|
7533
|
+
let count = 0;
|
|
7534
|
+
for (const filter of filters) {
|
|
7535
|
+
if (filter.type !== "binary" || filter.operator !== "eq") break;
|
|
7536
|
+
count += 1;
|
|
7537
|
+
}
|
|
7538
|
+
return count;
|
|
7539
|
+
};
|
|
7540
|
+
if (queryConfig.index && queryConfig.index.filters.length > 0) return countEqPrefix(queryConfig.index.filters);
|
|
7541
|
+
if (queryConfig.probeFilters.length === 0) return 0;
|
|
7542
|
+
let pinned = Number.POSITIVE_INFINITY;
|
|
7543
|
+
for (const probe of queryConfig.probeFilters) pinned = Math.min(pinned, countEqPrefix(probe));
|
|
7544
|
+
return Number.isFinite(pinned) ? pinned : 0;
|
|
7545
|
+
}
|
|
7289
7546
|
_buildIndexPredicate(q, fields, values) {
|
|
7290
7547
|
let builder = q.eq(fields[0], values[0]);
|
|
7291
7548
|
for (let i = 1; i < fields.length; i += 1) builder = builder.eq(fields[i], values[i]);
|
|
@@ -7494,16 +7751,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7494
7751
|
if (typeof record.where === "function") throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `with._count.${relationName}.where callback is unsupported in v1`);
|
|
7495
7752
|
return record.where;
|
|
7496
7753
|
}
|
|
7497
|
-
_normalizeRelationCountCacheValue(value) {
|
|
7498
|
-
if (Array.isArray(value)) return value.map((entry) => this._normalizeRelationCountCacheValue(entry));
|
|
7499
|
-
if (value && typeof value === "object") {
|
|
7500
|
-
const normalized = {};
|
|
7501
|
-
const entries = Object.entries(value).sort(([left], [right]) => left.localeCompare(right));
|
|
7502
|
-
for (const [key, entry] of entries) normalized[key] = this._normalizeRelationCountCacheValue(entry);
|
|
7503
|
-
return normalized;
|
|
7504
|
-
}
|
|
7505
|
-
return value;
|
|
7506
|
-
}
|
|
7507
7754
|
_getRelationCountParentKey(row, edge) {
|
|
7508
7755
|
const sourceFields = edge.sourceFields.length > 0 ? edge.sourceFields : [edge.fieldName];
|
|
7509
7756
|
const values = [];
|
|
@@ -7514,26 +7761,20 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7514
7761
|
}
|
|
7515
7762
|
return JSON.stringify(values);
|
|
7516
7763
|
}
|
|
7517
|
-
|
|
7518
|
-
return JSON.stringify({
|
|
7519
|
-
relationName,
|
|
7520
|
-
where: this._normalizeRelationCountCacheValue(where ?? null),
|
|
7521
|
-
parentKey
|
|
7522
|
-
});
|
|
7523
|
-
}
|
|
7524
|
-
async _readIndexedRelationCount(tableConfig, where, relationPath) {
|
|
7764
|
+
async _readIndexedRelationCount(tableConfig, where, relationPath, bucketCache) {
|
|
7525
7765
|
ensureCountAllowedForRls(tableConfig, this.rls?.mode);
|
|
7526
7766
|
try {
|
|
7527
7767
|
const plan = compileCountQueryPlan(tableConfig, where);
|
|
7528
7768
|
if (isIndexCountZero(plan)) return 0;
|
|
7529
7769
|
await this._ensureCountIndexReadyOnce(plan.tableName, plan.indexName);
|
|
7530
|
-
return await readCountFromBuckets(this.db, plan);
|
|
7770
|
+
return await readCountFromBuckets(this.db, plan, bucketCache);
|
|
7531
7771
|
} catch (error) {
|
|
7532
7772
|
throw this._remapRelationCountError(error, relationPath);
|
|
7533
7773
|
}
|
|
7534
7774
|
}
|
|
7535
|
-
async _countRelationForRow(row, relationName, edge, where, tableConfig) {
|
|
7775
|
+
async _countRelationForRow(row, relationName, edge, where, tableConfig, caches) {
|
|
7536
7776
|
const relationPath = `${tableConfig.name}.${relationName}`;
|
|
7777
|
+
const bucketCache = caches?.buckets;
|
|
7537
7778
|
if (edge.through) {
|
|
7538
7779
|
const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
|
|
7539
7780
|
if (!throughTableConfig) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `${relationPath} through table '${edge.through.table}' is not registered`);
|
|
@@ -7548,7 +7789,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7548
7789
|
throughWhere[throughField] = value;
|
|
7549
7790
|
sourceValues.push(value);
|
|
7550
7791
|
}
|
|
7551
|
-
if (this._isEmptyWhere(where) || where === void 0) return await this._readIndexedRelationCount(throughTableConfig, throughWhere, relationPath);
|
|
7792
|
+
if (this._isEmptyWhere(where) || where === void 0) return await this._readIndexedRelationCount(throughTableConfig, throughWhere, relationPath, bucketCache);
|
|
7552
7793
|
const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
|
|
7553
7794
|
if (!targetTableConfig) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `${relationPath} target table '${edge.targetTable}' is not registered`);
|
|
7554
7795
|
ensureCountAllowedForRls(targetTableConfig, this.rls?.mode);
|
|
@@ -7583,13 +7824,28 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7583
7824
|
if (targetKeyCounts.size === 0) return 0;
|
|
7584
7825
|
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
7585
7826
|
const targetIndexName = useGetById ? null : findRelationIndex(targetTableConfig.table, targetFields, relationPath, edge.targetTable, strict, this.allowFullScan);
|
|
7586
|
-
const
|
|
7587
|
-
return (await this._mapWithConcurrency(targetEntries, async ({ values, occurrences }) => {
|
|
7827
|
+
const resolveTargetMatch = async (values) => {
|
|
7588
7828
|
let target = null;
|
|
7589
7829
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
7590
7830
|
else target = await this._queryByFields(this.db.query(edge.targetTable), targetFields, values, targetIndexName).first();
|
|
7591
|
-
if (!target) return
|
|
7592
|
-
return this._evaluateTableFilter(target, targetTableConfig, whereRecord)
|
|
7831
|
+
if (!target) return false;
|
|
7832
|
+
return this._evaluateTableFilter(target, targetTableConfig, whereRecord);
|
|
7833
|
+
};
|
|
7834
|
+
const targetMatchCache = caches?.throughTargetMatches;
|
|
7835
|
+
const targetEntries = Array.from(targetKeyCounts.entries());
|
|
7836
|
+
return (await this._mapWithConcurrency(targetEntries, async ([targetKey, { values, occurrences }]) => {
|
|
7837
|
+
if (!targetMatchCache) return await resolveTargetMatch(values) ? occurrences : 0;
|
|
7838
|
+
let pending = targetMatchCache.get(targetKey);
|
|
7839
|
+
if (!pending) {
|
|
7840
|
+
pending = resolveTargetMatch(values);
|
|
7841
|
+
targetMatchCache.set(targetKey, pending);
|
|
7842
|
+
}
|
|
7843
|
+
try {
|
|
7844
|
+
return await pending ? occurrences : 0;
|
|
7845
|
+
} catch (error) {
|
|
7846
|
+
targetMatchCache.delete(targetKey);
|
|
7847
|
+
throw error;
|
|
7848
|
+
}
|
|
7593
7849
|
})).reduce((sum, value) => sum + value, 0);
|
|
7594
7850
|
}
|
|
7595
7851
|
const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
|
|
@@ -7605,7 +7861,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7605
7861
|
relationWhere[targetField] = value;
|
|
7606
7862
|
}
|
|
7607
7863
|
const mergedWhere = this._isEmptyWhere(where) || where === void 0 ? relationWhere : { AND: [relationWhere, where] };
|
|
7608
|
-
return await this._readIndexedRelationCount(targetTableConfig, mergedWhere, relationPath);
|
|
7864
|
+
return await this._readIndexedRelationCount(targetTableConfig, mergedWhere, relationPath, bucketCache);
|
|
7609
7865
|
}
|
|
7610
7866
|
async _loadRelationCounts(rows, relationCountConfig, targetTableEdges, tableConfig) {
|
|
7611
7867
|
if (!relationCountConfig || typeof relationCountConfig !== "object" || Array.isArray(relationCountConfig)) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `with._count on '${tableConfig.name}' requires an object of relation names`);
|
|
@@ -7617,18 +7873,21 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7617
7873
|
if (!edge) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `with._count.${relationName} is not a relation on '${tableConfig.name}'`);
|
|
7618
7874
|
const where = this._coerceRelationCountWhere(relationName, relationSelection);
|
|
7619
7875
|
const relationCountExecutionCache = /* @__PURE__ */ new Map();
|
|
7876
|
+
const caches = {
|
|
7877
|
+
buckets: /* @__PURE__ */ new Map(),
|
|
7878
|
+
throughTargetMatches: /* @__PURE__ */ new Map()
|
|
7879
|
+
};
|
|
7620
7880
|
const counts = await this._mapWithConcurrency(rows, async (row) => {
|
|
7621
7881
|
const parentKey = this._getRelationCountParentKey(row, edge);
|
|
7622
7882
|
if (parentKey === null) return 0;
|
|
7623
|
-
const
|
|
7624
|
-
const existing = relationCountExecutionCache.get(executionKey);
|
|
7883
|
+
const existing = relationCountExecutionCache.get(parentKey);
|
|
7625
7884
|
if (existing) return await existing;
|
|
7626
|
-
const pending = this._countRelationForRow(row, relationName, edge, where, tableConfig);
|
|
7627
|
-
relationCountExecutionCache.set(
|
|
7885
|
+
const pending = this._countRelationForRow(row, relationName, edge, where, tableConfig, caches);
|
|
7886
|
+
relationCountExecutionCache.set(parentKey, pending);
|
|
7628
7887
|
try {
|
|
7629
7888
|
return await pending;
|
|
7630
7889
|
} catch (error) {
|
|
7631
|
-
relationCountExecutionCache.delete(
|
|
7890
|
+
relationCountExecutionCache.delete(parentKey);
|
|
7632
7891
|
throw error;
|
|
7633
7892
|
}
|
|
7634
7893
|
});
|
|
@@ -7708,6 +7967,108 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7708
7967
|
}
|
|
7709
7968
|
}
|
|
7710
7969
|
/**
|
|
7970
|
+
* Junction links per parent, stopped once each parent holds `fetchLimit`
|
|
7971
|
+
* links whose target actually reaches the page.
|
|
7972
|
+
*
|
|
7973
|
+
* A link only contributes if its target exists and survives target RLS and
|
|
7974
|
+
* the relation `where` — all of which run after the junction read. Sizing the
|
|
7975
|
+
* read on links alone therefore under-fills: three dangling or filtered links
|
|
7976
|
+
* first and `{ limit: 3 }` returns nothing. So the read is refilled in rounds,
|
|
7977
|
+
* each round resolving only the targets it newly needs and asking again for
|
|
7978
|
+
* whatever the survivors did not cover.
|
|
7979
|
+
*
|
|
7980
|
+
* Rounds are the unit rather than single links because both the target fetch
|
|
7981
|
+
* and the relation `where` de-duplicate and batch their own reads across the
|
|
7982
|
+
* parents in the round.
|
|
7983
|
+
*/
|
|
7984
|
+
async _readBoundedThroughLinks(params) {
|
|
7985
|
+
const { applyTargetFilters, edge, enforceTargetKeyCap, entries, fetchLimit, fetchTargets, targetFields, throughIndexName, throughTableConfig } = params;
|
|
7986
|
+
const throughTargetFields = edge.through.targetFields;
|
|
7987
|
+
const cursors = entries.map(([key, values]) => ({
|
|
7988
|
+
buffered: [],
|
|
7989
|
+
exhausted: false,
|
|
7990
|
+
iterator: this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName)[Symbol.asyncIterator](),
|
|
7991
|
+
key,
|
|
7992
|
+
links: []
|
|
7993
|
+
}));
|
|
7994
|
+
const bufferVisibleLinks = async (cursor, count) => {
|
|
7995
|
+
let batch = [];
|
|
7996
|
+
const drain = async () => {
|
|
7997
|
+
const visible = await this._applyRlsSelectFilter(batch, throughTableConfig);
|
|
7998
|
+
batch = [];
|
|
7999
|
+
cursor.buffered.push(...visible);
|
|
8000
|
+
};
|
|
8001
|
+
while (!cursor.exhausted && cursor.buffered.length + batch.length < count) {
|
|
8002
|
+
const next = await cursor.iterator.next();
|
|
8003
|
+
if (next.done) {
|
|
8004
|
+
cursor.exhausted = true;
|
|
8005
|
+
break;
|
|
8006
|
+
}
|
|
8007
|
+
batch.push(next.value);
|
|
8008
|
+
if (batch.length >= RELATION_FILTER_STREAM_CHUNK) await drain();
|
|
8009
|
+
}
|
|
8010
|
+
await drain();
|
|
8011
|
+
};
|
|
8012
|
+
/** Target key -> the surviving document, absent when it did not survive. */
|
|
8013
|
+
const survivorByKey = /* @__PURE__ */ new Map();
|
|
8014
|
+
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
8015
|
+
const survivors = [];
|
|
8016
|
+
while (true) {
|
|
8017
|
+
const active = cursors.filter((cursor) => cursor.links.length < fetchLimit && !(cursor.exhausted && cursor.buffered.length === 0));
|
|
8018
|
+
if (active.length === 0) break;
|
|
8019
|
+
const candidatesPerCursor = await this._mapWithConcurrency(active, async (cursor) => {
|
|
8020
|
+
const need = fetchLimit - cursor.links.length;
|
|
8021
|
+
await bufferVisibleLinks(cursor, need);
|
|
8022
|
+
return cursor.buffered.splice(0, need);
|
|
8023
|
+
});
|
|
8024
|
+
const newKeys = /* @__PURE__ */ new Map();
|
|
8025
|
+
for (const candidates of candidatesPerCursor) for (const link of candidates) {
|
|
8026
|
+
const values = throughTargetFields.map((field) => link[field]);
|
|
8027
|
+
if (values.some((value) => value === null || value === void 0)) continue;
|
|
8028
|
+
const key = JSON.stringify(values);
|
|
8029
|
+
if (resolvedKeys.has(key) || newKeys.has(key)) continue;
|
|
8030
|
+
newKeys.set(key, values);
|
|
8031
|
+
}
|
|
8032
|
+
if (newKeys.size > 0) {
|
|
8033
|
+
enforceTargetKeyCap(resolvedKeys.size + newKeys.size);
|
|
8034
|
+
const fetched = await fetchTargets(Array.from(newKeys.entries()));
|
|
8035
|
+
for (const key of newKeys.keys()) resolvedKeys.add(key);
|
|
8036
|
+
const surviving = await applyTargetFilters(fetched.map((entry) => entry.target).filter((target) => !!target));
|
|
8037
|
+
for (const target of surviving) {
|
|
8038
|
+
const key = this._buildRelationKey(target, targetFields);
|
|
8039
|
+
if (!key || survivorByKey.has(key)) continue;
|
|
8040
|
+
survivorByKey.set(key, target);
|
|
8041
|
+
survivors.push(target);
|
|
8042
|
+
}
|
|
8043
|
+
}
|
|
8044
|
+
for (let i = 0; i < active.length; i += 1) {
|
|
8045
|
+
const cursor = active[i];
|
|
8046
|
+
for (const link of candidatesPerCursor[i]) {
|
|
8047
|
+
if (cursor.links.length >= fetchLimit) break;
|
|
8048
|
+
const key = this._buildRelationKey(link, throughTargetFields);
|
|
8049
|
+
if (!key || !survivorByKey.has(key)) continue;
|
|
8050
|
+
cursor.links.push(link);
|
|
8051
|
+
}
|
|
8052
|
+
}
|
|
8053
|
+
}
|
|
8054
|
+
const linksBySourceKey = /* @__PURE__ */ new Map();
|
|
8055
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
8056
|
+
for (const cursor of cursors) {
|
|
8057
|
+
linksBySourceKey.set(cursor.key, cursor.links);
|
|
8058
|
+
for (const link of cursor.links) {
|
|
8059
|
+
const key = this._buildRelationKey(link, throughTargetFields);
|
|
8060
|
+
if (key) usedKeys.add(key);
|
|
8061
|
+
}
|
|
8062
|
+
}
|
|
8063
|
+
return {
|
|
8064
|
+
linksBySourceKey,
|
|
8065
|
+
targets: survivors.filter((target) => {
|
|
8066
|
+
const key = this._buildRelationKey(target, targetFields);
|
|
8067
|
+
return key !== null && usedKeys.has(key);
|
|
8068
|
+
})
|
|
8069
|
+
};
|
|
8070
|
+
}
|
|
8071
|
+
/**
|
|
7711
8072
|
* Load many() relation (one-to-many)
|
|
7712
8073
|
* Example: users.posts where posts.authorId → users.id
|
|
7713
8074
|
*
|
|
@@ -7781,65 +8142,108 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7781
8142
|
if (!throughTableConfig) throw new Error(`Relation '${relationName}' through table '${edge.through.table}' not found.`);
|
|
7782
8143
|
const throughIndexName = findRelationIndex(throughTableConfig.table, edge.through.sourceFields, `${tableConfig.name}.${relationName}`, edge.through.table, strict, this.allowFullScan);
|
|
7783
8144
|
const entries = Array.from(sourceKeyMap.entries());
|
|
7784
|
-
const
|
|
7785
|
-
const query = this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName);
|
|
7786
|
-
return {
|
|
7787
|
-
key,
|
|
7788
|
-
rows: await this._applyRlsSelectFilter(await query.collect(), throughTableConfig)
|
|
7789
|
-
};
|
|
7790
|
-
});
|
|
7791
|
-
throughBySourceKey = /* @__PURE__ */ new Map();
|
|
7792
|
-
const targetKeyMap = /* @__PURE__ */ new Map();
|
|
7793
|
-
for (const entry of throughRowsPerSource) {
|
|
7794
|
-
throughBySourceKey.set(entry.key, entry.rows);
|
|
7795
|
-
for (const row of entry.rows) {
|
|
7796
|
-
const values = edge.through.targetFields.map((field) => row[field]);
|
|
7797
|
-
if (values.some((value) => value === null || value === void 0)) continue;
|
|
7798
|
-
const key = JSON.stringify(values);
|
|
7799
|
-
if (!targetKeyMap.has(key)) targetKeyMap.set(key, values);
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
this._enforceRelationFanOutKeyCap({
|
|
8145
|
+
const enforceTargetKeyCap = (keyCount) => this._enforceRelationFanOutKeyCap({
|
|
7803
8146
|
tableConfig,
|
|
7804
8147
|
relationName,
|
|
7805
|
-
keyCount
|
|
8148
|
+
keyCount,
|
|
7806
8149
|
scope: "through-target"
|
|
7807
8150
|
});
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
8151
|
+
let targetLookup = null;
|
|
8152
|
+
const fetchThroughTargets = async (keyEntries) => {
|
|
8153
|
+
if (!targetLookup) {
|
|
8154
|
+
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
8155
|
+
targetLookup = {
|
|
8156
|
+
useGetById,
|
|
8157
|
+
indexName: useGetById ? null : findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan)
|
|
8158
|
+
};
|
|
8159
|
+
}
|
|
8160
|
+
const { useGetById, indexName } = targetLookup;
|
|
8161
|
+
return await this._mapWithConcurrency(keyEntries, async ([key, values]) => {
|
|
7813
8162
|
let target = null;
|
|
7814
8163
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
7815
|
-
else target = await this._queryByFields(this.db.query(edge.targetTable), targetFields, values,
|
|
8164
|
+
else target = await this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName).first();
|
|
7816
8165
|
return {
|
|
7817
8166
|
key,
|
|
7818
8167
|
target
|
|
7819
8168
|
};
|
|
7820
|
-
})
|
|
8169
|
+
});
|
|
8170
|
+
};
|
|
8171
|
+
if (orderSpecs.length === 0 && effectivePerParentLimit !== void 0) {
|
|
8172
|
+
const bounded = await this._readBoundedThroughLinks({
|
|
8173
|
+
applyTargetFilters: applyPostFetchTargetFilters,
|
|
8174
|
+
edge,
|
|
8175
|
+
enforceTargetKeyCap,
|
|
8176
|
+
entries,
|
|
8177
|
+
fetchLimit: Math.max(perParentOffset ?? 0, 0) + effectivePerParentLimit,
|
|
8178
|
+
fetchTargets: fetchThroughTargets,
|
|
8179
|
+
targetFields,
|
|
8180
|
+
throughIndexName,
|
|
8181
|
+
throughTableConfig
|
|
8182
|
+
});
|
|
8183
|
+
throughBySourceKey = bounded.linksBySourceKey;
|
|
8184
|
+
targets = bounded.targets;
|
|
8185
|
+
targetFiltersApplied = true;
|
|
8186
|
+
} else {
|
|
8187
|
+
const throughRowsPerSource = await this._mapWithConcurrency(entries, async ([key, values]) => {
|
|
8188
|
+
const query = this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName);
|
|
8189
|
+
return {
|
|
8190
|
+
key,
|
|
8191
|
+
rows: await this._applyRlsSelectFilter(await query.collect(), throughTableConfig)
|
|
8192
|
+
};
|
|
8193
|
+
});
|
|
8194
|
+
throughBySourceKey = /* @__PURE__ */ new Map();
|
|
8195
|
+
const targetKeyMap = /* @__PURE__ */ new Map();
|
|
8196
|
+
for (const entry of throughRowsPerSource) {
|
|
8197
|
+
throughBySourceKey.set(entry.key, entry.rows);
|
|
8198
|
+
for (const row of entry.rows) {
|
|
8199
|
+
const values = edge.through.targetFields.map((field) => row[field]);
|
|
8200
|
+
if (values.some((value) => value === null || value === void 0)) continue;
|
|
8201
|
+
const key = JSON.stringify(values);
|
|
8202
|
+
if (!targetKeyMap.has(key)) targetKeyMap.set(key, values);
|
|
8203
|
+
}
|
|
8204
|
+
}
|
|
8205
|
+
enforceTargetKeyCap(targetKeyMap.size);
|
|
8206
|
+
if (targetKeyMap.size > 0) targets = (await fetchThroughTargets(Array.from(targetKeyMap.entries()))).map((entry) => entry.target).filter((value) => !!value);
|
|
7821
8207
|
}
|
|
7822
8208
|
} else {
|
|
7823
|
-
const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
8209
|
+
const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan, orderSpecs);
|
|
7824
8210
|
const entries = Array.from(sourceKeyMap.entries());
|
|
7825
|
-
const
|
|
8211
|
+
const orderPushdownDirection = resolveIndexOrderPushdown({
|
|
8212
|
+
indexFields: indexName ? getIndexes(targetTableConfig.table).find((idx) => idx.name === indexName)?.fields ?? null : null,
|
|
8213
|
+
pinnedEqCount: targetFields.length,
|
|
8214
|
+
orderSpecs
|
|
8215
|
+
});
|
|
8216
|
+
const orderServedByIndex = orderSpecs.length === 0 || orderPushdownDirection !== null;
|
|
8217
|
+
const applyPushdownOrder = (query) => orderPushdownDirection ? query.order(orderPushdownDirection) : query;
|
|
8218
|
+
const streamPostFetchTargetFilters = orderServedByIndex && hasPostFetchTargetFilter && effectivePerParentLimit !== void 0;
|
|
7826
8219
|
targetFiltersApplied = streamPostFetchTargetFilters;
|
|
7827
8220
|
targets = (await this._mapWithConcurrency(entries, async ([, values]) => {
|
|
7828
8221
|
const query = this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName);
|
|
7829
|
-
if (
|
|
8222
|
+
if (orderServedByIndex && !hasPostFetchTargetFilter && effectivePerParentLimit !== void 0) {
|
|
7830
8223
|
const fetchLimit = (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
|
|
7831
|
-
return await query.take(fetchLimit);
|
|
8224
|
+
return await applyPushdownOrder(query).take(fetchLimit);
|
|
7832
8225
|
}
|
|
7833
8226
|
if (streamPostFetchTargetFilters) {
|
|
7834
8227
|
const visibleTargets = [];
|
|
7835
8228
|
const fetchLimit = Math.max(perParentOffset ?? 0, 0) + (effectivePerParentLimit ?? 0);
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
if (
|
|
7839
|
-
|
|
7840
|
-
|
|
8229
|
+
let batch = [];
|
|
8230
|
+
const drain = async () => {
|
|
8231
|
+
if (batch.length === 0) return;
|
|
8232
|
+
const filtered = await applyPostFetchTargetFilters(batch);
|
|
8233
|
+
batch = [];
|
|
8234
|
+
for (const row of filtered) {
|
|
8235
|
+
if (visibleTargets.length >= fetchLimit) return;
|
|
8236
|
+
visibleTargets.push(row);
|
|
7841
8237
|
}
|
|
8238
|
+
};
|
|
8239
|
+
for await (const target of applyPushdownOrder(query)) {
|
|
8240
|
+
batch.push(target);
|
|
8241
|
+
const chunk = Math.min(RELATION_FILTER_STREAM_CHUNK, fetchLimit - visibleTargets.length);
|
|
8242
|
+
if (batch.length < chunk) continue;
|
|
8243
|
+
await drain();
|
|
8244
|
+
if (visibleTargets.length >= fetchLimit) break;
|
|
7842
8245
|
}
|
|
8246
|
+
if (visibleTargets.length < fetchLimit) await drain();
|
|
7843
8247
|
return visibleTargets;
|
|
7844
8248
|
}
|
|
7845
8249
|
return await query.collect();
|
|
@@ -7847,6 +8251,19 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7847
8251
|
}
|
|
7848
8252
|
if (!targetFiltersApplied) targets = await applyPostFetchTargetFilters(targets);
|
|
7849
8253
|
if (orderSpecs.length > 0) targets.sort((a, b) => this._compareByOrderSpecs(a, b, orderSpecs));
|
|
8254
|
+
if (!edge.through && (perParentOffset !== void 0 || effectivePerParentLimit !== void 0)) {
|
|
8255
|
+
const groupedTargets = /* @__PURE__ */ new Map();
|
|
8256
|
+
for (const target of targets) {
|
|
8257
|
+
const parentKey = this._buildRelationKey(target, targetFields);
|
|
8258
|
+
if (!parentKey) continue;
|
|
8259
|
+
const group = groupedTargets.get(parentKey);
|
|
8260
|
+
if (group) group.push(target);
|
|
8261
|
+
else groupedTargets.set(parentKey, [target]);
|
|
8262
|
+
}
|
|
8263
|
+
const trimmed = [];
|
|
8264
|
+
for (const children of groupedTargets.values()) for (const child of applyOffsetAndLimit(children)) trimmed.push(child);
|
|
8265
|
+
targets = trimmed;
|
|
8266
|
+
}
|
|
7850
8267
|
if (relationConfig && typeof relationConfig === "object" && "with" in relationConfig) {
|
|
7851
8268
|
const targetTableEdges = this._getTargetTableEdges(edge.targetTable);
|
|
7852
8269
|
await this._loadRelations(targets, relationConfig.with, depth + 1, maxDepth, targetTableEdges, targetTableConfig);
|
|
@@ -7891,7 +8308,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7891
8308
|
if (!byParentKey.has(parentKey)) byParentKey.set(parentKey, []);
|
|
7892
8309
|
byParentKey.get(parentKey).push(mappedTarget);
|
|
7893
8310
|
}
|
|
7894
|
-
if (perParentOffset !== void 0 || effectivePerParentLimit !== void 0) for (const [parentKey, children] of byParentKey.entries()) byParentKey.set(parentKey, applyOffsetAndLimit(children));
|
|
7895
8311
|
for (const row of rows) {
|
|
7896
8312
|
const rowKey = this._buildRelationKey(row, sourceFields);
|
|
7897
8313
|
row[relationName] = rowKey ? byParentKey.get(rowKey) ?? [] : [];
|
|
@@ -9531,6 +9947,38 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
9531
9947
|
|
|
9532
9948
|
//#endregion
|
|
9533
9949
|
//#region src/orm/database.ts
|
|
9950
|
+
/**
|
|
9951
|
+
* `createDatabase` runs on every Convex query and mutation entry, but the
|
|
9952
|
+
* foreign-key graph and the per-table edge partition are pure functions of the
|
|
9953
|
+
* schema and edge metadata, both fixed for the process lifetime. Caching them
|
|
9954
|
+
* on the identity of those objects keeps the per-request work proportional to
|
|
9955
|
+
* the table count instead of `tables x edges`.
|
|
9956
|
+
*
|
|
9957
|
+
* Both caches are only sound because their inputs are module-level immutables:
|
|
9958
|
+
* nothing in the ORM mutates a schema or an edge list after construction.
|
|
9959
|
+
*/
|
|
9960
|
+
const foreignKeyGraphCache = /* @__PURE__ */ new WeakMap();
|
|
9961
|
+
const edgesBySourceTableCache = /* @__PURE__ */ new WeakMap();
|
|
9962
|
+
const NO_EDGES = [];
|
|
9963
|
+
function getForeignKeyGraph(schema) {
|
|
9964
|
+
const cached = foreignKeyGraphCache.get(schema);
|
|
9965
|
+
if (cached) return cached;
|
|
9966
|
+
const graph = buildForeignKeyGraph(schema);
|
|
9967
|
+
foreignKeyGraphCache.set(schema, graph);
|
|
9968
|
+
return graph;
|
|
9969
|
+
}
|
|
9970
|
+
function getEdgesBySourceTable(edgeMetadata) {
|
|
9971
|
+
const cached = edgesBySourceTableCache.get(edgeMetadata);
|
|
9972
|
+
if (cached) return cached;
|
|
9973
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
9974
|
+
for (const edge of edgeMetadata) {
|
|
9975
|
+
const existing = grouped.get(edge.sourceTable);
|
|
9976
|
+
if (existing) existing.push(edge);
|
|
9977
|
+
else grouped.set(edge.sourceTable, [edge]);
|
|
9978
|
+
}
|
|
9979
|
+
edgesBySourceTableCache.set(edgeMetadata, grouped);
|
|
9980
|
+
return grouped;
|
|
9981
|
+
}
|
|
9534
9982
|
function createDatabase(db, schema, edgeMetadata, options) {
|
|
9535
9983
|
const schemaOptions = schema[OrmSchemaOptions];
|
|
9536
9984
|
const strict = schemaOptions?.strict ?? true;
|
|
@@ -9541,7 +9989,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9541
9989
|
scheduledMutationBatch: options?.scheduledMutationBatch
|
|
9542
9990
|
});
|
|
9543
9991
|
const ormContext = {
|
|
9544
|
-
foreignKeyGraph:
|
|
9992
|
+
foreignKeyGraph: getForeignKeyGraph(schema),
|
|
9545
9993
|
schema,
|
|
9546
9994
|
edgeMetadata,
|
|
9547
9995
|
relationLoading: options?.relationLoading,
|
|
@@ -9555,7 +10003,8 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9555
10003
|
};
|
|
9556
10004
|
const baseDb = Object.assign(Object.create(db), { [OrmContext]: ormContext });
|
|
9557
10005
|
const query = {};
|
|
9558
|
-
|
|
10006
|
+
const edgesBySourceTable = getEdgesBySourceTable(edgeMetadata);
|
|
10007
|
+
for (const [tableName, tableConfig] of Object.entries(schema)) query[tableName] = new RelationalQueryBuilder(schema, tableConfig, edgesBySourceTable.get(tableConfig.name) ?? NO_EDGES, baseDb, edgeMetadata, rls, options?.relationLoading, options?.vectorSearch);
|
|
9559
10008
|
const isWriter = typeof db.insert === "function" && typeof db.patch === "function";
|
|
9560
10009
|
const isConvexTable = (value) => !!value && typeof value === "object" && value[Brand] === "ConvexTable";
|
|
9561
10010
|
const insert = (table) => {
|
|
@@ -9600,14 +10049,18 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9600
10049
|
return built;
|
|
9601
10050
|
};
|
|
9602
10051
|
const table = buildDatabase(options?.rls);
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
10052
|
+
let skipRulesTable;
|
|
10053
|
+
return Object.defineProperty({ ...table }, "skipRules", {
|
|
10054
|
+
enumerable: true,
|
|
10055
|
+
configurable: true,
|
|
10056
|
+
get() {
|
|
10057
|
+
skipRulesTable ??= buildDatabase({
|
|
10058
|
+
...options?.rls ?? {},
|
|
10059
|
+
mode: "skip"
|
|
10060
|
+
});
|
|
10061
|
+
return skipRulesTable;
|
|
10062
|
+
}
|
|
9606
10063
|
});
|
|
9607
|
-
return {
|
|
9608
|
-
...table,
|
|
9609
|
-
skipRules: skipRulesTable
|
|
9610
|
-
};
|
|
9611
10064
|
}
|
|
9612
10065
|
|
|
9613
10066
|
//#endregion
|
|
@@ -11176,8 +11629,7 @@ function resolveOrmSchemaConfig(schemaInput) {
|
|
|
11176
11629
|
triggers: getSchemaTriggers(schemaInput)
|
|
11177
11630
|
};
|
|
11178
11631
|
}
|
|
11179
|
-
function createDbFactory(schema, dbLifecycle, ormFunctions) {
|
|
11180
|
-
const edgeMetadata = extractRelationsConfig(schema);
|
|
11632
|
+
function createDbFactory(schema, edgeMetadata, dbLifecycle, ormFunctions) {
|
|
11181
11633
|
return ((source, options) => {
|
|
11182
11634
|
const ctxSource = isOrmCtx(source) ? source : void 0;
|
|
11183
11635
|
const rawDb = ctxSource ? ctxSource.db : source;
|
|
@@ -11203,7 +11655,7 @@ function createOrm(config) {
|
|
|
11203
11655
|
const { schema: resolvedSchema, triggers } = resolveOrmSchemaConfig(config.schema);
|
|
11204
11656
|
const dbLifecycle = createOrmDbLifecycle(resolvedSchema, triggers);
|
|
11205
11657
|
const edgeMetadata = extractRelationsConfig(resolvedSchema);
|
|
11206
|
-
const db = createDbFactory(resolvedSchema, dbLifecycle, config.ormFunctions);
|
|
11658
|
+
const db = createDbFactory(resolvedSchema, edgeMetadata, dbLifecycle, config.ormFunctions);
|
|
11207
11659
|
const withContext = (ctx, options) => {
|
|
11208
11660
|
const lifecycleCtx = { ...ctx };
|
|
11209
11661
|
const wrappedCtx = dbLifecycle.wrapDB(lifecycleCtx);
|