kitcn 0.32.2 → 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/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 indexKeyWithinBounds, c as streamIndexRange, i as getIndexFields, l as isUnsetToken, n as EmptyStream, o as mergedStream, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-D4z1CnDH.js";
8
- import { $ as patchReferencingRows, A as enforcePolymorphicWrite, B as getForeignKeys, C as collectPrimaryIdLookupRows, D as encodeUndefinedDeep, E as deserializeFilterExpression, F as evaluateCheckConstraintTriState, G as getTableColumns$2, H as getMutationCollectionLimits, I as evaluateFilter, J as getUniqueIndexes, K as getTableDeleteConfig, L as extractPrimaryIdLookup, M as ensureDefaultColumns, N as ensureNonNullValues, O as enforceCheckConstraints, P as ensureNullableColumns, Q as normalizeTemporalComparableValue, R as getChecks, S as collectMutationRowsBounded, T as decodeUndefinedDeep, U as getMutationExecutionMode, V as getMutationAsyncDelayMs, W as getOrmContext, X as hydrateDateFieldsForRead, Y as hardDeleteRow, Z as normalizeDateFieldsForWrite, _ as applyDefaults, at as splitReturningSelection, b as buildForeignKeyGraph, c as getAggregateIndexDefinitions, ct as toConvexFilter, d as COUNT_ERROR, dt as markLifecycleHookedTables, et as resolveOrmRuntimeDefaults, f as createAggregateError, ft as compileConvexFilter, g as ensureCountAllowedForRls, h as ensureAggregateAllowedForRls, ht as mapWithConcurrency, it as softDeleteRow, j as enforceUniqueIndexes, k as enforceForeignKeys, l as getRankIndexDefinitions, lt as unsetFieldsOf, mt as isConvexEnforceableFilter, nt as selectReturningRowWithHydration, o as aggregateExtension, ot as stripUnsetFields, p as createCountError, pt as convexAnd, q as getTableName, rt as serializeFilterExpression, st as takeRowsWithinByteBudget, tt as returningSelectionReadsCreationTime, u as AGGREGATE_ERROR, ut as hasLifecycleHooks, v as applyIncomingForeignKeyActionsOnDelete, w as createForeignKeyProbeMemo, x as canUsePrimaryIdLookupCursor, y as applyIncomingForeignKeyActionsOnUpdate, z as getColumnName$1 } from "../schema-Bh7AmJwY.js";
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 opening. Each probe becomes its own index range, and
707
- * a streamed union holds every one of them open at once, so past this width the
708
- * fan-out costs more than the single scan it would replace.
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
- * Owns both ends of that decision: whether to promote an `in` inside an AND to
711
- * a union here, and whether the executor may read a compiled union as a merged
712
- * stream instead of a bounded scan.
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 || probePlan.probeFilters.length > MAX_INDEX_UNION_PROBES) continue;
793
+ if (!probePlan) continue;
793
794
  return {
794
795
  ...probePlan,
795
796
  postFilters: [expression]
@@ -2713,34 +2714,70 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2713
2714
  for (let i = 0; i < fields.length; i += 1) builder = builder.eq(fields[i], values[i]);
2714
2715
  return builder;
2715
2716
  }
2716
- _buildTableFilterPredicate(where, tableConfig) {
2717
- if (!where) return null;
2718
- if (typeof where === "function") {
2719
- const whereResult = this._resolveWhereCallbackExpression(where, tableConfig, { context: "pipeline" });
2720
- if (!whereResult) return null;
2721
- if (this._isPredicateWhereClause(whereResult)) return async (row) => await whereResult.predicate(row);
2722
- return async (row) => this._evaluatePostFetchFilter(row, whereResult);
2723
- }
2724
- return async (row) => {
2725
- const expression = this._buildFilterExpression(where, tableConfig);
2726
- if (!expression) return true;
2727
- return this._evaluatePostFetchFilter(row, expression);
2728
- };
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);
2729
2734
  }
2730
- _assertWhereIndexRequirement(options) {
2731
- const { where, tableConfig, hasConfiguredIndex, context } = options;
2732
- if (!where) return;
2733
- let whereExpression;
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" };
2734
2746
  if (typeof where === "function") {
2735
2747
  const result = this._resolveWhereCallbackExpression(where, tableConfig, { context: "pipeline" });
2736
- if (!result) return;
2737
- if (this._isPredicateWhereClause(result)) {
2738
- if (!hasConfiguredIndex) throw new Error(`${context} where uses predicate(...) and requires .withIndex(...).`);
2739
- return;
2740
- }
2741
- whereExpression = result;
2742
- } else whereExpression = this._buildFilterExpression(where, tableConfig);
2743
- if (!whereExpression) return;
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}`);
2744
2781
  }
2745
2782
  _isFilterExpressionNode(value) {
2746
2783
  return typeof value === "object" && value !== null && "accept" in value && typeof value.accept === "function";
@@ -2837,16 +2874,23 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2837
2874
  /**
2838
2875
  * The compiled index union, as one ordered stream.
2839
2876
  *
2840
- * Each probe is its own index range, so the union is only usable where the
2841
- * merged order is the order the read has to produce. `mergedStream` orders by
2842
- * a *suffix* of the index key, and a suffix may only drop key components the
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
2843
2880
  * probes all pin to a single value — so the requested field has to sit inside
2844
2881
  * the pinned run or immediately after it. Returns null when it does not, and
2845
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.
2846
2890
  */
2847
2891
  _buildProbeUnionStream(params) {
2848
2892
  const { probeFilters } = params;
2849
- if (probeFilters.length === 0 || probeFilters.length > MAX_INDEX_UNION_PROBES) return null;
2893
+ if (probeFilters.length === 0) return null;
2850
2894
  const streamIndexFields = getIndexFields(this.tableConfig.name, params.indexName, params.schemaDefinition);
2851
2895
  const pinned = this._indexEqPrefixCount({ probeFilters });
2852
2896
  let mergeOffset = -1;
@@ -2855,11 +2899,102 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2855
2899
  break;
2856
2900
  }
2857
2901
  if (mergeOffset === -1) return null;
2858
- return mergedStream(probeFilters.map((filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
2902
+ const probeStream = (filters) => stream(this.db, params.schemaDefinition).query(this.tableConfig.name).withIndex(params.indexName, (q) => {
2859
2903
  let indexQuery = q;
2860
2904
  for (const filter of filters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
2861
2905
  return indexQuery;
2862
- }).order(params.order)), streamIndexFields.slice(mergeOffset));
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();
2863
2998
  }
2864
2999
  /**
2865
3000
  * The read the compiled plan describes, as a stream, with nothing filtered
@@ -2886,7 +3021,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2886
3021
  indexName: probeIndex.name,
2887
3022
  probeFilters: queryConfig.probeFilters,
2888
3023
  order,
2889
- orderField: primaryOrder?.field ?? probeIndex.fields[0]
3024
+ orderField: params.probeOrderField ?? primaryOrder?.field ?? probeIndex.fields[0]
2890
3025
  });
2891
3026
  if (probeUnion) return {
2892
3027
  stream: probeUnion,
@@ -2959,22 +3094,82 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2959
3094
  });
2960
3095
  return streamQuery;
2961
3096
  }
2962
- _buildUnionSourceStream(source, fallbackOrder) {
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) {
2963
3115
  const sourceIndex = source.index ?? this.configuredIndex;
2964
- this._assertWhereIndexRequirement({
2965
- where: source.where,
2966
- tableConfig: this.tableConfig,
2967
- hasConfiguredIndex: Boolean(sourceIndex?.name),
2968
- 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."
2969
3122
  });
2970
3123
  const schemaDefinition = this._getSchemaDefinitionOrThrow();
2971
- let sourceStream = stream(this.db, schemaDefinition).query(this.tableConfig.name);
2972
- if (sourceIndex?.name) sourceStream = sourceStream.withIndex(sourceIndex.name, sourceIndex.range ? sourceIndex.range : (q) => q);
2973
- sourceStream = sourceStream.order(fallbackOrder);
2974
- const sourcePredicate = this._buildTableFilterPredicate(source.where, this.tableConfig);
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);
2975
3139
  if (sourcePredicate) sourceStream = sourceStream.filterWith(sourcePredicate);
2976
3140
  return sourceStream;
2977
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
+ }
2978
3173
  async _applyFlatMapStage(sourceStream, stage) {
2979
3174
  const relationName = stage.relation;
2980
3175
  const edge = this.edgeMetadata.find((e) => e.edgeName === relationName);
@@ -2990,24 +3185,36 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
2990
3185
  rls: this.rls
2991
3186
  });
2992
3187
  const strict = this.tableConfig.strict !== false;
2993
- const indexName = targetFields.length === 1 && targetFields[0] === "_id" ? "by_id" : findRelationIndex(targetTableConfig.table, targetFields, `${this.tableConfig.name}.${relationName}`, edge.targetTable, strict, this.allowFullScan);
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;
2994
3197
  const outerOrder = sourceStream.getOrder();
2995
3198
  const schemaDefinition = this._getSchemaDefinitionOrThrow();
2996
3199
  const innerIndexFields = getIndexFields(edge.targetTable, indexName ?? "by_creation_time" ?? "by_creation_time", schemaDefinition);
2997
3200
  if (stage.limit !== void 0 && (!Number.isInteger(stage.limit) || stage.limit < 1)) throw new Error("pipeline.flatMap.limit must be a positive integer");
2998
3201
  const mappedIndexFields = stage.limit === void 0 ? innerIndexFields : [...innerIndexFields, PIPELINE_LIMIT_ORDINAL_FIELD];
2999
- const stageWherePredicate = this._buildTableFilterPredicate(stage.where, targetTableConfig);
3000
- this._assertWhereIndexRequirement({
3001
- where: stage.where,
3002
- tableConfig: targetTableConfig,
3003
- hasConfiguredIndex: Boolean(indexName),
3004
- context: `pipeline.flatMap(${relationName})`
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().`
3005
3208
  });
3006
3209
  return sourceStream.flatMap(async (parent) => {
3007
3210
  const values = sourceFields.map((field) => parent[field]);
3008
3211
  if (values.some((value) => value === null || value === void 0)) return new EmptyStream(outerOrder, mappedIndexFields);
3009
3212
  let inner = stream(this.db, schemaDefinition).query(edge.targetTable);
3010
- if (indexName) inner = inner.withIndex(indexName, (q) => this._applyEqBounds(q, targetFields, values));
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
+ });
3011
3218
  inner = inner.order(outerOrder);
3012
3219
  if (stageWherePredicate) inner = inner.filterWith(stageWherePredicate);
3013
3220
  if (isRlsEnabled(targetTableConfig.table)) inner = inner.filterWith(async (row) => {
@@ -4109,12 +4316,14 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
4109
4316
  let streamQuery;
4110
4317
  const unionSources = pipeline?.union ?? [];
4111
4318
  if (unionSources.length > 0) {
4112
- const streams = unionSources.map((source) => this._buildUnionSourceStream(source, fallbackOrder));
4113
- if (streams.length === 1) streamQuery = streams[0];
4114
- else {
4115
- if (!pipeline?.interleaveBy || pipeline.interleaveBy.length === 0) throw new Error("pipeline.interleaveBy is required when pipeline.union has multiple sources.");
4116
- streamQuery = mergedStream(streams, pipeline.interleaveBy.map((field) => this._normalizePublicFieldName(field)));
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));
4117
4324
  }
4325
+ const streams = unionSources.map((source) => this._buildUnionSourceStream(source, fallbackOrder, interleaveFields));
4326
+ streamQuery = interleaveFields ? mergedStream(streams, interleaveFields) : streams[0];
4118
4327
  } else {
4119
4328
  const orderedIdList = isCursorPaginated && maxScan !== void 0 && idLookup?.kind === "in" ? queryConfig.order?.[0] : void 0;
4120
4329
  if (orderedIdList) {
@@ -4399,41 +4608,53 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
4399
4608
  const selectedRows = await this._finalizeRows(rows);
4400
4609
  return this._returnSelectedRows(selectedRows);
4401
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
+ };
4402
4617
  if (queryConfig.strategy === "multiProbe" && queryConfig.index && !isCursorPaginated) {
4403
4618
  const probeOffset = config.offset ?? 0;
4404
4619
  if (typeof probeOffset !== "number") throw new Error("Only numeric offset is supported in kitcn ORM.");
4405
4620
  const probeLimit = this._resolveNonPaginatedLimit(config);
4406
4621
  const convexProbeFilters = queryConfig.postFilters.filter((filter) => this._isConvexEnforceableFilter(filter));
4407
4622
  const probeHasResidualFilter = convexProbeFilters.length !== queryConfig.postFilters.length;
4408
- const probeHasPostFetchMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig) || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
4623
+ const probeHasRelationMembership = this._hasSearchDisallowedRelationFilter(whereFilter, this.tableConfig);
4624
+ const probeHasPostFetchMembership = probeHasRelationMembership || this.rls?.mode !== "skip" && isRlsEnabled(this.tableConfig.table);
4409
4625
  const probeOrderDirection = orderPushdownDirection;
4410
- const probeBound = probeLimit !== void 0 && !probeHasResidualFilter && !probeHasPostFetchMembership && (postFetchOrders.length === 0 || probeOrderDirection !== null) ? probeOffset + 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;
4411
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);
4412
4639
  let probeQuery = this.db.query(queryConfig.table).withIndex(queryConfig.index.name, (q) => {
4413
4640
  let indexQuery = q;
4414
4641
  for (const filter of probeFilters) indexQuery = this._applyFilterToQuery(indexQuery, filter);
4415
4642
  return indexQuery;
4416
4643
  });
4417
- if (probeBound !== void 0 && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
4644
+ if (probeBoundedTake && probeOrderDirection) probeQuery = probeQuery.order(probeOrderDirection);
4418
4645
  if (convexProbeFilters.length > 0) probeQuery = probeQuery.filter((q) => convexAnd(q, convexProbeFilters.map((filter) => this._toConvexExpression(filter)(q))));
4419
- return probeBound === void 0 ? await probeQuery.collect() : await probeQuery.take(probeBound);
4646
+ return probeBoundedTake ? await probeQuery.take(probeBound) : await probeQuery.collect();
4420
4647
  }));
4421
4648
  let rows = Array.from(new Map(probeRows.flat().map((row) => [String(row._id), row])).values());
4422
4649
  if (queryConfig.postFilters.length > 0) rows = rows.filter((row) => queryConfig.postFilters.every((filter) => this._evaluatePostFetchFilter(row, filter)));
4423
4650
  rows = await this._applyRlsSelectFilter(rows, this.tableConfig);
4424
- 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);
4425
4652
  if (postFetchOrders.length > 0) rows = rows.sort((a, b) => this._compareByOrderSpecs(a, b, postFetchOrders));
4426
4653
  if (probeOffset > 0) rows = rows.slice(probeOffset);
4427
4654
  if (probeLimit !== void 0) rows = rows.slice(0, probeLimit);
4428
4655
  const selectedRows = await this._finalizeRows(rows);
4429
4656
  return this._returnSelectedRows(selectedRows);
4430
4657
  }
4431
- const matchesPostFetchMembership = async (row) => {
4432
- const visibleRows = await this._applyRlsSelectFilter([row], this.tableConfig);
4433
- if (visibleRows.length === 0) return false;
4434
- if (!whereFilter) return true;
4435
- return (await this._applyRelationsFilterToRows(visibleRows, this.tableConfig, whereFilter, this.edgeMetadata, 0, MAX_RELATION_DEPTH, this.config.with)).length > 0;
4436
- };
4437
4658
  if (isCursorPaginated) {
4438
4659
  const isMultiProbePlan = queryConfig.strategy === "multiProbe" && !!queryConfig.index;
4439
4660
  const isScanFallbackPlan = !queryConfig.index && queryConfig.postFilters.length > 0;
@@ -4583,11 +4804,6 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
4583
4804
  */
4584
4805
  _toConvexQuery(whereExpressionOverride, configuredIndex) {
4585
4806
  const config = this.config;
4586
- const tableIndexes = getIndexes(this.tableConfig.table).map((index) => ({
4587
- indexName: index.name,
4588
- indexFields: index.fields
4589
- }));
4590
- const compiler = new WhereClauseCompiler(this.tableConfig.table.tableName, tableIndexes);
4591
4807
  let orderSpecs = [];
4592
4808
  if (config.orderBy) {
4593
4809
  const orderByValue = typeof config.orderBy === "function" ? config.orderBy(this.tableConfig.table, {
@@ -4598,7 +4814,29 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
4598
4814
  }
4599
4815
  let whereExpression = whereExpressionOverride;
4600
4816
  if (!whereExpression && config.where && typeof config.where !== "function") whereExpression = this._buildFilterExpression(config.where, this.tableConfig);
4601
- const planned = compiler.compile(whereExpression, { orderFields: orderSpecs.map((spec) => spec.field) });
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) });
4602
4840
  const plannedUsesIndex = !!planned.selectedIndex && (planned.indexFilters.length > 0 || planned.probeFilters.length > 0);
4603
4841
  const compiled = !configuredIndex?.name || !plannedUsesIndex || planned.selectedIndex?.indexName === configuredIndex.name && !configuredIndex.range ? planned : {
4604
4842
  strategy: "none",
@@ -4680,11 +4918,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
4680
4918
  * Apply a single filter expression to a Convex query builder
4681
4919
  * Used for index filters (eq operations)
4682
4920
  */
4683
- _applyFilterToQuery(query, filter) {
4921
+ _applyFilterToQuery(query, filter, tableConfig = this.tableConfig) {
4684
4922
  if (filter.type === "binary") {
4685
4923
  const [field, value] = filter.operands;
4686
4924
  if (!isFieldReference(field)) return query;
4687
- const normalizedValue = this._normalizeComparableValue(field.fieldName, value);
4925
+ const normalizedValue = this._normalizeComparableValue(field.fieldName, value, tableConfig);
4688
4926
  switch (filter.operator) {
4689
4927
  case "eq": return query.eq(field.fieldName, normalizedValue);
4690
4928
  case "gt": return query.gt(field.fieldName, normalizedValue);
@@ -5637,6 +5875,9 @@ var ConvexDeleteBuilder = class extends QueryPromise {
5637
5875
  });
5638
5876
  }
5639
5877
  async execute(...args) {
5878
+ return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
5879
+ }
5880
+ async _runStatement(...args) {
5640
5881
  const config = args[0];
5641
5882
  const tableName = getTableName(this.table);
5642
5883
  const ormContext = getOrmContext(this.db);
@@ -5928,6 +6169,9 @@ var ConvexInsertBuilder = class extends QueryPromise {
5928
6169
  return this;
5929
6170
  }
5930
6171
  async execute() {
6172
+ return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement()));
6173
+ }
6174
+ async _runStatement() {
5931
6175
  if (this.valuesList.length === 0) throw new Error("values() must be called before execute()");
5932
6176
  const ormContext = getOrmContext(this.db);
5933
6177
  const tableName = getTableName(this.table);
@@ -6180,6 +6424,7 @@ function normalizeOrmTriggers(triggers) {
6180
6424
  //#region src/orm/lifecycle.ts
6181
6425
  const ORMLIFECYCLE_WRAPPED_DB = Symbol.for("kitcn:OrmLifecycleWrappedDB");
6182
6426
  const ORMLIFECYCLE_INNER_DB = Symbol.for("kitcn:OrmLifecycleInnerDB");
6427
+ const wrapUserHook = (hook) => hook ? (...args) => runInOrmUserCallback(() => hook(...args)) : void 0;
6183
6428
  const isWriterDb = (db) => typeof db.insert === "function" && typeof db.patch === "function" && typeof db.delete === "function";
6184
6429
  const isLifecycleWrappedDb = (db) => db[ORMLIFECYCLE_WRAPPED_DB] === true;
6185
6430
  const markLifecycleWrappedDb = (db) => {
@@ -6363,6 +6608,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
6363
6608
  const tableHooks = hooksByTable.get(tableName);
6364
6609
  if (!tableHooks) return innerDb.patch(tableName, id, value);
6365
6610
  return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
6611
+ await tableHooks.writeBarrier?.(innerDb);
6366
6612
  const needsDocuments = Boolean(tableHooks.update?.after || tableHooks.change);
6367
6613
  const oldDoc = needsDocuments ? await innerDb.get(tableName, id) : null;
6368
6614
  const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
@@ -6408,6 +6654,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
6408
6654
  const tableHooks = hooksByTable.get(tableName);
6409
6655
  if (!tableHooks) return innerDb.replace(tableName, id, value);
6410
6656
  return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
6657
+ await tableHooks.writeBarrier?.(innerDb);
6411
6658
  const oldDoc = Boolean(tableHooks.update?.after || tableHooks.change) ? await innerDb.get(tableName, id) : null;
6412
6659
  const updatePayload = await mergeBeforeData(tableName, "update", tableHooks.update?.before, value, hookCtx);
6413
6660
  await innerDb.replace(tableName, id, updatePayload);
@@ -6443,6 +6690,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
6443
6690
  const tableHooks = hooksByTable.get(tableName);
6444
6691
  if (!tableHooks) return innerDb.delete(tableName, id);
6445
6692
  return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
6693
+ await tableHooks.writeBarrier?.(innerDb);
6446
6694
  const oldDoc = Boolean(tableHooks.delete || tableHooks.change) ? await innerDb.get(tableName, id) : null;
6447
6695
  if (!oldDoc) {
6448
6696
  await innerDb.delete(tableName, id);
@@ -6478,6 +6726,7 @@ function writerWithHooks(ctx, innerDb, hooksByTable, isWithinHook = false) {
6478
6726
  const tableHooks = hooksByTable.get(table);
6479
6727
  if (!tableHooks) return innerDb.insert(table, value);
6480
6728
  return executeThenDrainHooks(ctx, innerDb, hooksByTable, isWithinHook, async (hookCtx) => {
6729
+ await tableHooks.writeBarrier?.(innerDb);
6481
6730
  const insertPayload = await mergeBeforeData(table, "create", tableHooks.create?.before, value, hookCtx);
6482
6731
  const id = await innerDb.insert(table, insertPayload);
6483
6732
  const newDoc = Boolean(tableHooks.create?.after || tableHooks.change) ? await innerDb.get(table, id) : null;
@@ -6540,53 +6789,57 @@ function createOrmDbLifecycle(schema, triggerDefinitions, capabilities) {
6540
6789
  for (const [triggerKey, hooks] of normalizedTriggers.entries()) {
6541
6790
  const tableName = tableNameBySchemaKey.get(triggerKey) ?? (tableNames.has(triggerKey) ? triggerKey : void 0);
6542
6791
  if (!tableName) throw new Error(`Unknown trigger table '${triggerKey}'. Export triggers with defineTriggers(relations, { ... }) using keys from your relations export.`);
6543
- tableHooks.set(tableName, hooks);
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
+ });
6544
6807
  }
6545
6808
  for (const tableConfig of Object.values(schema)) {
6546
6809
  if (!tableConfig?.table || !tableConfig?.name) continue;
6547
6810
  const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
6548
6811
  const rankIndexes = getRankIndexDefinitions(tableConfig);
6549
6812
  if (aggregateIndexes.length === 0 && rankIndexes.length === 0) continue;
6550
- const aggregate = requireAggregateCapability(capabilities, `Table '${tableConfig.name}' declares an aggregateIndex/rankIndex, which`);
6551
- const existing = tableHooks.get(tableConfig.name) ?? {};
6813
+ const tableName = tableConfig.name;
6814
+ const aggregate = requireAggregateCapability(capabilities, `Table '${tableName}' declares an aggregateIndex/rankIndex, which`);
6815
+ const existing = tableHooks.get(tableName) ?? {};
6552
6816
  const existingChange = existing.change;
6817
+ const existingBarrier = existing.writeBarrier;
6553
6818
  const metricIndexNames = aggregateIndexes.map((entry) => entry.name);
6554
6819
  const rankIndexNames = rankIndexes.map((entry) => entry.name);
6555
- const prependWriteBarrier = (before) => async (data, ctx) => {
6556
- await aggregate.assertAggregateIndexesWritable(ctx.db, tableConfig.name, metricIndexNames, rankIndexNames);
6557
- return before?.(data, ctx);
6558
- };
6559
- tableHooks.set(tableConfig.name, {
6820
+ tableHooks.set(tableName, {
6560
6821
  ...existing,
6561
- create: {
6562
- ...existing.create,
6563
- before: prependWriteBarrier(existing.create?.before)
6564
- },
6565
- update: {
6566
- ...existing.update,
6567
- before: prependWriteBarrier(existing.update?.before)
6568
- },
6569
- delete: {
6570
- ...existing.delete,
6571
- before: prependWriteBarrier(existing.delete?.before)
6822
+ writeBarrier: async (db) => {
6823
+ await existingBarrier?.(db);
6824
+ await aggregate.assertAggregateIndexesWritable(db, tableName, metricIndexNames, rankIndexNames);
6572
6825
  },
6573
6826
  change: async (change, ctx) => {
6574
6827
  if (change.operation === "delete") {
6575
- if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
6828
+ if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
6576
6829
  operation: "delete",
6577
6830
  id: change.id
6578
6831
  });
6579
- if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
6832
+ if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
6580
6833
  operation: "delete",
6581
6834
  id: change.id
6582
6835
  });
6583
6836
  } else {
6584
- if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
6837
+ if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableName, aggregateIndexes, {
6585
6838
  operation: change.operation,
6586
6839
  id: change.id,
6587
6840
  newDoc: change.newDoc
6588
6841
  });
6589
- if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
6842
+ if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableName, rankIndexes, {
6590
6843
  operation: change.operation,
6591
6844
  id: change.id,
6592
6845
  newDoc: change.newDoc
@@ -6857,6 +7110,9 @@ var ConvexUpdateBuilder = class extends QueryPromise {
6857
7110
  });
6858
7111
  }
6859
7112
  async execute(...args) {
7113
+ return await runInOrmWriteScope(this.db, () => runInOrmWriteBatch(this.db, () => this._runStatement(...args)));
7114
+ }
7115
+ async _runStatement(...args) {
6860
7116
  if (!this.setValues) throw new Error("set() must be called before execute()");
6861
7117
  if (Object.keys(this.setValues).length === 0) {
6862
7118
  if (this.paginateConfig !== void 0) {
@@ -7204,7 +7460,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
7204
7460
  defaults,
7205
7461
  resolvedDefaults
7206
7462
  };
7207
- const baseDb = Object.assign(Object.create(db), { [OrmContext]: ormContext });
7463
+ const baseDb = markOrmTransactionAnchor(Object.assign(Object.create(db), { [OrmContext]: ormContext }), db);
7208
7464
  const query = {};
7209
7465
  const edgesBySourceTable = getEdgesBySourceTable(edgeMetadata);
7210
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);