kitcn 0.31.0 → 0.32.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,100 @@
1
1
  # kitcn
2
2
 
3
+ ## 0.32.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#425](https://github.com/udecode/kitcn/pull/425) [`495eb0b`](https://github.com/udecode/kitcn/commit/495eb0b2bd285484250ca69d75de135287531bbb) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Breaking changes
8
+
9
+ - Cursor pages for an index-union filter with no `orderBy` are now in the order of the index the read walks, grouped by the probed value, instead of creation order. Add `orderBy` to keep newest-first paging.
10
+
11
+ ```ts
12
+ // Before
13
+ const page = await db.query.users.withIndex("by_status").findMany({
14
+ where: { status: { in: ["active", "pending"] } },
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: ["active", "pending"] } },
23
+ orderBy: { createdAt: "desc" },
24
+ cursor: null,
25
+ limit: 20,
26
+ });
27
+ ```
28
+
29
+ ## Features
30
+
31
+ - Page `in`, `notIn`, `ne`, and same-field equality `OR` filters from one index range per value instead of scanning the table. Cursor pagination over these filters no longer needs `maxScan`, and `orderBy` sorts across the whole result rather than per value.
32
+
33
+ ## Patches
34
+
35
+ - Fix `select()` composition and `endCursor` pagination reading a whole index instead of the compiled index ranges when the filter is an index union.
36
+ - Keep no-`orderBy` cursor direction consistent when `endCursor` routes an index union through the advanced stream path.
37
+ - Prevent `endCursor` narrowing from reopening disjoint equality ranges and duplicating merged-stream rows.
38
+ - Fix cursor pagination with a residual post-filter reading a whole index instead of the compiled index ranges when the filter is an index union.
39
+ - Fall back to a bounded scan when the probed index cannot supply the requested `orderBy` or the union is wider than 64 ranges.
40
+
41
+ ### Patch Changes
42
+
43
+ - [#426](https://github.com/udecode/kitcn/pull/426) [`466623c`](https://github.com/udecode/kitcn/commit/466623c8b581c38a066fa078309d25cfab166ea7) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
44
+
45
+ - Fix a multi-field `orderBy` reading the whole table even when a declared
46
+ compound index already produces that exact order. `orderBy: [asc(type),
47
+ asc(numLikes)]` with `limit: 5` against an index on `(type, numLikes)` now
48
+ reads 5 documents instead of every row, at any table size — previously the
49
+ read cost was the same whether you asked for 5 rows or 50. The same bound
50
+ applies to relations: `with: { posts: { orderBy: { numLikes: 'asc' },
51
+ limit: 2 } }` now reads 2 children per parent instead of all of them.
52
+ - Prefer an index that supplies more of the requested sort when several serve
53
+ the filter equally well, so `(orgId, createdAt, title)` is chosen over
54
+ `(orgId, createdAt)` for a sort on both `createdAt` and `title`. An index with
55
+ another unrequested key after `title` is not treated as an exact sort because
56
+ that key would change which tied rows survive `limit`.
57
+ - Stop warning that secondary `orderBy` fields are unstable across pages when
58
+ the index carries the whole sort. A Convex cursor is the index key, so those
59
+ pages are stable. The warning still fires — with corrected wording — when no
60
+ index serves the full sort and the extra fields really are dropped.
61
+ - Sorts that mix directions, skip an index key, or run over a column that can
62
+ be missing or null keep using the post-fetch sort, so row order and null
63
+ placement are unchanged.
64
+ - Use Convex value ordering for non-null post-fetch values, including UTF-8
65
+ strings, signed zero, and NaN, so an index-backed top-k and its post-fetch
66
+ fallback select the same rows.
67
+ - Preserve the existing implicit creation-time tie order when an
68
+ equality-pinned leading sort field points opposite to the moving fields.
69
+ Add `createdAt` in the moving direction to make that sort index-bounded.
70
+
71
+ ## 0.31.1
72
+
73
+ ### Patch Changes
74
+
75
+ - [#424](https://github.com/udecode/kitcn/pull/424) [`0e3a221`](https://github.com/udecode/kitcn/commit/0e3a2214e8e1037b323e603f19c2df89b0d95602) Thanks [@MikeyZhang75](https://github.com/MikeyZhang75)! - ## Patches
76
+
77
+ - Stop `aggregateBackfill` rewriting the stored state it is about to delete.
78
+ Clearing a `rankIndex` walked the btree once per member — a root-to-leaf
79
+ descent plus a patch on every node along the way — and then dropped the whole
80
+ tree regardless, so all of that work was thrown away. Clearing an
81
+ `aggregateIndex` did the same thing with buckets, decrementing each one down to
82
+ zero before deleting it. Both now delete their member rows outright and let the
83
+ existing tree and bucket sweeps reclaim the rest. Clearing 120 rank members
84
+ across three partitions went from 201 btree node writes to 15, one per node.
85
+ - Fix a `rankIndex` clear crashing with `Unexpected field 'deletionStack'` once a
86
+ partition holds more than a few hundred rows. The rank storage tables now
87
+ reuse the same definitions the btree writes to, so a large tree can persist its
88
+ traversal state and resume across mutations. Run
89
+ `npx convex dev` (or your usual codegen) to pick the schema up.
90
+ - Report real work from a clear chunk instead of a fixed guess, so
91
+ `aggregateBackfill --batch-size` now bounds a clear by the documents it
92
+ actually touches. Multi-partition rank indexes no longer schedule far more
93
+ chunks than the remaining work needs.
94
+ - Allow an app to declare `aggregateStorageTables` from `kitcn/aggregate`
95
+ alongside a table that also declares a `rankIndex`. `defineSchema` used to
96
+ reject that combination as a duplicate table name.
97
+
3
98
  ## 0.31.0
4
99
 
5
100
  ### Minor Changes
@@ -1,5 +1,5 @@
1
- import { Vn as ConvexTextBuilderInitial, Zt as ConvexTableWithColumns } from "../capabilities-DPB-JKlm.js";
2
- import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-Dddhscyy.js";
1
+ import { Vn as ConvexTextBuilderInitial, Zt as ConvexTableWithColumns } from "../capabilities-CD-Ij91k.js";
2
+ import { C as ConvexNumberBuilderInitial, E as ConvexIdBuilderInitial, N as ConvexCustomBuilderInitial } from "../where-clause-compiler-FDJTqLDC.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";
@@ -136,6 +136,10 @@ declare const aggregateStorageTables: {
136
136
  //#endregion
137
137
  //#region src/aggregate-core/btree.d.ts
138
138
  type Key$2 = Value;
139
+ type DeleteTreesResult = {
140
+ /** True once this aggregate owns no tree row at all. */done: boolean; /** Documents this call wrote, so callers can charge a real work budget. */
141
+ documents: number;
142
+ };
139
143
  //#endregion
140
144
  //#region src/aggregate-core/positions.d.ts
141
145
  type Bound$1<K extends Key$2, ID extends string> = {
@@ -240,11 +244,12 @@ declare class Aggregate<K extends Key, ID extends string, TNamespace extends Val
240
244
  rootLazy?: boolean;
241
245
  }, TNamespace>): Promise<void>;
242
246
  /**
243
- * Deletes up to `limit` namespace trees without recreating them. Returns true
244
- * once this aggregate owns no trees, so callers can drain every namespace
245
- * across several mutations instead of walking them all in one.
247
+ * Deletes up to `limit` nodes from one namespace tree without recreating it,
248
+ * reporting `done` once this aggregate owns no trees and how many documents
249
+ * the call wrote. Callers drain every namespace across several mutations
250
+ * instead of walking them all in one, and charge their budget by `documents`.
246
251
  */
247
- deleteTrees(ctx: RunMutationCtx, limit: number): Promise<boolean>;
252
+ deleteTrees(ctx: RunMutationCtx, limit: number): Promise<DeleteTreesResult>;
248
253
  makeRootLazy(ctx: RunMutationCtx, namespace: TNamespace): Promise<void>;
249
254
  paginateNamespaces(ctx: RunQueryCtx, cursor?: string, pageSize?: number): Promise<{
250
255
  cursor: string;
@@ -1,4 +1,5 @@
1
- import { n as TableAggregate$1, r as aggregateStorageTables, t as DirectAggregate$1 } from "../runtime-BcvcfaP8.js";
1
+ import { n as TableAggregate$1, t as DirectAggregate$1 } from "../runtime-B-8HKSIE.js";
2
+ import { i as aggregateStorageTables } from "../schema-BFP_awgP.js";
2
3
 
3
4
  //#region src/aggregate/index.ts
4
5
  const wrapTriggerFactory = (methodName, factory) => ((...args) => {
@@ -1,2 +1,2 @@
1
- import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, b as GenericAuthBeforeResult, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../../generated-contract-disabled-CTUQ9nxL.js";
1
+ import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, b as GenericAuthBeforeResult, i as getGeneratedAuthDisabledReason, n as GeneratedAuthDisabledReasonKind, r as createDisabledAuthRuntime, t as AuthRuntime, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../../generated-contract-disabled-PdNGvNYP.js";
2
2
  export { type AuthRuntime, BetterAuthOptionsWithoutDatabase, type GeneratedAuthDisabledReasonKind, GenericAuthBeforeResult, GenericAuthDefinition, GenericAuthTriggerChange, GenericAuthTriggerHandlers, GenericAuthTriggers, createDisabledAuthRuntime, defineAuth, getGeneratedAuthDisabledReason };
@@ -1,7 +1,7 @@
1
1
  import { a as QueryCtxWithPreferredOrmQueryTable, n as LookupByIdResultByCtx, t as DocByCtx } from "../query-context-BkNjkDCk.js";
2
2
  import { t as GetAuth } from "../types-ex0-J-SC.js";
3
3
  import { t as GenericCtx } from "../context-utils-Cbv4r0AA.js";
4
- import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, _ as updateManyHandler, a as AuthFunctions, b as GenericAuthBeforeResult, c as consumeOneHandler, d as createHandler, f as deleteManyHandler, g as incrementOneHandler, h as findOneHandler, i as getGeneratedAuthDisabledReason, l as countHandler, m as findManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as deleteOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as createApi, v as updateOneHandler, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../generated-contract-disabled-CTUQ9nxL.js";
4
+ import { C as GenericAuthTriggerHandlers, S as GenericAuthTriggerChange, T as defineAuth, _ as updateManyHandler, a as AuthFunctions, b as GenericAuthBeforeResult, c as consumeOneHandler, d as createHandler, f as deleteManyHandler, g as incrementOneHandler, h as findOneHandler, i as getGeneratedAuthDisabledReason, l as countHandler, m as findManyHandler, n as GeneratedAuthDisabledReasonKind, o as Triggers, p as deleteOneHandler, r as createDisabledAuthRuntime, s as createClient, t as AuthRuntime, u as createApi, v as updateOneHandler, w as GenericAuthTriggers, x as GenericAuthDefinition, y as BetterAuthOptionsWithoutDatabase } from "../generated-contract-disabled-PdNGvNYP.js";
5
5
  import * as convex_values0 from "convex/values";
6
6
  import { Infer } from "convex/values";
7
7
  import { AuthConfig, DocumentByName, GenericDataModel, GenericMutationCtx, GenericQueryCtx, GenericSchema, PaginationOptions, PaginationResult, SchemaDefinition, TableNamesInDataModel } from "convex/server";
@@ -109,28 +109,28 @@ type AdapterPaginationOptions = PaginationOptions & {
109
109
  };
110
110
  declare const adapterWhereValidator: convex_values0.VObject<{
111
111
  mode?: "sensitive" | "insensitive" | undefined;
112
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
113
112
  connector?: "AND" | "OR" | undefined;
114
- value: string | number | boolean | string[] | number[] | null;
113
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
115
114
  field: string;
115
+ value: string | number | boolean | string[] | number[] | null;
116
116
  }, {
117
117
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
118
118
  field: convex_values0.VString<string, "required">;
119
119
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
120
- operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
120
+ operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
121
121
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
122
- }, "required", "mode" | "operator" | "value" | "field" | "connector">;
122
+ }, "required", "mode" | "connector" | "field" | "operator" | "value">;
123
123
  declare const adapterArgsValidator: convex_values0.VObject<{
124
- limit?: number | undefined;
125
124
  where?: {
126
125
  mode?: "sensitive" | "insensitive" | undefined;
127
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
128
126
  connector?: "AND" | "OR" | undefined;
129
- value: string | number | boolean | string[] | number[] | null;
127
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
130
128
  field: string;
129
+ value: string | number | boolean | string[] | number[] | null;
131
130
  }[] | undefined;
132
- select?: string[] | undefined;
131
+ limit?: number | undefined;
133
132
  offset?: number | undefined;
133
+ select?: string[] | undefined;
134
134
  sortBy?: {
135
135
  field: string;
136
136
  direction: "asc" | "desc";
@@ -150,24 +150,24 @@ declare const adapterArgsValidator: convex_values0.VObject<{
150
150
  }, "optional", "field" | "direction">;
151
151
  where: convex_values0.VArray<{
152
152
  mode?: "sensitive" | "insensitive" | undefined;
153
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
154
153
  connector?: "AND" | "OR" | undefined;
155
- value: string | number | boolean | string[] | number[] | null;
154
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
156
155
  field: string;
156
+ value: string | number | boolean | string[] | number[] | null;
157
157
  }[] | undefined, convex_values0.VObject<{
158
158
  mode?: "sensitive" | "insensitive" | undefined;
159
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
160
159
  connector?: "AND" | "OR" | undefined;
161
- value: string | number | boolean | string[] | number[] | null;
160
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
162
161
  field: string;
162
+ value: string | number | boolean | string[] | number[] | null;
163
163
  }, {
164
164
  connector: convex_values0.VUnion<"AND" | "OR" | undefined, [convex_values0.VLiteral<"AND", "required">, convex_values0.VLiteral<"OR", "required">], "optional", never>;
165
165
  field: convex_values0.VString<string, "required">;
166
166
  mode: convex_values0.VUnion<"sensitive" | "insensitive" | undefined, [convex_values0.VLiteral<"sensitive", "required">, convex_values0.VLiteral<"insensitive", "required">], "optional", never>;
167
- operator: convex_values0.VUnion<"eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
167
+ operator: convex_values0.VUnion<"lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined, [convex_values0.VLiteral<"lt", "required">, convex_values0.VLiteral<"lte", "required">, convex_values0.VLiteral<"gt", "required">, convex_values0.VLiteral<"gte", "required">, convex_values0.VLiteral<"eq", "required">, convex_values0.VLiteral<"in", "required">, convex_values0.VLiteral<"not_in", "required">, convex_values0.VLiteral<"ne", "required">, convex_values0.VLiteral<"contains", "required">, convex_values0.VLiteral<"starts_with", "required">, convex_values0.VLiteral<"ends_with", "required">], "optional", never>;
168
168
  value: convex_values0.VUnion<string | number | boolean | string[] | number[] | null, [convex_values0.VString<string, "required">, convex_values0.VFloat64<number, "required">, convex_values0.VBoolean<boolean, "required">, convex_values0.VArray<string[], convex_values0.VString<string, "required">, "required">, convex_values0.VArray<number[], convex_values0.VFloat64<number, "required">, "required">, convex_values0.VNull<null, "required">], "required", never>;
169
- }, "required", "mode" | "operator" | "value" | "field" | "connector">, "optional">;
170
- }, "required", "limit" | "where" | "model" | "select" | "offset" | "sortBy" | "sortBy.field" | "sortBy.direction">;
169
+ }, "required", "mode" | "connector" | "field" | "operator" | "value">, "optional">;
170
+ }, "required", "model" | "where" | "limit" | "offset" | "select" | "sortBy" | "sortBy.field" | "sortBy.direction">;
171
171
  declare const hasUniqueFields: (betterAuthSchema: BetterAuthDBSchema, model: string, input: Record<string, any>) => boolean;
172
172
  declare const checkUniqueFields: <Schema extends SchemaDefinition<any, any>>(ctx: GenericQueryCtx<GenericDataModel>, schema: Schema, betterAuthSchema: BetterAuthDBSchema, table: string, input: Record<string, any>, doc?: Record<string, any>) => Promise<void>;
173
173
  declare const selectFields: <T extends TableNamesInDataModel<GenericDataModel>, D extends DocumentByName<GenericDataModel, T>>(doc: D | null, select?: string[]) => D | null;
@@ -3,8 +3,8 @@ import { r as partial } from "../validators-CIoUYCqO.js";
3
3
  import { i as defineAuth, n as createDisabledAuthRuntime, r as getGeneratedAuthDisabledReason, t as DEFAULT_AUTH_DEFINITION_PATH } from "../generated-contract-disabled-RyPYlwq0.js";
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
- import { C as eq, o as getAggregateIndexes } from "../index-utils-C1DyktHe.js";
7
- import { o as mergedStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-DdSg3fuk.js";
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";
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";
@@ -2341,12 +2341,16 @@ type ClearIndexChunkResult = {
2341
2341
  * reports whether anything is left. Callers drive it to completion across
2342
2342
  * transactions, so clearing a large index never has to fit in one mutation.
2343
2343
  *
2344
- * Members are removed through the normal delta machinery rather than raw
2345
- * deletes, so buckets and extrema stay consistent with the members that remain
2346
- * at every intermediate step. That keeps concurrent writers correct while the
2347
- * clear drains. Residual bucket/extrema rows (drift with no member behind them)
2348
- * are swept only once no members are left, and the loop re-checks members
2349
- * afterwards.
2344
+ * Member rows are deleted outright rather than run back through the delta
2345
+ * machinery. The bucket and extrema branches below drop every row this index
2346
+ * owns whatever it holds, so folding a removal delta into a bucket first would
2347
+ * only rewrite a document this same clear is about to delete.
2348
+ *
2349
+ * Branch order is load-bearing: buckets and extrema are swept only once no
2350
+ * members are left, and the loop re-checks members afterwards. The intermediate
2351
+ * "members gone, buckets still populated" state is safe because `setCountState`
2352
+ * refuses to leave CLEARING while a bucket or extrema row survives, and the
2353
+ * CLEARING write barrier keeps concurrent writers out of the index.
2350
2354
  */
2351
2355
  declare const clearCountIndexChunk: (db: GenericDatabaseWriter<any>, tableName: string, indexName: string, batchSize: number) => Promise<ClearIndexChunkResult>;
2352
2356
  declare const listCountStates: (db: GenericDatabaseReader<any> | GenericDatabaseWriter<any>) => Promise<CountState[]>;
@@ -2392,10 +2396,20 @@ declare const ensureRankAllowedForRls: (tableConfig: TableRelationalConfig, rlsM
2392
2396
  declare const compileRankPlan: (tableConfig: TableRelationalConfig, indexName: string, where: unknown) => RankQueryPlan;
2393
2397
  declare const ensureRankIndexReady: (db: GenericDatabaseReader<any> | GenericDatabaseWriter<any>, tableName: string, indexName: string) => Promise<void>;
2394
2398
  /**
2395
- * Removes at most `batchSize` rank members and reports whether anything is
2396
- * left. Each member is removed from the btree before its row is dropped, so the
2397
- * tree stays consistent with the members that remain and a partially drained
2398
- * clear can safely resume in a later mutation.
2399
+ * Removes at most `batchSize` documents of a rank index's stored state and
2400
+ * reports whether anything is left. Callers drive it to completion across
2401
+ * transactions, so clearing a large index never has to fit in one mutation.
2402
+ *
2403
+ * Member rows are dropped without touching the btree. The tree branch below
2404
+ * deletes every node whatever it contains, so removing a member key first would
2405
+ * only buy a root-to-leaf descent plus an aggregate patch per level on nodes
2406
+ * this same clear is about to delete.
2407
+ *
2408
+ * Branch order is load-bearing: a member delete recreates the tree it lands on,
2409
+ * so the members must be gone before the first node is dropped. The
2410
+ * intermediate "members gone, tree still full" state is safe because
2411
+ * `setCountState` refuses to leave CLEARING while a tree row survives, and the
2412
+ * CLEARING write barrier keeps concurrent writers out of the index.
2399
2413
  */
2400
2414
  declare const clearRankIndexChunk: (db: GenericDatabaseWriter<any>, tableName: string, indexName: string, batchSize: number) => Promise<{
2401
2415
  done: boolean;
@@ -2953,6 +2967,17 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
2953
2967
  private _normalizeOrderByValue;
2954
2968
  private _normalizeOrderBy;
2955
2969
  private _orderBySpecs;
2970
+ /**
2971
+ * Whether rows can be missing this sort column or hold null in it.
2972
+ *
2973
+ * Only the declared validator can answer: a column that is not `.notNull()`
2974
+ * compiles to `v.optional(v.union(v.null(), T))`, so both absent and null are
2975
+ * legal stored values — and `timestamp().notNull().defaultNow()` on
2976
+ * `createdAt` is deliberately emitted as optional for migration safety, so
2977
+ * reading `config.notNull` alone would answer wrong for it. Anything the
2978
+ * lookup cannot resolve counts as nullable, which only costs a pushdown.
2979
+ */
2980
+ private _isNullableOrderField;
2956
2981
  private _resolveNonPaginatedLimit;
2957
2982
  private _compareByOrderSpecs;
2958
2983
  /**
@@ -3043,6 +3068,33 @@ declare class GelRelationalQuery<TSchema extends TablesRelationalConfig, TTableC
3043
3068
  * the field produces that order.
3044
3069
  */
3045
3070
  private _findStreamOrderIndex;
3071
+ /**
3072
+ * The compiled index union, as one ordered stream.
3073
+ *
3074
+ * Each probe is its own index range, so the union is only usable where the
3075
+ * merged order is the order the read has to produce. `mergedStream` orders by
3076
+ * a *suffix* of the index key, and a suffix may only drop key components the
3077
+ * probes all pin to a single value — so the requested field has to sit inside
3078
+ * the pinned run or immediately after it. Returns null when it does not, and
3079
+ * the caller falls back to the plan's plain index range or a bounded scan.
3080
+ */
3081
+ private _buildProbeUnionStream;
3082
+ /**
3083
+ * The read the compiled plan describes, as a stream, with nothing filtered
3084
+ * yet.
3085
+ *
3086
+ * Precedence: the compiled index union, then the compiled index range, then
3087
+ * the caller's pinned `.withIndex(...)`, then an index that supplies the
3088
+ * requested order, then a full scan. The first two rungs can only ever refine
3089
+ * what the caller pinned — `_toConvexQuery` discards a compiled plan that
3090
+ * would displace a caller's index or its bounds before it gets here.
3091
+ *
3092
+ * `probeUnion` tells the caller the read is bounded by index ranges rather
3093
+ * than by scan length, which is what makes a scan budget unnecessary. A
3094
+ * rejected union stays unanchored; its original predicate remains in
3095
+ * `queryConfig.postFilters` for the caller to apply while pulling the scan.
3096
+ */
3097
+ private _buildPlanStream;
3046
3098
  private _buildBasePipelineStream;
3047
3099
  /**
3048
3100
  * Stream equivalent of the `db.query(...)` chain, used when a post-fetch
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as Columns, D as TableName, E as RlsPolicies, O as createSystemFields, S as getRankIndexes, T as OrmSchemaExtensions, _ as loadEsbuild, a as generateMeta, b as getAggregateIndexes, c as resolveSchemaDefaultExport, f as createProjectJiti, g as loadDotenv, h as loadClackPrompts, i as resolveConfiguredBackend, l as schemaDeclaresAggregateIndexes, m as loadBabelParser, n as withLocalCodegenEnv, o as getConvexConfig, p as CRPC_BUILDER_STUB_SOURCE, r as loadCliConfig, s as collectSchemaTables, t as getLocalBackendEnvVars, u as logger, v as highlighter, w as EnableRLS, x as getIndexes, y as isColorEnabled } from "./local-env-DGSkmXQk.mjs";
2
+ import { C as Columns, D as TableName, E as RlsPolicies, O as createSystemFields, S as getRankIndexes, T as OrmSchemaExtensions, _ as loadEsbuild, a as generateMeta, b as getAggregateIndexes, c as resolveSchemaDefaultExport, f as createProjectJiti, g as loadDotenv, h as loadClackPrompts, i as resolveConfiguredBackend, l as schemaDeclaresAggregateIndexes, m as loadBabelParser, n as withLocalCodegenEnv, o as getConvexConfig, p as CRPC_BUILDER_STUB_SOURCE, r as loadCliConfig, s as collectSchemaTables, t as getLocalBackendEnvVars, u as logger, v as highlighter, w as EnableRLS, x as getIndexes, y as isColorEnabled } from "./local-env-CYYSNf_o.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import fs, { existsSync, readFileSync } from "node:fs";
5
5
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve } from "node:path";
@@ -199,20 +199,20 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
199
199
  count: convex_server0.RegisteredQuery<"internal", {
200
200
  where?: {
201
201
  mode?: "sensitive" | "insensitive" | undefined;
202
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
203
202
  connector?: "AND" | "OR" | undefined;
204
- value: string | number | boolean | string[] | number[] | null;
203
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
205
204
  field: string;
205
+ value: string | number | boolean | string[] | number[] | null;
206
206
  }[] | undefined;
207
207
  model: string;
208
208
  }, Promise<number | null>>;
209
209
  consumeOne: convex_server0.RegisteredMutation<"internal", {
210
210
  input: {
211
211
  where?: {
212
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
213
212
  connector?: "AND" | "OR" | undefined;
214
- value: string | number | boolean | string[] | number[] | null;
213
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
215
214
  field: string;
215
+ value: string | number | boolean | string[] | number[] | null;
216
216
  }[] | undefined;
217
217
  model: string;
218
218
  } | {
@@ -235,26 +235,26 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
235
235
  };
236
236
  }, Promise<any>>;
237
237
  deleteMany: convex_server0.RegisteredMutation<"internal", {
238
- paginationOpts: {
239
- id?: number;
240
- endCursor?: string | null;
241
- maximumRowsRead?: number;
242
- maximumBytesRead?: number;
243
- numItems: number;
244
- cursor: string | null;
245
- };
246
238
  input: {
247
239
  where?: {
248
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
249
240
  connector?: "AND" | "OR" | undefined;
250
- value: string | number | boolean | string[] | number[] | null;
241
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
251
242
  field: string;
243
+ value: string | number | boolean | string[] | number[] | null;
252
244
  }[] | undefined;
253
245
  model: string;
254
246
  } | {
255
247
  where?: any[] | undefined;
256
248
  model: string;
257
249
  };
250
+ paginationOpts: {
251
+ id?: number;
252
+ endCursor?: string | null;
253
+ maximumRowsRead?: number;
254
+ maximumBytesRead?: number;
255
+ numItems: number;
256
+ cursor: string | null;
257
+ };
258
258
  }, Promise<{
259
259
  count: number;
260
260
  ids: any[];
@@ -266,10 +266,10 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
266
266
  deleteOne: convex_server0.RegisteredMutation<"internal", {
267
267
  input: {
268
268
  where?: {
269
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
270
269
  connector?: "AND" | "OR" | undefined;
271
- value: string | number | boolean | string[] | number[] | null;
270
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
272
271
  field: string;
272
+ value: string | number | boolean | string[] | number[] | null;
273
273
  }[] | undefined;
274
274
  model: string;
275
275
  } | {
@@ -278,20 +278,21 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
278
278
  };
279
279
  }, Promise<Record<string, unknown> | undefined>>;
280
280
  findMany: convex_server0.RegisteredQuery<"internal", {
281
- limit?: number | undefined;
282
281
  join?: any;
283
282
  where?: {
284
283
  mode?: "sensitive" | "insensitive" | undefined;
285
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
286
284
  connector?: "AND" | "OR" | undefined;
287
- value: string | number | boolean | string[] | number[] | null;
285
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
288
286
  field: string;
287
+ value: string | number | boolean | string[] | number[] | null;
289
288
  }[] | undefined;
289
+ limit?: number | undefined;
290
290
  offset?: number | undefined;
291
291
  sortBy?: {
292
292
  field: string;
293
293
  direction: "asc" | "desc";
294
294
  } | undefined;
295
+ model: string;
295
296
  paginationOpts: {
296
297
  id?: number;
297
298
  endCursor?: string | null;
@@ -300,16 +301,15 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
300
301
  numItems: number;
301
302
  cursor: string | null;
302
303
  };
303
- model: string;
304
304
  }, Promise<PaginationResult<convex_server0.GenericDocument>>>;
305
305
  findOne: convex_server0.RegisteredQuery<"internal", {
306
306
  join?: any;
307
307
  where?: {
308
308
  mode?: "sensitive" | "insensitive" | undefined;
309
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
310
309
  connector?: "AND" | "OR" | undefined;
311
- value: string | number | boolean | string[] | number[] | null;
310
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
312
311
  field: string;
312
+ value: string | number | boolean | string[] | number[] | null;
313
313
  }[] | undefined;
314
314
  select?: string[] | undefined;
315
315
  model: string;
@@ -319,10 +319,10 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
319
319
  input: {
320
320
  where?: {
321
321
  mode?: "sensitive" | "insensitive" | undefined;
322
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
323
322
  connector?: "AND" | "OR" | undefined;
324
- value: string | number | boolean | string[] | number[] | null;
323
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
325
324
  field: string;
325
+ value: string | number | boolean | string[] | number[] | null;
326
326
  }[] | undefined;
327
327
  set?: Record<string, any> | undefined;
328
328
  model: string;
@@ -331,31 +331,31 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
331
331
  }, Promise<any>>;
332
332
  rotateKeys: convex_server0.RegisteredAction<"internal", {}, Promise<unknown>>;
333
333
  updateMany: convex_server0.RegisteredMutation<"internal", {
334
- paginationOpts: {
335
- id?: number;
336
- endCursor?: string | null;
337
- maximumRowsRead?: number;
338
- maximumBytesRead?: number;
339
- numItems: number;
340
- cursor: string | null;
341
- };
342
334
  input: {
343
335
  where?: {
344
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
345
336
  connector?: "AND" | "OR" | undefined;
346
- value: string | number | boolean | string[] | number[] | null;
337
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
347
338
  field: string;
339
+ value: string | number | boolean | string[] | number[] | null;
348
340
  }[] | undefined;
341
+ model: string;
349
342
  update: {
350
343
  [x: string]: unknown;
351
344
  [x: number]: unknown;
352
345
  [x: symbol]: unknown;
353
346
  };
354
- model: string;
355
347
  } | {
356
348
  where?: any[] | undefined;
357
- update: any;
358
349
  model: string;
350
+ update: any;
351
+ };
352
+ paginationOpts: {
353
+ id?: number;
354
+ endCursor?: string | null;
355
+ maximumRowsRead?: number;
356
+ maximumBytesRead?: number;
357
+ numItems: number;
358
+ cursor: string | null;
359
359
  };
360
360
  }, Promise<{
361
361
  count: number;
@@ -368,21 +368,21 @@ declare const createApi: <Schema extends SchemaDefinition<any, any>, DataModel e
368
368
  updateOne: convex_server0.RegisteredMutation<"internal", {
369
369
  input: {
370
370
  where?: {
371
- operator?: "eq" | "ne" | "lt" | "lte" | "gt" | "gte" | "in" | "not_in" | "contains" | "starts_with" | "ends_with" | undefined;
372
371
  connector?: "AND" | "OR" | undefined;
373
- value: string | number | boolean | string[] | number[] | null;
372
+ operator?: "lt" | "lte" | "gt" | "gte" | "eq" | "in" | "not_in" | "ne" | "contains" | "starts_with" | "ends_with" | undefined;
374
373
  field: string;
374
+ value: string | number | boolean | string[] | number[] | null;
375
375
  }[] | undefined;
376
+ model: string;
376
377
  update: {
377
378
  [x: string]: unknown;
378
379
  [x: number]: unknown;
379
380
  [x: symbol]: unknown;
380
381
  };
381
- model: string;
382
382
  } | {
383
383
  where?: any[] | undefined;
384
- update: any;
385
384
  model: string;
385
+ update: any;
386
386
  };
387
387
  }, Promise<any>>;
388
388
  };
@@ -458,10 +458,11 @@ const hasColumnPrefix = (index, columns) => {
458
458
  *
459
459
  * Convex walks an index in full key order, so once a leading run of fields is
460
460
  * pinned to a single value by `eq`, the remainder of the scan is already sorted
461
- * by the next index field — and by `_creationTime` once every declared field is
462
- * pinned, because Convex appends it as the implicit trailing key. When that
463
- * field is the one the caller asked to sort by, `.order(dir).take(n)` returns
464
- * the exact page and nothing has to be collected and sorted afterwards.
461
+ * by the next index field, then the one after that — and by `_creationTime`
462
+ * once every declared field is consumed, because Convex appends it as the
463
+ * implicit trailing key. When that sequence is the one the caller asked to sort
464
+ * by, `.order(dir).take(n)` returns the exact page and nothing has to be
465
+ * collected and sorted afterwards.
465
466
  *
466
467
  * This is the single owner of that decision. The top-level query planner and
467
468
  * the relation loader both call it, so a bound one of them can push into the
@@ -472,12 +473,34 @@ const hasColumnPrefix = (index, columns) => {
472
473
  */
473
474
  function resolveIndexOrderPushdown(params) {
474
475
  const { indexFields, pinnedEqCount, orderSpecs } = params;
475
- if (!indexFields || orderSpecs.length !== 1) return null;
476
- const primary = orderSpecs[0];
476
+ if (!indexFields || orderSpecs.length === 0) return null;
477
477
  const eqCount = Math.min(Math.max(pinnedEqCount, 0), indexFields.length);
478
- for (let i = 0; i < eqCount; i += 1) if (indexFields[i] === primary.field) return primary.direction;
479
- const nativeField = eqCount >= indexFields.length ? INTERNAL_CREATION_TIME_FIELD : indexFields[eqCount];
480
- return primary.field === nativeField ? primary.direction : null;
478
+ const pinnedFields = new Set(indexFields.slice(0, eqCount));
479
+ /** Next unpinned index key the scan will sort by. */
480
+ let cursor = eqCount;
481
+ /** Direction every unpinned spec has to agree on. */
482
+ let direction = null;
483
+ let consumedCreationTime = false;
484
+ let hasNullableSpec = false;
485
+ for (const spec of orderSpecs) {
486
+ if (pinnedFields.has(spec.field)) continue;
487
+ if (direction === null) direction = spec.direction;
488
+ else if (direction !== spec.direction) return null;
489
+ if (spec.nullable !== false) hasNullableSpec = true;
490
+ if (consumedCreationTime) return null;
491
+ if (cursor >= indexFields.length) {
492
+ if (spec.field !== INTERNAL_CREATION_TIME_FIELD) return null;
493
+ consumedCreationTime = true;
494
+ continue;
495
+ }
496
+ if (spec.field !== indexFields[cursor]) return null;
497
+ cursor += 1;
498
+ }
499
+ if (orderSpecs.length > 1 && direction !== null && cursor < indexFields.length) return null;
500
+ const primary = orderSpecs[0];
501
+ if (direction !== null && pinnedFields.has(primary.field) && primary.direction !== direction && !consumedCreationTime) return null;
502
+ if (orderSpecs.length > 1 && hasNullableSpec) return null;
503
+ return direction ?? orderSpecs[0].direction;
481
504
  }
482
505
  function findRelationIndex(table, columns, relationName, targetTableName, strict = true, allowFullScan = false, orderSpecs = []) {
483
506
  const indexes = getIndexes(table);