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.
@@ -1,77 +1,8 @@
1
1
  import { t as DirectAggregate } from "../../runtime-B-8HKSIE.js";
2
2
  import { n as Columns } from "../../symbols-DDNAddkd.js";
3
3
  import { d as INTERNAL_CREATION_TIME_FIELD, f as PUBLIC_CREATED_AT_FIELD, m as usesSystemCreatedAtAlias } from "../../index-utils-DvK7P6Q1.js";
4
- import { Q as normalizeTemporalComparableValue, a as AGGREGATE_STATE_TABLE, c as getAggregateIndexDefinitions, d as COUNT_ERROR, ht as mapWithConcurrency, i as AGGREGATE_RANK_TREE_TABLE, l as getRankIndexDefinitions, m as createError, n as AGGREGATE_EXTREMA_TABLE, r as AGGREGATE_MEMBER_TABLE, s as rankAggregateName, t as AGGREGATE_BUCKET_TABLE, u as AGGREGATE_ERROR } from "../../schema-Bh7AmJwY.js";
4
+ import { C as createError, _ as createOrmTransactionMemo, a as AGGREGATE_STATE_TABLE, b as COUNT_ERROR, c as getAggregateIndexDefinitions, d as flushOrmWriteBatch, f as isOrmWriteBatchOpen, h as createOrmWriteMemo, i as AGGREGATE_RANK_TREE_TABLE, l as getRankIndexDefinitions, n as AGGREGATE_EXTREMA_TABLE, r as AGGREGATE_MEMBER_TABLE, s as rankAggregateName, st as normalizeTemporalComparableValue, t as AGGREGATE_BUCKET_TABLE, u as enqueueOrmWriteBatch, wt as mapWithConcurrency, y as AGGREGATE_ERROR } from "../../schema-BF4P0ZjS.js";
5
5
 
6
- //#region src/orm/transaction-cache.ts
7
- /**
8
- * Per-transaction memo storage for the ORM.
9
- *
10
- * The ORM already has isolate-, execution-, statement- and row-scoped memos.
11
- * The lifetime it lacked is the one a hook needs: `prependWriteBarrier` is
12
- * built inside `createOrmDbLifecycle`, which `createOrm` runs at module scope,
13
- * so a flag in that closure lives as long as the isolate and would leak an
14
- * answer from one transaction into the next.
15
- *
16
- * Deliberately dependency-free, for the same reason as `write-fanout`:
17
- * `aggregate-index/runtime` is contractually unreachable from `orm/index`
18
- * (`import-graph.test.ts`), so importing `lifecycle` here to read one symbol
19
- * would drag the trigger runtime into the aggregate entry's bundle.
20
- * `Symbol.for` is registry-based, so re-declaring the key resolves to the same
21
- * symbol `lifecycle` installs.
22
- */
23
- const ORMLIFECYCLE_INNER_DB = Symbol.for("kitcn:OrmLifecycleInnerDB");
24
- /**
25
- * The object whose identity stands in for "this transaction".
26
- *
27
- * Convex builds `ctx.db` fresh on every UDF invocation, so it can never be
28
- * shared by two transactions. `getOrmLifecycleInnerDb` cannot be used on its
29
- * own: the lifecycle refuses to wrap readers and returns a no-op wrapper for
30
- * schemas with no triggers and no aggregate indexes, so the inner-db symbol is
31
- * absent for every query and for most mutations. Resolving through it when it
32
- * is there, and falling back to the db itself when it is not, converges on the
33
- * same raw writer from the main scope, `skipRules`, `withoutTriggers` and the
34
- * scheduled workers.
35
- *
36
- * A nested `ctx.runMutation` shares the transaction but gets its own `ctx.db`,
37
- * so it starts a fresh memo. That direction only costs extra reads.
38
- */
39
- const resolveTransactionAnchor = (db) => {
40
- if (typeof db !== "object" || db === null) return;
41
- const inner = db[ORMLIFECYCLE_INNER_DB];
42
- return typeof inner === "object" && inner !== null ? inner : db;
43
- };
44
- /**
45
- * One memo namespace with transaction lifetime.
46
- *
47
- * The store is a `WeakMap` keyed on the anchor rather than a slot on the db,
48
- * because `createDatabase` promises not to mutate the `ctx.db` it was handed.
49
- * Entries die with the transaction's db object.
50
- *
51
- * Callers own staleness: only memoize a fact that nothing inside the
52
- * transaction can invalidate.
53
- */
54
- const createOrmTransactionMemo = () => {
55
- const byTransaction = /* @__PURE__ */ new WeakMap();
56
- return {
57
- get(db, key) {
58
- const anchor = resolveTransactionAnchor(db);
59
- return anchor ? byTransaction.get(anchor)?.get(key) : void 0;
60
- },
61
- set(db, key, value) {
62
- const anchor = resolveTransactionAnchor(db);
63
- if (!anchor) return;
64
- const existing = byTransaction.get(anchor);
65
- if (existing) {
66
- existing.set(key, value);
67
- return;
68
- }
69
- byTransaction.set(anchor, new Map([[key, value]]));
70
- }
71
- };
72
- };
73
-
74
- //#endregion
75
6
  //#region src/orm/aggregate-index/runtime.ts
76
7
  const UNDEFINED_SENTINEL = "__kitcnUndefined";
77
8
  const FLOAT64_SIGN_BIT = 1n << 63n;
@@ -801,6 +732,54 @@ const listBucketsByHash = async (db, tableName, indexName, keyHash) => await db.
801
732
  const getBucketByKey = async (db, tableName, indexName, keyParts) => {
802
733
  return (await listBucketsByHash(db, tableName, indexName, serializeCountKeyParts(keyParts))).find((bucket) => deepEquals(bucket.keyParts, keyParts)) ?? null;
803
734
  };
735
+ const bucketRowByKey = createOrmWriteMemo();
736
+ const memberRowByDoc = createOrmWriteMemo();
737
+ const aggregateRowMemoKey = (...parts) => parts.join("\0");
738
+ const bucketMemoKey = (tableName, indexName, keyHash) => aggregateRowMemoKey(tableName, indexName, keyHash);
739
+ /**
740
+ * `kind` leads, because the member table stores a metric and a rank row for the
741
+ * same document under index names the schema lets collide.
742
+ */
743
+ const memberMemoKey = (tableName, indexName, docId) => aggregateRowMemoKey(AGGREGATE_STATE_KIND_METRIC, tableName, indexName, docId);
744
+ /**
745
+ * A stored row is the backend's object, and Convex's test backend hands out the
746
+ * document it holds rather than a copy. One shallow clone of the row and of the
747
+ * containers it owns makes the memo entry the memo's own, so an entry means the
748
+ * same thing whatever the backend does with the document afterwards.
749
+ */
750
+ const cloneStoredRow = (row) => {
751
+ const clone = { ...row };
752
+ for (const [field, value] of Object.entries(clone)) {
753
+ if (Array.isArray(value)) {
754
+ clone[field] = [...value];
755
+ continue;
756
+ }
757
+ if (typeof value === "object" && value !== null) clone[field] = { ...value };
758
+ }
759
+ return clone;
760
+ };
761
+ const rememberBucket = (db, memoKey, row) => {
762
+ bucketRowByKey.set(db, memoKey, { row: row === null ? null : cloneStoredRow(row) });
763
+ };
764
+ const rememberMember = (db, memoKey, row) => {
765
+ memberRowByDoc.set(db, memoKey, { row: row === null ? null : cloneStoredRow(row) });
766
+ };
767
+ /** `getBucketByKey` within the current write segment. Write path only. */
768
+ const readBucketForWrite = async (db, tableName, indexName, keyParts, memoKey) => {
769
+ const memoized = bucketRowByKey.get(db, memoKey);
770
+ if (memoized) return memoized.row;
771
+ const bucket = await getBucketByKey(db, tableName, indexName, keyParts);
772
+ rememberBucket(db, memoKey, bucket);
773
+ return bucket;
774
+ };
775
+ /** `getMemberByDoc` within the current write segment. Write path only. */
776
+ const readMemberForWrite = async (db, tableName, indexName, docId, memoKey) => {
777
+ const memoized = memberRowByDoc.get(db, memoKey);
778
+ if (memoized) return memoized.row;
779
+ const member = await getMemberByDoc(db, tableName, indexName, docId);
780
+ rememberMember(db, memoKey, member);
781
+ return member;
782
+ };
804
783
  const listBucketsByHashPrefix = async (db, tableName, indexName, scan, limit) => await db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index_hash", (q) => {
805
784
  const scoped = q.eq("tableKey", tableName).eq("indexName", indexName);
806
785
  return (scan.after === void 0 ? scoped.gte("keyHash", scan.start) : scoped.gt("keyHash", scan.after)).lt("keyHash", scan.end);
@@ -814,34 +793,45 @@ const takeBucketsForIndex = async (db, tableName, indexName, limit) => await db.
814
793
  const takeExtremaForIndex = async (db, tableName, indexName, limit) => await db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).take(limit);
815
794
  const applyBucketDelta = async (db, tableName, indexName, keyParts, deltaCount, deltaSums, deltaNonNullCounts) => {
816
795
  if (deltaCount === 0 && Object.keys(deltaSums).length === 0 && Object.keys(deltaNonNullCounts).length === 0) return;
817
- const existing = await getBucketByKey(db, tableName, indexName, keyParts);
796
+ const keyHash = serializeCountKeyParts(keyParts);
797
+ const memoKey = bucketMemoKey(tableName, indexName, keyHash);
798
+ const existing = await readBucketForWrite(db, tableName, indexName, keyParts, memoKey);
818
799
  const now = Date.now();
819
800
  if (!existing) {
820
801
  if (deltaCount < 0) return;
821
- await db.insert(AGGREGATE_BUCKET_TABLE, {
802
+ const inserted = {
822
803
  tableKey: tableName,
823
804
  indexName,
824
- keyHash: serializeCountKeyParts(keyParts),
805
+ keyHash,
825
806
  keyParts,
826
807
  count: deltaCount,
827
808
  sumValues: deltaSums,
828
809
  nonNullCountValues: deltaNonNullCounts,
829
810
  updatedAt: now
811
+ };
812
+ const id = await db.insert(AGGREGATE_BUCKET_TABLE, inserted);
813
+ rememberBucket(db, memoKey, {
814
+ ...inserted,
815
+ _id: id
830
816
  });
831
817
  return;
832
818
  }
833
819
  const nextCount = existing.count + deltaCount;
834
820
  if (nextCount <= 0) {
835
821
  await db.delete(AGGREGATE_BUCKET_TABLE, existing._id);
822
+ rememberBucket(db, memoKey, null);
836
823
  return;
837
824
  }
838
- const nextSumValues = mergeSumValues(normalizeSumValues(existing.sumValues), deltaSums);
839
- const nextNonNullCountValues = mergeCountValues(normalizeNonNullCountValues(existing.nonNullCountValues), deltaNonNullCounts);
840
- await db.patch(AGGREGATE_BUCKET_TABLE, existing._id, {
825
+ const patch = {
841
826
  count: nextCount,
842
- sumValues: nextSumValues,
843
- nonNullCountValues: nextNonNullCountValues,
827
+ sumValues: mergeSumValues(normalizeSumValues(existing.sumValues), deltaSums),
828
+ nonNullCountValues: mergeCountValues(normalizeNonNullCountValues(existing.nonNullCountValues), deltaNonNullCounts),
844
829
  updatedAt: now
830
+ };
831
+ await db.patch(AGGREGATE_BUCKET_TABLE, existing._id, patch);
832
+ rememberBucket(db, memoKey, {
833
+ ...existing,
834
+ ...patch
845
835
  });
846
836
  };
847
837
  const encodeNumberSortKey = (value) => {
@@ -926,6 +916,7 @@ const matchesRangePlanBucket = (options) => {
926
916
  return true;
927
917
  };
928
918
  const readPlanBuckets = async (db, plan) => {
919
+ await flushOrmWriteBatch(db);
929
920
  if (!plan.rangeConstraint) return (await mapWithConcurrency(plan.keyCandidates ?? buildCandidateKeys(plan.indexFields, plan.fieldValues), AGGREGATE_BUCKET_READ_CONCURRENCY, (keyParts) => getBucketByKey(db, plan.tableName, plan.indexName, keyParts))).filter((bucket) => bucket !== null);
930
921
  const prefixCandidates = buildCandidateKeys(plan.rangeConstraint.prefixFields, plan.fieldValues);
931
922
  const rangeFieldIndex = plan.indexFields.indexOf(plan.rangeConstraint.fieldName);
@@ -992,6 +983,7 @@ const getPlanBucketCacheKey = (plan) => serializeStable({
992
983
  postFieldValues: sortFieldValueRecord(plan.postFieldValues)
993
984
  });
994
985
  const readPlanBucketsWithCache = async (db, plan, bucketCache) => {
986
+ await flushOrmWriteBatch(db);
995
987
  if (!bucketCache) return await readPlanBuckets(db, plan);
996
988
  const cacheKey = getPlanBucketCacheKey(plan);
997
989
  const existing = bucketCache.get(cacheKey);
@@ -1053,6 +1045,7 @@ const readKeyExtrema = async (db, params) => {
1053
1045
  };
1054
1046
  const readExtremaFromBuckets = async (db, plan, bucketCache) => {
1055
1047
  if (plan.metric.kind !== "min" && plan.metric.kind !== "max") throw new Error("readExtremaFromBuckets() requires a min/max aggregate plan.");
1048
+ await flushOrmWriteBatch(db);
1056
1049
  let selected = null;
1057
1050
  const buckets = await readPlanBucketsWithCache(db, plan, bucketCache);
1058
1051
  const metric = plan.metric;
@@ -1151,11 +1144,13 @@ const computeMembershipDelta = (existing, params) => {
1151
1144
  const { tableName, indexName, docId, keyParts, metricValues } = params;
1152
1145
  if (!keyParts || !metricValues) {
1153
1146
  if (!existing) return {
1147
+ docId,
1154
1148
  buckets: [],
1155
1149
  extrema: [],
1156
1150
  member: { kind: "none" }
1157
1151
  };
1158
1152
  return {
1153
+ docId,
1159
1154
  buckets: [{
1160
1155
  keyHash: serializeCountKeyParts(existing.keyParts),
1161
1156
  keyParts: existing.keyParts,
@@ -1177,6 +1172,7 @@ const computeMembershipDelta = (existing, params) => {
1177
1172
  const normalizedNextNonNullCountValues = normalizeNonNullCountValues(metricValues.nonNullCountValues);
1178
1173
  const normalizedNextExtremaValues = normalizeExtremaValues(metricValues.extremaValues);
1179
1174
  if (existing && existing.keyHash === keyHash && deepEquals(existing.keyParts, normalizedKeyParts) && deepEquals(normalizeSumValues(existing.sumValues), normalizedNextSumValues) && deepEquals(normalizeNonNullCountValues(existing.nonNullCountValues), normalizedNextNonNullCountValues) && deepEquals(normalizeExtremaValues(existing.extremaValues), normalizedNextExtremaValues)) return {
1175
+ docId,
1180
1176
  buckets: [],
1181
1177
  extrema: [],
1182
1178
  member: { kind: "none" }
@@ -1210,14 +1206,28 @@ const computeMembershipDelta = (existing, params) => {
1210
1206
  extremaValues: normalizedNextExtremaValues,
1211
1207
  updatedAt: now
1212
1208
  };
1209
+ if (existing) {
1210
+ const { _id, ...priorFields } = existing;
1211
+ return {
1212
+ docId,
1213
+ buckets,
1214
+ extrema,
1215
+ member: {
1216
+ kind: "patch",
1217
+ id: _id,
1218
+ doc: memberFields,
1219
+ post: {
1220
+ ...priorFields,
1221
+ ...memberFields
1222
+ }
1223
+ }
1224
+ };
1225
+ }
1213
1226
  return {
1227
+ docId,
1214
1228
  buckets,
1215
1229
  extrema,
1216
- member: existing ? {
1217
- kind: "patch",
1218
- id: existing._id,
1219
- doc: memberFields
1220
- } : {
1230
+ member: {
1221
1231
  kind: "insert",
1222
1232
  doc: {
1223
1233
  ...memberFields,
@@ -1229,14 +1239,14 @@ const computeMembershipDelta = (existing, params) => {
1229
1239
  };
1230
1240
  };
1231
1241
  const computeAggregateMembershipDelta = async (db, params) => {
1232
- return computeMembershipDelta(await getMemberByDoc(db, params.tableName, params.indexName, params.docId), params);
1242
+ return computeMembershipDelta(await readMemberForWrite(db, params.tableName, params.indexName, params.docId, memberMemoKey(params.tableName, params.indexName, params.docId)), params);
1233
1243
  };
1234
1244
  /**
1235
- * Write half of aggregate reconciliation. Folds bucket deltas by key tuple and
1236
- * extrema deltas by (keyHash, field, value) so each storage document is read and
1237
- * written once regardless of how many source documents contributed to it.
1245
+ * Folds bucket deltas by key tuple and extrema deltas by
1246
+ * (keyHash, field, value), so each storage document is read and written once
1247
+ * regardless of how many source documents contributed to it.
1238
1248
  */
1239
- const flushAggregateMembershipDeltas = async (db, tableName, indexName, deltas) => {
1249
+ const foldStorageDeltas = (deltas) => {
1240
1250
  const bucketDeltas = /* @__PURE__ */ new Map();
1241
1251
  const extremaDeltas = /* @__PURE__ */ new Map();
1242
1252
  for (const delta of deltas) {
@@ -1270,28 +1280,139 @@ const flushAggregateMembershipDeltas = async (db, tableName, indexName, deltas)
1270
1280
  current.delta += entry.delta;
1271
1281
  }
1272
1282
  }
1273
- for (const bucket of bucketDeltas.values()) await applyBucketDelta(db, tableName, indexName, bucket.keyParts, bucket.deltaCount, bucket.deltaSums, bucket.deltaNonNullCounts);
1274
- for (const entry of extremaDeltas.values()) await applyExtremaDelta(db, tableName, indexName, entry.keyHash, entry.fieldName, entry.value, entry.delta);
1283
+ return {
1284
+ buckets: bucketDeltas,
1285
+ extrema: extremaDeltas
1286
+ };
1287
+ };
1288
+ /** Applies a fold to the bucket and extrema documents it names. */
1289
+ const applyFoldedStorageDeltas = async (db, tableName, indexName, folded) => {
1290
+ for (const bucket of folded.buckets.values()) await applyBucketDelta(db, tableName, indexName, bucket.keyParts, bucket.deltaCount, bucket.deltaSums, bucket.deltaNonNullCounts);
1291
+ for (const entry of folded.extrema.values()) await applyExtremaDelta(db, tableName, indexName, entry.keyHash, entry.fieldName, entry.value, entry.delta);
1292
+ };
1293
+ /**
1294
+ * One member row per source document. Nothing folds here — the member table
1295
+ * stores the per-document post-image the next reconciliation of that document
1296
+ * subtracts, so it is written eagerly even when the bucket write is deferred.
1297
+ */
1298
+ const applyMemberWrites = async (db, tableName, indexName, deltas) => {
1275
1299
  for (const delta of deltas) {
1276
1300
  const member = delta.member;
1301
+ if (member.kind === "none") continue;
1302
+ const memoKey = memberMemoKey(tableName, indexName, delta.docId);
1277
1303
  if (member.kind === "delete") {
1278
1304
  await db.delete(AGGREGATE_MEMBER_TABLE, member.id);
1305
+ rememberMember(db, memoKey, null);
1279
1306
  continue;
1280
1307
  }
1281
1308
  if (member.kind === "patch") {
1282
1309
  await db.patch(AGGREGATE_MEMBER_TABLE, member.id, member.doc);
1310
+ rememberMember(db, memoKey, {
1311
+ ...member.post,
1312
+ _id: member.id
1313
+ });
1283
1314
  continue;
1284
1315
  }
1285
- if (member.kind === "insert") await db.insert(AGGREGATE_MEMBER_TABLE, member.doc);
1316
+ const id = await db.insert(AGGREGATE_MEMBER_TABLE, member.doc);
1317
+ rememberMember(db, memoKey, {
1318
+ ...member.doc,
1319
+ _id: id
1320
+ });
1286
1321
  }
1287
1322
  };
1288
1323
  /**
1289
- * Single-document reconciliation. Flushes eagerly so user code reading an
1290
- * aggregate later in the same mutation sees its own writes.
1324
+ * Write half of aggregate reconciliation. Folds bucket deltas by key tuple and
1325
+ * extrema deltas by (keyHash, field, value) so each storage document is read and
1326
+ * written once regardless of how many source documents contributed to it.
1327
+ */
1328
+ const flushAggregateMembershipDeltas = async (db, tableName, indexName, deltas) => {
1329
+ await applyFoldedStorageDeltas(db, tableName, indexName, foldStorageDeltas(deltas));
1330
+ await applyMemberWrites(db, tableName, indexName, deltas);
1331
+ };
1332
+ const PENDING_STORAGE_WRITES_KEY = "aggregateStorageWrites";
1333
+ /**
1334
+ * Bucket and extrema deltas the open write batch has not applied yet, keyed by
1335
+ * the index that owns them.
1336
+ *
1337
+ * Reconciliation runs once per document, so there is nothing for the fold to
1338
+ * collapse inside a single call. Holding the deltas until the statement ends is
1339
+ * what gives the fold something to fold: a 40-row statement over one key tuple
1340
+ * becomes one bucket read and one bucket write instead of forty of each.
1341
+ */
1342
+ const pendingStorageWrites = createOrmTransactionMemo();
1343
+ /**
1344
+ * Grouped per index, never flat. `serializeCountKeyParts` is `JSON.stringify` of
1345
+ * the key tuple alone, so two indexes on different single string fields both
1346
+ * holding `"a"` produce the identical `keyHash`; a flat fold would write one
1347
+ * index's counts onto the other's bucket.
1348
+ */
1349
+ const pendingIndexKey = (tableName, indexName) => `${tableName}\u0000${indexName}`;
1350
+ const getPendingStorageWrites = (db) => {
1351
+ const existing = pendingStorageWrites.get(db, PENDING_STORAGE_WRITES_KEY);
1352
+ if (existing) return existing;
1353
+ const created = {
1354
+ byIndex: /* @__PURE__ */ new Map(),
1355
+ flush: async () => {
1356
+ for (;;) {
1357
+ const next = created.byIndex.entries().next();
1358
+ if (next.done) return;
1359
+ const [key, entry] = next.value;
1360
+ created.byIndex.delete(key);
1361
+ await applyFoldedStorageDeltas(db, entry.tableName, entry.indexName, foldStorageDeltas(entry.deltas));
1362
+ }
1363
+ }
1364
+ };
1365
+ pendingStorageWrites.set(db, PENDING_STORAGE_WRITES_KEY, created);
1366
+ return created;
1367
+ };
1368
+ const isEmptyMembershipDelta = (delta) => delta.buckets.length === 0 && delta.extrema.length === 0 && delta.member.kind === "none";
1369
+ /**
1370
+ * Single-document reconciliation.
1371
+ *
1372
+ * The bucket and extrema writes always go on the transaction's write queue,
1373
+ * never straight to storage: `applyBucketDelta` writes an absolute count
1374
+ * computed from the row it just read, so two of them interleaving would be a
1375
+ * lost update, and routing every one of them through a single drain is what
1376
+ * keeps them serialized. Inside a mutation statement the queue is held to the
1377
+ * end of the statement, which is what lets the fold collapse a page of
1378
+ * documents into one write per key tuple; outside one it is drained
1379
+ * immediately, so a raw `ctx.db` write behaves exactly as it did before.
1380
+ *
1381
+ * The member row is written per document either way, because it is the
1382
+ * pre-image the next reconciliation of this document subtracts. Outside a
1383
+ * statement it is still written after the bucket, preserving the previous
1384
+ * order; inside one it necessarily lands first, and a flush that throws
1385
+ * part-way leaves the index needing a backfill — which a partially applied fold
1386
+ * would anyway, whichever order the two halves ran in.
1387
+ *
1388
+ * Deferral is invisible to aggregate readers: every bucket- and extrema-backed
1389
+ * read path drains the queue first, so user code reading a count later in the
1390
+ * same mutation — including from a trigger firing mid-statement — still sees
1391
+ * its own writes.
1291
1392
  */
1292
1393
  const reconcileAggregateMembership = async (db, params) => {
1394
+ const { tableName, indexName } = params;
1293
1395
  const delta = await computeAggregateMembershipDelta(db, params);
1294
- await flushAggregateMembershipDeltas(db, params.tableName, params.indexName, [delta]);
1396
+ if (isEmptyMembershipDelta(delta)) return;
1397
+ const pending = getPendingStorageWrites(db);
1398
+ if (!enqueueOrmWriteBatch(db, pending.flush)) {
1399
+ await flushAggregateMembershipDeltas(db, tableName, indexName, [delta]);
1400
+ return;
1401
+ }
1402
+ const key = pendingIndexKey(tableName, indexName);
1403
+ const entry = pending.byIndex.get(key);
1404
+ if (entry) entry.deltas.push(delta);
1405
+ else pending.byIndex.set(key, {
1406
+ tableName,
1407
+ indexName,
1408
+ deltas: [delta]
1409
+ });
1410
+ if (isOrmWriteBatchOpen(db)) {
1411
+ await applyMemberWrites(db, tableName, indexName, [delta]);
1412
+ return;
1413
+ }
1414
+ await flushOrmWriteBatch(db);
1415
+ await applyMemberWrites(db, tableName, indexName, [delta]);
1295
1416
  };
1296
1417
  const computeCountKeyParts = (doc, fields) => fields.map((field) => normalizeUndefined(doc[field]));
1297
1418
  const applyAggregateIndexesForChange = async (db, tableName, aggregateIndexes, change) => {
@@ -1361,6 +1482,7 @@ const assertAggregateIndexesWritable = async (db, tableName, metricIndexNames, r
1361
1482
  * writing anything.
1362
1483
  */
1363
1484
  const isIndexStateDrained = async (db, kind, tableName, indexName) => {
1485
+ await flushOrmWriteBatch(db);
1364
1486
  if (await db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", kind).eq("tableKey", tableName).eq("indexName", indexName)).first()) return false;
1365
1487
  if (kind === AGGREGATE_STATE_KIND_RANK) return await db.query(AGGREGATE_RANK_TREE_TABLE).withIndex("by_aggregate_name", (q) => q.eq("aggregateName", rankAggregateName(tableName, indexName))).first() === null;
1366
1488
  const [bucket, extrema] = await Promise.all([db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first(), db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first()]);
@@ -1423,9 +1545,13 @@ const setCountStateError = async (db, tableName, indexName, error, kind = AGGREG
1423
1545
  * CLEARING write barrier keeps concurrent writers out of the index.
1424
1546
  */
1425
1547
  const clearCountIndexChunk = async (db, tableName, indexName, batchSize) => {
1548
+ await flushOrmWriteBatch(db);
1426
1549
  const members = await takeMembersForIndex(db, tableName, indexName, batchSize);
1427
1550
  if (members.length > 0) {
1428
- for (const member of members) await db.delete(AGGREGATE_MEMBER_TABLE, member._id);
1551
+ for (const member of members) {
1552
+ await db.delete(AGGREGATE_MEMBER_TABLE, member._id);
1553
+ rememberMember(db, memberMemoKey(member.tableKey, member.indexName, member.docId), null);
1554
+ }
1429
1555
  return {
1430
1556
  done: false,
1431
1557
  processed: members.length
@@ -1433,7 +1559,10 @@ const clearCountIndexChunk = async (db, tableName, indexName, batchSize) => {
1433
1559
  }
1434
1560
  const buckets = await takeBucketsForIndex(db, tableName, indexName, batchSize);
1435
1561
  if (buckets.length > 0) {
1436
- for (const bucket of buckets) await db.delete(AGGREGATE_BUCKET_TABLE, bucket._id);
1562
+ for (const bucket of buckets) {
1563
+ await db.delete(AGGREGATE_BUCKET_TABLE, bucket._id);
1564
+ rememberBucket(db, bucketMemoKey(bucket.tableKey, bucket.indexName, bucket.keyHash), null);
1565
+ }
1437
1566
  return {
1438
1567
  done: false,
1439
1568
  processed: buckets.length
@@ -1882,6 +2011,7 @@ function createCountBackfillHandlers(schema, getChunkRef) {
1882
2011
  if (args.tableName && args.indexName) {
1883
2012
  const tableName = args.tableName;
1884
2013
  const indexName = args.indexName;
2014
+ await flushOrmWriteBatch(ctx.db);
1885
2015
  const [bucket, extrema, metricMember, rankMember] = await Promise.all([
1886
2016
  ctx.db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first(),
1887
2017
  ctx.db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first(),
@@ -1,5 +1,5 @@
1
- import { $n as TableName, $t as OrmLifecycleChange, An as ConvexCheckBuilder, At as UpdateSet, Bn as ConvexTextBuilder, Br as IsPrimaryKey, Bt as RelationsBuilderColumnBase, C as MigrationStep, Cn as ConvexVectorIndexConfig, Cr as not, Ct as OrderByClause, D as defineMigration, Dn as searchIndex, Dr as startsWith, Dt as ReturningAll, E as buildMigrationPlan, En as rankIndex, Er as or, Et as PredicateWhereIndexConfig, Fn as ConvexUniqueConstraintBuilderOn, Fr as ColumnBuilderTypeConfig, Ft as ExtractTablesFromSchema, Gn as Columns, Gt as TablesRelationalConfig, H as RlsMode, Hn as text, Hr as NotNull, In as ConvexUniqueConstraintConfig, Ir as ColumnBuilderWithTableName, It as ExtractTablesWithRelations, Jn as OrmSchemaExtensionTables, Jt as ConvexDeletionBuilder, Kt as defineRelations, Ln as check, Lr as ColumnDataType, Lt as ManyConfig, M as DatabaseWithQuery, Mn as ConvexForeignKeyBuilder, Mr as ColumnBuilder, Mt as VectorSearchProvider, N as OrmReader$1, Nn as ConvexForeignKeyConfig, Nr as ColumnBuilderBaseConfig, Nt as unsetToken, O as defineMigrationSet, On as uniqueIndex, Ot as ReturningResult, P as OrmWriter$1, Pn as ConvexUniqueConstraintBuilder, Pr as ColumnBuilderRuntimeConfig, Qn as OrmSchemaTriggers, Qt as DiscriminatorBuilderConfig, Rn as foreignKey, Rr as DrizzleEntity, Rt as OneConfig, S as MigrationStateMap, Sn as ConvexVectorIndexBuilderOn, Sr as ne, St as MutationRunMode, T as MigrationWriteMode, Tn as index, Tr as notInArray, Tt as PaginatedResult, U as EdgeMetadata, V as RlsContext, Vn as ConvexTextBuilderInitial, Vr as IsUnique, Vt as RelationsBuilderColumnConfig, W as extractRelationsConfig, Wn as Brand, Wt as TableRelationalConfig, Xn as OrmSchemaExtensions, Xt as ConvexTable, Yt as ConvexDeletionConfig, Zn as OrmSchemaRelations, Zt as ConvexTableWithColumns, _ as MigrationManifestEntry, _n as ConvexRankIndexBuilderOn, _r as isNotNull, _t as MutationExecutionMode, an as RlsPolicy, ar as UnaryExpression, at as BuildRelationResult, b as MigrationRunStatus, bn as ConvexSearchIndexConfig, br as lt, bt as MutationResult, cn as rlsPolicy, cr as contains, ct as DBQueryConfig, d as MigrationAppliedState, dn as rlsRole, dr as fieldRef, dt as InferInsertModel, en as OrmLifecycleOperation, er as BinaryExpression, f as MigrationDefinition, fn as ConvexAggregateIndexBuilder, fr as gt, ft as InferModelFromColumns, g as MigrationDriftIssue, gn as ConvexRankIndexBuilder, gr as isFieldReference, gt as MutationExecuteResult, h as MigrationDocContext, hn as ConvexIndexBuilderOn, hr as inArray, ht as MutationExecuteConfig, i as OrmMigrationCapability, in as discriminator, ir as LogicalExpression, it as BuildQueryResult, j as DatabaseWithMutations, jn as ConvexCheckConfig, jr as AnyColumn, jt as VectorQueryConfig, k as detectMigrationDrift, kn as vectorIndex, kr as SystemFields, kt as ReturningSelection, ln as RlsRole, lr as endsWith, lt as FilterOperators, m as MigrationDoc, mn as ConvexIndexBuilder, mr as ilike, mt as InsertValue, n as OrmCapabilities, nn as convexTable, nr as FieldReference, nt as AggregateFieldValue, on as RlsPolicyConfig, or as and, ot as CountConfig, p as MigrationDirection, pn as ConvexAggregateIndexBuilderOn, pr as gte, pt as InferSelectModel, qt as defineRelationsPart, r as OrmCapability, rn as deletion, rr as FilterExpression, rt as AggregateResult, sn as RlsPolicyToOption, sr as between, st as CountResult, t as OrmAggregateCapability, tn as TableConfig, tr as ExpressionVisitor, tt as AggregateConfig, un as RlsRoleConfig, ur as eq, ut as GetColumnData, v as MigrationMigrateOne, vn as ConvexSearchIndexBuilder, vr as isNull, vt as MutationPaginateConfig, w as MigrationTableName, wn as aggregateIndex, wr as notBetween, wt as OrderDirection, x as MigrationSet, xn as ConvexVectorIndexBuilder, xr as lte, xt as MutationReturning, y as MigrationPlan, yn as ConvexSearchIndexBuilderOn, yr as like, yt as MutationPaginatedResult, zn as unique, zr as HasDefault, zt as RelationsBuilder } from "../capabilities-BJm_VSDT.js";
2
- import { $ as OrmClientWithApi$1, A as ConvexDateMode, B as ConvexBytesBuilderInitial, C as ConvexNumberBuilderInitial, D as id, E as ConvexIdBuilderInitial, F as custom, G as ConvexBigIntBuilder, H as ConvexBooleanBuilder, I as json, J as CreateOrmOptions, K as ConvexBigIntBuilderInitial, L as objectOf, M as ConvexCustomBuilder, N as ConvexCustomBuilderInitial, O as ConvexDateBuilder, P as arrayOf, Q as OrmClientBase$1, R as unionOf, S as ConvexNumberBuilder, St as defineTriggers, T as ConvexIdBuilder, U as ConvexBooleanBuilderInitial, V as bytes, W as boolean, X as GenericOrmCtx$1, Y as GenericOrm$1, Z as OrmApiResult, _ as ConvexTimestampMode, _t as OrmBeforeResult, a as requireSchemaRelations, at as ScheduledMutationBatchArgs, b as ConvexTextEnumBuilderInitial, bt as OrmTriggerContext, c as TableConfigResult, ct as scheduledDeleteFactory, d as OrmNotFoundError, et as OrmFunctions, f as ConvexVectorBuilder, g as ConvexTimestampBuilderInitial, gt as defineSchemaExtension, h as ConvexTimestampBuilder, ht as SchemaExtension, i as getSchemaTriggers, it as createOrm, j as date, k as ConvexDateBuilderInitial, l as getTableColumns, m as vector, n as defineSchema, nt as OrmWriterCtx, o as asc, ot as scheduledMutationBatchFactory, p as ConvexVectorBuilderInitial, q as bigint, r as getSchemaRelations, rt as ResolveOrmSchema, s as desc, st as ScheduledDeleteArgs, t as WhereClauseResult, tt as OrmReaderCtx, u as getTableConfig, v as timestamp, vt as OrmTableTriggers, w as integer, x as textEnum, xt as OrmTriggers, y as ConvexTextEnumBuilder, yt as OrmTriggerChange, z as ConvexBytesBuilder } from "../where-clause-compiler-Dgf4lrO-.js";
1
+ import { $n as TableName, $t as OrmLifecycleChange, An as ConvexCheckBuilder, At as UpdateSet, Bn as ConvexTextBuilder, Br as IsPrimaryKey, Bt as RelationsBuilderColumnBase, C as MigrationStep, Cn as ConvexVectorIndexConfig, Cr as not, Ct as OrderByClause, D as defineMigration, Dn as searchIndex, Dr as startsWith, Dt as ReturningAll, E as buildMigrationPlan, En as rankIndex, Er as or, Et as PredicateWhereIndexConfig, Fn as ConvexUniqueConstraintBuilderOn, Fr as ColumnBuilderTypeConfig, Ft as ExtractTablesFromSchema, Gn as Columns, Gt as TablesRelationalConfig, H as RlsMode, Hn as text, Hr as NotNull, In as ConvexUniqueConstraintConfig, Ir as ColumnBuilderWithTableName, It as ExtractTablesWithRelations, Jn as OrmSchemaExtensionTables, Jt as ConvexDeletionBuilder, Kt as defineRelations, Ln as check, Lr as ColumnDataType, Lt as ManyConfig, M as DatabaseWithQuery, Mn as ConvexForeignKeyBuilder, Mr as ColumnBuilder, Mt as VectorSearchProvider, N as OrmReader$1, Nn as ConvexForeignKeyConfig, Nr as ColumnBuilderBaseConfig, Nt as unsetToken, O as defineMigrationSet, On as uniqueIndex, Ot as ReturningResult, P as OrmWriter$1, Pn as ConvexUniqueConstraintBuilder, Pr as ColumnBuilderRuntimeConfig, Qn as OrmSchemaTriggers, Qt as DiscriminatorBuilderConfig, Rn as foreignKey, Rr as DrizzleEntity, Rt as OneConfig, S as MigrationStateMap, Sn as ConvexVectorIndexBuilderOn, Sr as ne, St as MutationRunMode, T as MigrationWriteMode, Tn as index, Tr as notInArray, Tt as PaginatedResult, U as EdgeMetadata, V as RlsContext, Vn as ConvexTextBuilderInitial, Vr as IsUnique, Vt as RelationsBuilderColumnConfig, W as extractRelationsConfig, Wn as Brand, Wt as TableRelationalConfig, Xn as OrmSchemaExtensions, Xt as ConvexTable, Yt as ConvexDeletionConfig, Zn as OrmSchemaRelations, Zt as ConvexTableWithColumns, _ as MigrationManifestEntry, _n as ConvexRankIndexBuilderOn, _r as isNotNull, _t as MutationExecutionMode, an as RlsPolicy, ar as UnaryExpression, at as BuildRelationResult, b as MigrationRunStatus, bn as ConvexSearchIndexConfig, br as lt, bt as MutationResult, cn as rlsPolicy, cr as contains, ct as DBQueryConfig, d as MigrationAppliedState, dn as rlsRole, dr as fieldRef, dt as InferInsertModel, en as OrmLifecycleOperation, er as BinaryExpression, f as MigrationDefinition, fn as ConvexAggregateIndexBuilder, fr as gt, ft as InferModelFromColumns, g as MigrationDriftIssue, gn as ConvexRankIndexBuilder, gr as isFieldReference, gt as MutationExecuteResult, h as MigrationDocContext, hn as ConvexIndexBuilderOn, hr as inArray, ht as MutationExecuteConfig, i as OrmMigrationCapability, in as discriminator, ir as LogicalExpression, it as BuildQueryResult, j as DatabaseWithMutations, jn as ConvexCheckConfig, jr as AnyColumn, jt as VectorQueryConfig, k as detectMigrationDrift, kn as vectorIndex, kr as SystemFields, kt as ReturningSelection, ln as RlsRole, lr as endsWith, lt as FilterOperators, m as MigrationDoc, mn as ConvexIndexBuilder, mr as ilike, mt as InsertValue, n as OrmCapabilities, nn as convexTable, nr as FieldReference, nt as AggregateFieldValue, on as RlsPolicyConfig, or as and, ot as CountConfig, p as MigrationDirection, pn as ConvexAggregateIndexBuilderOn, pr as gte, pt as InferSelectModel, qt as defineRelationsPart, r as OrmCapability, rn as deletion, rr as FilterExpression, rt as AggregateResult, sn as RlsPolicyToOption, sr as between, st as CountResult, t as OrmAggregateCapability, tn as TableConfig, tr as ExpressionVisitor, tt as AggregateConfig, un as RlsRoleConfig, ur as eq, ut as GetColumnData, v as MigrationMigrateOne, vn as ConvexSearchIndexBuilder, vr as isNull, vt as MutationPaginateConfig, w as MigrationTableName, wn as aggregateIndex, wr as notBetween, wt as OrderDirection, x as MigrationSet, xn as ConvexVectorIndexBuilder, xr as lte, xt as MutationReturning, y as MigrationPlan, yn as ConvexSearchIndexBuilderOn, yr as like, yt as MutationPaginatedResult, zn as unique, zr as HasDefault, zt as RelationsBuilder } from "../capabilities-Bxzoofl4.js";
2
+ import { $ as OrmClientWithApi$1, A as ConvexDateMode, B as ConvexBytesBuilderInitial, C as ConvexNumberBuilderInitial, D as id, E as ConvexIdBuilderInitial, F as custom, G as ConvexBigIntBuilder, H as ConvexBooleanBuilder, I as json, J as CreateOrmOptions, K as ConvexBigIntBuilderInitial, L as objectOf, M as ConvexCustomBuilder, N as ConvexCustomBuilderInitial, O as ConvexDateBuilder, P as arrayOf, Q as OrmClientBase$1, R as unionOf, S as ConvexNumberBuilder, St as defineTriggers, T as ConvexIdBuilder, U as ConvexBooleanBuilderInitial, V as bytes, W as boolean, X as GenericOrmCtx$1, Y as GenericOrm$1, Z as OrmApiResult, _ as ConvexTimestampMode, _t as OrmBeforeResult, a as requireSchemaRelations, at as ScheduledMutationBatchArgs, b as ConvexTextEnumBuilderInitial, bt as OrmTriggerContext, c as TableConfigResult, ct as scheduledDeleteFactory, d as OrmNotFoundError, et as OrmFunctions, f as ConvexVectorBuilder, g as ConvexTimestampBuilderInitial, gt as defineSchemaExtension, h as ConvexTimestampBuilder, ht as SchemaExtension, i as getSchemaTriggers, it as createOrm, j as date, k as ConvexDateBuilderInitial, l as getTableColumns, m as vector, n as defineSchema, nt as OrmWriterCtx, o as asc, ot as scheduledMutationBatchFactory, p as ConvexVectorBuilderInitial, q as bigint, r as getSchemaRelations, rt as ResolveOrmSchema, s as desc, st as ScheduledDeleteArgs, t as WhereClauseResult, tt as OrmReaderCtx, u as getTableConfig, v as timestamp, vt as OrmTableTriggers, w as integer, x as textEnum, xt as OrmTriggers, y as ConvexTextEnumBuilder, yt as OrmTriggerChange, z as ConvexBytesBuilder } from "../where-clause-compiler-DrV6lQg0.js";
3
3
  import { i as pretendRequired, n as deprecated, r as pretend } from "../validators-0I-Ik1EN.js";
4
4
  import { a as QueryCtxWithPreferredOrmQueryTable, i as QueryCtxWithOrmQueryTable, n as LookupByIdResultByCtx, o as getByIdWithOrmQueryFallback, r as QueryCtxWithOptionalOrmQueryTable, t as DocByCtx } from "../query-context-BkNjkDCk.js";
5
5
  import { DefineSchemaOptions, GenericDatabaseReader, GenericDatabaseWriter, GenericSchema, SchemaDefinition } from "convex/server";