kitcn 0.32.1 → 0.33.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/CHANGELOG.md +114 -0
- package/dist/aggregate/index.d.ts +2 -2
- package/dist/auth/index.js +1 -1
- package/dist/{capabilities-Ctcw2VRq.d.ts → capabilities-Bxzoofl4.d.ts} +195 -9
- package/dist/orm/aggregate-index/index.d.ts +1 -1
- package/dist/orm/aggregate-index/index.js +226 -96
- package/dist/orm/index.d.ts +2 -2
- package/dist/orm/index.js +440 -113
- package/dist/orm/migrations/index.d.ts +2 -2
- package/dist/{query-context-D4z1CnDH.js → query-context-DQLv58LH.js} +34 -1
- package/dist/react/index.js +4 -3
- package/dist/{schema-Bh7AmJwY.js → schema-BF4P0ZjS.js} +322 -1
- package/dist/{where-clause-compiler-BHGHbfb9.d.ts → where-clause-compiler-DrV6lQg0.d.ts} +126 -126
- package/package.json +1 -1
- package/skills/kitcn/references/features/aggregates.md +13 -0
- package/skills/kitcn/references/features/orm.md +24 -4
package/dist/orm/index.js
CHANGED
|
@@ -4,8 +4,8 @@ import { a as pretendRequired, i as pretend, n as deprecated } from "../validato
|
|
|
4
4
|
import { o as id } from "../schema-BFP_awgP.js";
|
|
5
5
|
import { a as OrmSchemaDefinition, c as OrmSchemaExtensionTriggers, d as OrmSchemaRelations, f as OrmSchemaTriggers, g as TablePolymorphic, h as TableName, i as OrmContext, l as OrmSchemaExtensions, n as Columns, o as OrmSchemaExtensionRelations, p as RlsPolicies, r as EnableRLS, s as OrmSchemaExtensionTables, t as Brand, u as OrmSchemaOptions } from "../symbols-DDNAddkd.js";
|
|
6
6
|
import { A as inArray, B as notBetween, C as eq, D as gt, E as filterValuesEqual, F as lt, G as startsWith, H as notInArray, I as lte, L as matchLikePattern, M as isNotNull, N as isNull, O as gte, P as like, R as ne, S as endsWith, T as filterValueInList, U as notLike, V as notIlike, W as or, _ as arrayContains, a as findVectorIndexByName, b as column, c as getRankIndexes, d as INTERNAL_CREATION_TIME_FIELD, f as PUBLIC_CREATED_AT_FIELD, g as arrayContained, h as and, i as findSearchIndexByName, j as isFieldReference, k as ilike, l as resolveIndexOrderPushdown, m as usesSystemCreatedAtAlias, n as findIndexForColumns, o as getAggregateIndexes, r as findRelationIndex, s as getIndexes, t as findExactIndexForColumns, u as CREATED_AT_MIGRATION_MESSAGE, v as arrayOverlaps, w as fieldRef, x as contains, y as between, z as not } from "../index-utils-DvK7P6Q1.js";
|
|
7
|
-
import { a as
|
|
8
|
-
import { $ as
|
|
7
|
+
import { a as getIndexFields, c as stream, d as isUnsetToken, f as unsetToken, i as concatStreams, l as streamCanOrderBy, n as EmptyStream, o as indexKeyWithinBounds, r as QueryStream, s as mergedStream, t as getByIdWithOrmQueryFallback, u as streamIndexRange } from "../query-context-DQLv58LH.js";
|
|
8
|
+
import { $ as getOrmContext, A as canUsePrimaryIdLookupCursor, B as enforceUniqueIndexes, Ct as isConvexEnforceableFilter, D as applyIncomingForeignKeyActionsOnDelete, E as applyDefaults, F as deserializeFilterExpression, G as evaluateFilter, H as ensureNonNullValues, I as encodeUndefinedDeep, J as getColumnName$1, K as extractPrimaryIdLookup, L as enforceCheckConstraints, M as collectPrimaryIdLookupRows, N as createForeignKeyProbeMemo, O as applyIncomingForeignKeyActionsOnUpdate, P as decodeUndefinedDeep, Q as getMutationExecutionMode, R as enforceForeignKeys, S as createCountError, St as convexAnd, T as ensureCountAllowedForRls, U as ensureNullableColumns, V as ensureDefaultColumns, W as evaluateCheckConstraintTriState, X as getMutationAsyncDelayMs, Y as getForeignKeys, Z as getMutationCollectionLimits, _t as toConvexFilter, at as hydrateDateFieldsForRead, b as COUNT_ERROR, bt as markLifecycleHookedTables, c as getAggregateIndexDefinitions, ct as patchReferencingRows, dt as selectReturningRowWithHydration, et as getTableColumns$2, ft as serializeFilterExpression, g as runInOrmWriteScope, gt as takeRowsWithinByteBudget, ht as stripUnsetFields, it as hardDeleteRow, j as collectMutationRowsBounded, k as buildForeignKeyGraph, l as getRankIndexDefinitions, lt as resolveOrmRuntimeDefaults, m as runInOrmWriteBatch, mt as splitReturningSelection, nt as getTableName, o as aggregateExtension, ot as normalizeDateFieldsForWrite, p as runInOrmUserCallback, pt as softDeleteRow, q as getChecks, rt as getUniqueIndexes, st as normalizeTemporalComparableValue, tt as getTableDeleteConfig, ut as returningSelectionReadsCreationTime, v as markOrmTransactionAnchor, vt as unsetFieldsOf, w as ensureAggregateAllowedForRls, wt as mapWithConcurrency, x as createAggregateError, xt as compileConvexFilter, y as AGGREGATE_ERROR, yt as hasLifecycleHooks, z as enforcePolymorphicWrite } from "../schema-BF4P0ZjS.js";
|
|
9
9
|
import { t as defineSchemaExtension } from "../extensions-Bp0XHanR.js";
|
|
10
10
|
import { compareValues, convexToJson, jsonToConvex, v } from "convex/values";
|
|
11
11
|
import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric, internalQueryGeneric } from "convex/server";
|
|
@@ -560,7 +560,7 @@ function policyExpressionPasses(row, expression) {
|
|
|
560
560
|
async function resolveExpression(policy, checkType, ctx, table) {
|
|
561
561
|
const candidate = checkType === "withCheck" ? policy.withCheck ?? policy.using : policy.using;
|
|
562
562
|
if (!candidate) return;
|
|
563
|
-
if (typeof candidate === "function") return await candidate(ctx, table);
|
|
563
|
+
if (typeof candidate === "function") return await runInOrmUserCallback(() => candidate(ctx, table));
|
|
564
564
|
return candidate;
|
|
565
565
|
}
|
|
566
566
|
/**
|
|
@@ -703,13 +703,14 @@ async function filterSelectRows(options) {
|
|
|
703
703
|
*/
|
|
704
704
|
const INDEX_ORDER_BONUS = 30;
|
|
705
705
|
/**
|
|
706
|
-
* Widest index union worth
|
|
707
|
-
*
|
|
708
|
-
*
|
|
706
|
+
* Widest index union worth reading as one merged stream. A merge holds every
|
|
707
|
+
* probe's index range open at once, and past this width that fan-out costs more
|
|
708
|
+
* than reading the same ranges one after another.
|
|
709
709
|
*
|
|
710
|
-
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
710
|
+
* Only the executor's business: it picks between a merged and a concatenated
|
|
711
|
+
* union. Width is never a reason to compile a scan instead of a union — the
|
|
712
|
+
* ranges an `in` opens are what bounds the read, and a table scan does not stop
|
|
713
|
+
* being a table scan because the list got longer.
|
|
713
714
|
*/
|
|
714
715
|
const MAX_INDEX_UNION_PROBES = 64;
|
|
715
716
|
var WhereClauseCompiler = class {
|
|
@@ -789,7 +790,7 @@ var WhereClauseCompiler = class {
|
|
|
789
790
|
for (const term of terms) {
|
|
790
791
|
if (term.type !== "binary" || term.operator !== "inArray") continue;
|
|
791
792
|
const probePlan = this.tryCompileInArray(term);
|
|
792
|
-
if (!probePlan
|
|
793
|
+
if (!probePlan) continue;
|
|
793
794
|
return {
|
|
794
795
|
...probePlan,
|
|
795
796
|
postFilters: [expression]
|
|
@@ -1633,6 +1634,20 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
1633
1634
|
* intervening write would make it stale.
|
|
1634
1635
|
*/
|
|
1635
1636
|
_documentByNormalizedId = /* @__PURE__ */ new Map();
|
|
1637
|
+
/**
|
|
1638
|
+
* Single target documents resolved by an eq-pinned key during one execution,
|
|
1639
|
+
* keyed on the read itself: table, index, join columns and their values.
|
|
1640
|
+
*
|
|
1641
|
+
* `_documentByNormalizedId` only covers a join on the primary id. A relation
|
|
1642
|
+
* joined on any other column resolves its target with `.first()` instead, and
|
|
1643
|
+
* the same one-row-at-a-time membership predicate re-issues that read once per
|
|
1644
|
+
* drain. The index is fixed for the whole relation and the key is pinned by
|
|
1645
|
+
* `eq`, so the first row is a pure function of this key within an execution.
|
|
1646
|
+
*
|
|
1647
|
+
* Same scope and staleness argument as `_documentByNormalizedId`: not handed
|
|
1648
|
+
* to the next run by `_forExecution`, so an intervening write is still seen.
|
|
1649
|
+
*/
|
|
1650
|
+
_firstDocumentByFieldKey = /* @__PURE__ */ new Map();
|
|
1636
1651
|
constructor(schema, tableConfig, edgeMetadata, db, config, mode, _allEdges, rls, relationLoading, vectorSearchProvider, configuredIndex, countIndexReadiness) {
|
|
1637
1652
|
super();
|
|
1638
1653
|
this.schema = schema;
|
|
@@ -2699,34 +2714,70 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2699
2714
|
for (let i = 0; i < fields.length; i += 1) builder = builder.eq(fields[i], values[i]);
|
|
2700
2715
|
return builder;
|
|
2701
2716
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2717
|
+
/**
|
|
2718
|
+
* The row filter for an already-resolved pipeline stage `where`.
|
|
2719
|
+
*
|
|
2720
|
+
* Takes the resolved shape rather than the clause so the rows that are read
|
|
2721
|
+
* and the rows that are kept come from the same resolution. Resolving twice
|
|
2722
|
+
* would let a `where` callback that closes over changing state — a clock, a
|
|
2723
|
+
* counter — bound the read with one expression and filter it with another,
|
|
2724
|
+
* dropping rows that the second expression matched but the first never read.
|
|
2725
|
+
*/
|
|
2726
|
+
_buildResolvedWherePredicate(where) {
|
|
2727
|
+
if (where.kind === "none") return null;
|
|
2728
|
+
if (where.kind === "predicate") {
|
|
2729
|
+
const { clause } = where;
|
|
2730
|
+
return async (row) => await clause.predicate(row);
|
|
2731
|
+
}
|
|
2732
|
+
const { expression } = where;
|
|
2733
|
+
return async (row) => this._evaluatePostFetchFilter(row, expression);
|
|
2715
2734
|
}
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2735
|
+
/**
|
|
2736
|
+
* What a pipeline stage `where` is, resolved once.
|
|
2737
|
+
*
|
|
2738
|
+
* Object clauses and callbacks that return an expression can be compiled and
|
|
2739
|
+
* therefore index-lowered. `predicate(...)` is opaque JavaScript and never
|
|
2740
|
+
* can be. Both the anchoring check and the index lowering need to tell those
|
|
2741
|
+
* apart, and a callback `where` should be run once per read rather than once
|
|
2742
|
+
* per caller that asks.
|
|
2743
|
+
*/
|
|
2744
|
+
_resolvePipelineWhere(where, tableConfig) {
|
|
2745
|
+
if (!where) return { kind: "none" };
|
|
2720
2746
|
if (typeof where === "function") {
|
|
2721
2747
|
const result = this._resolveWhereCallbackExpression(where, tableConfig, { context: "pipeline" });
|
|
2722
|
-
if (!result) return;
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2748
|
+
if (!result) return { kind: "none" };
|
|
2749
|
+
return this._isPredicateWhereClause(result) ? {
|
|
2750
|
+
kind: "predicate",
|
|
2751
|
+
clause: result
|
|
2752
|
+
} : {
|
|
2753
|
+
kind: "expression",
|
|
2754
|
+
expression: result
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
const expression = this._buildFilterExpression(where, tableConfig);
|
|
2758
|
+
return expression ? {
|
|
2759
|
+
kind: "expression",
|
|
2760
|
+
expression
|
|
2761
|
+
} : { kind: "none" };
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Reject a `predicate(...)` stage `where` on a read nothing bounds.
|
|
2765
|
+
*
|
|
2766
|
+
* A predicate can only run after the row is read, so it costs one read per
|
|
2767
|
+
* candidate row. That is fine over an index range and unbounded over a table
|
|
2768
|
+
* scan, which is the same line `findMany` draws for a chain-level
|
|
2769
|
+
* `predicate(...)`. An object `where` is not held to it: the compiler
|
|
2770
|
+
* lowers what it can and post-filters the rest, exactly as the chain-level
|
|
2771
|
+
* read does.
|
|
2772
|
+
*
|
|
2773
|
+
* `remedy` is per call site because the anchor a caller can add differs: a
|
|
2774
|
+
* union source takes an index, a `flatMap` stage has no index option at all
|
|
2775
|
+
* and is anchored by the relation's own declared index.
|
|
2776
|
+
*/
|
|
2777
|
+
_assertPipelineWhereIsAnchored(options) {
|
|
2778
|
+
const { where, isAnchored, context, remedy } = options;
|
|
2779
|
+
if (isAnchored || where.kind !== "predicate") return;
|
|
2780
|
+
throw new Error(`${context} where uses predicate(...), which is evaluated after each row is read, so it needs an index-bounded read. ${remedy}`);
|
|
2730
2781
|
}
|
|
2731
2782
|
_isFilterExpressionNode(value) {
|
|
2732
2783
|
return typeof value === "object" && value !== null && "accept" in value && typeof value.accept === "function";
|
|
@@ -2823,16 +2874,23 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2823
2874
|
/**
|
|
2824
2875
|
* The compiled index union, as one ordered stream.
|
|
2825
2876
|
*
|
|
2826
|
-
* Each probe is its own index range, so the union is only usable where
|
|
2827
|
-
*
|
|
2828
|
-
*
|
|
2877
|
+
* Each probe is its own index range, so the union is only usable where its
|
|
2878
|
+
* order is the order the read has to produce. `mergedStream` orders by a
|
|
2879
|
+
* suffix of the index key, and a suffix may only drop key components the
|
|
2829
2880
|
* probes all pin to a single value — so the requested field has to sit inside
|
|
2830
2881
|
* the pinned run or immediately after it. Returns null when it does not, and
|
|
2831
2882
|
* the caller falls back to the plan's plain index range or a bounded scan.
|
|
2883
|
+
*
|
|
2884
|
+
* Two executors, because a union that is too wide to merge is not too wide to
|
|
2885
|
+
* read. A merge registers every probe up front and holds them open for the
|
|
2886
|
+
* life of the read, which is what `MAX_INDEX_UNION_PROBES` refuses; past that
|
|
2887
|
+
* width the probes are concatenated instead, one open query at a time. What
|
|
2888
|
+
* the cap picks is therefore fan-out versus sequential — never index versus
|
|
2889
|
+
* table scan, which is a trade no read wins.
|
|
2832
2890
|
*/
|
|
2833
2891
|
_buildProbeUnionStream(params) {
|
|
2834
2892
|
const { probeFilters } = params;
|
|
2835
|
-
if (probeFilters.length === 0
|
|
2893
|
+
if (probeFilters.length === 0) return null;
|
|
2836
2894
|
const streamIndexFields = getIndexFields(this.tableConfig.name, params.indexName, params.schemaDefinition);
|
|
2837
2895
|
const pinned = this._indexEqPrefixCount({ probeFilters });
|
|
2838
2896
|
let mergeOffset = -1;
|
|
@@ -2841,11 +2899,102 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2841
2899
|
break;
|
|
2842
2900
|
}
|
|
2843
2901
|
if (mergeOffset === -1) return null;
|
|
2844
|
-
|
|
2902
|
+
const probeStream = (filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
|
|
2845
2903
|
let indexQuery = q;
|
|
2846
2904
|
for (const filter of filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
2847
2905
|
return indexQuery;
|
|
2848
|
-
}).order(params.order)
|
|
2906
|
+
}).order(params.order);
|
|
2907
|
+
if (probeFilters.length > MAX_INDEX_UNION_PROBES) {
|
|
2908
|
+
if (mergeOffset !== 0) return null;
|
|
2909
|
+
const orderedProbes = this._orderDisjointProbes({
|
|
2910
|
+
probeFilters,
|
|
2911
|
+
indexField: streamIndexFields[0],
|
|
2912
|
+
order: params.order
|
|
2913
|
+
});
|
|
2914
|
+
if (!orderedProbes) return null;
|
|
2915
|
+
return concatStreams(orderedProbes.map(probeStream));
|
|
2916
|
+
}
|
|
2917
|
+
return mergedStream(probeFilters.map(probeStream), streamIndexFields.slice(mergeOffset));
|
|
2918
|
+
}
|
|
2919
|
+
/**
|
|
2920
|
+
* `probeFilters` in the order a scan of `indexField` reaches them, or null
|
|
2921
|
+
* when they cannot be proven to be pairwise-disjoint ranges on that field.
|
|
2922
|
+
*
|
|
2923
|
+
* Concatenation stands in for a merge only under those two properties, and
|
|
2924
|
+
* both have to come from the filters themselves rather than from trust in the
|
|
2925
|
+
* compiler that emitted them: overlapping probes would emit a row twice, and
|
|
2926
|
+
* probes handed over out of order would emit index keys backwards.
|
|
2927
|
+
*/
|
|
2928
|
+
_orderDisjointProbes(params) {
|
|
2929
|
+
const ranges = [];
|
|
2930
|
+
for (const filters of params.probeFilters) {
|
|
2931
|
+
let lower = null;
|
|
2932
|
+
let upper = null;
|
|
2933
|
+
let usable = filters.length > 0;
|
|
2934
|
+
for (const filter of filters) {
|
|
2935
|
+
if (filter.type !== "binary") {
|
|
2936
|
+
usable = false;
|
|
2937
|
+
break;
|
|
2938
|
+
}
|
|
2939
|
+
const [field, rawValue] = filter.operands;
|
|
2940
|
+
if (!isFieldReference(field) || field.fieldName !== params.indexField || rawValue === void 0 || isFieldReference(rawValue)) {
|
|
2941
|
+
usable = false;
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2944
|
+
const value = this._normalizeComparableValue(field.fieldName, rawValue);
|
|
2945
|
+
switch (filter.operator) {
|
|
2946
|
+
case "eq":
|
|
2947
|
+
usable = lower === null && upper === null;
|
|
2948
|
+
lower = {
|
|
2949
|
+
inclusive: true,
|
|
2950
|
+
value
|
|
2951
|
+
};
|
|
2952
|
+
upper = {
|
|
2953
|
+
inclusive: true,
|
|
2954
|
+
value
|
|
2955
|
+
};
|
|
2956
|
+
break;
|
|
2957
|
+
case "gt":
|
|
2958
|
+
case "gte":
|
|
2959
|
+
usable = lower === null;
|
|
2960
|
+
lower = {
|
|
2961
|
+
inclusive: filter.operator === "gte",
|
|
2962
|
+
value
|
|
2963
|
+
};
|
|
2964
|
+
break;
|
|
2965
|
+
case "lt":
|
|
2966
|
+
case "lte":
|
|
2967
|
+
usable = upper === null;
|
|
2968
|
+
upper = {
|
|
2969
|
+
inclusive: filter.operator === "lte",
|
|
2970
|
+
value
|
|
2971
|
+
};
|
|
2972
|
+
break;
|
|
2973
|
+
default: usable = false;
|
|
2974
|
+
}
|
|
2975
|
+
if (!usable) break;
|
|
2976
|
+
}
|
|
2977
|
+
if (!usable) return null;
|
|
2978
|
+
ranges.push({
|
|
2979
|
+
filters,
|
|
2980
|
+
lower,
|
|
2981
|
+
upper
|
|
2982
|
+
});
|
|
2983
|
+
}
|
|
2984
|
+
ranges.sort((a, b) => {
|
|
2985
|
+
if (!(a.lower && b.lower)) return a.lower ? 1 : b.lower ? -1 : 0;
|
|
2986
|
+
return compareValues(a.lower.value, b.lower.value) || Number(!a.lower.inclusive) - Number(!b.lower.inclusive);
|
|
2987
|
+
});
|
|
2988
|
+
for (let i = 1; i < ranges.length; i += 1) {
|
|
2989
|
+
const previous = ranges[i - 1];
|
|
2990
|
+
const current = ranges[i];
|
|
2991
|
+
if (!(previous.upper && current.lower)) return null;
|
|
2992
|
+
const gap = compareValues(previous.upper.value, current.lower.value);
|
|
2993
|
+
if (gap > 0) return null;
|
|
2994
|
+
if (gap === 0 && previous.upper.inclusive && current.lower.inclusive) return null;
|
|
2995
|
+
}
|
|
2996
|
+
const ordered = ranges.map((range) => range.filters);
|
|
2997
|
+
return params.order === "asc" ? ordered : ordered.reverse();
|
|
2849
2998
|
}
|
|
2850
2999
|
/**
|
|
2851
3000
|
* The read the compiled plan describes, as a stream, with nothing filtered
|
|
@@ -2872,7 +3021,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2872
3021
|
indexName: probeIndex.name,
|
|
2873
3022
|
probeFilters: queryConfig.probeFilters,
|
|
2874
3023
|
order,
|
|
2875
|
-
orderField: primaryOrder?.field ?? probeIndex.fields[0]
|
|
3024
|
+
orderField: params.probeOrderField ?? primaryOrder?.field ?? probeIndex.fields[0]
|
|
2876
3025
|
});
|
|
2877
3026
|
if (probeUnion) return {
|
|
2878
3027
|
stream: probeUnion,
|
|
@@ -2945,22 +3094,82 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2945
3094
|
});
|
|
2946
3095
|
return streamQuery;
|
|
2947
3096
|
}
|
|
2948
|
-
|
|
3097
|
+
/**
|
|
3098
|
+
* One `pipeline.union` source, as a stream.
|
|
3099
|
+
*
|
|
3100
|
+
* The source's own `where` is compiled into an index plan first, so an
|
|
3101
|
+
* object `where` bounds the read instead of being discarded row by row after
|
|
3102
|
+
* it. Two rules keep that from changing what the union produces:
|
|
3103
|
+
*
|
|
3104
|
+
* - `_compileQueryPlan` discards a plan that would displace an index the
|
|
3105
|
+
* caller pinned, so a source's own range (a tenant scope, say) still wins.
|
|
3106
|
+
* - A merged union is ordered by `interleaveBy`, which only some index
|
|
3107
|
+
* shapes can supply. The lowered read is checked against that before it is
|
|
3108
|
+
* committed to, and the unlowered scan is used when it cannot serve the
|
|
3109
|
+
* merge — an index that saves reads is not worth an ordering that throws.
|
|
3110
|
+
*
|
|
3111
|
+
* The `where` stays applied in JS either way. The index range is a bound on
|
|
3112
|
+
* what is read, not a replacement for the predicate.
|
|
3113
|
+
*/
|
|
3114
|
+
_buildUnionSourceStream(source, fallbackOrder, interleaveFields) {
|
|
2949
3115
|
const sourceIndex = source.index ?? this.configuredIndex;
|
|
2950
|
-
this.
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
context: "pipeline.union source"
|
|
3116
|
+
const sourceWhere = this._resolvePipelineWhere(source.where, this.tableConfig);
|
|
3117
|
+
this._assertPipelineWhereIsAnchored({
|
|
3118
|
+
where: sourceWhere,
|
|
3119
|
+
isAnchored: Boolean(sourceIndex?.name),
|
|
3120
|
+
context: "pipeline.union source",
|
|
3121
|
+
remedy: "Give the source an index: { name, range }, or pin .withIndex(name, range?) on the chain."
|
|
2955
3122
|
});
|
|
2956
3123
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
3124
|
+
const buildRead = (whereExpression) => this._buildPlanStream({
|
|
3125
|
+
queryConfig: this._compileQueryPlan({
|
|
3126
|
+
whereExpression,
|
|
3127
|
+
configuredIndex: sourceIndex,
|
|
3128
|
+
orderSpecs: []
|
|
3129
|
+
}),
|
|
3130
|
+
configuredIndex: sourceIndex,
|
|
3131
|
+
order: fallbackOrder,
|
|
3132
|
+
orderIndexName: null,
|
|
3133
|
+
probeOrderField: interleaveFields?.[0],
|
|
3134
|
+
schemaDefinition
|
|
3135
|
+
}).stream;
|
|
3136
|
+
const planned = sourceWhere.kind === "expression" ? buildRead(sourceWhere.expression) : null;
|
|
3137
|
+
let sourceStream = planned !== null && (!interleaveFields || streamCanOrderBy(planned, interleaveFields)) ? planned : buildRead();
|
|
3138
|
+
const sourcePredicate = this._buildResolvedWherePredicate(sourceWhere);
|
|
2961
3139
|
if (sourcePredicate) sourceStream = sourceStream.filterWith(sourcePredicate);
|
|
2962
3140
|
return sourceStream;
|
|
2963
3141
|
}
|
|
3142
|
+
/**
|
|
3143
|
+
* The index a `flatMap` stage `where` can ride on top of the join keys.
|
|
3144
|
+
*
|
|
3145
|
+
* The join is only correct while `targetFields` stay pinned by equality, and
|
|
3146
|
+
* a Convex range builder pins fields in index-key order with no gaps, so the
|
|
3147
|
+
* candidates are exactly the declared indexes that lead with `targetFields`
|
|
3148
|
+
* and carry at least one more field. Those trailing fields are what the
|
|
3149
|
+
* stage `where` is compiled against — the compiler never sees the join keys,
|
|
3150
|
+
* because their values are per parent and the index choice has to be made
|
|
3151
|
+
* once for all of them.
|
|
3152
|
+
*
|
|
3153
|
+
* Returns null when nothing is gained, and the caller keeps the relation's
|
|
3154
|
+
* own index plus the post-filter. A multi-probe plan is refused: each probe
|
|
3155
|
+
* is its own range, and a union of ranges per parent is a different stream
|
|
3156
|
+
* shape than the one `flatMap` was handed as `mappedIndexFields`.
|
|
3157
|
+
*/
|
|
3158
|
+
_resolveFlatMapStageIndex(params) {
|
|
3159
|
+
const { where, targetTableConfig, targetFields } = params;
|
|
3160
|
+
if (where.kind !== "expression") return null;
|
|
3161
|
+
const candidates = getIndexes(targetTableConfig.table).filter((index) => index.fields.length > targetFields.length && targetFields.every((field, i) => index.fields[i] === field)).map((index) => ({
|
|
3162
|
+
indexName: index.name,
|
|
3163
|
+
indexFields: index.fields.slice(targetFields.length)
|
|
3164
|
+
}));
|
|
3165
|
+
if (candidates.length === 0) return null;
|
|
3166
|
+
const compiled = new WhereClauseCompiler(targetTableConfig.table.tableName, candidates).compile(where.expression);
|
|
3167
|
+
if (!compiled.selectedIndex || compiled.probeFilters.length > 0 || compiled.indexFilters.length === 0) return null;
|
|
3168
|
+
return {
|
|
3169
|
+
name: compiled.selectedIndex.indexName,
|
|
3170
|
+
filters: compiled.indexFilters
|
|
3171
|
+
};
|
|
3172
|
+
}
|
|
2964
3173
|
async _applyFlatMapStage(sourceStream, stage) {
|
|
2965
3174
|
const relationName = stage.relation;
|
|
2966
3175
|
const edge = this.edgeMetadata.find((e) => e.edgeName === relationName);
|
|
@@ -2976,24 +3185,36 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
2976
3185
|
rls: this.rls
|
|
2977
3186
|
});
|
|
2978
3187
|
const strict = this.tableConfig.strict !== false;
|
|
2979
|
-
const
|
|
3188
|
+
const useGetById = targetFields.length === 1 && targetFields[0] === "_id";
|
|
3189
|
+
const relationIndexName = useGetById ? "by_id" : findRelationIndex(targetTableConfig.table, targetFields, `${this.tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
|
|
3190
|
+
const stageWhere = this._resolvePipelineWhere(stage.where, targetTableConfig);
|
|
3191
|
+
const stageIndex = useGetById ? null : this._resolveFlatMapStageIndex({
|
|
3192
|
+
where: stageWhere,
|
|
3193
|
+
targetTableConfig,
|
|
3194
|
+
targetFields
|
|
3195
|
+
});
|
|
3196
|
+
const indexName = stageIndex?.name ?? relationIndexName;
|
|
2980
3197
|
const outerOrder = sourceStream.getOrder();
|
|
2981
3198
|
const schemaDefinition = this._getSchemaDefinitionOrThrow();
|
|
2982
3199
|
const innerIndexFields = getIndexFields(edge.targetTable, indexName ?? "by_creation_time" ?? "by_creation_time", schemaDefinition);
|
|
2983
3200
|
if (stage.limit !== void 0 && (!Number.isInteger(stage.limit) || stage.limit < 1)) throw new Error("pipeline.flatMap.limit must be a positive integer");
|
|
2984
3201
|
const mappedIndexFields = stage.limit === void 0 ? innerIndexFields : [...innerIndexFields, PIPELINE_LIMIT_ORDINAL_FIELD];
|
|
2985
|
-
const stageWherePredicate = this.
|
|
2986
|
-
this.
|
|
2987
|
-
where:
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
3202
|
+
const stageWherePredicate = this._buildResolvedWherePredicate(stageWhere);
|
|
3203
|
+
this._assertPipelineWhereIsAnchored({
|
|
3204
|
+
where: stageWhere,
|
|
3205
|
+
isAnchored: Boolean(indexName),
|
|
3206
|
+
context: `pipeline.flatMap(${relationName})`,
|
|
3207
|
+
remedy: `flatMap takes no index of its own — add an index on ${edge.targetTable}(${targetFields.join(", ")}) so the join is anchored, or use an object where().`
|
|
2991
3208
|
});
|
|
2992
3209
|
return sourceStream.flatMap(async (parent) => {
|
|
2993
3210
|
const values = sourceFields.map((field) => parent[field]);
|
|
2994
3211
|
if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, mappedIndexFields);
|
|
2995
3212
|
let inner = stream(this.db, schemaDefinition).query(edge.targetTable);
|
|
2996
|
-
if (indexName) inner = inner.withIndex(indexName, (q) =>
|
|
3213
|
+
if (indexName) inner = inner.withIndex(indexName, (q) => {
|
|
3214
|
+
let range = this._applyEqBounds(q, targetFields, values);
|
|
3215
|
+
for (const filter of stageIndex?.filters ?? []) range = this._applyFilterToQuery(range, filter, targetTableConfig);
|
|
3216
|
+
return range;
|
|
3217
|
+
});
|
|
2997
3218
|
inner = inner.order(outerOrder);
|
|
2998
3219
|
if (stageWherePredicate) inner = inner.filterWith(stageWherePredicate);
|
|
2999
3220
|
if (isRlsEnabled(targetTableConfig.table)) inner = inner.filterWith(async (row) => {
|
|
@@ -4095,12 +4316,14 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4095
4316
|
let streamQuery;
|
|
4096
4317
|
const unionSources = pipeline?.union ?? [];
|
|
4097
4318
|
if (unionSources.length > 0) {
|
|
4098
|
-
|
|
4099
|
-
if (
|
|
4100
|
-
|
|
4101
|
-
if (!
|
|
4102
|
-
|
|
4319
|
+
let interleaveFields;
|
|
4320
|
+
if (unionSources.length > 1) {
|
|
4321
|
+
const declared = pipeline?.interleaveBy;
|
|
4322
|
+
if (!declared || declared.length === 0) throw new Error("pipeline.interleaveBy is required when pipeline.union has multiple sources.");
|
|
4323
|
+
interleaveFields = declared.map((field) => this._normalizePublicFieldName(field));
|
|
4103
4324
|
}
|
|
4325
|
+
const streams = unionSources.map((source) => this._buildUnionSourceStream(source, fallbackOrder, interleaveFields));
|
|
4326
|
+
streamQuery = interleaveFields ? mergedStream(streams, interleaveFields) : streams[0];
|
|
4104
4327
|
} else {
|
|
4105
4328
|
const orderedIdList = isCursorPaginated && maxScan !== void 0 && idLookup?.kind === "in" ? queryConfig.order?.[0] : void 0;
|
|
4106
4329
|
if (orderedIdList) {
|
|
@@ -4385,41 +4608,53 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4385
4608
|
const selectedRows = await this._finalizeRows(rows);
|
|
4386
4609
|
return this._returnSelectedRows(selectedRows);
|
|
4387
4610
|
}
|
|
4611
|
+
const matchesPostFetchMembership = async (row) => {
|
|
4612
|
+
const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
|
|
4613
|
+
if (visibleRows.length === 0) return false;
|
|
4614
|
+
if (!whereFilter) return true;
|
|
4615
|
+
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4616
|
+
};
|
|
4388
4617
|
if (queryConfig.strategy === "multiProbe" && queryConfig.index && !isCursorPaginated) {
|
|
4389
4618
|
const probeOffset = config.offset ?? 0;
|
|
4390
4619
|
if (typeof probeOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
|
|
4391
4620
|
const probeLimit = this._resolveNonPaginatedLimit(config);
|
|
4392
4621
|
const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
|
|
4393
4622
|
const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
|
|
4394
|
-
const
|
|
4623
|
+
const probeHasRelationMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig);
|
|
4624
|
+
const probeHasPostFetchMembership = probeHasRelationMembership || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
|
|
4395
4625
|
const probeOrderDirection = orderPushdownDirection;
|
|
4396
|
-
const probeBound = probeLimit !== void 0 &&
|
|
4626
|
+
const probeBound = probeLimit !== void 0 && (postFetchOrders.length === 0 || probeOrderDirection !== null) ? probeOffset + probeLimit : void 0;
|
|
4627
|
+
const probeBoundedTake = probeBound !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership;
|
|
4628
|
+
const probeSchemaDefinition = this.schema[OrmSchemaDefinition];
|
|
4629
|
+
const probeStreamed = probeBound !== void 0 && !probeBoundedTake && !probeHasRelationMembership && !!probeSchemaDefinition;
|
|
4397
4630
|
const probeRows = await Promise.all(queryConfig.probeFilters.map(async (probeFilters) => {
|
|
4631
|
+
if (probeStreamed) return await stream(this.db, probeSchemaDefinition).query(this.tableConfig.name).withIndex(queryConfig.index.name, (q) => {
|
|
4632
|
+
let indexQuery = q;
|
|
4633
|
+
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
4634
|
+
return indexQuery;
|
|
4635
|
+
}).order(probeOrderDirection ?? "asc").filterWith(async (row) => {
|
|
4636
|
+
for (const filter of queryConfig.postFilters) if (!this._evaluatePostFetchFilter(row, filter)) return false;
|
|
4637
|
+
return await matchesPostFetchMembership(row);
|
|
4638
|
+
}).take(probeBound);
|
|
4398
4639
|
let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
|
|
4399
4640
|
let indexQuery = q;
|
|
4400
4641
|
for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
|
|
4401
4642
|
return indexQuery;
|
|
4402
4643
|
});
|
|
4403
|
-
if (
|
|
4644
|
+
if (probeBoundedTake && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
|
|
4404
4645
|
if (convexProbeFilters.length > 0) probeQuery = probeQuery.filter((q) => convexAnd(q, convexProbeFilters.map((filter) => this._toConvexExpression(filter)(q))));
|
|
4405
|
-
return
|
|
4646
|
+
return probeBoundedTake ? await probeQuery.take(probeBound) : await probeQuery.collect();
|
|
4406
4647
|
}));
|
|
4407
4648
|
let rows = Array.from(new Map(probeRows.flat().map((row) => [String(row._id), row])).values());
|
|
4408
4649
|
if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
|
|
4409
4650
|
rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
|
|
4410
|
-
if (whereFilter) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4651
|
+
if (whereFilter && !probeStreamed) rows = await this._applyRelationsFilterToRows(rows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with);
|
|
4411
4652
|
if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
|
|
4412
4653
|
if (probeOffset > 0) rows = rows.slice(probeOffset);
|
|
4413
4654
|
if (probeLimit !== void 0) rows = rows.slice(0, probeLimit);
|
|
4414
4655
|
const selectedRows = await this._finalizeRows(rows);
|
|
4415
4656
|
return this._returnSelectedRows(selectedRows);
|
|
4416
4657
|
}
|
|
4417
|
-
const matchesPostFetchMembership = async (row) => {
|
|
4418
|
-
const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
|
|
4419
|
-
if (visibleRows.length === 0) return false;
|
|
4420
|
-
if (!whereFilter) return true;
|
|
4421
|
-
return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
|
|
4422
|
-
};
|
|
4423
4658
|
if (isCursorPaginated) {
|
|
4424
4659
|
const isMultiProbePlan = queryConfig.strategy === "multiProbe" && !!queryConfig.index;
|
|
4425
4660
|
const isScanFallbackPlan = !queryConfig.index && queryConfig.postFilters.length > 0;
|
|
@@ -4569,11 +4804,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4569
4804
|
*/
|
|
4570
4805
|
_toConvexQuery(whereExpressionOverride, configuredIndex) {
|
|
4571
4806
|
const config = this.config;
|
|
4572
|
-
const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
|
|
4573
|
-
indexName: index.name,
|
|
4574
|
-
indexFields: index.fields
|
|
4575
|
-
}));
|
|
4576
|
-
const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
|
|
4577
4807
|
let orderSpecs = [];
|
|
4578
4808
|
if (config.orderBy) {
|
|
4579
4809
|
const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
|
|
@@ -4584,7 +4814,29 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4584
4814
|
}
|
|
4585
4815
|
let whereExpression = whereExpressionOverride;
|
|
4586
4816
|
if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
|
|
4587
|
-
|
|
4817
|
+
return this._compileQueryPlan({
|
|
4818
|
+
whereExpression,
|
|
4819
|
+
configuredIndex,
|
|
4820
|
+
orderSpecs
|
|
4821
|
+
});
|
|
4822
|
+
}
|
|
4823
|
+
/**
|
|
4824
|
+
* The read plan for one `where` on this table.
|
|
4825
|
+
*
|
|
4826
|
+
* Split out of `_toConvexQuery` because the chain-level `where` is not the
|
|
4827
|
+
* only one that has to be index-lowered: a `pipeline.union` source carries
|
|
4828
|
+
* its own `where` and its own pinned index, and compiles against the same
|
|
4829
|
+
* table with a different ordering contract (`interleaveBy`, not `orderBy`).
|
|
4830
|
+
* Both go through here so a plain-object `where` is planned the same way
|
|
4831
|
+
* wherever it is written.
|
|
4832
|
+
*/
|
|
4833
|
+
_compileQueryPlan(params) {
|
|
4834
|
+
const { whereExpression, configuredIndex, orderSpecs } = params;
|
|
4835
|
+
const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
|
|
4836
|
+
indexName: index.name,
|
|
4837
|
+
indexFields: index.fields
|
|
4838
|
+
}));
|
|
4839
|
+
const planned = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes).compile(whereExpression, { orderFields: orderSpecs.map((spec) => spec.field) });
|
|
4588
4840
|
const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
|
|
4589
4841
|
const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
|
|
4590
4842
|
strategy: "none",
|
|
@@ -4666,11 +4918,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4666
4918
|
* Apply a single filter expression to a Convex query builder
|
|
4667
4919
|
* Used for index filters (eq operations)
|
|
4668
4920
|
*/
|
|
4669
|
-
_applyFilterToQuery(query, filter) {
|
|
4921
|
+
_applyFilterToQuery(query, filter, tableConfig = this.tableConfig) {
|
|
4670
4922
|
if (filter.type === "binary") {
|
|
4671
4923
|
const [field, value] = filter.operands;
|
|
4672
4924
|
if (!isFieldReference(field)) return query;
|
|
4673
|
-
const normalizedValue = this._normalizeComparableValue(field.fieldName, value);
|
|
4925
|
+
const normalizedValue = this._normalizeComparableValue(field.fieldName, value, tableConfig);
|
|
4674
4926
|
switch (filter.operator) {
|
|
4675
4927
|
case "eq": return query.eq(field.fieldName, normalizedValue);
|
|
4676
4928
|
case "gt": return query.gt(field.fieldName, normalizedValue);
|
|
@@ -4700,18 +4952,75 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4700
4952
|
if (!this._allEdges) return [];
|
|
4701
4953
|
return this._allEdges.filter((edge) => edge.sourceTable === tableName);
|
|
4702
4954
|
}
|
|
4955
|
+
/**
|
|
4956
|
+
* A memo entry is a snapshot, so every caller has to get its own document.
|
|
4957
|
+
*
|
|
4958
|
+
* Relation loaders write nested `with` results and `extras` straight onto the
|
|
4959
|
+
* target they were handed, and `hydrateDateFieldsForRead` copies every own key
|
|
4960
|
+
* it finds. Two loads that share one entry would therefore publish each
|
|
4961
|
+
* other's fields — a relation asked for as `true` coming back carrying a
|
|
4962
|
+
* nested relation only the `where` requested. Those writes are all top-level,
|
|
4963
|
+
* so a shallow copy is exactly as much isolation as they need.
|
|
4964
|
+
*/
|
|
4965
|
+
_ownedCopy(doc) {
|
|
4966
|
+
return doc === null || doc === void 0 ? null : { ...doc };
|
|
4967
|
+
}
|
|
4703
4968
|
async _getById(tableName, id) {
|
|
4704
4969
|
if (id === null || id === void 0) return null;
|
|
4705
4970
|
const normalizedId = this.db.normalizeId(tableName, id);
|
|
4706
4971
|
if (normalizedId === null) return null;
|
|
4707
4972
|
const existing = this._documentByNormalizedId.get(normalizedId);
|
|
4708
|
-
if (existing) return await existing;
|
|
4973
|
+
if (existing) return this._ownedCopy(await existing);
|
|
4709
4974
|
const pending = Promise.resolve(this.db.get(normalizedId)).catch((error) => {
|
|
4710
4975
|
this._documentByNormalizedId.delete(normalizedId);
|
|
4711
4976
|
throw error;
|
|
4712
4977
|
});
|
|
4713
4978
|
this._documentByNormalizedId.set(normalizedId, pending);
|
|
4714
|
-
return await pending;
|
|
4979
|
+
return this._ownedCopy(await pending);
|
|
4980
|
+
}
|
|
4981
|
+
/**
|
|
4982
|
+
* Identity of one `_firstByFields` read, or null when the join values cannot
|
|
4983
|
+
* be encoded losslessly.
|
|
4984
|
+
*
|
|
4985
|
+
* `JSON.stringify` alone is not safe here. It renders every `ArrayBuffer` as
|
|
4986
|
+
* `{}` and `NaN`/`Infinity`/`-Infinity` as `null`, so two distinct join values
|
|
4987
|
+
* would share one entry, and it throws outright on `int64`. The relation
|
|
4988
|
+
* loader's own per-batch key map cannot catch that, because the residual
|
|
4989
|
+
* relation `where` hands it one row at a time — a map of one never compares
|
|
4990
|
+
* two values. This key is the only thing that tells them apart.
|
|
4991
|
+
*
|
|
4992
|
+
* `convexToJson` is the same wire encoding the client uses for query keys, so
|
|
4993
|
+
* every Convex value round-trips distinctly. Anything it rejects is not a
|
|
4994
|
+
* Convex value and simply goes unmemoized.
|
|
4995
|
+
*/
|
|
4996
|
+
_firstByFieldsMemoKey(tableName, fields, values, indexName) {
|
|
4997
|
+
try {
|
|
4998
|
+
return JSON.stringify([
|
|
4999
|
+
tableName,
|
|
5000
|
+
indexName,
|
|
5001
|
+
fields,
|
|
5002
|
+
values.map((value) => convexToJson(value))
|
|
5003
|
+
]);
|
|
5004
|
+
} catch {
|
|
5005
|
+
return null;
|
|
5006
|
+
}
|
|
5007
|
+
}
|
|
5008
|
+
/**
|
|
5009
|
+
* `_getById` for a relation joined on something other than the primary id:
|
|
5010
|
+
* resolve one target document by an eq-pinned key, memoized per execution.
|
|
5011
|
+
*/
|
|
5012
|
+
async _firstByFields(tableName, fields, values, indexName) {
|
|
5013
|
+
const read = () => this._queryByFields(this.db.query(tableName), fields, values, indexName).first();
|
|
5014
|
+
const key = this._firstByFieldsMemoKey(tableName, fields, values, indexName);
|
|
5015
|
+
if (key === null) return await read();
|
|
5016
|
+
const existing = this._firstDocumentByFieldKey.get(key);
|
|
5017
|
+
if (existing) return this._ownedCopy(await existing);
|
|
5018
|
+
const pending = Promise.resolve(read()).catch((error) => {
|
|
5019
|
+
this._firstDocumentByFieldKey.delete(key);
|
|
5020
|
+
throw error;
|
|
5021
|
+
});
|
|
5022
|
+
this._firstDocumentByFieldKey.set(key, pending);
|
|
5023
|
+
return this._ownedCopy(await pending);
|
|
4715
5024
|
}
|
|
4716
5025
|
_getRelationConcurrency() {
|
|
4717
5026
|
const value = this.relationLoading?.concurrency;
|
|
@@ -4863,7 +5172,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4863
5172
|
const resolveTargetMatch = async (values) => {
|
|
4864
5173
|
let target = null;
|
|
4865
5174
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
4866
|
-
else target = await this.
|
|
5175
|
+
else target = await this._firstByFields(edge.targetTable, targetFields, values, targetIndexName);
|
|
4867
5176
|
if (!target) return false;
|
|
4868
5177
|
return this._evaluateTableFilter(target, targetTableConfig, whereRecord);
|
|
4869
5178
|
};
|
|
@@ -4965,7 +5274,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
4965
5274
|
const fetched = await this._mapWithConcurrency(entries, async ([key, values]) => {
|
|
4966
5275
|
let target = null;
|
|
4967
5276
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
4968
|
-
else target = await this.
|
|
5277
|
+
else target = await this._firstByFields(edge.targetTable, targetFields, values, indexName);
|
|
4969
5278
|
return {
|
|
4970
5279
|
key,
|
|
4971
5280
|
target
|
|
@@ -5217,7 +5526,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
|
|
|
5217
5526
|
return await this._mapWithConcurrency(keyEntries, async ([key, values]) => {
|
|
5218
5527
|
let target = null;
|
|
5219
5528
|
if (useGetById) target = await this._getById(edge.targetTable, values[0]);
|
|
5220
|
-
else target = await this.
|
|
5529
|
+
else target = await this._firstByFields(edge.targetTable, targetFields, values, indexName);
|
|
5221
5530
|
return {
|
|
5222
5531
|
key,
|
|
5223
5532
|
target
|
|
@@ -5566,6 +5875,9 @@ var ConvexDeleteBuilder = class extends QueryPromise {
|
|
|
5566
5875
|
});
|
|
5567
5876
|
}
|
|
5568
5877
|
async execute(...args) {
|
|
5878
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
|
|
5879
|
+
}
|
|
5880
|
+
async _runStatement(...args) {
|
|
5569
5881
|
const config = args[0];
|
|
5570
5882
|
const tableName = getTableName(this.table);
|
|
5571
5883
|
const ormContext = getOrmContext(this.db);
|
|
@@ -5857,6 +6169,9 @@ var ConvexInsertBuilder = class extends QueryPromise {
|
|
|
5857
6169
|
return this;
|
|
5858
6170
|
}
|
|
5859
6171
|
async execute() {
|
|
6172
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement()));
|
|
6173
|
+
}
|
|
6174
|
+
async _runStatement() {
|
|
5860
6175
|
if (this.valuesList.length === 0) throw new Error("values() must be called before execute()");
|
|
5861
6176
|
const ormContext = getOrmContext(this.db);
|
|
5862
6177
|
const tableName = getTableName(this.table);
|
|
@@ -6109,6 +6424,7 @@ function normalizeOrmTriggers(triggers) {
|
|
|
6109
6424
|
//#region src/orm/lifecycle.ts
|
|
6110
6425
|
const ORMLIFECYCLE_WRAPPED_DB = Symbol.for("kitcn:OrmLifecycleWrappedDB");
|
|
6111
6426
|
const ORMLIFECYCLE_INNER_DB = Symbol.for("kitcn:OrmLifecycleInnerDB");
|
|
6427
|
+
const wrapUserHook = (hook) => hook ? (...args) => runInOrmUserCallback(() => hook(...args)) : void 0;
|
|
6112
6428
|
const isWriterDb = (db) => typeof db.insert === "function" && typeof db.patch === "function" && typeof db.delete === "function";
|
|
6113
6429
|
const isLifecycleWrappedDb = (db) => db[ORMLIFECYCLE_WRAPPED_DB] === true;
|
|
6114
6430
|
const markLifecycleWrappedDb = (db) => {
|
|
@@ -6292,6 +6608,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6292
6608
|
const tableHooks = hooksByTable.get(tableName);
|
|
6293
6609
|
if (!tableHooks) return innerDb.patch(tableName, id, value);
|
|
6294
6610
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6611
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6295
6612
|
const needsDocuments = Boolean(tableHooks.update?.after || tableHooks.change);
|
|
6296
6613
|
const oldDoc = needsDocuments ? await innerDb.get(tableName, id) : null;
|
|
6297
6614
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
@@ -6337,6 +6654,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6337
6654
|
const tableHooks = hooksByTable.get(tableName);
|
|
6338
6655
|
if (!tableHooks) return innerDb.replace(tableName, id, value);
|
|
6339
6656
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6657
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6340
6658
|
const oldDoc = Boolean(tableHooks.update?.after || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
6341
6659
|
const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
|
|
6342
6660
|
await innerDb.replace(tableName, id, updatePayload);
|
|
@@ -6372,6 +6690,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6372
6690
|
const tableHooks = hooksByTable.get(tableName);
|
|
6373
6691
|
if (!tableHooks) return innerDb.delete(tableName, id);
|
|
6374
6692
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6693
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6375
6694
|
const oldDoc = Boolean(tableHooks.delete || tableHooks.change) ? await innerDb.get(tableName, id) : null;
|
|
6376
6695
|
if (!oldDoc) {
|
|
6377
6696
|
await innerDb.delete(tableName, id);
|
|
@@ -6407,6 +6726,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
|
|
|
6407
6726
|
const tableHooks = hooksByTable.get(table);
|
|
6408
6727
|
if (!tableHooks) return innerDb.insert(table, value);
|
|
6409
6728
|
return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
|
|
6729
|
+
await tableHooks.writeBarrier?.(innerDb);
|
|
6410
6730
|
const insertPayload = await mergeBeforeData(table, "create", tableHooks.create?.before, value, hookCtx);
|
|
6411
6731
|
const id = await innerDb.insert(table, insertPayload);
|
|
6412
6732
|
const newDoc = Boolean(tableHooks.create?.after || tableHooks.change) ? await innerDb.get(table, id) : null;
|
|
@@ -6469,53 +6789,57 @@ function createOrmDbLifecycle(schema, triggerDefinitions, capabilities) {
|
|
|
6469
6789
|
for (const [triggerKey, hooks] of normalizedTriggers.entries()) {
|
|
6470
6790
|
const tableName = tableNameBySchemaKey.get(triggerKey) ?? (tableNames.has(triggerKey) ? triggerKey : void 0);
|
|
6471
6791
|
if (!tableName) throw new Error(`Unknown trigger table '${triggerKey}'. Export triggers with defineTriggers(relations, { ... }) using keys from your relations export.`);
|
|
6472
|
-
tableHooks.set(tableName,
|
|
6792
|
+
tableHooks.set(tableName, {
|
|
6793
|
+
create: hooks.create && {
|
|
6794
|
+
before: wrapUserHook(hooks.create.before),
|
|
6795
|
+
after: wrapUserHook(hooks.create.after)
|
|
6796
|
+
},
|
|
6797
|
+
update: hooks.update && {
|
|
6798
|
+
before: wrapUserHook(hooks.update.before),
|
|
6799
|
+
after: wrapUserHook(hooks.update.after)
|
|
6800
|
+
},
|
|
6801
|
+
delete: hooks.delete && {
|
|
6802
|
+
before: wrapUserHook(hooks.delete.before),
|
|
6803
|
+
after: wrapUserHook(hooks.delete.after)
|
|
6804
|
+
},
|
|
6805
|
+
change: wrapUserHook(hooks.change)
|
|
6806
|
+
});
|
|
6473
6807
|
}
|
|
6474
6808
|
for (const tableConfig of Object.values(schema)) {
|
|
6475
6809
|
if (!tableConfig?.table || !tableConfig?.name) continue;
|
|
6476
6810
|
const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
|
|
6477
6811
|
const rankIndexes = getRankIndexDefinitions(tableConfig);
|
|
6478
6812
|
if (aggregateIndexes.length === 0 && rankIndexes.length === 0) continue;
|
|
6479
|
-
const
|
|
6480
|
-
const
|
|
6813
|
+
const tableName = tableConfig.name;
|
|
6814
|
+
const aggregate = requireAggregateCapability(capabilities, `Table '${tableName}' declares an aggregateIndex/rankIndex, which`);
|
|
6815
|
+
const existing = tableHooks.get(tableName) ?? {};
|
|
6481
6816
|
const existingChange = existing.change;
|
|
6817
|
+
const existingBarrier = existing.writeBarrier;
|
|
6482
6818
|
const metricIndexNames = aggregateIndexes.map((entry) => entry.name);
|
|
6483
6819
|
const rankIndexNames = rankIndexes.map((entry) => entry.name);
|
|
6484
|
-
|
|
6485
|
-
await aggregate.assertAggregateIndexesWritable(ctx.db, tableConfig.name, metricIndexNames, rankIndexNames);
|
|
6486
|
-
return before?.(data, ctx);
|
|
6487
|
-
};
|
|
6488
|
-
tableHooks.set(tableConfig.name, {
|
|
6820
|
+
tableHooks.set(tableName, {
|
|
6489
6821
|
...existing,
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
},
|
|
6494
|
-
update: {
|
|
6495
|
-
...existing.update,
|
|
6496
|
-
before: prependWriteBarrier(existing.update?.before)
|
|
6497
|
-
},
|
|
6498
|
-
delete: {
|
|
6499
|
-
...existing.delete,
|
|
6500
|
-
before: prependWriteBarrier(existing.delete?.before)
|
|
6822
|
+
writeBarrier: async (db) => {
|
|
6823
|
+
await existingBarrier?.(db);
|
|
6824
|
+
await aggregate.assertAggregateIndexesWritable(db, tableName, metricIndexNames, rankIndexNames);
|
|
6501
6825
|
},
|
|
6502
6826
|
change: async (change, ctx) => {
|
|
6503
6827
|
if (change.operation === "delete") {
|
|
6504
|
-
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db,
|
|
6828
|
+
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
|
|
6505
6829
|
operation: "delete",
|
|
6506
6830
|
id: change.id
|
|
6507
6831
|
});
|
|
6508
|
-
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db,
|
|
6832
|
+
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
|
|
6509
6833
|
operation: "delete",
|
|
6510
6834
|
id: change.id
|
|
6511
6835
|
});
|
|
6512
6836
|
} else {
|
|
6513
|
-
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db,
|
|
6837
|
+
if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
|
|
6514
6838
|
operation: change.operation,
|
|
6515
6839
|
id: change.id,
|
|
6516
6840
|
newDoc: change.newDoc
|
|
6517
6841
|
});
|
|
6518
|
-
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db,
|
|
6842
|
+
if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
|
|
6519
6843
|
operation: change.operation,
|
|
6520
6844
|
id: change.id,
|
|
6521
6845
|
newDoc: change.newDoc
|
|
@@ -6786,6 +7110,9 @@ var ConvexUpdateBuilder = class extends QueryPromise {
|
|
|
6786
7110
|
});
|
|
6787
7111
|
}
|
|
6788
7112
|
async execute(...args) {
|
|
7113
|
+
return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
|
|
7114
|
+
}
|
|
7115
|
+
async _runStatement(...args) {
|
|
6789
7116
|
if (!this.setValues) throw new Error("set() must be called before execute()");
|
|
6790
7117
|
if (Object.keys(this.setValues).length === 0) {
|
|
6791
7118
|
if (this.paginateConfig !== void 0) {
|
|
@@ -7133,7 +7460,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
|
|
|
7133
7460
|
defaults,
|
|
7134
7461
|
resolvedDefaults
|
|
7135
7462
|
};
|
|
7136
|
-
const baseDb = Object.assign(Object.create(db), { [OrmContext]: ormContext });
|
|
7463
|
+
const baseDb = markOrmTransactionAnchor(Object.assign(Object.create(db), { [OrmContext]: ormContext }), db);
|
|
7137
7464
|
const query = {};
|
|
7138
7465
|
const edgesBySourceTable = getEdgesBySourceTable(edgeMetadata);
|
|
7139
7466
|
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);
|