kitcn 0.17.4 → 0.18.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/client/index.d.ts +1 -1
- package/dist/auth/client/index.js +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/{auth-store-GDNKweSK.d.ts → auth-store-47WTg13B.d.ts} +4 -3
- package/dist/{auth-store-BnGZxmnY.js → auth-store-BHk8eMnX.js} +37 -13
- package/dist/orm/index.d.ts +1 -1
- package/dist/orm/index.js +530 -134
- package/dist/{query-context-C90vNlc9.js → query-context-CJE_hYA3.js} +22 -1
- package/dist/react/index.d.ts +9 -8
- package/dist/react/index.js +138 -58
- package/dist/{where-clause-compiler-BnVpjKn6.d.ts → where-clause-compiler-B1weGkP4.d.ts} +62 -33
- package/package.json +1 -1
- package/skills/kitcn/references/features/orm.md +8 -1
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-CJE_hYA3.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
|
|
@@ -4565,6 +4677,33 @@ var GelRankQuery = class {
|
|
|
4565
4677
|
return await readRankRandom(this.db, plan);
|
|
4566
4678
|
}
|
|
4567
4679
|
};
|
|
4680
|
+
const CONFIGURED_INDEX_RANGE_OPERATORS = new Set([
|
|
4681
|
+
"eq",
|
|
4682
|
+
"gt",
|
|
4683
|
+
"gte",
|
|
4684
|
+
"lt",
|
|
4685
|
+
"lte"
|
|
4686
|
+
]);
|
|
4687
|
+
const observeConfiguredIndexRange = (builder, operations) => new Proxy(builder, { get(target, property) {
|
|
4688
|
+
const value = Reflect.get(target, property, target);
|
|
4689
|
+
if (typeof property === "string" && CONFIGURED_INDEX_RANGE_OPERATORS.has(property) && typeof value === "function") return (field, ...args) => {
|
|
4690
|
+
operations.push({
|
|
4691
|
+
field,
|
|
4692
|
+
operator: property
|
|
4693
|
+
});
|
|
4694
|
+
return observeConfiguredIndexRange(Reflect.apply(value, target, [field, ...args]), operations);
|
|
4695
|
+
};
|
|
4696
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
4697
|
+
} });
|
|
4698
|
+
const countConfiguredIndexEqPrefix = (indexFields, operations) => {
|
|
4699
|
+
if (!indexFields) return 0;
|
|
4700
|
+
let count = 0;
|
|
4701
|
+
for (const operation of operations) {
|
|
4702
|
+
if (operation.operator !== "eq" || operation.field !== indexFields[count]) break;
|
|
4703
|
+
count += 1;
|
|
4704
|
+
}
|
|
4705
|
+
return count;
|
|
4706
|
+
};
|
|
4568
4707
|
/**
|
|
4569
4708
|
* Relational query builder with promise-based execution
|
|
4570
4709
|
*
|
|
@@ -4754,8 +4893,19 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4754
4893
|
}
|
|
4755
4894
|
return 0;
|
|
4756
4895
|
}
|
|
4896
|
+
/**
|
|
4897
|
+
* Physical table name to relational config. Called once per row on the
|
|
4898
|
+
* relation-count path, so the linear schema scan is indexed once per schema
|
|
4899
|
+
* object rather than repeated. The schema is fixed for the process lifetime.
|
|
4900
|
+
*/
|
|
4757
4901
|
_getTableConfigByDbName(dbName) {
|
|
4758
|
-
|
|
4902
|
+
let byDbName = tableConfigByDbNameCache.get(this.schema);
|
|
4903
|
+
if (!byDbName) {
|
|
4904
|
+
byDbName = /* @__PURE__ */ new Map();
|
|
4905
|
+
for (const table of Object.values(this.schema)) if (table?.name && !byDbName.has(table.name)) byDbName.set(table.name, table);
|
|
4906
|
+
tableConfigByDbNameCache.set(this.schema, byDbName);
|
|
4907
|
+
}
|
|
4908
|
+
return byDbName.get(dbName);
|
|
4759
4909
|
}
|
|
4760
4910
|
_matchLike(value, pattern, caseInsensitive) {
|
|
4761
4911
|
return matchLikePattern(value, pattern, caseInsensitive);
|
|
@@ -6959,14 +7109,28 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
6959
7109
|
return indexQuery;
|
|
6960
7110
|
});
|
|
6961
7111
|
if (primaryOrder) {
|
|
6962
|
-
const
|
|
6963
|
-
|
|
7112
|
+
const pushdownDirection = resolveIndexOrderPushdown({
|
|
7113
|
+
indexFields: indexConfig.fields,
|
|
7114
|
+
pinnedEqCount: this._indexEqPrefixCount(queryConfig),
|
|
7115
|
+
orderSpecs: [primaryOrder]
|
|
7116
|
+
});
|
|
7117
|
+
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
6964
7118
|
else needsPostFetchSortForPrimary = true;
|
|
6965
7119
|
}
|
|
6966
7120
|
} else if (configuredIndex?.name) {
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
7121
|
+
const configuredIndexFields = getIndexes(this.tableConfig.table).find((idx) => idx.name === configuredIndex.name)?.fields;
|
|
7122
|
+
const rangeOperations = [];
|
|
7123
|
+
const configuredRange = configuredIndex.range;
|
|
7124
|
+
query = query.withIndex(configuredIndex.name, configuredRange ? (q) => configuredRange(observeConfiguredIndexRange(q, rangeOperations)) : (q) => q);
|
|
7125
|
+
if (primaryOrder) {
|
|
7126
|
+
const pushdownDirection = resolveIndexOrderPushdown({
|
|
7127
|
+
indexFields: configuredIndexFields,
|
|
7128
|
+
pinnedEqCount: countConfiguredIndexEqPrefix(configuredIndexFields, rangeOperations),
|
|
7129
|
+
orderSpecs: [primaryOrder]
|
|
7130
|
+
});
|
|
7131
|
+
if (pushdownDirection) query = query.order(pushdownDirection);
|
|
7132
|
+
else needsPostFetchSortForPrimary = true;
|
|
7133
|
+
}
|
|
6970
7134
|
} else if (queryConfig.order && primaryOrder) {
|
|
6971
7135
|
const orderField = primaryOrder.field;
|
|
6972
7136
|
if (orderField === "_creationTime") query = query.order(primaryOrder.direction);
|
|
@@ -7033,32 +7197,42 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7033
7197
|
return this._returnSelectedRows(selectedRows);
|
|
7034
7198
|
}
|
|
7035
7199
|
if (queryConfig.strategy === "multiProbe" && queryConfig.index && !isCursorPaginated) {
|
|
7200
|
+
const probeOffset = config.offset ?? 0;
|
|
7201
|
+
if (typeof probeOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
7202
|
+
const probeLimit = this._resolveNonPaginatedLimit(config);
|
|
7203
|
+
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
7204
|
+
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
7205
|
+
const probeHasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
7206
|
+
const probeOrderDirection = primaryOrder ? resolveIndexOrderPushdown({
|
|
7207
|
+
indexFields: queryConfig.index.fields,
|
|
7208
|
+
pinnedEqCount: this._indexEqPrefixCount(queryConfig),
|
|
7209
|
+
orderSpecs: [primaryOrder]
|
|
7210
|
+
}) : null;
|
|
7211
|
+
const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null && !hasSecondaryOrders) ? probeOffset + probeLimit : void 0;
|
|
7036
7212
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
7037
7213
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
7038
7214
|
let indexQuery = q;
|
|
7039
7215
|
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
7040
7216
|
return indexQuery;
|
|
7041
7217
|
});
|
|
7042
|
-
if (
|
|
7218
|
+
if (probeBound !== void 0 && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
|
|
7219
|
+
if (convexProbeFilters.length > 0) probeQuery = probeQuery.filter((q) => {
|
|
7043
7220
|
let result = null;
|
|
7044
|
-
for (const filter of
|
|
7221
|
+
for (const filter of convexProbeFilters) {
|
|
7045
7222
|
const expr = this._toConvexExpression(filter)(q);
|
|
7046
7223
|
result = result ? q.and(result, expr) : expr;
|
|
7047
7224
|
}
|
|
7048
7225
|
return result ?? q;
|
|
7049
7226
|
});
|
|
7050
|
-
return await probeQuery.collect();
|
|
7227
|
+
return probeBound === void 0 ? await probeQuery.collect() : await probeQuery.take(probeBound);
|
|
7051
7228
|
}));
|
|
7052
7229
|
let rows = Array.from(new Map(probeRows.flat().map((row) => [String(row._id), row])).values());
|
|
7053
7230
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
7054
7231
|
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
7055
7232
|
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, 3, this.config.with);
|
|
7056
7233
|
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);
|
|
7234
|
+
if (probeOffset > 0) rows = rows.slice(probeOffset);
|
|
7235
|
+
if (probeLimit !== void 0) rows = rows.slice(0, probeLimit);
|
|
7062
7236
|
const selectedRows = await this._finalizeRows(rows);
|
|
7063
7237
|
return this._returnSelectedRows(selectedRows);
|
|
7064
7238
|
}
|
|
@@ -7195,6 +7369,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7195
7369
|
}
|
|
7196
7370
|
const convexPostFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
7197
7371
|
const hasResidualPostFilter = convexPostFilters.length !== queryConfig.postFilters.length;
|
|
7372
|
+
const hasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
7198
7373
|
if (convexPostFilters.length > 0) query = query.filter((q) => {
|
|
7199
7374
|
let result = null;
|
|
7200
7375
|
for (const filter of convexPostFilters) {
|
|
@@ -7207,7 +7382,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7207
7382
|
if (typeof offset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
7208
7383
|
const limit = this._resolveNonPaginatedLimit(config);
|
|
7209
7384
|
const paginateAfterPostFetchSort = usePostFetchSort && postFetchOrders.length > 0;
|
|
7210
|
-
const sizeAfterPostFilter = hasResidualPostFilter && !paginateAfterPostFetchSort;
|
|
7385
|
+
const sizeAfterPostFilter = (hasResidualPostFilter || hasPostFetchMembership) && !paginateAfterPostFetchSort;
|
|
7211
7386
|
const residualLimitStream = sizeAfterPostFilter && limit !== void 0 ? this._buildResidualFilterStream({
|
|
7212
7387
|
queryConfig,
|
|
7213
7388
|
configuredIndex,
|
|
@@ -7218,7 +7393,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7218
7393
|
}) : null;
|
|
7219
7394
|
let rows;
|
|
7220
7395
|
if (residualLimitStream) rows = await residualLimitStream.take(offset > 0 ? offset + limit : limit);
|
|
7221
|
-
else if (limit === void 0 || paginateAfterPostFetchSort || hasResidualPostFilter) rows = await query.collect();
|
|
7396
|
+
else if (limit === void 0 || paginateAfterPostFetchSort || hasResidualPostFilter || hasPostFetchMembership) rows = await query.collect();
|
|
7222
7397
|
else rows = await query.take(offset > 0 ? offset + limit : limit);
|
|
7223
7398
|
if (!(paginateAfterPostFetchSort || sizeAfterPostFilter) && offset > 0) rows = rows.slice(offset);
|
|
7224
7399
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
@@ -7249,9 +7424,17 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7249
7424
|
indexFields: index.fields
|
|
7250
7425
|
}));
|
|
7251
7426
|
const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
|
|
7427
|
+
let orderSpecs = [];
|
|
7428
|
+
if (config.orderBy) {
|
|
7429
|
+
const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
|
|
7430
|
+
asc,
|
|
7431
|
+
desc
|
|
7432
|
+
}) : config.orderBy;
|
|
7433
|
+
orderSpecs = this._orderBySpecs(orderByValue);
|
|
7434
|
+
}
|
|
7252
7435
|
let whereExpression = whereExpressionOverride;
|
|
7253
7436
|
if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
|
|
7254
|
-
const planned = compiler.compile(whereExpression);
|
|
7437
|
+
const planned = compiler.compile(whereExpression, { orderFields: orderSpecs.map((spec) => spec.field) });
|
|
7255
7438
|
const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
|
|
7256
7439
|
const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
|
|
7257
7440
|
strategy: "none",
|
|
@@ -7268,16 +7451,10 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7268
7451
|
};
|
|
7269
7452
|
if (compiled.selectedIndex && (compiled.indexFilters.length > 0 || compiled.probeFilters.length > 0)) result.index = {
|
|
7270
7453
|
name: compiled.selectedIndex.indexName,
|
|
7454
|
+
fields: compiled.selectedIndex.indexFields,
|
|
7271
7455
|
filters: compiled.indexFilters
|
|
7272
7456
|
};
|
|
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
|
-
}
|
|
7457
|
+
if (orderSpecs.length > 0) result.order = orderSpecs;
|
|
7281
7458
|
return result;
|
|
7282
7459
|
}
|
|
7283
7460
|
_buildRelationKey(row, fields) {
|
|
@@ -7286,6 +7463,30 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7286
7463
|
if (values.some((value) => value === null || value === void 0)) return null;
|
|
7287
7464
|
return JSON.stringify(values);
|
|
7288
7465
|
}
|
|
7466
|
+
/**
|
|
7467
|
+
* How many leading fields of the scanned index are pinned to a single value.
|
|
7468
|
+
*
|
|
7469
|
+
* `splitFilters` emits index filters in index-key order — a run of `eq`, then
|
|
7470
|
+
* at most one range on the first unpinned field — so the leading `eq` run is
|
|
7471
|
+
* the prefix Convex holds constant. A multi-probe plan carries no index
|
|
7472
|
+
* filters; each probe supplies its own bound instead, and the union is only
|
|
7473
|
+
* as pinned as its least pinned probe.
|
|
7474
|
+
*/
|
|
7475
|
+
_indexEqPrefixCount(queryConfig) {
|
|
7476
|
+
const countEqPrefix = (filters) => {
|
|
7477
|
+
let count = 0;
|
|
7478
|
+
for (const filter of filters) {
|
|
7479
|
+
if (filter.type !== "binary" || filter.operator !== "eq") break;
|
|
7480
|
+
count += 1;
|
|
7481
|
+
}
|
|
7482
|
+
return count;
|
|
7483
|
+
};
|
|
7484
|
+
if (queryConfig.index && queryConfig.index.filters.length > 0) return countEqPrefix(queryConfig.index.filters);
|
|
7485
|
+
if (queryConfig.probeFilters.length === 0) return 0;
|
|
7486
|
+
let pinned = Number.POSITIVE_INFINITY;
|
|
7487
|
+
for (const probe of queryConfig.probeFilters) pinned = Math.min(pinned, countEqPrefix(probe));
|
|
7488
|
+
return Number.isFinite(pinned) ? pinned : 0;
|
|
7489
|
+
}
|
|
7289
7490
|
_buildIndexPredicate(q, fields, values) {
|
|
7290
7491
|
let builder = q.eq(fields[0], values[0]);
|
|
7291
7492
|
for (let i = 1; i < fields.length; i += 1) builder = builder.eq(fields[i], values[i]);
|
|
@@ -7494,16 +7695,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7494
7695
|
if (typeof record.where === "function") throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `with._count.${relationName}.where callback is unsupported in v1`);
|
|
7495
7696
|
return record.where;
|
|
7496
7697
|
}
|
|
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
7698
|
_getRelationCountParentKey(row, edge) {
|
|
7508
7699
|
const sourceFields = edge.sourceFields.length > 0 ? edge.sourceFields : [edge.fieldName];
|
|
7509
7700
|
const values = [];
|
|
@@ -7514,26 +7705,20 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7514
7705
|
}
|
|
7515
7706
|
return JSON.stringify(values);
|
|
7516
7707
|
}
|
|
7517
|
-
|
|
7518
|
-
return JSON.stringify({
|
|
7519
|
-
relationName,
|
|
7520
|
-
where: this._normalizeRelationCountCacheValue(where ?? null),
|
|
7521
|
-
parentKey
|
|
7522
|
-
});
|
|
7523
|
-
}
|
|
7524
|
-
async _readIndexedRelationCount(tableConfig, where, relationPath) {
|
|
7708
|
+
async _readIndexedRelationCount(tableConfig, where, relationPath, bucketCache) {
|
|
7525
7709
|
ensureCountAllowedForRls(tableConfig, this.rls?.mode);
|
|
7526
7710
|
try {
|
|
7527
7711
|
const plan = compileCountQueryPlan(tableConfig, where);
|
|
7528
7712
|
if (isIndexCountZero(plan)) return 0;
|
|
7529
7713
|
await this._ensureCountIndexReadyOnce(plan.tableName, plan.indexName);
|
|
7530
|
-
return await readCountFromBuckets(this.db, plan);
|
|
7714
|
+
return await readCountFromBuckets(this.db, plan, bucketCache);
|
|
7531
7715
|
} catch (error) {
|
|
7532
7716
|
throw this._remapRelationCountError(error, relationPath);
|
|
7533
7717
|
}
|
|
7534
7718
|
}
|
|
7535
|
-
async _countRelationForRow(row, relationName, edge, where, tableConfig) {
|
|
7719
|
+
async _countRelationForRow(row, relationName, edge, where, tableConfig, caches) {
|
|
7536
7720
|
const relationPath = `${tableConfig.name}.${relationName}`;
|
|
7721
|
+
const bucketCache = caches?.buckets;
|
|
7537
7722
|
if (edge.through) {
|
|
7538
7723
|
const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
|
|
7539
7724
|
if (!throughTableConfig) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `${relationPath} through table '${edge.through.table}' is not registered`);
|
|
@@ -7548,7 +7733,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7548
7733
|
throughWhere[throughField] = value;
|
|
7549
7734
|
sourceValues.push(value);
|
|
7550
7735
|
}
|
|
7551
|
-
if (this._isEmptyWhere(where) || where === void 0) return await this._readIndexedRelationCount(throughTableConfig, throughWhere, relationPath);
|
|
7736
|
+
if (this._isEmptyWhere(where) || where === void 0) return await this._readIndexedRelationCount(throughTableConfig, throughWhere, relationPath, bucketCache);
|
|
7552
7737
|
const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
|
|
7553
7738
|
if (!targetTableConfig) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `${relationPath} target table '${edge.targetTable}' is not registered`);
|
|
7554
7739
|
ensureCountAllowedForRls(targetTableConfig, this.rls?.mode);
|
|
@@ -7583,13 +7768,28 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7583
7768
|
if (targetKeyCounts.size === 0) return 0;
|
|
7584
7769
|
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
7585
7770
|
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 }) => {
|
|
7771
|
+
const resolveTargetMatch = async (values) => {
|
|
7588
7772
|
let target = null;
|
|
7589
7773
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
7590
7774
|
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)
|
|
7775
|
+
if (!target) return false;
|
|
7776
|
+
return this._evaluateTableFilter(target, targetTableConfig, whereRecord);
|
|
7777
|
+
};
|
|
7778
|
+
const targetMatchCache = caches?.throughTargetMatches;
|
|
7779
|
+
const targetEntries = Array.from(targetKeyCounts.entries());
|
|
7780
|
+
return (await this._mapWithConcurrency(targetEntries, async ([targetKey, { values, occurrences }]) => {
|
|
7781
|
+
if (!targetMatchCache) return await resolveTargetMatch(values) ? occurrences : 0;
|
|
7782
|
+
let pending = targetMatchCache.get(targetKey);
|
|
7783
|
+
if (!pending) {
|
|
7784
|
+
pending = resolveTargetMatch(values);
|
|
7785
|
+
targetMatchCache.set(targetKey, pending);
|
|
7786
|
+
}
|
|
7787
|
+
try {
|
|
7788
|
+
return await pending ? occurrences : 0;
|
|
7789
|
+
} catch (error) {
|
|
7790
|
+
targetMatchCache.delete(targetKey);
|
|
7791
|
+
throw error;
|
|
7792
|
+
}
|
|
7593
7793
|
})).reduce((sum, value) => sum + value, 0);
|
|
7594
7794
|
}
|
|
7595
7795
|
const targetTableConfig = this._getTableConfigByDbName(edge.targetTable);
|
|
@@ -7605,7 +7805,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7605
7805
|
relationWhere[targetField] = value;
|
|
7606
7806
|
}
|
|
7607
7807
|
const mergedWhere = this._isEmptyWhere(where) || where === void 0 ? relationWhere : { AND: [relationWhere, where] };
|
|
7608
|
-
return await this._readIndexedRelationCount(targetTableConfig, mergedWhere, relationPath);
|
|
7808
|
+
return await this._readIndexedRelationCount(targetTableConfig, mergedWhere, relationPath, bucketCache);
|
|
7609
7809
|
}
|
|
7610
7810
|
async _loadRelationCounts(rows, relationCountConfig, targetTableEdges, tableConfig) {
|
|
7611
7811
|
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 +7817,21 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7617
7817
|
if (!edge) throw this._createRelationCountError(RELATION_COUNT_ERROR.FILTER_UNSUPPORTED, `with._count.${relationName} is not a relation on '${tableConfig.name}'`);
|
|
7618
7818
|
const where = this._coerceRelationCountWhere(relationName, relationSelection);
|
|
7619
7819
|
const relationCountExecutionCache = /* @__PURE__ */ new Map();
|
|
7820
|
+
const caches = {
|
|
7821
|
+
buckets: /* @__PURE__ */ new Map(),
|
|
7822
|
+
throughTargetMatches: /* @__PURE__ */ new Map()
|
|
7823
|
+
};
|
|
7620
7824
|
const counts = await this._mapWithConcurrency(rows, async (row) => {
|
|
7621
7825
|
const parentKey = this._getRelationCountParentKey(row, edge);
|
|
7622
7826
|
if (parentKey === null) return 0;
|
|
7623
|
-
const
|
|
7624
|
-
const existing = relationCountExecutionCache.get(executionKey);
|
|
7827
|
+
const existing = relationCountExecutionCache.get(parentKey);
|
|
7625
7828
|
if (existing) return await existing;
|
|
7626
|
-
const pending = this._countRelationForRow(row, relationName, edge, where, tableConfig);
|
|
7627
|
-
relationCountExecutionCache.set(
|
|
7829
|
+
const pending = this._countRelationForRow(row, relationName, edge, where, tableConfig, caches);
|
|
7830
|
+
relationCountExecutionCache.set(parentKey, pending);
|
|
7628
7831
|
try {
|
|
7629
7832
|
return await pending;
|
|
7630
7833
|
} catch (error) {
|
|
7631
|
-
relationCountExecutionCache.delete(
|
|
7834
|
+
relationCountExecutionCache.delete(parentKey);
|
|
7632
7835
|
throw error;
|
|
7633
7836
|
}
|
|
7634
7837
|
});
|
|
@@ -7708,6 +7911,108 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7708
7911
|
}
|
|
7709
7912
|
}
|
|
7710
7913
|
/**
|
|
7914
|
+
* Junction links per parent, stopped once each parent holds `fetchLimit`
|
|
7915
|
+
* links whose target actually reaches the page.
|
|
7916
|
+
*
|
|
7917
|
+
* A link only contributes if its target exists and survives target RLS and
|
|
7918
|
+
* the relation `where` — all of which run after the junction read. Sizing the
|
|
7919
|
+
* read on links alone therefore under-fills: three dangling or filtered links
|
|
7920
|
+
* first and `{ limit: 3 }` returns nothing. So the read is refilled in rounds,
|
|
7921
|
+
* each round resolving only the targets it newly needs and asking again for
|
|
7922
|
+
* whatever the survivors did not cover.
|
|
7923
|
+
*
|
|
7924
|
+
* Rounds are the unit rather than single links because both the target fetch
|
|
7925
|
+
* and the relation `where` de-duplicate and batch their own reads across the
|
|
7926
|
+
* parents in the round.
|
|
7927
|
+
*/
|
|
7928
|
+
async _readBoundedThroughLinks(params) {
|
|
7929
|
+
const { applyTargetFilters, edge, enforceTargetKeyCap, entries, fetchLimit, fetchTargets, targetFields, throughIndexName, throughTableConfig } = params;
|
|
7930
|
+
const throughTargetFields = edge.through.targetFields;
|
|
7931
|
+
const cursors = entries.map(([key, values]) => ({
|
|
7932
|
+
buffered: [],
|
|
7933
|
+
exhausted: false,
|
|
7934
|
+
iterator: this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName)[Symbol.asyncIterator](),
|
|
7935
|
+
key,
|
|
7936
|
+
links: []
|
|
7937
|
+
}));
|
|
7938
|
+
const bufferVisibleLinks = async (cursor, count) => {
|
|
7939
|
+
let batch = [];
|
|
7940
|
+
const drain = async () => {
|
|
7941
|
+
const visible = await this._applyRlsSelectFilter(batch, throughTableConfig);
|
|
7942
|
+
batch = [];
|
|
7943
|
+
cursor.buffered.push(...visible);
|
|
7944
|
+
};
|
|
7945
|
+
while (!cursor.exhausted && cursor.buffered.length + batch.length < count) {
|
|
7946
|
+
const next = await cursor.iterator.next();
|
|
7947
|
+
if (next.done) {
|
|
7948
|
+
cursor.exhausted = true;
|
|
7949
|
+
break;
|
|
7950
|
+
}
|
|
7951
|
+
batch.push(next.value);
|
|
7952
|
+
if (batch.length >= RELATION_FILTER_STREAM_CHUNK) await drain();
|
|
7953
|
+
}
|
|
7954
|
+
await drain();
|
|
7955
|
+
};
|
|
7956
|
+
/** Target key -> the surviving document, absent when it did not survive. */
|
|
7957
|
+
const survivorByKey = /* @__PURE__ */ new Map();
|
|
7958
|
+
const resolvedKeys = /* @__PURE__ */ new Set();
|
|
7959
|
+
const survivors = [];
|
|
7960
|
+
while (true) {
|
|
7961
|
+
const active = cursors.filter((cursor) => cursor.links.length < fetchLimit && !(cursor.exhausted && cursor.buffered.length === 0));
|
|
7962
|
+
if (active.length === 0) break;
|
|
7963
|
+
const candidatesPerCursor = await this._mapWithConcurrency(active, async (cursor) => {
|
|
7964
|
+
const need = fetchLimit - cursor.links.length;
|
|
7965
|
+
await bufferVisibleLinks(cursor, need);
|
|
7966
|
+
return cursor.buffered.splice(0, need);
|
|
7967
|
+
});
|
|
7968
|
+
const newKeys = /* @__PURE__ */ new Map();
|
|
7969
|
+
for (const candidates of candidatesPerCursor) for (const link of candidates) {
|
|
7970
|
+
const values = throughTargetFields.map((field) => link[field]);
|
|
7971
|
+
if (values.some((value) => value === null || value === void 0)) continue;
|
|
7972
|
+
const key = JSON.stringify(values);
|
|
7973
|
+
if (resolvedKeys.has(key) || newKeys.has(key)) continue;
|
|
7974
|
+
newKeys.set(key, values);
|
|
7975
|
+
}
|
|
7976
|
+
if (newKeys.size > 0) {
|
|
7977
|
+
enforceTargetKeyCap(resolvedKeys.size + newKeys.size);
|
|
7978
|
+
const fetched = await fetchTargets(Array.from(newKeys.entries()));
|
|
7979
|
+
for (const key of newKeys.keys()) resolvedKeys.add(key);
|
|
7980
|
+
const surviving = await applyTargetFilters(fetched.map((entry) => entry.target).filter((target) => !!target));
|
|
7981
|
+
for (const target of surviving) {
|
|
7982
|
+
const key = this._buildRelationKey(target, targetFields);
|
|
7983
|
+
if (!key || survivorByKey.has(key)) continue;
|
|
7984
|
+
survivorByKey.set(key, target);
|
|
7985
|
+
survivors.push(target);
|
|
7986
|
+
}
|
|
7987
|
+
}
|
|
7988
|
+
for (let i = 0; i < active.length; i += 1) {
|
|
7989
|
+
const cursor = active[i];
|
|
7990
|
+
for (const link of candidatesPerCursor[i]) {
|
|
7991
|
+
if (cursor.links.length >= fetchLimit) break;
|
|
7992
|
+
const key = this._buildRelationKey(link, throughTargetFields);
|
|
7993
|
+
if (!key || !survivorByKey.has(key)) continue;
|
|
7994
|
+
cursor.links.push(link);
|
|
7995
|
+
}
|
|
7996
|
+
}
|
|
7997
|
+
}
|
|
7998
|
+
const linksBySourceKey = /* @__PURE__ */ new Map();
|
|
7999
|
+
const usedKeys = /* @__PURE__ */ new Set();
|
|
8000
|
+
for (const cursor of cursors) {
|
|
8001
|
+
linksBySourceKey.set(cursor.key, cursor.links);
|
|
8002
|
+
for (const link of cursor.links) {
|
|
8003
|
+
const key = this._buildRelationKey(link, throughTargetFields);
|
|
8004
|
+
if (key) usedKeys.add(key);
|
|
8005
|
+
}
|
|
8006
|
+
}
|
|
8007
|
+
return {
|
|
8008
|
+
linksBySourceKey,
|
|
8009
|
+
targets: survivors.filter((target) => {
|
|
8010
|
+
const key = this._buildRelationKey(target, targetFields);
|
|
8011
|
+
return key !== null && usedKeys.has(key);
|
|
8012
|
+
})
|
|
8013
|
+
};
|
|
8014
|
+
}
|
|
8015
|
+
/**
|
|
7711
8016
|
* Load many() relation (one-to-many)
|
|
7712
8017
|
* Example: users.posts where posts.authorId → users.id
|
|
7713
8018
|
*
|
|
@@ -7781,65 +8086,108 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7781
8086
|
if (!throughTableConfig) throw new Error(`Relation '${relationName}' through table '${edge.through.table}' not found.`);
|
|
7782
8087
|
const throughIndexName = findRelationIndex(throughTableConfig.table, edge.through.sourceFields, `${tableConfig.name}.${relationName}`, edge.through.table, strict, this.allowFullScan);
|
|
7783
8088
|
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({
|
|
8089
|
+
const enforceTargetKeyCap = (keyCount) => this._enforceRelationFanOutKeyCap({
|
|
7803
8090
|
tableConfig,
|
|
7804
8091
|
relationName,
|
|
7805
|
-
keyCount
|
|
8092
|
+
keyCount,
|
|
7806
8093
|
scope: "through-target"
|
|
7807
8094
|
});
|
|
7808
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
8095
|
+
let targetLookup = null;
|
|
8096
|
+
const fetchThroughTargets = async (keyEntries) => {
|
|
8097
|
+
if (!targetLookup) {
|
|
8098
|
+
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
8099
|
+
targetLookup = {
|
|
8100
|
+
useGetById,
|
|
8101
|
+
indexName: useGetById ? null : findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan)
|
|
8102
|
+
};
|
|
8103
|
+
}
|
|
8104
|
+
const { useGetById, indexName } = targetLookup;
|
|
8105
|
+
return await this._mapWithConcurrency(keyEntries, async ([key, values]) => {
|
|
7813
8106
|
let target = null;
|
|
7814
8107
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
7815
|
-
else target = await this._queryByFields(this.db.query(edge.targetTable), targetFields, values,
|
|
8108
|
+
else target = await this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName).first();
|
|
7816
8109
|
return {
|
|
7817
8110
|
key,
|
|
7818
8111
|
target
|
|
7819
8112
|
};
|
|
7820
|
-
})
|
|
8113
|
+
});
|
|
8114
|
+
};
|
|
8115
|
+
if (orderSpecs.length === 0 && effectivePerParentLimit !== void 0) {
|
|
8116
|
+
const bounded = await this._readBoundedThroughLinks({
|
|
8117
|
+
applyTargetFilters: applyPostFetchTargetFilters,
|
|
8118
|
+
edge,
|
|
8119
|
+
enforceTargetKeyCap,
|
|
8120
|
+
entries,
|
|
8121
|
+
fetchLimit: Math.max(perParentOffset ?? 0, 0) + effectivePerParentLimit,
|
|
8122
|
+
fetchTargets: fetchThroughTargets,
|
|
8123
|
+
targetFields,
|
|
8124
|
+
throughIndexName,
|
|
8125
|
+
throughTableConfig
|
|
8126
|
+
});
|
|
8127
|
+
throughBySourceKey = bounded.linksBySourceKey;
|
|
8128
|
+
targets = bounded.targets;
|
|
8129
|
+
targetFiltersApplied = true;
|
|
8130
|
+
} else {
|
|
8131
|
+
const throughRowsPerSource = await this._mapWithConcurrency(entries, async ([key, values]) => {
|
|
8132
|
+
const query = this._queryByFields(this.db.query(edge.through.table), edge.through.sourceFields, values, throughIndexName);
|
|
8133
|
+
return {
|
|
8134
|
+
key,
|
|
8135
|
+
rows: await this._applyRlsSelectFilter(await query.collect(), throughTableConfig)
|
|
8136
|
+
};
|
|
8137
|
+
});
|
|
8138
|
+
throughBySourceKey = /* @__PURE__ */ new Map();
|
|
8139
|
+
const targetKeyMap = /* @__PURE__ */ new Map();
|
|
8140
|
+
for (const entry of throughRowsPerSource) {
|
|
8141
|
+
throughBySourceKey.set(entry.key, entry.rows);
|
|
8142
|
+
for (const row of entry.rows) {
|
|
8143
|
+
const values = edge.through.targetFields.map((field) => row[field]);
|
|
8144
|
+
if (values.some((value) => value === null || value === void 0)) continue;
|
|
8145
|
+
const key = JSON.stringify(values);
|
|
8146
|
+
if (!targetKeyMap.has(key)) targetKeyMap.set(key, values);
|
|
8147
|
+
}
|
|
8148
|
+
}
|
|
8149
|
+
enforceTargetKeyCap(targetKeyMap.size);
|
|
8150
|
+
if (targetKeyMap.size > 0) targets = (await fetchThroughTargets(Array.from(targetKeyMap.entries()))).map((entry) => entry.target).filter((value) => !!value);
|
|
7821
8151
|
}
|
|
7822
8152
|
} else {
|
|
7823
|
-
const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
8153
|
+
const indexName = findRelationIndex(targetTableConfig.table, targetFields, `${tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan, orderSpecs);
|
|
7824
8154
|
const entries = Array.from(sourceKeyMap.entries());
|
|
7825
|
-
const
|
|
8155
|
+
const orderPushdownDirection = resolveIndexOrderPushdown({
|
|
8156
|
+
indexFields: indexName ? getIndexes(targetTableConfig.table).find((idx) => idx.name === indexName)?.fields ?? null : null,
|
|
8157
|
+
pinnedEqCount: targetFields.length,
|
|
8158
|
+
orderSpecs
|
|
8159
|
+
});
|
|
8160
|
+
const orderServedByIndex = orderSpecs.length === 0 || orderPushdownDirection !== null;
|
|
8161
|
+
const applyPushdownOrder = (query) => orderPushdownDirection ? query.order(orderPushdownDirection) : query;
|
|
8162
|
+
const streamPostFetchTargetFilters = orderServedByIndex && hasPostFetchTargetFilter && effectivePerParentLimit !== void 0;
|
|
7826
8163
|
targetFiltersApplied = streamPostFetchTargetFilters;
|
|
7827
8164
|
targets = (await this._mapWithConcurrency(entries, async ([, values]) => {
|
|
7828
8165
|
const query = this._queryByFields(this.db.query(edge.targetTable), targetFields, values, indexName);
|
|
7829
|
-
if (
|
|
8166
|
+
if (orderServedByIndex && !hasPostFetchTargetFilter && effectivePerParentLimit !== void 0) {
|
|
7830
8167
|
const fetchLimit = (perParentOffset ?? 0) + (effectivePerParentLimit ?? 0);
|
|
7831
|
-
return await query.take(fetchLimit);
|
|
8168
|
+
return await applyPushdownOrder(query).take(fetchLimit);
|
|
7832
8169
|
}
|
|
7833
8170
|
if (streamPostFetchTargetFilters) {
|
|
7834
8171
|
const visibleTargets = [];
|
|
7835
8172
|
const fetchLimit = Math.max(perParentOffset ?? 0, 0) + (effectivePerParentLimit ?? 0);
|
|
7836
|
-
|
|
7837
|
-
|
|
7838
|
-
if (
|
|
7839
|
-
|
|
7840
|
-
|
|
8173
|
+
let batch = [];
|
|
8174
|
+
const drain = async () => {
|
|
8175
|
+
if (batch.length === 0) return;
|
|
8176
|
+
const filtered = await applyPostFetchTargetFilters(batch);
|
|
8177
|
+
batch = [];
|
|
8178
|
+
for (const row of filtered) {
|
|
8179
|
+
if (visibleTargets.length >= fetchLimit) return;
|
|
8180
|
+
visibleTargets.push(row);
|
|
7841
8181
|
}
|
|
8182
|
+
};
|
|
8183
|
+
for await (const target of applyPushdownOrder(query)) {
|
|
8184
|
+
batch.push(target);
|
|
8185
|
+
const chunk = Math.min(RELATION_FILTER_STREAM_CHUNK, fetchLimit - visibleTargets.length);
|
|
8186
|
+
if (batch.length < chunk) continue;
|
|
8187
|
+
await drain();
|
|
8188
|
+
if (visibleTargets.length >= fetchLimit) break;
|
|
7842
8189
|
}
|
|
8190
|
+
if (visibleTargets.length < fetchLimit) await drain();
|
|
7843
8191
|
return visibleTargets;
|
|
7844
8192
|
}
|
|
7845
8193
|
return await query.collect();
|
|
@@ -7847,6 +8195,19 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7847
8195
|
}
|
|
7848
8196
|
if (!targetFiltersApplied) targets = await applyPostFetchTargetFilters(targets);
|
|
7849
8197
|
if (orderSpecs.length > 0) targets.sort((a, b) => this._compareByOrderSpecs(a, b, orderSpecs));
|
|
8198
|
+
if (!edge.through && (perParentOffset !== void 0 || effectivePerParentLimit !== void 0)) {
|
|
8199
|
+
const groupedTargets = /* @__PURE__ */ new Map();
|
|
8200
|
+
for (const target of targets) {
|
|
8201
|
+
const parentKey = this._buildRelationKey(target, targetFields);
|
|
8202
|
+
if (!parentKey) continue;
|
|
8203
|
+
const group = groupedTargets.get(parentKey);
|
|
8204
|
+
if (group) group.push(target);
|
|
8205
|
+
else groupedTargets.set(parentKey, [target]);
|
|
8206
|
+
}
|
|
8207
|
+
const trimmed = [];
|
|
8208
|
+
for (const children of groupedTargets.values()) for (const child of applyOffsetAndLimit(children)) trimmed.push(child);
|
|
8209
|
+
targets = trimmed;
|
|
8210
|
+
}
|
|
7850
8211
|
if (relationConfig && typeof relationConfig === "object" && "with" in relationConfig) {
|
|
7851
8212
|
const targetTableEdges = this._getTargetTableEdges(edge.targetTable);
|
|
7852
8213
|
await this._loadRelations(targets, relationConfig.with, depth + 1, maxDepth, targetTableEdges, targetTableConfig);
|
|
@@ -7891,7 +8252,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
7891
8252
|
if (!byParentKey.has(parentKey)) byParentKey.set(parentKey, []);
|
|
7892
8253
|
byParentKey.get(parentKey).push(mappedTarget);
|
|
7893
8254
|
}
|
|
7894
|
-
if (perParentOffset !== void 0 || effectivePerParentLimit !== void 0) for (const [parentKey, children] of byParentKey.entries()) byParentKey.set(parentKey, applyOffsetAndLimit(children));
|
|
7895
8255
|
for (const row of rows) {
|
|
7896
8256
|
const rowKey = this._buildRelationKey(row, sourceFields);
|
|
7897
8257
|
row[relationName] = rowKey ? byParentKey.get(rowKey) ?? [] : [];
|
|
@@ -9531,6 +9891,38 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
9531
9891
|
|
|
9532
9892
|
//#endregion
|
|
9533
9893
|
//#region src/orm/database.ts
|
|
9894
|
+
/**
|
|
9895
|
+
* `createDatabase` runs on every Convex query and mutation entry, but the
|
|
9896
|
+
* foreign-key graph and the per-table edge partition are pure functions of the
|
|
9897
|
+
* schema and edge metadata, both fixed for the process lifetime. Caching them
|
|
9898
|
+
* on the identity of those objects keeps the per-request work proportional to
|
|
9899
|
+
* the table count instead of `tables x edges`.
|
|
9900
|
+
*
|
|
9901
|
+
* Both caches are only sound because their inputs are module-level immutables:
|
|
9902
|
+
* nothing in the ORM mutates a schema or an edge list after construction.
|
|
9903
|
+
*/
|
|
9904
|
+
const foreignKeyGraphCache = /* @__PURE__ */ new WeakMap();
|
|
9905
|
+
const edgesBySourceTableCache = /* @__PURE__ */ new WeakMap();
|
|
9906
|
+
const NO_EDGES = [];
|
|
9907
|
+
function getForeignKeyGraph(schema) {
|
|
9908
|
+
const cached = foreignKeyGraphCache.get(schema);
|
|
9909
|
+
if (cached) return cached;
|
|
9910
|
+
const graph = buildForeignKeyGraph(schema);
|
|
9911
|
+
foreignKeyGraphCache.set(schema, graph);
|
|
9912
|
+
return graph;
|
|
9913
|
+
}
|
|
9914
|
+
function getEdgesBySourceTable(edgeMetadata) {
|
|
9915
|
+
const cached = edgesBySourceTableCache.get(edgeMetadata);
|
|
9916
|
+
if (cached) return cached;
|
|
9917
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
9918
|
+
for (const edge of edgeMetadata) {
|
|
9919
|
+
const existing = grouped.get(edge.sourceTable);
|
|
9920
|
+
if (existing) existing.push(edge);
|
|
9921
|
+
else grouped.set(edge.sourceTable, [edge]);
|
|
9922
|
+
}
|
|
9923
|
+
edgesBySourceTableCache.set(edgeMetadata, grouped);
|
|
9924
|
+
return grouped;
|
|
9925
|
+
}
|
|
9534
9926
|
function createDatabase(db, schema, edgeMetadata, options) {
|
|
9535
9927
|
const schemaOptions = schema[OrmSchemaOptions];
|
|
9536
9928
|
const strict = schemaOptions?.strict ?? true;
|
|
@@ -9541,7 +9933,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9541
9933
|
scheduledMutationBatch: options?.scheduledMutationBatch
|
|
9542
9934
|
});
|
|
9543
9935
|
const ormContext = {
|
|
9544
|
-
foreignKeyGraph:
|
|
9936
|
+
foreignKeyGraph: getForeignKeyGraph(schema),
|
|
9545
9937
|
schema,
|
|
9546
9938
|
edgeMetadata,
|
|
9547
9939
|
relationLoading: options?.relationLoading,
|
|
@@ -9555,7 +9947,8 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9555
9947
|
};
|
|
9556
9948
|
const baseDb = Object.assign(Object.create(db), { [OrmContext]: ormContext });
|
|
9557
9949
|
const query = {};
|
|
9558
|
-
|
|
9950
|
+
const edgesBySourceTable = getEdgesBySourceTable(edgeMetadata);
|
|
9951
|
+
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
9952
|
const isWriter = typeof db.insert === "function" && typeof db.patch === "function";
|
|
9560
9953
|
const isConvexTable = (value) => !!value && typeof value === "object" && value[Brand] === "ConvexTable";
|
|
9561
9954
|
const insert = (table) => {
|
|
@@ -9600,14 +9993,18 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
9600
9993
|
return built;
|
|
9601
9994
|
};
|
|
9602
9995
|
const table = buildDatabase(options?.rls);
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9996
|
+
let skipRulesTable;
|
|
9997
|
+
return Object.defineProperty({ ...table }, "skipRules", {
|
|
9998
|
+
enumerable: true,
|
|
9999
|
+
configurable: true,
|
|
10000
|
+
get() {
|
|
10001
|
+
skipRulesTable ??= buildDatabase({
|
|
10002
|
+
...options?.rls ?? {},
|
|
10003
|
+
mode: "skip"
|
|
10004
|
+
});
|
|
10005
|
+
return skipRulesTable;
|
|
10006
|
+
}
|
|
9606
10007
|
});
|
|
9607
|
-
return {
|
|
9608
|
-
...table,
|
|
9609
|
-
skipRules: skipRulesTable
|
|
9610
|
-
};
|
|
9611
10008
|
}
|
|
9612
10009
|
|
|
9613
10010
|
//#endregion
|
|
@@ -11176,8 +11573,7 @@ function resolveOrmSchemaConfig(schemaInput) {
|
|
|
11176
11573
|
triggers: getSchemaTriggers(schemaInput)
|
|
11177
11574
|
};
|
|
11178
11575
|
}
|
|
11179
|
-
function createDbFactory(schema, dbLifecycle, ormFunctions) {
|
|
11180
|
-
const edgeMetadata = extractRelationsConfig(schema);
|
|
11576
|
+
function createDbFactory(schema, edgeMetadata, dbLifecycle, ormFunctions) {
|
|
11181
11577
|
return ((source, options) => {
|
|
11182
11578
|
const ctxSource = isOrmCtx(source) ? source : void 0;
|
|
11183
11579
|
const rawDb = ctxSource ? ctxSource.db : source;
|
|
@@ -11203,7 +11599,7 @@ function createOrm(config) {
|
|
|
11203
11599
|
const { schema: resolvedSchema, triggers } = resolveOrmSchemaConfig(config.schema);
|
|
11204
11600
|
const dbLifecycle = createOrmDbLifecycle(resolvedSchema, triggers);
|
|
11205
11601
|
const edgeMetadata = extractRelationsConfig(resolvedSchema);
|
|
11206
|
-
const db = createDbFactory(resolvedSchema, dbLifecycle, config.ormFunctions);
|
|
11602
|
+
const db = createDbFactory(resolvedSchema, edgeMetadata, dbLifecycle, config.ormFunctions);
|
|
11207
11603
|
const withContext = (ctx, options) => {
|
|
11208
11604
|
const lifecycleCtx = { ...ctx };
|
|
11209
11605
|
const wrappedCtx = dbLifecycle.wrapDB(lifecycleCtx);
|