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 CHANGED
@@ -1,5 +1,119 @@
1
1
  # kitcn
2
2
 
3
+ ## 0.33.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#456](https://github.com/udecode/kitcn/pull/456) [`5b0bd5a`](https://github.com/udecode/kitcn/commit/5b0bd5a1debb4a0e0bb87f79ef1a18f357b614c1) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Breaking changes
8
+
9
+ - Support index-ordered pagination for indexed filters with more than 64 values. Pages follow index order, grouped by the filtered value, rather than creation order. Add `orderBy` and `maxScan` to preserve newest-first paging.
10
+
11
+ ```ts
12
+ // Before
13
+ const page = await db.query.users.withIndex("by_status").findMany({
14
+ where: { status: { in: manyStatuses } },
15
+ cursor: null,
16
+ limit: 20,
17
+ maxScan: 500,
18
+ });
19
+
20
+ // After
21
+ const page = await db.query.users.withIndex("by_status").findMany({
22
+ where: { status: { in: manyStatuses } },
23
+ orderBy: { createdAt: "desc" },
24
+ cursor: null,
25
+ limit: 20,
26
+ maxScan: 500,
27
+ });
28
+ ```
29
+
30
+ ## Patches
31
+
32
+ - Fix authenticated cRPC query results disappearing when a server-rendered page
33
+ hydrates.
34
+ - Fix unnecessary full-table reads for `select()` filters containing more than 64 values.
35
+ - Fix unnecessary full-table reads for long `in` lists combined with another condition, such as `name: { contains: 'x' }`.
36
+ - Improve limited reads with additional conditions so they stop after enough matching rows are found when index order satisfies the requested sort.
37
+ - Support index-bounded reads for indexed `in`, `notIn`, `ne`, and same-field equality `OR` filters regardless of list length.
38
+ - Support pagination without `maxScan` for wide filters whose requested order follows their indexed values; cross-value sorting, such as `orderBy: { createdAt: 'desc' }`, still requires `maxScan` past 64 values.
39
+
40
+ - [#449](https://github.com/udecode/kitcn/pull/449) [`b8df2da`](https://github.com/udecode/kitcn/commit/b8df2da49a8733b545f02b84bb538b1cbef275b0) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Breaking changes
41
+
42
+ - Read a `select().flatMap(relation, { where })` stage through an index that extends the relation's foreign key when the schema declares one. Children arrive in that index's order, so a lowered range field now orders them ahead of creation time, and outstanding page cursors for those queries do not carry over.
43
+
44
+ ```ts
45
+ // Before: every post by the author is read, then filtered, in creation order.
46
+ // After: only the by_author_likes range is read, in numLikes order.
47
+ index("by_author_likes").on(t.authorId, t.numLikes);
48
+
49
+ await ctx.orm.query.users
50
+ .select()
51
+ .flatMap("posts", { includeParent: false, where: { numLikes: { gt: 10 } } })
52
+ .paginate({ cursor: null, limit: 20 });
53
+ ```
54
+
55
+ ## Patches
56
+
57
+ - Compile a `select().union([{ where }])` source `where` against the table's indexes instead of filtering every scanned row, so an object `where` on an indexed field bounds the read. A source keeps its unanchored read when the lowered one could not supply `interleaveBy`, and a `where` never displaces an index the source or the chain pinned with a range.
58
+ - Report what a caller can actually do when a `predicate(...)` `where` runs over an unbounded pipeline read: a union source names its own `index` option, and a `flatMap` stage names the relation index it needs.
59
+ - Resolve a `select()` union source or `flatMap` stage `where` once per read. A callback `where` runs a single time instead of twice, and an object `where` compiles once instead of once per row.
60
+
61
+ ### Patch Changes
62
+
63
+ - [#455](https://github.com/udecode/kitcn/pull/455) [`a73de28`](https://github.com/udecode/kitcn/commit/a73de28bacf3e518a38762442270d2c54f6989e8) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
64
+
65
+ - Fix `findMany` losing its read bound when a `limit` is combined with `in`, `ne`,
66
+ `notIn` or a same-field `OR` on a table that has RLS enabled, or alongside a
67
+ filter Convex cannot evaluate such as `contains`. Either one used to make the
68
+ query read every row it matched against, so
69
+ `findMany({ where: { ownerId: { in: [a, b] } }, limit: 3 })` read 500 documents
70
+ on a 500-row table and 200 on a 200-row table. It now reads 6 at either size,
71
+ and the count no longer grows with the table.
72
+ - Improve how that `limit` is counted, so it bounds rows the caller can actually
73
+ see: with 80 rows an RLS policy hides sitting in front of the matches,
74
+ `limit: 3` still returns three rows and reads 86 documents instead of 200.
75
+ - Support that bound for an `in` list of any length.
76
+
77
+ Rows and their order are unchanged. A `where` that filters through a relation
78
+ keeps its previous read cost, as does `ne`, `notIn` or `isNotNull` ordered by a
79
+ field no index can serve.
80
+
81
+ - [#450](https://github.com/udecode/kitcn/pull/450) [`781af65`](https://github.com/udecode/kitcn/commit/781af65c6e2f9128362378f0cf358d2c8e509b7d) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
82
+
83
+ - Improve update read costs on `aggregateIndex` and `rankIndex` tables: read
84
+ each row once unless a user `update.before` hook requires a fresh read.
85
+ - Fix CLEARING checks for deletes of already-deleted rows: report the
86
+ transient aggregate-index state instead of `Delete on non-existent doc`.
87
+
88
+ - [#451](https://github.com/udecode/kitcn/pull/451) [`80f7609`](https://github.com/udecode/kitcn/commit/80f7609cb107d23e4688b6877f35787107dd0a39) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
89
+
90
+ - Improve `aggregateIndex` bulk-write read costs by reusing bucket and member
91
+ reads within uninterrupted ORM statements. User hooks and policy callbacks
92
+ end reuse so nested mutation writes remain visible to later maintenance.
93
+
94
+ - [#454](https://github.com/udecode/kitcn/pull/454) [`f399843`](https://github.com/udecode/kitcn/commit/f399843c6bfb46e080558bc22d2cc4a77842cd16) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
95
+
96
+ - Improve bulk aggregate writes by folding shared bucket and extrema updates
97
+ within uninterrupted statements. A 40-row key migration writes two shared
98
+ buckets and two extrema entries, plus forty membership rows.
99
+ - Preserve read-your-own-writes through aggregate reads and nested functions
100
+ called by lifecycle hooks or RLS policies. Reads and callbacks flush pending
101
+ writes; callbacks suspend batching until they settle.
102
+ - Fix mid-statement aggregate reads through `withoutTriggers()` and an ORM
103
+ rebuilt from a hook context to observe the same rows as `ctx.orm`.
104
+
105
+ ## 0.32.2
106
+
107
+ ### Patch Changes
108
+
109
+ - [#448](https://github.com/udecode/kitcn/pull/448) [`75ceda6`](https://github.com/udecode/kitcn/commit/75ceda65a12b4dfdd525c97a9401a31c54fd89d0) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
110
+
111
+ - Improve the read cost of a relation `where` on a relation joined on a column
112
+ other than the primary id, including `through` targets and `_count` on a
113
+ `through` relation.
114
+ - Fix relation targets sharing one loaded document, which could add fields to a
115
+ relation that only another relation in the same query requested.
116
+
3
117
  ## 0.32.1
4
118
 
5
119
  ### Patch Changes
@@ -1,5 +1,5 @@
1
- import { Vn as ConvexTextBuilderInitial, Zt as ConvexTableWithColumns } from "../capabilities-Ctcw2VRq.js";
2
- import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-BHGHbfb9.js";
1
+ import { Vn as ConvexTextBuilderInitial, Zt as ConvexTableWithColumns } from "../capabilities-Bxzoofl4.js";
2
+ import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-DrV6lQg0.js";
3
3
  import * as convex_values0 from "convex/values";
4
4
  import { GenericId, Infer, Value } from "convex/values";
5
5
  import { DocumentByName, GenericDataModel, GenericDatabaseReader, GenericDatabaseWriter, TableNamesInDataModel } from "convex/server";
@@ -4,7 +4,7 @@ import { i as defineAuth, n as createDisabledAuthRuntime, r as getGeneratedAuthD
4
4
  import { n as createGeneratedFunctionReference, o as isQueryCtx, s as isRunMutationCtx } from "../api-entry-Buvjl9hn.js";
5
5
  import { i as customQuery, n as customCtx, r as customMutation } from "../customFunctions-X_B4kET8.js";
6
6
  import { C as eq, o as getAggregateIndexes } from "../index-utils-DvK7P6Q1.js";
7
- import { o as mergedStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-D4z1CnDH.js";
7
+ import { c as stream, f as unsetToken, s as mergedStream, t as getByIdWithOrmQueryFallback } from "../query-context-DQLv58LH.js";
8
8
  import { n as convex } from "../convex-plugin-AbNQl1Sg.js";
9
9
  import { v } from "convex/values";
10
10
  import { internalActionGeneric, internalMutationGeneric, internalQueryGeneric, paginationOptsValidator } from "convex/server";
@@ -2149,7 +2149,7 @@ declare const createAggregateError: (code: AggregateErrorCode, message: string)
2149
2149
  declare const ensureCountAllowedForRls: (tableConfig: TableRelationalConfig, rlsMode: "skip" | "default" | undefined) => void;
2150
2150
  declare const ensureAggregateAllowedForRls: (tableConfig: TableRelationalConfig, rlsMode: "skip" | "default" | undefined, methodName: string) => void;
2151
2151
  declare namespace runtime_d_exports$1 {
2152
- export { AGGREGATE_ERROR, AGGREGATE_STATE_KIND_METRIC, AGGREGATE_STATE_KIND_RANK, AggregateBucketDelta, AggregateErrorCode, AggregateExtremaDelta, AggregateIndexDefinition, AggregateMemberWrite, AggregateMembershipDelta, AggregateQueryPlan, AggregateRangeConstraint, COUNT_ERROR, COUNT_STATUS_BUILDING, COUNT_STATUS_CLEARING, COUNT_STATUS_READY, ClearIndexChunkResult, CountErrorCode, CountIndexDefinition, CountQueryPlan, CountState, NULLISH_PROBE_VALUES, PlanBucketReadCache, applyAggregateIndexesForChange, applyCountIndexesForChange, asUnknownArray, assertAggregateIndexesWritable, clearCountIndexChunk, compileAggregateQueryPlan, compileCountFieldQueryPlan, compileCountQueryPlan, computeAggregateMembershipDelta, computeAggregateMetricValues, computeCountKeyParts, createAggregateError, createCountError, ensureAggregateAllowedForRls, ensureAggregateIndexReady, ensureCountAllowedForRls, ensureCountIndexReady, flushAggregateMembershipDeltas, getAggregateIndexDefinitions, getCountIndexDefinitions, getCountIndexValuesForFields, getCountState, isAggregatePlanZero, isIndexCountZero, listCountStates, listSchemaAggregateIndexes, listSchemaCountIndexes, parseAggregateWhere, parseCountWhere, readAverageFromBuckets, readCountFieldFromBuckets, readCountFromBuckets, readExtremaFromBuckets, readPlanBuckets, readSumFromBuckets, reconcileAggregateMembership, serializeCountKeyParts, setCountState, setCountStateError };
2152
+ export { AGGREGATE_ERROR, AGGREGATE_STATE_KIND_METRIC, AGGREGATE_STATE_KIND_RANK, AggregateBucketDelta, AggregateErrorCode, AggregateExtremaDelta, AggregateIndexDefinition, AggregateMemberWrite, AggregateMembershipDelta, AggregateQueryPlan, AggregateRangeConstraint, COUNT_ERROR, COUNT_STATUS_BUILDING, COUNT_STATUS_CLEARING, COUNT_STATUS_READY, ClearIndexChunkResult, CountErrorCode, CountIndexDefinition, CountMemberRow, CountQueryPlan, CountState, NULLISH_PROBE_VALUES, PlanBucketReadCache, applyAggregateIndexesForChange, applyCountIndexesForChange, asUnknownArray, assertAggregateIndexesWritable, clearCountIndexChunk, compileAggregateQueryPlan, compileCountFieldQueryPlan, compileCountQueryPlan, computeAggregateMembershipDelta, computeAggregateMetricValues, computeCountKeyParts, createAggregateError, createCountError, ensureAggregateAllowedForRls, ensureAggregateIndexReady, ensureCountAllowedForRls, ensureCountIndexReady, flushAggregateMembershipDeltas, getAggregateIndexDefinitions, getCountIndexDefinitions, getCountIndexValuesForFields, getCountState, isAggregatePlanZero, isIndexCountZero, listCountStates, listSchemaAggregateIndexes, listSchemaCountIndexes, parseAggregateWhere, parseCountWhere, readAverageFromBuckets, readCountFieldFromBuckets, readCountFromBuckets, readExtremaFromBuckets, readPlanBuckets, readSumFromBuckets, reconcileAggregateMembership, serializeCountKeyParts, setCountState, setCountStateError };
2153
2153
  }
2154
2154
  declare const AGGREGATE_STATE_KIND_METRIC = "metric";
2155
2155
  declare const AGGREGATE_STATE_KIND_RANK = "rank";
@@ -2188,6 +2188,21 @@ type CountState = {
2188
2188
  completedAt?: number | null;
2189
2189
  lastError?: string | null;
2190
2190
  };
2191
+ type CountMemberRow = {
2192
+ _id: GenericId<any>;
2193
+ kind: string;
2194
+ tableKey: string;
2195
+ indexName: string;
2196
+ docId: string;
2197
+ keyHash: string;
2198
+ keyParts: unknown[];
2199
+ sumValues: Record<string, number>;
2200
+ nonNullCountValues: Record<string, number>;
2201
+ extremaValues: Record<string, unknown>;
2202
+ rankNamespace?: unknown;
2203
+ rankKey?: unknown;
2204
+ rankSumValue?: number;
2205
+ };
2191
2206
  type CountBucketRow = {
2192
2207
  _id: GenericId<any>;
2193
2208
  tableKey: string;
@@ -2261,6 +2276,11 @@ type AggregateExtremaDelta = {
2261
2276
  value: unknown;
2262
2277
  delta: number;
2263
2278
  };
2279
+ /**
2280
+ * `post` is what the write leaves in the member table, so the flush can bring
2281
+ * the member memo forward without reading the row back. The `_id` is missing
2282
+ * because an insert only learns it once the insert returns.
2283
+ */
2264
2284
  type AggregateMemberWrite = {
2265
2285
  kind: 'none';
2266
2286
  } | {
@@ -2270,11 +2290,13 @@ type AggregateMemberWrite = {
2270
2290
  kind: 'patch';
2271
2291
  id: GenericId<any>;
2272
2292
  doc: Record<string, unknown>;
2293
+ post: Omit<CountMemberRow, '_id'>;
2273
2294
  } | {
2274
2295
  kind: 'insert';
2275
- doc: Record<string, unknown>;
2296
+ doc: Omit<CountMemberRow, '_id'>;
2276
2297
  };
2277
2298
  type AggregateMembershipDelta = {
2299
+ /** The document this delta reconciles, and the member memo's key. */docId: string;
2278
2300
  buckets: AggregateBucketDelta[];
2279
2301
  extrema: AggregateExtremaDelta[];
2280
2302
  member: AggregateMemberWrite;
@@ -2293,8 +2315,28 @@ declare const computeAggregateMembershipDelta: (db: GenericDatabaseWriter<any>,
2293
2315
  */
2294
2316
  declare const flushAggregateMembershipDeltas: (db: GenericDatabaseWriter<any>, tableName: string, indexName: string, deltas: AggregateMembershipDelta[]) => Promise<void>;
2295
2317
  /**
2296
- * Single-document reconciliation. Flushes eagerly so user code reading an
2297
- * aggregate later in the same mutation sees its own writes.
2318
+ * Single-document reconciliation.
2319
+ *
2320
+ * The bucket and extrema writes always go on the transaction's write queue,
2321
+ * never straight to storage: `applyBucketDelta` writes an absolute count
2322
+ * computed from the row it just read, so two of them interleaving would be a
2323
+ * lost update, and routing every one of them through a single drain is what
2324
+ * keeps them serialized. Inside a mutation statement the queue is held to the
2325
+ * end of the statement, which is what lets the fold collapse a page of
2326
+ * documents into one write per key tuple; outside one it is drained
2327
+ * immediately, so a raw `ctx.db` write behaves exactly as it did before.
2328
+ *
2329
+ * The member row is written per document either way, because it is the
2330
+ * pre-image the next reconciliation of this document subtracts. Outside a
2331
+ * statement it is still written after the bucket, preserving the previous
2332
+ * order; inside one it necessarily lands first, and a flush that throws
2333
+ * part-way leaves the index needing a backfill — which a partially applied fold
2334
+ * would anyway, whichever order the two halves ran in.
2335
+ *
2336
+ * Deferral is invisible to aggregate readers: every bucket- and extrema-backed
2337
+ * read path drains the queue first, so user code reading a count later in the
2338
+ * same mutation — including from a trigger firing mid-statement — still sees
2339
+ * its own writes.
2298
2340
  */
2299
2341
  declare const reconcileAggregateMembership: (db: GenericDatabaseWriter<any>, params: {
2300
2342
  tableName: string;
@@ -2755,6 +2797,7 @@ declare class ConvexDeleteBuilder<TTable extends ConvexTable<any>, TReturning ex
2755
2797
  }): this;
2756
2798
  executeAsync(this: ConvexDeleteExecutableThis<TTable, TReturning, TMode>, ...args: TMode extends 'single' ? [config?: Omit<MutationExecuteConfig, 'mode'>] : [config: never]): Promise<TMode extends 'single' ? MutationExecuteResult<TTable, TReturning, 'single'> : never>;
2757
2799
  execute(this: ConvexDeleteExecutableThis<TTable, TReturning, TMode>, ...args: TMode extends 'single' ? [config?: MutationExecuteConfig] : [config?: never]): Promise<MutationExecuteResult<TTable, TReturning, TMode>>;
2800
+ private _runStatement;
2758
2801
  }
2759
2802
  //#endregion
2760
2803
  //#region src/orm/insert.d.ts
@@ -2795,6 +2838,7 @@ declare class ConvexInsertBuilder<TTable extends ConvexTable<any>, TReturning ex
2795
2838
  onConflictDoNothing(config?: InsertOnConflictDoNothingConfig<TTable>): ConvexInsertWithout<this, 'onConflictDoNothing' | 'onConflictDoUpdate'>;
2796
2839
  onConflictDoUpdate(config: InsertOnConflictDoUpdateConfig<TTable>): ConvexInsertWithout<this, 'onConflictDoNothing' | 'onConflictDoUpdate'>;
2797
2840
  execute(): Promise<MutationResult<TTable, TReturning>>;
2841
+ private _runStatement;
2798
2842
  private resolveReturningRow;
2799
2843
  private handleConflict;
2800
2844
  private findConflictRow;
@@ -2922,6 +2966,20 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
2922
2966
  * intervening write would make it stale.
2923
2967
  */
2924
2968
  private readonly _documentByNormalizedId;
2969
+ /**
2970
+ * Single target documents resolved by an eq-pinned key during one execution,
2971
+ * keyed on the read itself: table, index, join columns and their values.
2972
+ *
2973
+ * `_documentByNormalizedId` only covers a join on the primary id. A relation
2974
+ * joined on any other column resolves its target with `.first()` instead, and
2975
+ * the same one-row-at-a-time membership predicate re-issues that read once per
2976
+ * drain. The index is fixed for the whole relation and the key is pinned by
2977
+ * `eq`, so the first row is a pure function of this key within an execution.
2978
+ *
2979
+ * Same scope and staleness argument as `_documentByNormalizedId`: not handed
2980
+ * to the next run by `_forExecution`, so an intervening write is still seen.
2981
+ */
2982
+ private readonly _firstDocumentByFieldKey;
2925
2983
  constructor(schema: TSchema, tableConfig: TTableConfig, edgeMetadata: EdgeMetadata[], db: GenericDatabaseReader<any>, config: DBQueryConfig<'one' | 'many', boolean, TSchema, TTableConfig>, mode: 'many' | 'first' | 'firstOrThrow' | 'count' | 'aggregate' | 'groupBy', _allEdges?: EdgeMetadata[] | undefined, // M6.5 Phase 2: All edges for nested loading
2926
2984
  rls?: RlsContext | undefined, relationLoading?: {
2927
2985
  concurrency?: number;
@@ -3051,8 +3109,41 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3051
3109
  private _finalizeRows;
3052
3110
  private _getSchemaDefinitionOrThrow;
3053
3111
  private _applyEqBounds;
3054
- private _buildTableFilterPredicate;
3055
- private _assertWhereIndexRequirement;
3112
+ /**
3113
+ * The row filter for an already-resolved pipeline stage `where`.
3114
+ *
3115
+ * Takes the resolved shape rather than the clause so the rows that are read
3116
+ * and the rows that are kept come from the same resolution. Resolving twice
3117
+ * would let a `where` callback that closes over changing state — a clock, a
3118
+ * counter — bound the read with one expression and filter it with another,
3119
+ * dropping rows that the second expression matched but the first never read.
3120
+ */
3121
+ private _buildResolvedWherePredicate;
3122
+ /**
3123
+ * What a pipeline stage `where` is, resolved once.
3124
+ *
3125
+ * Object clauses and callbacks that return an expression can be compiled and
3126
+ * therefore index-lowered. `predicate(...)` is opaque JavaScript and never
3127
+ * can be. Both the anchoring check and the index lowering need to tell those
3128
+ * apart, and a callback `where` should be run once per read rather than once
3129
+ * per caller that asks.
3130
+ */
3131
+ private _resolvePipelineWhere;
3132
+ /**
3133
+ * Reject a `predicate(...)` stage `where` on a read nothing bounds.
3134
+ *
3135
+ * A predicate can only run after the row is read, so it costs one read per
3136
+ * candidate row. That is fine over an index range and unbounded over a table
3137
+ * scan, which is the same line `findMany` draws for a chain-level
3138
+ * `predicate(...)`. An object `where` is not held to it: the compiler
3139
+ * lowers what it can and post-filters the rest, exactly as the chain-level
3140
+ * read does.
3141
+ *
3142
+ * `remedy` is per call site because the anchor a caller can add differs: a
3143
+ * union source takes an index, a `flatMap` stage has no index option at all
3144
+ * and is anchored by the relation's own declared index.
3145
+ */
3146
+ private _assertPipelineWhereIsAnchored;
3056
3147
  private _isFilterExpressionNode;
3057
3148
  private _isPredicateWhereClause;
3058
3149
  private _createFilterOperators;
@@ -3085,14 +3176,31 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3085
3176
  /**
3086
3177
  * The compiled index union, as one ordered stream.
3087
3178
  *
3088
- * Each probe is its own index range, so the union is only usable where the
3089
- * merged order is the order the read has to produce. `mergedStream` orders by
3090
- * a *suffix* of the index key, and a suffix may only drop key components the
3179
+ * Each probe is its own index range, so the union is only usable where its
3180
+ * order is the order the read has to produce. `mergedStream` orders by a
3181
+ * suffix of the index key, and a suffix may only drop key components the
3091
3182
  * probes all pin to a single value — so the requested field has to sit inside
3092
3183
  * the pinned run or immediately after it. Returns null when it does not, and
3093
3184
  * the caller falls back to the plan's plain index range or a bounded scan.
3185
+ *
3186
+ * Two executors, because a union that is too wide to merge is not too wide to
3187
+ * read. A merge registers every probe up front and holds them open for the
3188
+ * life of the read, which is what `MAX_INDEX_UNION_PROBES` refuses; past that
3189
+ * width the probes are concatenated instead, one open query at a time. What
3190
+ * the cap picks is therefore fan-out versus sequential — never index versus
3191
+ * table scan, which is a trade no read wins.
3094
3192
  */
3095
3193
  private _buildProbeUnionStream;
3194
+ /**
3195
+ * `probeFilters` in the order a scan of `indexField` reaches them, or null
3196
+ * when they cannot be proven to be pairwise-disjoint ranges on that field.
3197
+ *
3198
+ * Concatenation stands in for a merge only under those two properties, and
3199
+ * both have to come from the filters themselves rather than from trust in the
3200
+ * compiler that emitted them: overlapping probes would emit a row twice, and
3201
+ * probes handed over out of order would emit index keys backwards.
3202
+ */
3203
+ private _orderDisjointProbes;
3096
3204
  /**
3097
3205
  * The read the compiled plan describes, as a stream, with nothing filtered
3098
3206
  * yet.
@@ -3124,7 +3232,41 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3124
3232
  * the caller then falls back to its plain-query path.
3125
3233
  */
3126
3234
  private _buildResidualFilterStream;
3235
+ /**
3236
+ * One `pipeline.union` source, as a stream.
3237
+ *
3238
+ * The source's own `where` is compiled into an index plan first, so an
3239
+ * object `where` bounds the read instead of being discarded row by row after
3240
+ * it. Two rules keep that from changing what the union produces:
3241
+ *
3242
+ * - `_compileQueryPlan` discards a plan that would displace an index the
3243
+ * caller pinned, so a source's own range (a tenant scope, say) still wins.
3244
+ * - A merged union is ordered by `interleaveBy`, which only some index
3245
+ * shapes can supply. The lowered read is checked against that before it is
3246
+ * committed to, and the unlowered scan is used when it cannot serve the
3247
+ * merge — an index that saves reads is not worth an ordering that throws.
3248
+ *
3249
+ * The `where` stays applied in JS either way. The index range is a bound on
3250
+ * what is read, not a replacement for the predicate.
3251
+ */
3127
3252
  private _buildUnionSourceStream;
3253
+ /**
3254
+ * The index a `flatMap` stage `where` can ride on top of the join keys.
3255
+ *
3256
+ * The join is only correct while `targetFields` stay pinned by equality, and
3257
+ * a Convex range builder pins fields in index-key order with no gaps, so the
3258
+ * candidates are exactly the declared indexes that lead with `targetFields`
3259
+ * and carry at least one more field. Those trailing fields are what the
3260
+ * stage `where` is compiled against — the compiler never sees the join keys,
3261
+ * because their values are per parent and the index choice has to be made
3262
+ * once for all of them.
3263
+ *
3264
+ * Returns null when nothing is gained, and the caller keeps the relation's
3265
+ * own index plus the post-filter. A multi-probe plan is refused: each probe
3266
+ * is its own range, and a union of ranges per parent is a different stream
3267
+ * shape than the one `flatMap` was handed as `mappedIndexFields`.
3268
+ */
3269
+ private _resolveFlatMapStageIndex;
3128
3270
  private _applyFlatMapStage;
3129
3271
  private _applyPipelineStages;
3130
3272
  private _tryNativeUnfilteredCount;
@@ -3189,6 +3331,17 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3189
3331
  * Phase 4 implementation with WhereClauseCompiler
3190
3332
  */
3191
3333
  private _toConvexQuery;
3334
+ /**
3335
+ * The read plan for one `where` on this table.
3336
+ *
3337
+ * Split out of `_toConvexQuery` because the chain-level `where` is not the
3338
+ * only one that has to be index-lowered: a `pipeline.union` source carries
3339
+ * its own `where` and its own pinned index, and compiles against the same
3340
+ * table with a different ordering contract (`interleaveBy`, not `orderBy`).
3341
+ * Both go through here so a plain-object `where` is planned the same way
3342
+ * wherever it is written.
3343
+ */
3344
+ private _compileQueryPlan;
3192
3345
  private _buildRelationKey;
3193
3346
  /**
3194
3347
  * How many leading fields of the scanned index are pinned to a single value.
@@ -3219,7 +3372,39 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3219
3372
  * Helper for recursive relation loading
3220
3373
  */
3221
3374
  private _getTargetTableEdges;
3375
+ /**
3376
+ * A memo entry is a snapshot, so every caller has to get its own document.
3377
+ *
3378
+ * Relation loaders write nested `with` results and `extras` straight onto the
3379
+ * target they were handed, and `hydrateDateFieldsForRead` copies every own key
3380
+ * it finds. Two loads that share one entry would therefore publish each
3381
+ * other's fields — a relation asked for as `true` coming back carrying a
3382
+ * nested relation only the `where` requested. Those writes are all top-level,
3383
+ * so a shallow copy is exactly as much isolation as they need.
3384
+ */
3385
+ private _ownedCopy;
3222
3386
  private _getById;
3387
+ /**
3388
+ * Identity of one `_firstByFields` read, or null when the join values cannot
3389
+ * be encoded losslessly.
3390
+ *
3391
+ * `JSON.stringify` alone is not safe here. It renders every `ArrayBuffer` as
3392
+ * `{}` and `NaN`/`Infinity`/`-Infinity` as `null`, so two distinct join values
3393
+ * would share one entry, and it throws outright on `int64`. The relation
3394
+ * loader's own per-batch key map cannot catch that, because the residual
3395
+ * relation `where` hands it one row at a time — a map of one never compares
3396
+ * two values. This key is the only thing that tells them apart.
3397
+ *
3398
+ * `convexToJson` is the same wire encoding the client uses for query keys, so
3399
+ * every Convex value round-trips distinctly. Anything it rejects is not a
3400
+ * Convex value and simply goes unmemoized.
3401
+ */
3402
+ private _firstByFieldsMemoKey;
3403
+ /**
3404
+ * `_getById` for a relation joined on something other than the primary id:
3405
+ * resolve one target document by an eq-pinned key, memoized per execution.
3406
+ */
3407
+ private _firstByFields;
3223
3408
  private _getRelationConcurrency;
3224
3409
  private _getRelationFanOutKeyCap;
3225
3410
  private _enforceRelationFanOutKeyCap;
@@ -3588,6 +3773,7 @@ declare class ConvexUpdateBuilder<TTable extends ConvexTable<any>, TReturning ex
3588
3773
  allowFullScan(): ConvexUpdateBuilder<TTable, TReturning, TMode, true>;
3589
3774
  executeAsync(this: ConvexUpdateExecutableThis<TTable, TReturning, TMode>, ...args: TMode extends 'single' ? [config?: Omit<MutationExecuteConfig, 'mode'>] : [config: never]): Promise<TMode extends 'single' ? MutationExecuteResult<TTable, TReturning, 'single'> : never>;
3590
3775
  execute(this: ConvexUpdateExecutableThis<TTable, TReturning, TMode>, ...args: TMode extends 'single' ? [config?: MutationExecuteConfig] : [config?: never]): Promise<MutationExecuteResult<TTable, TReturning, TMode>>;
3776
+ private _runStatement;
3591
3777
  }
3592
3778
  //#endregion
3593
3779
  //#region src/orm/database.d.ts
@@ -1,4 +1,4 @@
1
- import { $ as RankIndexDefinition, G as CountBackfillChunkArgs, J as CountBackfillStatusArgs, K as CountBackfillKickoffArgs, Q as CountIndexDefinition, X as CountQueryPlan, Y as AggregateQueryPlan, Z as AggregateIndexDefinition, et as RankOrderField, q as CountBackfillMode, r as OrmCapability } from "../../capabilities-Ctcw2VRq.js";
1
+ import { $ as RankIndexDefinition, G as CountBackfillChunkArgs, J as CountBackfillStatusArgs, K as CountBackfillKickoffArgs, Q as CountIndexDefinition, X as CountQueryPlan, Y as AggregateQueryPlan, Z as AggregateIndexDefinition, et as RankOrderField, q as CountBackfillMode, r as OrmCapability } from "../../capabilities-Bxzoofl4.js";
2
2
 
3
3
  //#region src/orm/aggregate-index/capability.d.ts
4
4
  /**