kitcn 0.23.0 → 0.25.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.
Files changed (85) hide show
  1. package/dist/aggregate/index.d.ts +11 -1
  2. package/dist/aggregate/index.js +1 -1
  3. package/dist/auth/generated/index.d.ts +1 -1
  4. package/dist/auth/generated/index.js +1 -1
  5. package/dist/auth/http/index.d.ts +1 -1
  6. package/dist/auth/index.d.ts +4 -4
  7. package/dist/auth/index.js +10 -9
  8. package/dist/auth/nextjs/index.d.ts +2 -2
  9. package/dist/auth/nextjs/index.js +3 -3
  10. package/dist/auth/start/server/index.d.ts +1 -1
  11. package/dist/auth/start/server/index.js +1 -1
  12. package/dist/{builder-CsxVc5xC.js → builder-1Vx-tEnS.js} +3 -3
  13. package/dist/{caller-factory-CWa0ELLD.js → caller-factory-Dd3H7j3V.js} +3 -3
  14. package/dist/{where-clause-compiler-B_H3oio5.d.ts → capabilities-Bem7xvGK.d.ts} +2958 -4145
  15. package/dist/cli.mjs +155 -60
  16. package/dist/{convex-plugin-D8B0oCFq.js → convex-plugin-DfOhBU9g.js} +1 -1
  17. package/dist/{create-schema-orm-B3f2Kc8O.js → create-schema-orm-Bu2JCiBS.js} +1 -1
  18. package/dist/crpc/index.d.ts +3 -3
  19. package/dist/crpc/index.js +4 -4
  20. package/dist/{customFunctions-DxEEO4Dq.js → customFunctions-BbhgdRGl.js} +2 -2
  21. package/dist/definitions-D9vhJ6OV.js +215 -0
  22. package/dist/extensions-DzjCJXYl.js +55 -0
  23. package/dist/filter-expression-Dydt8wS0.js +402 -0
  24. package/dist/{generated-contract-disabled-B-nmd7Ne.d.ts → generated-contract-disabled-BEc4d98x.d.ts} +2 -2
  25. package/dist/{http-types-BoSDAh4Y.d.ts → http-types-90mapMP9.d.ts} +33 -3
  26. package/dist/id-Bibg34Yb.js +37 -0
  27. package/dist/{local-env-yFKub75x.mjs → local-env-Du24tdbp.mjs} +2243 -2113
  28. package/dist/{middleware-Br5zubu7.d.ts → middleware-CE_MC6Gi.d.ts} +1 -1
  29. package/dist/{middleware-DUd1Sj39.js → middleware-cJDRHTRY.js} +1 -1
  30. package/dist/orm/aggregate-index/index.d.ts +20 -0
  31. package/dist/orm/aggregate-index/index.js +2136 -0
  32. package/dist/orm/index.d.ts +5 -4
  33. package/dist/orm/index.js +126 -4292
  34. package/dist/orm/migrations/index.d.ts +3 -0
  35. package/dist/orm/migrations/index.js +506 -0
  36. package/dist/plugins/index.d.ts +1 -1
  37. package/dist/plugins/index.js +1 -1
  38. package/dist/{procedure-caller-DQxnLBS_.js → procedure-caller-DIzXfX2E.js} +2 -2
  39. package/dist/{procedure-name-C55TynK3.d.ts → procedure-name-B09-cf1-.d.ts} +3 -3
  40. package/dist/{query-context-BzihIpnM.js → query-context-DOm5Xm3H.js} +1 -402
  41. package/dist/{query-options-C_eBSIXG.js → query-options-DUiOj01a.js} +33 -1
  42. package/dist/ratelimit/index.d.ts +47 -11
  43. package/dist/ratelimit/index.js +256 -102
  44. package/dist/react/index.d.ts +9 -6
  45. package/dist/react/index.js +36 -17
  46. package/dist/rsc/index.d.ts +6 -9
  47. package/dist/rsc/index.js +7 -11
  48. package/dist/runtime-DfGsZtx3.js +1074 -0
  49. package/dist/schema-Besl-rpv.js +1411 -0
  50. package/dist/server/index.d.ts +4 -4
  51. package/dist/server/index.js +4 -4
  52. package/dist/solid/index.d.ts +9 -6
  53. package/dist/solid/index.js +36 -17
  54. package/dist/{runtime-6DJxpDfb.js → table-rmJm5Qio.js} +3 -1095
  55. package/dist/{token-xlpENMVn.js → token-DcV_0fkF.js} +1 -1
  56. package/dist/{types-C0Xl7P8K.d.ts → types-In-ZZOsd.d.ts} +1 -1
  57. package/dist/{validators-C7LelqTN.js → validators-Dmbhsd3U.js} +1 -1
  58. package/dist/watcher.mjs +1 -1
  59. package/dist/where-clause-compiler-mohWFiG0.d.ts +2042 -0
  60. package/package.json +3 -2
  61. package/skills/kitcn/SKILL.md +19 -18
  62. package/skills/kitcn/references/features/aggregates.md +21 -0
  63. package/skills/kitcn/references/features/auth-organizations.md +3 -3
  64. package/skills/kitcn/references/features/auth-polar.md +2 -1
  65. package/skills/kitcn/references/features/auth.md +8 -1
  66. package/skills/kitcn/references/features/http.md +26 -1
  67. package/skills/kitcn/references/features/ratelimit.md +11 -0
  68. package/skills/kitcn/references/features/react.md +1 -1
  69. package/skills/kitcn/references/setup/auth.md +52 -42
  70. package/skills/kitcn/references/setup/index.md +1 -1
  71. package/skills/kitcn/references/setup/server.md +37 -6
  72. /package/dist/{api-entry-N3nBOlI2.js → api-entry-CkDpGYVg.js} +0 -0
  73. /package/dist/{context-utils-BBUtBqjN.d.ts → context-utils-DwZ3Cam1.d.ts} +0 -0
  74. /package/dist/{create-schema-BXrKE2YY.js → create-schema-BRFjdgRr.js} +0 -0
  75. /package/dist/{error-Bvo7YEhk.js → error-CMLeCadS.js} +0 -0
  76. /package/dist/{generated-contract-disabled-BXaz7JCE.js → generated-contract-disabled-CZa0iyV0.js} +0 -0
  77. /package/dist/{meta-utils-D9K4fICl.js → meta-utils-D2lqN2Lg.js} +0 -0
  78. /package/dist/{query-context-CNo9ffvI.d.ts → query-context-DJONf8X5.d.ts} +0 -0
  79. /package/dist/{token-B9Bjcqug.d.ts → token-kQaqFby4.d.ts} +0 -0
  80. /package/dist/{transformer-yZuBWo8v.js → transformer-D8wO-kEj.js} +0 -0
  81. /package/dist/{types-BCl8gfGy.d.ts → types-Bf3XQex5.d.ts} +0 -0
  82. /package/dist/{types-DF2cg_w0.d.ts → types-MHYXboyy.d.ts} +0 -0
  83. /package/dist/{types-CnTpHR1F.d.ts → types-eNzQmbqM.d.ts} +0 -0
  84. /package/dist/{upstream-BR6sBLg3.js → upstream-BCgGZX6q.js} +0 -0
  85. /package/dist/{validators-BhsByJeg.d.ts → validators-V4eM2-Jg.d.ts} +0 -0
package/dist/orm/index.js CHANGED
@@ -1,6 +1,11 @@
1
- import { A as vectorIndex, B as ConvexColumnBuilder, C as RlsPolicy, D as rankIndex, E as index, F as arrayOf, I as custom, L as json, M as createSystemFields, N as integer, O as searchIndex, P as id, R as objectOf, S as TablePolymorphic, T as aggregateIndex, V as entityKind, _ as OrmSchemaRelations, a as deletion, b as TableDeleteConfig, c as Columns, d as OrmSchemaDefinition, f as OrmSchemaExtensionRelations, g as OrmSchemaOptions, h as OrmSchemaExtensions, i as convexTable, j as text, k as uniqueIndex, l as EnableRLS, m as OrmSchemaExtensionTriggers, o as discriminator, p as OrmSchemaExtensionTables, s as Brand, t as DirectAggregate, u as OrmContext, v as OrmSchemaTriggers, w as rlsPolicy, x as TableName, y as RlsPolicies, z as unionOf } from "../runtime-6DJxpDfb.js";
2
- import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-C7LelqTN.js";
3
- import { A as like, B as or, C as gt, D as isFieldReference, E as inArray, F as not, I as notBetween, L as notIlike, M as lte, N as matchLikePattern, O as isNotNull, P as ne, R as notInArray, S as filterValuesEqual, T as ilike, V as startsWith, _ as contains, a as indexKeyWithinBounds, b as fieldRef, c as streamIndexRange, d as and, f as arrayContained, g as column, h as between, i as getIndexFields, j as lt, k as isNull, l as isUnsetToken, m as arrayOverlaps, n as EmptyStream, o as mergedStream, p as arrayContains, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken, v as endsWith, w as gte, x as filterValueInList, y as eq, z as notLike } from "../query-context-BzihIpnM.js";
1
+ import { A as integer, C as index, D as vectorIndex, E as uniqueIndex, F as unionOf, I as ConvexColumnBuilder, L as entityKind, M as custom, N as json, O as text, P as objectOf, S as aggregateIndex, T as searchIndex, a as Columns, b as RlsPolicy, c as OrmSchemaDefinition, d as OrmSchemaExtensionTriggers, f as OrmSchemaExtensions, g as RlsPolicies, h as OrmSchemaTriggers, i as Brand, j as arrayOf, k as createSystemFields, l as OrmSchemaExtensionRelations, m as OrmSchemaRelations, n as deletion, o as EnableRLS, p as OrmSchemaOptions, r as discriminator, s as OrmContext, t as convexTable, u as OrmSchemaExtensionTables, v as TableName, w as rankIndex, x as rlsPolicy, y as TablePolymorphic } from "../table-rmJm5Qio.js";
2
+ import { d as boolean, i as detectMigrationDrift, l as migrationExtension, n as defineMigration, r as defineMigrationSet, t as buildMigrationPlan } from "../definitions-D9vhJ6OV.js";
3
+ import { a as pretendRequired, i as pretend, n as deprecated } from "../validators-Dmbhsd3U.js";
4
+ import { t as id } from "../id-Bibg34Yb.js";
5
+ import { A as or, C as matchLikePattern, D as notIlike, E as notBetween, O as notInArray, S as lte, T as not, _ as isFieldReference, a as between, b as like, c as endsWith, d as filterValueInList, f as filterValuesEqual, g as inArray, h as ilike, i as arrayOverlaps, j as startsWith, k as notLike, l as eq, m as gte, n as arrayContained, o as column, p as gt, r as arrayContains, s as contains, t as and, u as fieldRef, v as isNotNull, w as ne, x as lt, y as isNull } from "../filter-expression-Dydt8wS0.js";
6
+ import { a as indexKeyWithinBounds, c as streamIndexRange, i as getIndexFields, l as isUnsetToken, n as EmptyStream, o as mergedStream, r as QueryStream, s as stream, t as getByIdWithOrmQueryFallback, u as unsetToken } from "../query-context-DOm5Xm3H.js";
7
+ import { $ as serializeFilterExpression, A as ensureNonNullValues, B as getMutationExecutionMode, C as deserializeFilterExpression, D as enforcePolymorphicWrite, E as enforceForeignKeys, F as getChecks, G as getUniqueIndexes, H as getTableColumns$2, I as getColumnName$1, J as normalizeDateFieldsForWrite, K as hardDeleteRow, L as getForeignKeys, M as evaluateCheckConstraintTriState, N as evaluateFilter, O as enforceUniqueIndexes, P as extractPrimaryIdLookup, Q as selectReturningRowWithHydration, R as getMutationAsyncDelayMs, S as decodeUndefinedDeep, T as enforceCheckConstraints, U as getTableDeleteConfig, V as getOrmContext, W as getTableName, X as patchReferencingRows, Y as normalizeTemporalComparableValue, Z as resolveOrmRuntimeDefaults, _ as applyIncomingForeignKeyActionsOnUpdate, _t as usesSystemCreatedAtAlias, a as aggregateExtension, at as markLifecycleHookedTables, b as collectMutationRowsBounded, c as AGGREGATE_ERROR, ct as findSearchIndexByName, d as createCountError, dt as getIndexes, et as softDeleteRow, ft as getRankIndexes, g as applyIncomingForeignKeyActionsOnDelete, gt as PUBLIC_CREATED_AT_FIELD, h as applyDefaults, ht as INTERNAL_CREATION_TIME_FIELD, it as mapWithConcurrency, j as ensureNullableColumns, k as ensureDefaultColumns, l as COUNT_ERROR, lt as findVectorIndexByName, m as ensureCountAllowedForRls, mt as CREATED_AT_MIGRATION_MESSAGE, nt as takeRowsWithinByteBudget, o as getAggregateIndexDefinitions, ot as findIndexForColumns, p as ensureAggregateAllowedForRls, pt as resolveIndexOrderPushdown, q as hydrateDateFieldsForRead, rt as toConvexFilter, s as getRankIndexDefinitions, st as findRelationIndex, tt as splitReturningSelection, u as createAggregateError, ut as getAggregateIndexes, v as buildForeignKeyGraph, w as encodeUndefinedDeep, x as collectPrimaryIdLookupRows, y as canUsePrimaryIdLookupCursor, z as getMutationCollectionLimits } from "../schema-Besl-rpv.js";
8
+ import { t as defineSchemaExtension } from "../extensions-DzjCJXYl.js";
4
9
  import { compareValues, v } from "convex/values";
5
10
  import { defineSchema as defineSchema$1, internalActionGeneric, internalMutationGeneric } from "convex/server";
6
11
 
@@ -34,37 +39,6 @@ function bigint(name) {
34
39
  return new ConvexBigIntBuilder(name ?? "");
35
40
  }
36
41
 
37
- //#endregion
38
- //#region src/orm/builders/boolean.ts
39
- /**
40
- * Boolean column builder class
41
- * Compiles to v.boolean() or v.optional(v.boolean())
42
- */
43
- var ConvexBooleanBuilder = class extends ConvexColumnBuilder {
44
- static [entityKind] = "ConvexBooleanBuilder";
45
- constructor(name) {
46
- super(name, "boolean", "ConvexBoolean");
47
- }
48
- /**
49
- * Expose Convex validator for schema integration
50
- */
51
- get convexValidator() {
52
- if (this.config.notNull) return v.boolean();
53
- return v.optional(v.union(v.null(), v.boolean()));
54
- }
55
- /**
56
- * Compile to Convex validator
57
- * .notNull() → v.boolean()
58
- * nullable → v.optional(v.boolean())
59
- */
60
- build() {
61
- return this.convexValidator;
62
- }
63
- };
64
- function boolean(name) {
65
- return new ConvexBooleanBuilder(name ?? "");
66
- }
67
-
68
42
  //#endregion
69
43
  //#region src/orm/builders/bytes.ts
70
44
  var ConvexBytesBuilder = class extends ConvexColumnBuilder {
@@ -280,3548 +254,41 @@ var ConvexCheckBuilder = class {
280
254
  [entityKind] = "ConvexCheckBuilder";
281
255
  config;
282
256
  constructor(name, expression) {
283
- this.config = {
284
- name,
285
- expression
286
- };
287
- }
288
- };
289
- function check(name, expression) {
290
- return new ConvexCheckBuilder(name, expression);
291
- }
292
-
293
- //#endregion
294
- //#region src/orm/timestamp-mode.ts
295
- const PUBLIC_CREATED_AT_FIELD = "createdAt";
296
- const INTERNAL_CREATION_TIME_FIELD = "_creationTime";
297
- const CREATED_AT_MIGRATION_MESSAGE = "`_creationTime` is no longer public. Use `createdAt` instead.";
298
- const hasUserCreatedAtColumn = (table) => {
299
- if (!table || typeof table !== "object") return false;
300
- const columns = table[Columns];
301
- if (!columns || typeof columns !== "object") return false;
302
- return Object.hasOwn(columns, PUBLIC_CREATED_AT_FIELD);
303
- };
304
- const usesSystemCreatedAtAlias = (_table) => true;
305
-
306
- //#endregion
307
- //#region src/orm/index-utils.ts
308
- function getIndexes(table) {
309
- const indexes = table.getIndexes?.();
310
- return Array.isArray(indexes) ? indexes : [];
311
- }
312
- function getAggregateIndexes(table) {
313
- const indexes = table.getAggregateIndexes?.();
314
- return Array.isArray(indexes) ? indexes : [];
315
- }
316
- function getRankIndexes(table) {
317
- const indexes = table.getRankIndexes?.();
318
- return Array.isArray(indexes) ? indexes : [];
319
- }
320
- function getSearchIndexes(table) {
321
- const indexes = table.getSearchIndexes?.();
322
- return Array.isArray(indexes) ? indexes : [];
323
- }
324
- function findSearchIndexByName(table, indexName) {
325
- return getSearchIndexes(table).find((index) => index.name === indexName) ?? null;
326
- }
327
- function getVectorIndexes(table) {
328
- const indexes = table.getVectorIndexes?.();
329
- return Array.isArray(indexes) ? indexes : [];
330
- }
331
- function findVectorIndexByName(table, indexName) {
332
- return getVectorIndexes(table).find((index) => index.name === indexName) ?? null;
333
- }
334
- function findIndexForColumns(indexes, columns) {
335
- for (const index of indexes) if (hasColumnPrefix(index, columns)) return index.name;
336
- return null;
337
- }
338
- const hasColumnPrefix = (index, columns) => {
339
- if (index.fields.length < columns.length) return false;
340
- for (let i = 0; i < columns.length; i += 1) if (index.fields[i] !== columns[i]) return false;
341
- return true;
342
- };
343
- /**
344
- * Answers "does scanning this index already produce the requested order?".
345
- *
346
- * Convex walks an index in full key order, so once a leading run of fields is
347
- * pinned to a single value by `eq`, the remainder of the scan is already sorted
348
- * by the next index field — and by `_creationTime` once every declared field is
349
- * pinned, because Convex appends it as the implicit trailing key. When that
350
- * field is the one the caller asked to sort by, `.order(dir).take(n)` returns
351
- * the exact page and nothing has to be collected and sorted afterwards.
352
- *
353
- * This is the single owner of that decision. The top-level query planner and
354
- * the relation loader both call it, so a bound one of them can push into the
355
- * index can never be one the other silently drops.
356
- *
357
- * Returns the direction to hand to `.order()`, or null when the sort must run
358
- * after the fetch.
359
- */
360
- function resolveIndexOrderPushdown(params) {
361
- const { indexFields, pinnedEqCount, orderSpecs } = params;
362
- if (!indexFields || orderSpecs.length !== 1) return null;
363
- const primary = orderSpecs[0];
364
- const eqCount = Math.min(Math.max(pinnedEqCount, 0), indexFields.length);
365
- for (let i = 0; i < eqCount; i += 1) if (indexFields[i] === primary.field) return primary.direction;
366
- const nativeField = eqCount >= indexFields.length ? INTERNAL_CREATION_TIME_FIELD : indexFields[eqCount];
367
- return primary.field === nativeField ? primary.direction : null;
368
- }
369
- function findRelationIndex(table, columns, relationName, targetTableName, strict = true, allowFullScan = false, orderSpecs = []) {
370
- const indexes = getIndexes(table);
371
- const fallbackIndex = findIndexForColumns(indexes, columns);
372
- const index = (orderSpecs.length === 0 ? void 0 : indexes.find((candidate) => hasColumnPrefix(candidate, columns) && resolveIndexOrderPushdown({
373
- indexFields: candidate.fields,
374
- pinnedEqCount: columns.length,
375
- orderSpecs
376
- }) !== null))?.name ?? fallbackIndex;
377
- if (!index && !allowFullScan) throw new Error(`Relation ${relationName} requires index on '${targetTableName}(${columns.join(", ")})'. Set allowFullScan: true to override.`);
378
- if (!index && strict) console.warn(`Relation ${relationName} running without index (allowFullScan: true).`);
379
- return index;
380
- }
381
-
382
- //#endregion
383
- //#region src/orm/write-fanout.ts
384
- /**
385
- * Shared primitives for fanning out independent writes.
386
- *
387
- * Deliberately dependency-free: `mutation-utils`, `lifecycle` and `query` all
388
- * import it, and Convex bundles every static import of a function entry.
389
- */
390
- const ORMLIFECYCLE_HOOKED_TABLES = Symbol.for("kitcn:OrmLifecycleHookedTables");
391
- /** Matches the ORM's relation-loading default. */
392
- const DEFAULT_WRITE_FANOUT_CONCURRENCY = 25;
393
- /**
394
- * Runs `worker` over `items` with at most `limit` in flight. Results keep input
395
- * order. Unbounded `Promise.all` is not an option here: a fan-out is bounded
396
- * only by `mutationMaxRows` (10,000), and that many simultaneous in-flight
397
- * syscalls is its own failure mode.
398
- */
399
- async function mapWithConcurrency(items, limit, worker) {
400
- if (items.length === 0) return [];
401
- const width = Math.max(1, Math.min(limit, items.length));
402
- const results = new Array(items.length);
403
- let nextIndex = 0;
404
- const runWorker = async () => {
405
- while (true) {
406
- const index = nextIndex;
407
- nextIndex += 1;
408
- if (index >= items.length) return;
409
- results[index] = await worker(items[index], index);
410
- }
411
- };
412
- await Promise.all(Array.from({ length: width }, () => runWorker()));
413
- return results;
414
- }
415
- /**
416
- * Records which tables the lifecycle writer intercepts, so write fan-out can
417
- * see it without importing `lifecycle` (which would close an import cycle
418
- * through `aggregate-index/runtime`).
419
- */
420
- const markLifecycleHookedTables = (db, tableNames) => {
421
- Object.defineProperty(db, ORMLIFECYCLE_HOOKED_TABLES, {
422
- configurable: false,
423
- enumerable: false,
424
- value: tableNames,
425
- writable: false
426
- });
427
- return db;
428
- };
429
- /**
430
- * True when writes to `tableName` run through trigger / aggregate-index hooks.
431
- * Those writes are serialized by the lifecycle write lock and their hooks fire
432
- * in write order, so they must keep their sequential loop; everything else is
433
- * free to fan out.
434
- */
435
- const hasLifecycleHooks = (db, tableName) => {
436
- return (db?.[ORMLIFECYCLE_HOOKED_TABLES])?.has(tableName) ?? false;
437
- };
438
-
439
- //#endregion
440
- //#region src/orm/mutation-utils.ts
441
- const UNDEFINED_SENTINEL_KEY = "__kitcnUndefined";
442
- const INTERNAL_ID_FIELD$2 = "_id";
443
- const PUBLIC_ID_FIELD$2 = "id";
444
- const DATE_COLUMN_TYPE = "ConvexDate";
445
- const TIMESTAMP_COLUMN_TYPE = "ConvexTimestamp";
446
- const isPlainObject$1 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
447
- const temporalColumnDescriptorCache = /* @__PURE__ */ new WeakMap();
448
- const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
449
- const toDateOnlyString = (value) => value.toISOString().slice(0, 10);
450
- const toDateOnlyDate = (value) => DATE_ONLY_REGEX.test(value) ? /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`) : value;
451
- const toTimestampMillis = (value) => {
452
- if (value instanceof Date) return value.getTime();
453
- if (typeof value === "string") {
454
- const parsed = Date.parse(value);
455
- if (!Number.isNaN(parsed)) return parsed;
456
- }
457
- return value;
458
- };
459
- const readTimestampValue = (value, mode) => {
460
- if (mode === "date" && typeof value === "number") return new Date(value);
461
- if (mode === "string" && typeof value === "number") return new Date(value).toISOString();
462
- return value;
463
- };
464
- const getTemporalDescriptorFromColumn = (name, column) => {
465
- const config = column?.config;
466
- const columnType = config?.columnType;
467
- if (columnType !== DATE_COLUMN_TYPE && columnType !== TIMESTAMP_COLUMN_TYPE) return;
468
- if (columnType === DATE_COLUMN_TYPE) return {
469
- name,
470
- columnType,
471
- mode: config?.mode === "date" ? "date" : "string"
472
- };
473
- return {
474
- name,
475
- columnType,
476
- mode: config?.mode === "string" ? "string" : "date"
477
- };
478
- };
479
- const getTemporalColumnDescriptors = (table) => {
480
- const cacheKey = table;
481
- const cached = temporalColumnDescriptorCache.get(cacheKey);
482
- if (cached) return cached;
483
- const descriptors = /* @__PURE__ */ new Map();
484
- for (const [name, column] of Object.entries(getTableColumns$2(table))) {
485
- const descriptor = getTemporalDescriptorFromColumn(name, column);
486
- if (!descriptor) continue;
487
- descriptors.set(name, descriptor);
488
- }
489
- temporalColumnDescriptorCache.set(cacheKey, descriptors);
490
- return descriptors;
491
- };
492
- const getTemporalColumnDescriptor = (table, columnName) => {
493
- const temporalColumns = getTemporalColumnDescriptors(table);
494
- const direct = temporalColumns.get(columnName);
495
- if (direct) return direct;
496
- const columns = getTableColumns$2(table);
497
- for (const descriptor of temporalColumns.values()) if (columns[descriptor.name]?.config?.name === columnName) return descriptor;
498
- };
499
- const normalizeTemporalWriteValue = (descriptor, value) => {
500
- if (descriptor.columnType === DATE_COLUMN_TYPE) {
501
- if (value instanceof Date) return toDateOnlyString(value);
502
- return value;
503
- }
504
- return toTimestampMillis(value);
505
- };
506
- const hydrateTemporalReadValue = (descriptor, value) => {
507
- if (descriptor.columnType === DATE_COLUMN_TYPE) {
508
- if (descriptor.mode === "date" && typeof value === "string") return toDateOnlyDate(value);
509
- return value;
510
- }
511
- return readTimestampValue(value, descriptor.mode);
512
- };
513
- const normalizeTemporalComparableValue = (table, fieldName, value) => {
514
- const descriptor = getTemporalColumnDescriptor(table, fieldName);
515
- if (!descriptor) return value;
516
- if (Array.isArray(value)) return value.map((entry) => normalizeTemporalWriteValue(descriptor, entry));
517
- return normalizeTemporalWriteValue(descriptor, value);
518
- };
519
- const normalizeDateFieldsForWrite = (table, value) => {
520
- const useSystemCreatedAt = usesSystemCreatedAtAlias(table);
521
- const hasUserCreatedAt = hasUserCreatedAtColumn(table);
522
- const temporalColumns = getTemporalColumnDescriptors(table);
523
- const result = { ...value };
524
- if (Object.hasOwn(result, INTERNAL_CREATION_TIME_FIELD)) throw new Error(CREATED_AT_MIGRATION_MESSAGE);
525
- if (useSystemCreatedAt && !hasUserCreatedAt && Object.hasOwn(result, PUBLIC_CREATED_AT_FIELD)) delete result[PUBLIC_CREATED_AT_FIELD];
526
- for (const [name, descriptor] of temporalColumns.entries()) {
527
- if (!Object.hasOwn(result, name)) continue;
528
- result[name] = normalizeTemporalWriteValue(descriptor, result[name]);
529
- }
530
- return result;
531
- };
532
- /**
533
- * Reshapes a stored document into its public form in a single pass: renames
534
- * `_id` to `id`, aliases `_creationTime` to `createdAt`, and hydrates temporal
535
- * columns. Deliberately allocates exactly one object and never calls `delete`
536
- * on it — deleting an existing own property drops the object into V8
537
- * dictionary mode, which then poisons every downstream spread.
538
- */
539
- const hydrateDateFieldsForRead = (table, value) => {
540
- if (!isPlainObject$1(value)) return value;
541
- const obj = value;
542
- const hasId = Object.hasOwn(obj, INTERNAL_ID_FIELD$2);
543
- const rawCreationTime = obj[INTERNAL_CREATION_TIME_FIELD];
544
- const aliasCreatedAt = usesSystemCreatedAtAlias(table) && Object.hasOwn(obj, INTERNAL_CREATION_TIME_FIELD) && rawCreationTime !== void 0;
545
- const temporalColumns = getTemporalColumnDescriptors(table);
546
- const result = {};
547
- for (const key of Object.keys(obj)) {
548
- if (key === INTERNAL_ID_FIELD$2 || key === INTERNAL_CREATION_TIME_FIELD) continue;
549
- if (key === PUBLIC_CREATED_AT_FIELD && aliasCreatedAt) {
550
- result[key] = void 0;
551
- continue;
552
- }
553
- const descriptor = temporalColumns.get(key);
554
- result[key] = descriptor ? hydrateTemporalReadValue(descriptor, obj[key]) : obj[key];
555
- }
556
- if (hasId) result[PUBLIC_ID_FIELD$2] = obj[INTERNAL_ID_FIELD$2];
557
- if (aliasCreatedAt) {
558
- const descriptor = temporalColumns.get(PUBLIC_CREATED_AT_FIELD);
559
- result[PUBLIC_CREATED_AT_FIELD] = descriptor ? hydrateTemporalReadValue(descriptor, rawCreationTime) : rawCreationTime;
560
- }
561
- return result;
562
- };
563
- const selectReturningRowWithHydration = (table, row, fields) => {
564
- const useSystemCreatedAt = usesSystemCreatedAtAlias(table);
565
- const temporalColumns = getTemporalColumnDescriptors(table);
566
- const selected = {};
567
- for (const [selectedKey, column] of Object.entries(fields)) {
568
- const columnName = getSelectionColumnName(column);
569
- let value = row[columnName];
570
- if (!(columnName === INTERNAL_CREATION_TIME_FIELD && useSystemCreatedAt)) {
571
- const descriptor = temporalColumns.get(columnName);
572
- if (descriptor) value = hydrateTemporalReadValue(descriptor, value);
573
- }
574
- selected[selectedKey] = value;
575
- }
576
- return { ...selected };
577
- };
578
- const encodeUndefinedDeep = (value) => {
579
- if (value === void 0) return { [UNDEFINED_SENTINEL_KEY]: true };
580
- if (Array.isArray(value)) return value.map((item) => encodeUndefinedDeep(item));
581
- if (isPlainObject$1(value)) {
582
- const result = {};
583
- for (const [key, nested] of Object.entries(value)) result[key] = encodeUndefinedDeep(nested);
584
- return result;
585
- }
586
- return value;
587
- };
588
- const decodeUndefinedDeep = (value) => {
589
- if (Array.isArray(value)) return value.map((item) => decodeUndefinedDeep(item));
590
- if (isPlainObject$1(value)) {
591
- if (Object.keys(value).length === 1 && value[UNDEFINED_SENTINEL_KEY] === true) return;
592
- const result = {};
593
- for (const [key, nested] of Object.entries(value)) result[key] = decodeUndefinedDeep(nested);
594
- return result;
595
- }
596
- return value;
597
- };
598
- const isSerializedFieldReference = (value) => isPlainObject$1(value) && typeof value.fieldName === "string";
599
- const createBinaryExpression = (operator, fieldName, value) => {
600
- return {
601
- type: "binary",
602
- operator,
603
- operands: [fieldRef(fieldName), value],
604
- accept(visitor) {
605
- return visitor.visitBinary(this);
606
- }
607
- };
608
- };
609
- const createLogicalExpression = (operator, operands) => ({
610
- type: "logical",
611
- operator,
612
- operands,
613
- accept(visitor) {
614
- return visitor.visitLogical(this);
615
- }
616
- });
617
- const createUnaryExpression = (operator, operand) => ({
618
- type: "unary",
619
- operator,
620
- operands: [operand],
621
- accept(visitor) {
622
- return visitor.visitUnary(this);
623
- }
624
- });
625
- const serializeFilterExpression = (expression) => {
626
- if (!expression) return;
627
- if (expression.type === "binary") {
628
- const binary = expression;
629
- const [field, value] = binary.operands;
630
- if (!isFieldReference(field)) throw new Error("Binary expression must have FieldReference as first operand");
631
- return {
632
- type: "binary",
633
- operator: binary.operator,
634
- field: { fieldName: field.fieldName },
635
- value: encodeUndefinedDeep(value)
636
- };
637
- }
638
- if (expression.type === "logical") {
639
- const logical = expression;
640
- return {
641
- type: "logical",
642
- operator: logical.operator,
643
- operands: logical.operands.map((operand) => serializeFilterExpression(operand))
644
- };
645
- }
646
- const unary = expression;
647
- const [operand] = unary.operands;
648
- return {
649
- type: "unary",
650
- operator: unary.operator,
651
- operand: isFieldReference(operand) ? { fieldName: operand.fieldName } : serializeFilterExpression(operand)
652
- };
653
- };
654
- const deserializeFilterExpression = (expression) => {
655
- if (!expression) return;
656
- if (expression.type === "binary") {
657
- const binary = expression;
658
- return createBinaryExpression(binary.operator, binary.field.fieldName, decodeUndefinedDeep(binary.value));
659
- }
660
- if (expression.type === "logical") {
661
- const logical = expression;
662
- return createLogicalExpression(logical.operator, logical.operands.map((operand) => deserializeFilterExpression(operand)).filter((operand) => !!operand));
663
- }
664
- const unary = expression;
665
- const operand = unary.operand;
666
- if (isSerializedFieldReference(operand)) return createUnaryExpression(unary.operator, fieldRef(operand.fieldName));
667
- const nested = deserializeFilterExpression(operand);
668
- if (!nested) throw new Error("Serialized unary operand is missing.");
669
- return createUnaryExpression(unary.operator, nested);
670
- };
671
- const PRIMARY_ID_LOOKUP_CURSOR_PREFIX = "kitcn:primary-id:";
672
- const dedupePrimaryIds = (values) => Array.from(new Map(values.map((value) => [String(value), value])).values());
673
- const parsePrimaryIdLookupCursor = (cursor) => {
674
- if (cursor === null) return 0;
675
- if (!cursor.startsWith(PRIMARY_ID_LOOKUP_CURSOR_PREFIX)) throw new Error("Invalid primary id mutation cursor.");
676
- const offset = Number(cursor.slice(17));
677
- if (!Number.isInteger(offset) || offset < 0) throw new Error("Invalid primary id mutation cursor.");
678
- return offset;
679
- };
680
- const canUsePrimaryIdLookupCursor = (cursor) => cursor === null || cursor === void 0 || cursor.startsWith(PRIMARY_ID_LOOKUP_CURSOR_PREFIX);
681
- const extractPrimaryIdLookup = (expression) => {
682
- if (!expression || expression.type !== "binary") return null;
683
- const binary = expression;
684
- const [left, right] = binary.operands;
685
- if (binary.operator === "eq") {
686
- if (isFieldReference(left) && left.fieldName === "_id") {
687
- if (isFieldReference(right)) return null;
688
- return {
689
- kind: "eq",
690
- values: [right]
691
- };
692
- }
693
- if (isFieldReference(right) && right.fieldName === "_id") {
694
- if (isFieldReference(left)) return null;
695
- return {
696
- kind: "eq",
697
- values: [left]
698
- };
699
- }
700
- }
701
- if (binary.operator === "inArray" && isFieldReference(left) && left.fieldName === "_id" && Array.isArray(right)) return {
702
- kind: "in",
703
- values: dedupePrimaryIds(right)
704
- };
705
- return null;
706
- };
707
- const windowPrimaryIdLookup = (lookup, pagination) => {
708
- if (!pagination) return {
709
- continueCursor: null,
710
- isDone: true,
711
- values: lookup.values
712
- };
713
- const offset = parsePrimaryIdLookupCursor(pagination.cursor);
714
- const nextOffset = offset + pagination.limit;
715
- const values = lookup.values.slice(offset, nextOffset);
716
- const isDone = nextOffset >= lookup.values.length;
717
- return {
718
- continueCursor: isDone ? null : `${PRIMARY_ID_LOOKUP_CURSOR_PREFIX}${nextOffset}`,
719
- isDone,
720
- values
721
- };
722
- };
723
- const collectPrimaryIdLookupRows = async (db, tableName, lookup, options) => {
724
- if (!options.pagination && lookup.values.length > options.maxRows) throw new Error(`${options.operation} exceeded mutationMaxRows (${options.maxRows}) on "${tableName}". Narrow the filter or increase defineSchema(..., { defaults: { mutationMaxRows } }).`);
725
- if (!options.pagination && lookup.values.length > options.batchSize) throw new Error(`${options.operation} matched more than mutationBatchSize (${options.batchSize}) primary ids on "${tableName}". Use executeAsync({ batchSize }) or paginate() to split the mutation.`);
726
- const primaryIdWindow = windowPrimaryIdLookup(lookup, options.pagination);
727
- if (primaryIdWindow.values.length > options.maxRows) throw new Error(`${options.operation} exceeded mutationMaxRows (${options.maxRows}) on "${tableName}". Narrow the filter or increase defineSchema(..., { defaults: { mutationMaxRows } }).`);
728
- const normalizedIds = primaryIdWindow.values.filter((value) => value !== null && value !== void 0).map((value) => db.normalizeId(tableName, value)).filter((value) => value !== null);
729
- const fetchedRows = await Promise.all(normalizedIds.map((value) => db.get(value)));
730
- return {
731
- continueCursor: primaryIdWindow.continueCursor,
732
- isDone: primaryIdWindow.isDone,
733
- rows: fetchedRows.filter((row) => !!row)
734
- };
735
- };
736
- const DEFAULT_MUTATION_BATCH_SIZE = 400;
737
- const DEFAULT_MUTATION_LEAF_BATCH_SIZE = 1600;
738
- const DEFAULT_MUTATION_MAX_ROWS = 1e4;
739
- const DEFAULT_MUTATION_MAX_BYTES_PER_BATCH = 2097152;
740
- const DEFAULT_MUTATION_SCHEDULE_CALL_CAP = 800;
741
- const DEFAULT_MUTATION_ASYNC_DELAY_MS = 0;
742
- const DEFAULT_COUNT_BACKFILL_BATCH_SIZE = 1e3;
743
- const DEFAULT_RELATION_FAN_OUT_MAX_KEYS$1 = 1e3;
744
- const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$2 = 4096;
745
- const DEFAULT_AGGREGATE_WORK_BUDGET$2 = 16384;
746
- const MEASURED_BYTE_SAFETY_MULTIPLIER = 2;
747
- const UTF8_LENGTH_THRESHOLD = 500;
748
- const UTF8_ENCODER = new TextEncoder();
749
- const getUtf8ByteLength = (value) => {
750
- if (value.length > UTF8_LENGTH_THRESHOLD) return UTF8_ENCODER.encode(value).length;
751
- let bytes = 0;
752
- for (let i = 0; i < value.length; i++) {
753
- const code = value.charCodeAt(i);
754
- if (code < 128) bytes += 1;
755
- else if (code < 2048) bytes += 2;
756
- else if (code >= 55296 && code <= 56319) {
757
- bytes += 4;
758
- i += 1;
759
- } else bytes += 3;
760
- }
761
- return bytes;
762
- };
763
- const resolveOrmRuntimeDefaults = (defaults, runtime = {}) => {
764
- const inferredMode = runtime.scheduler && runtime.scheduledMutationBatch ? "async" : "sync";
765
- return {
766
- defaultLimit: defaults?.defaultLimit,
767
- countBackfillBatchSize: defaults?.countBackfillBatchSize ?? DEFAULT_COUNT_BACKFILL_BATCH_SIZE,
768
- relationFanOutMaxKeys: defaults?.relationFanOutMaxKeys ?? DEFAULT_RELATION_FAN_OUT_MAX_KEYS$1,
769
- aggregateCartesianMaxKeys: defaults?.aggregateCartesianMaxKeys ?? DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$2,
770
- aggregateWorkBudget: defaults?.aggregateWorkBudget ?? DEFAULT_AGGREGATE_WORK_BUDGET$2,
771
- mutationBatchSize: defaults?.mutationBatchSize ?? DEFAULT_MUTATION_BATCH_SIZE,
772
- mutationLeafBatchSize: defaults?.mutationLeafBatchSize ?? DEFAULT_MUTATION_LEAF_BATCH_SIZE,
773
- mutationMaxRows: defaults?.mutationMaxRows ?? DEFAULT_MUTATION_MAX_ROWS,
774
- mutationMaxBytesPerBatch: defaults?.mutationMaxBytesPerBatch ?? DEFAULT_MUTATION_MAX_BYTES_PER_BATCH,
775
- mutationScheduleCallCap: defaults?.mutationScheduleCallCap ?? DEFAULT_MUTATION_SCHEDULE_CALL_CAP,
776
- mutationExecutionMode: defaults?.mutationExecutionMode ?? inferredMode,
777
- mutationAsyncDelayMs: defaults?.mutationAsyncDelayMs ?? DEFAULT_MUTATION_ASYNC_DELAY_MS
778
- };
779
- };
780
- const estimateMeasuredMutationRowBytes = (row) => {
781
- return getUtf8ByteLength(JSON.stringify(row)) * MEASURED_BYTE_SAFETY_MULTIPLIER;
782
- };
783
- const takeRowsWithinByteBudget = (rows, maxBytesPerBatch) => {
784
- if (!Number.isInteger(maxBytesPerBatch) || maxBytesPerBatch < 1) throw new Error("mutationMaxBytesPerBatch must be a positive integer.");
785
- if (rows.length === 0) return {
786
- rows,
787
- hitLimit: false
788
- };
789
- let bytes = 0;
790
- const selected = [];
791
- for (const row of rows) {
792
- const rowBytes = estimateMeasuredMutationRowBytes(row);
793
- if (selected.length > 0 && bytes + rowBytes > maxBytesPerBatch) return {
794
- rows: selected,
795
- hitLimit: true
796
- };
797
- selected.push(row);
798
- bytes += rowBytes;
799
- }
800
- return {
801
- rows: selected,
802
- hitLimit: false
803
- };
804
- };
805
- const getMutationCollectionLimits = (context) => {
806
- const defaults = context?.resolvedDefaults ?? context?.defaults;
807
- const batchSize = defaults?.mutationBatchSize ?? DEFAULT_MUTATION_BATCH_SIZE;
808
- const leafBatchSize = defaults?.mutationLeafBatchSize ?? DEFAULT_MUTATION_LEAF_BATCH_SIZE;
809
- const maxRows = defaults?.mutationMaxRows ?? DEFAULT_MUTATION_MAX_ROWS;
810
- const maxBytesPerBatch = defaults?.mutationMaxBytesPerBatch ?? DEFAULT_MUTATION_MAX_BYTES_PER_BATCH;
811
- const scheduleCallCap = defaults?.mutationScheduleCallCap ?? DEFAULT_MUTATION_SCHEDULE_CALL_CAP;
812
- if (!Number.isInteger(batchSize) || batchSize < 1) throw new Error("mutationBatchSize must be a positive integer.");
813
- if (!Number.isInteger(leafBatchSize) || leafBatchSize < 1) throw new Error("mutationLeafBatchSize must be a positive integer.");
814
- if (!Number.isInteger(maxRows) || maxRows < 1) throw new Error("mutationMaxRows must be a positive integer.");
815
- if (!Number.isInteger(maxBytesPerBatch) || maxBytesPerBatch < 1) throw new Error("mutationMaxBytesPerBatch must be a positive integer.");
816
- if (!Number.isInteger(scheduleCallCap) || scheduleCallCap < 1) throw new Error("mutationScheduleCallCap must be a positive integer.");
817
- return {
818
- batchSize,
819
- leafBatchSize,
820
- maxRows,
821
- maxBytesPerBatch,
822
- scheduleCallCap
823
- };
824
- };
825
- const consumeScheduleCall = (state) => {
826
- if (!state) return;
827
- if (state.remainingCalls < 1) throw new Error(`Async cascade scheduling exceeded mutationScheduleCallCap (${state.callCap}). Increase defineSchema(..., { defaults: { mutationScheduleCallCap } }) or reduce fan-out per mutation.`);
828
- state.remainingCalls -= 1;
829
- };
830
- const getMutationExecutionMode = (context, override) => {
831
- const requestedMode = override ?? context?.resolvedDefaults?.mutationExecutionMode ?? context?.defaults?.mutationExecutionMode;
832
- if (requestedMode === "sync") return "sync";
833
- if (requestedMode === "async") {
834
- if (override === "async") return "async";
835
- if (context?.scheduler && context?.scheduledMutationBatch) return "async";
836
- return "sync";
837
- }
838
- if (context?.scheduler && context?.scheduledMutationBatch) return "async";
839
- return "sync";
840
- };
841
- const getMutationAsyncDelayMs = (context, override) => override ?? context?.resolvedDefaults?.mutationAsyncDelayMs ?? context?.defaults?.mutationAsyncDelayMs ?? DEFAULT_MUTATION_ASYNC_DELAY_MS;
842
- const collectMutationRowsBounded = async (buildQuery, options) => {
843
- const rows = await buildQuery().take(options.maxRows + 1);
844
- if (rows.length > options.maxRows) throw new Error(`${options.operation} matched more than ${options.maxRows} rows on "${options.tableName}". Narrow the filter or increase defineSchema(..., { defaults: { mutationMaxRows } }).`);
845
- return rows;
846
- };
847
- function getTableName(table) {
848
- const name = table.tableName ?? table[TableName] ?? table?._?.name;
849
- if (!name) throw new Error("Table is missing a name");
850
- return name;
851
- }
852
- function getTableDeleteConfig(table) {
853
- return table[TableDeleteConfig];
854
- }
855
- function getUniqueIndexes(table) {
856
- const fromMethod = table.getUniqueIndexes?.();
857
- if (Array.isArray(fromMethod)) return fromMethod;
858
- const fromField = table.uniqueIndexes;
859
- return Array.isArray(fromField) ? fromField : [];
860
- }
861
- function getChecks(table) {
862
- const fromMethod = table.getChecks?.();
863
- if (Array.isArray(fromMethod)) return fromMethod;
864
- const fromField = table.checks;
865
- return Array.isArray(fromField) ? fromField : [];
866
- }
867
- function buildForeignKeyGraph(schema) {
868
- const tableByName = /* @__PURE__ */ new Map();
869
- for (const tableConfig of Object.values(schema)) if (tableConfig?.name && tableConfig.table) tableByName.set(tableConfig.name, tableConfig.table);
870
- const incomingByTable = /* @__PURE__ */ new Map();
871
- for (const tableConfig of Object.values(schema)) {
872
- const sourceTable = tableConfig.table;
873
- const sourceTableName = tableConfig.name;
874
- const foreignKeys = getForeignKeys(sourceTable);
875
- for (const foreignKey of foreignKeys) {
876
- const targetTableName = foreignKey.foreignTableName;
877
- if (!tableByName.get(targetTableName)) throw new Error(`Foreign key from '${sourceTableName}' references missing table '${targetTableName}'.`);
878
- const entry = {
879
- sourceTable,
880
- sourceTableName,
881
- sourceColumns: foreignKey.columns,
882
- targetTableName,
883
- targetColumns: foreignKey.foreignColumns,
884
- onDelete: foreignKey.onDelete,
885
- onUpdate: foreignKey.onUpdate
886
- };
887
- const list = incomingByTable.get(targetTableName) ?? [];
888
- list.push(entry);
889
- incomingByTable.set(targetTableName, list);
890
- }
891
- }
892
- return { incomingByTable };
893
- }
894
- function getOrmContext(db) {
895
- return db[OrmContext];
896
- }
897
- function getForeignKeys(table) {
898
- const fromMethod = table.getForeignKeys?.();
899
- if (Array.isArray(fromMethod)) return fromMethod;
900
- const fromField = table.foreignKeys;
901
- return Array.isArray(fromField) ? fromField : [];
902
- }
903
- function getColumnName$1(column) {
904
- const name = column.config?.name ?? column?._?.name;
905
- if (!name) throw new Error("Column builder is missing a column name");
906
- return name;
907
- }
908
- function getTableColumns$2(table) {
909
- return table[Columns] ?? {};
910
- }
911
- function getTablePolymorphicConfigs(table) {
912
- const fromMethod = table.getPolymorphicConfigs?.();
913
- if (Array.isArray(fromMethod)) return fromMethod;
914
- const fromSymbol = table[TablePolymorphic];
915
- return Array.isArray(fromSymbol) ? fromSymbol : [];
916
- }
917
- function enforcePolymorphicWrite(table, candidate, options) {
918
- const configs = getTablePolymorphicConfigs(table);
919
- if (configs.length === 0) return;
920
- for (const config of configs) {
921
- const changedFields = options?.changedFields;
922
- const discriminatorChanged = !changedFields || changedFields.has(config.discriminator);
923
- const generatedFieldChanged = !changedFields || config.generatedFieldNames.some((fieldName) => changedFields.has(fieldName));
924
- if (!discriminatorChanged && !generatedFieldChanged) continue;
925
- const discriminatorValue = candidate[config.discriminator];
926
- if (typeof discriminatorValue !== "string" || !Object.hasOwn(config.variants, discriminatorValue)) throw new Error(`Invalid discriminator '${config.discriminator}' on '${getTableName(table)}'. Expected one of: ${Object.keys(config.variants).join(", ")}.`);
927
- const activeVariant = config.variants[discriminatorValue];
928
- if (!activeVariant) throw new Error(`Invalid discriminator '${config.discriminator}' value '${discriminatorValue}' on '${getTableName(table)}'.`);
929
- for (const requiredFieldName of activeVariant.requiredFieldNames) {
930
- const value = candidate[requiredFieldName];
931
- if (value === null || value === void 0) throw new Error(`discriminator branch '${discriminatorValue}' requires '${requiredFieldName}' on '${getTableName(table)}'.`);
932
- }
933
- const activeFieldSet = new Set(activeVariant.fieldNames);
934
- for (const generatedFieldName of config.generatedFieldNames) {
935
- if (activeFieldSet.has(generatedFieldName)) continue;
936
- const value = candidate[generatedFieldName];
937
- if (value !== null && value !== void 0) throw new Error(`discriminator branch '${discriminatorValue}' cannot set '${generatedFieldName}' on '${getTableName(table)}' because it belongs to another variant.`);
938
- }
939
- }
940
- }
941
- function getColumnConfig(table, columnName) {
942
- const builder = getTableColumns$2(table)[columnName];
943
- if (!builder) return null;
944
- return builder.config ?? null;
945
- }
946
- function applyDefaults(table, value) {
947
- const columns = table[Columns];
948
- if (!columns) return value;
949
- const result = { ...value };
950
- const polymorphicConfigs = getTablePolymorphicConfigs(table);
951
- const activeGeneratedFields = /* @__PURE__ */ new Set();
952
- const generatedFields = /* @__PURE__ */ new Set();
953
- for (const config of polymorphicConfigs) {
954
- for (const fieldName of config.generatedFieldNames) generatedFields.add(fieldName);
955
- const discriminatorValue = result[config.discriminator];
956
- if (typeof discriminatorValue !== "string") continue;
957
- const activeVariant = config.variants[discriminatorValue];
958
- if (!activeVariant) continue;
959
- for (const fieldName of activeVariant.fieldNames) activeGeneratedFields.add(fieldName);
960
- }
961
- for (const [columnName, builder] of Object.entries(columns)) {
962
- if (result[columnName] !== void 0) continue;
963
- if (generatedFields.has(columnName) && !activeGeneratedFields.has(columnName)) continue;
964
- const config = builder.config;
965
- if (!config) continue;
966
- if (typeof config.defaultFn === "function") {
967
- result[columnName] = config.defaultFn();
968
- continue;
969
- }
970
- if (config.hasDefault) {
971
- result[columnName] = config.default;
972
- continue;
973
- }
974
- if (typeof config.onUpdateFn === "function") result[columnName] = config.onUpdateFn();
975
- }
976
- return result;
977
- }
978
- async function enforceUniqueIndexes(db, table, candidate, options) {
979
- const uniqueIndexes = getUniqueIndexes(table);
980
- if (uniqueIndexes.length === 0) return;
981
- const tableName = getTableName(table);
982
- const changedFields = options?.changedFields;
983
- for (const index of uniqueIndexes) {
984
- if (changedFields && !index.fields.some((field) => changedFields.has(field))) continue;
985
- const entries = index.fields.map((field) => [field, candidate[field]]);
986
- if (entries.some(([, value]) => value === void 0 || value === null) && !index.nullsNotDistinct) continue;
987
- const existing = await db.query(tableName).withIndex(index.name, (q) => {
988
- let builder = q.eq(entries[0][0], entries[0][1]);
989
- for (let i = 1; i < entries.length; i++) builder = builder.eq(entries[i][0], entries[i][1]);
990
- return builder;
991
- }).unique();
992
- if (existing !== null && (options?.currentId === void 0 || existing._id !== options.currentId)) throw new Error(`Unique index '${index.name}' violation on '${tableName}'.`);
993
- }
994
- }
995
- async function enforceForeignKeys(db, table, candidate, options) {
996
- const foreignKeys = getForeignKeys(table);
997
- if (foreignKeys.length === 0) return;
998
- const tableName = getTableName(table);
999
- const changedFields = options?.changedFields;
1000
- for (const foreignKey of foreignKeys) {
1001
- if (changedFields && !foreignKey.columns.some((field) => changedFields.has(field))) continue;
1002
- const entries = foreignKey.columns.map((field) => [field, candidate[field]]);
1003
- if (entries.some(([, value]) => value === void 0 || value === null)) continue;
1004
- if (foreignKey.foreignColumns.length === 1 && foreignKey.foreignColumns[0] === "_id") {
1005
- const foreignId = entries[0]?.[1];
1006
- if (!await db.get(foreignId)) throw new Error(`Foreign key violation on '${tableName}': missing document in '${foreignKey.foreignTableName}'.`);
1007
- continue;
1008
- }
1009
- if (!foreignKey.foreignTable) throw new Error(`Foreign key on '${tableName}' requires indexed foreign columns on '${foreignKey.foreignTableName}'.`);
1010
- const indexName = findIndexForColumns(getIndexes(foreignKey.foreignTable), foreignKey.foreignColumns);
1011
- if (!indexName) throw new Error(`Foreign key on '${tableName}' requires index on '${foreignKey.foreignTableName}(${foreignKey.foreignColumns.join(", ")})'.`);
1012
- if (!await db.query(foreignKey.foreignTableName).withIndex(indexName, (q) => {
1013
- let builder = q.eq(foreignKey.foreignColumns[0], entries[0][1]);
1014
- for (let i = 1; i < entries.length; i++) builder = builder.eq(foreignKey.foreignColumns[i], entries[i][1]);
1015
- return builder;
1016
- }).first()) throw new Error(`Foreign key violation on '${tableName}': missing document in '${foreignKey.foreignTableName}'.`);
1017
- }
1018
- }
1019
- function getIndexForForeignKey(foreignKey) {
1020
- return findIndexForColumns(getIndexes(foreignKey.sourceTable), foreignKey.sourceColumns);
1021
- }
1022
- function foreignKeyIndexError(foreignKey) {
1023
- return /* @__PURE__ */ new Error(`Foreign key on '${foreignKey.sourceTableName}' requires index on '${foreignKey.sourceTableName}(${foreignKey.sourceColumns.join(", ")})' for cascading actions.`);
1024
- }
1025
- function buildIndexPredicate(q, columns, values) {
1026
- let builder = q.eq(columns[0], values[0]);
1027
- for (let i = 1; i < columns.length; i++) builder = builder.eq(columns[i], values[i]);
1028
- return builder;
1029
- }
1030
- function buildFilterPredicate(q, columns, values) {
1031
- let expr = q.eq(q.field(columns[0]), values[0]);
1032
- for (let i = 1; i < columns.length; i++) expr = q.and(expr, q.eq(q.field(columns[i]), values[i]));
1033
- return expr;
1034
- }
1035
- function ensureNullableColumns(table, columns, context) {
1036
- for (const columnName of columns) {
1037
- const config = getColumnConfig(table, columnName);
1038
- if (!config) throw new Error(`${context}: missing column '${columnName}' in table '${getTableName(table)}'.`);
1039
- if (config.notNull) throw new Error(`${context}: column '${columnName}' is not nullable in '${getTableName(table)}'.`);
1040
- }
1041
- }
1042
- function ensureDefaultColumns(table, columns, context) {
1043
- const defaults = {};
1044
- for (const columnName of columns) {
1045
- const config = getColumnConfig(table, columnName);
1046
- if (!config) throw new Error(`${context}: missing column '${columnName}' in table '${getTableName(table)}'.`);
1047
- if (!config.hasDefault) throw new Error(`${context}: column '${columnName}' has no default in '${getTableName(table)}'.`);
1048
- defaults[columnName] = config.default;
1049
- }
1050
- return defaults;
1051
- }
1052
- function ensureNonNullValues(table, values, context) {
1053
- for (const [columnName, value] of Object.entries(values)) if (getColumnConfig(table, columnName)?.notNull && (value === null || value === void 0)) throw new Error(`${context}: column '${columnName}' cannot be null in '${getTableName(table)}'.`);
1054
- }
1055
- async function collectReferencingRows(db, foreignKey, targetValues, indexName, options) {
1056
- return collectMutationRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)), {
1057
- operation: options.operation,
1058
- tableName: foreignKey.sourceTableName,
1059
- batchSize: options.batchSize,
1060
- maxRows: options.maxRows
1061
- });
1062
- }
1063
- async function collectAsyncCascadeRowsBounded(buildQuery, batchSize, maxBytesPerBatch) {
1064
- const rows = await buildQuery().take(batchSize + 1);
1065
- const hasMoreRows = rows.length > batchSize;
1066
- const bounded = takeRowsWithinByteBudget(hasMoreRows ? rows.slice(0, batchSize) : rows, maxBytesPerBatch);
1067
- return {
1068
- rows: bounded.rows,
1069
- needsContinuation: hasMoreRows || bounded.hitLimit
1070
- };
1071
- }
1072
- async function hasReferencingRow(db, foreignKey, targetValues, indexName) {
1073
- const query = db.query(foreignKey.sourceTableName);
1074
- return (indexName ? await query.withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)).first() : await query.filter((q) => buildFilterPredicate(q, foreignKey.sourceColumns, targetValues)).first()) !== null;
1075
- }
1076
- async function softDeleteRow(db, table, row) {
1077
- const tableName = getTableName(table);
1078
- if (!("deletionTime" in getTableColumns$2(table))) throw new Error(`Soft delete requires 'deletionTime' field on '${tableName}'.`);
1079
- const deletionTime = Date.now();
1080
- await db.patch(tableName, row._id, { deletionTime });
1081
- return deletionTime;
1082
- }
1083
- /**
1084
- * Applies one loop-invariant payload to every referencing row. The ids come
1085
- * from a single index scan so they are distinct, and the write set is
1086
- * order-independent — but a table whose writes run through lifecycle hooks
1087
- * keeps the sequential loop: those writes are serialized by the lifecycle write
1088
- * lock anyway, and their hooks would otherwise fire out of write order.
1089
- */
1090
- async function patchReferencingRows(db, tableName, rows, patch) {
1091
- if (hasLifecycleHooks(db, tableName)) {
1092
- for (const row of rows) await db.patch(tableName, row._id, patch);
1093
- return;
1094
- }
1095
- await mapWithConcurrency(rows, DEFAULT_WRITE_FANOUT_CONCURRENCY, async (row) => {
1096
- await db.patch(tableName, row._id, patch);
1097
- });
1098
- }
1099
- async function hardDeleteRow(db, tableName, row) {
1100
- await db.delete(tableName, row._id);
1101
- }
1102
- async function applyIncomingForeignKeyActionsOnDelete(db, table, row, options) {
1103
- const tableName = getTableName(table);
1104
- const incoming = options.graph.incomingByTable.get(tableName) ?? [];
1105
- if (incoming.length === 0) return;
1106
- for (const foreignKey of incoming) {
1107
- const action = foreignKey.onDelete ?? "no action";
1108
- const targetValues = foreignKey.targetColumns.map((column) => row[column]);
1109
- if (targetValues.some((value) => value === void 0 || value === null)) continue;
1110
- const indexName = getIndexForForeignKey(foreignKey);
1111
- if (action === "restrict" || action === "no action") {
1112
- if (!indexName && !options.allowFullScan) throw foreignKeyIndexError(foreignKey);
1113
- if (!indexName && options.strict) console.warn(`Foreign key check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`);
1114
- if (await hasReferencingRow(db, foreignKey, targetValues, indexName)) throw new Error(`Foreign key restrict violation on '${tableName}' from '${foreignKey.sourceTableName}'.`);
1115
- continue;
1116
- }
1117
- if (!indexName) {
1118
- if (!options.allowFullScan) throw foreignKeyIndexError(foreignKey);
1119
- if (options.strict) console.warn(`Foreign key cascade check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`);
1120
- if (await hasReferencingRow(db, foreignKey, targetValues, null)) throw foreignKeyIndexError(foreignKey);
1121
- continue;
1122
- }
1123
- let referencingRows;
1124
- if (options.executionMode === "async") {
1125
- const asyncBatchSize = action === "cascade" ? options.batchSize : options.leafBatchSize;
1126
- const { rows, needsContinuation } = await collectAsyncCascadeRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, targetValues)), asyncBatchSize, options.maxBytesPerBatch);
1127
- referencingRows = rows;
1128
- if (needsContinuation) {
1129
- if (!options.scheduler || !options.scheduledMutationBatch) throw new Error("Async mutation execution requires orm.db(ctx) configured with scheduling (ormFunctions.scheduledMutationBatch).");
1130
- consumeScheduleCall(options.scheduleState);
1131
- await options.scheduler.runAfter(options.delayMs ?? 0, options.scheduledMutationBatch, {
1132
- workType: "cascade-delete",
1133
- mode: "async",
1134
- operation: "delete",
1135
- table: foreignKey.sourceTableName,
1136
- foreignIndexName: indexName,
1137
- foreignSourceColumns: foreignKey.sourceColumns,
1138
- targetValues: encodeUndefinedDeep(targetValues),
1139
- foreignAction: action,
1140
- deleteMode: options.deleteMode,
1141
- cascadeMode: options.cascadeMode,
1142
- cursor: null,
1143
- batchSize: asyncBatchSize,
1144
- maxBytesPerBatch: options.maxBytesPerBatch,
1145
- delayMs: options.delayMs ?? 0
1146
- });
1147
- }
1148
- } else referencingRows = await collectReferencingRows(db, foreignKey, targetValues, indexName, {
1149
- operation: "delete",
1150
- batchSize: options.batchSize,
1151
- maxRows: options.maxRows
1152
- });
1153
- if (referencingRows.length === 0) continue;
1154
- if (action === "set null") {
1155
- ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`);
1156
- const nullPatch = {};
1157
- for (const columnName of foreignKey.sourceColumns) nullPatch[columnName] = null;
1158
- await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, nullPatch);
1159
- continue;
1160
- }
1161
- if (action === "set default") {
1162
- const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`);
1163
- await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, defaults);
1164
- continue;
1165
- }
1166
- if (action === "cascade") for (const referencingRow of referencingRows) {
1167
- const key = `${foreignKey.sourceTableName}:${referencingRow._id}`;
1168
- if (options.visited.has(key)) continue;
1169
- options.visited.add(key);
1170
- await applyIncomingForeignKeyActionsOnDelete(db, foreignKey.sourceTable, referencingRow, options);
1171
- if (options.cascadeMode === "soft") await softDeleteRow(db, foreignKey.sourceTable, referencingRow);
1172
- else await hardDeleteRow(db, foreignKey.sourceTableName, referencingRow);
1173
- }
1174
- }
1175
- }
1176
- async function applyIncomingForeignKeyActionsOnUpdate(db, table, oldRow, newRow, options) {
1177
- const tableName = getTableName(table);
1178
- const incoming = options.graph.incomingByTable.get(tableName) ?? [];
1179
- if (incoming.length === 0) return;
1180
- for (const foreignKey of incoming) {
1181
- const action = foreignKey.onUpdate ?? "no action";
1182
- const oldValues = foreignKey.targetColumns.map((column) => oldRow[column]);
1183
- const newValues = foreignKey.targetColumns.map((column) => newRow[column]);
1184
- if (!oldValues.some((value, index) => !Object.is(value, newValues[index]))) continue;
1185
- if (oldValues.some((value) => value === void 0 || value === null)) continue;
1186
- const indexName = getIndexForForeignKey(foreignKey);
1187
- if (action === "restrict" || action === "no action") {
1188
- if (!indexName && !options.allowFullScan) throw foreignKeyIndexError(foreignKey);
1189
- if (!indexName && options.strict) console.warn(`Foreign key check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`);
1190
- if (await hasReferencingRow(db, foreignKey, oldValues, indexName)) throw new Error(`Foreign key restrict violation on '${tableName}' from '${foreignKey.sourceTableName}'.`);
1191
- continue;
1192
- }
1193
- if (!indexName) {
1194
- if (!options.allowFullScan) throw foreignKeyIndexError(foreignKey);
1195
- if (options.strict) console.warn(`Foreign key cascade check running without index (allowFullScan: true) on '${foreignKey.sourceTableName}'.`);
1196
- if (await hasReferencingRow(db, foreignKey, oldValues, null)) throw foreignKeyIndexError(foreignKey);
1197
- continue;
1198
- }
1199
- let referencingRows;
1200
- if (options.executionMode === "async") {
1201
- const asyncBatchSize = options.leafBatchSize;
1202
- const { rows, needsContinuation } = await collectAsyncCascadeRowsBounded(() => db.query(foreignKey.sourceTableName).withIndex(indexName, (q) => buildIndexPredicate(q, foreignKey.sourceColumns, oldValues)), asyncBatchSize, options.maxBytesPerBatch);
1203
- referencingRows = rows;
1204
- if (needsContinuation) {
1205
- if (!options.scheduler || !options.scheduledMutationBatch) throw new Error("Async mutation execution requires orm.db(ctx) configured with scheduling (ormFunctions.scheduledMutationBatch).");
1206
- consumeScheduleCall(options.scheduleState);
1207
- await options.scheduler.runAfter(options.delayMs ?? 0, options.scheduledMutationBatch, {
1208
- workType: "cascade-update",
1209
- mode: "async",
1210
- operation: "update",
1211
- table: foreignKey.sourceTableName,
1212
- foreignIndexName: indexName,
1213
- foreignSourceColumns: foreignKey.sourceColumns,
1214
- targetValues: encodeUndefinedDeep(oldValues),
1215
- newValues: encodeUndefinedDeep(newValues),
1216
- foreignAction: action,
1217
- cursor: null,
1218
- batchSize: asyncBatchSize,
1219
- maxBytesPerBatch: options.maxBytesPerBatch,
1220
- delayMs: options.delayMs ?? 0
1221
- });
1222
- }
1223
- } else referencingRows = await collectReferencingRows(db, foreignKey, oldValues, indexName, {
1224
- operation: "update",
1225
- batchSize: options.batchSize,
1226
- maxRows: options.maxRows
1227
- });
1228
- if (referencingRows.length === 0) continue;
1229
- if (action === "set null") {
1230
- ensureNullableColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set null on '${foreignKey.sourceTableName}'`);
1231
- const nullPatch = {};
1232
- for (const columnName of foreignKey.sourceColumns) nullPatch[columnName] = null;
1233
- await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, nullPatch);
1234
- continue;
1235
- }
1236
- if (action === "set default") {
1237
- const defaults = ensureDefaultColumns(foreignKey.sourceTable, foreignKey.sourceColumns, `Foreign key set default on '${foreignKey.sourceTableName}'`);
1238
- await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, defaults);
1239
- continue;
1240
- }
1241
- if (action === "cascade") {
1242
- const patchValues = {};
1243
- for (let i = 0; i < foreignKey.sourceColumns.length; i++) patchValues[foreignKey.sourceColumns[i]] = newValues[i];
1244
- ensureNonNullValues(foreignKey.sourceTable, patchValues, `Foreign key cascade update on '${foreignKey.sourceTableName}'`);
1245
- await patchReferencingRows(db, foreignKey.sourceTableName, referencingRows, patchValues);
1246
- }
1247
- }
1248
- }
1249
- function getSelectionColumnName(value) {
1250
- if (value && typeof value === "object") {
1251
- if ("columnName" in value) return value.columnName;
1252
- if ("config" in value && value.config?.name) return value.config.name;
1253
- }
1254
- throw new Error("Returning selection must reference a column");
1255
- }
1256
- function splitReturningSelection(fields) {
1257
- const columnSelection = {};
1258
- let countSelection;
1259
- for (const [key, value] of Object.entries(fields)) {
1260
- if (key !== "_count") {
1261
- getSelectionColumnName(value);
1262
- columnSelection[key] = value;
1263
- continue;
1264
- }
1265
- if (value === void 0) continue;
1266
- if (!isPlainObject$1(value)) throw new Error("returning({ _count }) requires an object.");
1267
- const nextCountSelection = {};
1268
- for (const [relationName, relationSelection] of Object.entries(value)) {
1269
- if (relationSelection === void 0 || relationSelection === false) continue;
1270
- if (relationSelection === true) {
1271
- nextCountSelection[relationName] = true;
1272
- continue;
1273
- }
1274
- if (!isPlainObject$1(relationSelection)) throw new Error(`returning({ _count.${relationName} }) must be true or { where }`);
1275
- if ("select" in relationSelection) throw new Error(`returning({ _count.${relationName}.select }) is removed. Use returning({ _count: { ${relationName}: true } })`);
1276
- for (const [optionKey, optionValue] of Object.entries(relationSelection)) if (optionKey !== "where" && optionValue !== void 0) throw new Error(`returning({ _count.${relationName} }) does not support '${optionKey}'`);
1277
- if (typeof relationSelection.where === "function") throw new Error(`returning({ _count.${relationName}.where }) callback is unsupported in v1`);
1278
- nextCountSelection[relationName] = { where: relationSelection.where };
1279
- }
1280
- countSelection = nextCountSelection;
1281
- }
1282
- return {
1283
- columnSelection: Object.keys(columnSelection).length > 0 ? columnSelection : void 0,
1284
- countSelection
1285
- };
1286
- }
1287
- const matchLike = matchLikePattern;
1288
- function evaluateFilter(row, filter) {
1289
- if (filter.type === "binary") {
1290
- const [field, value] = filter.operands;
1291
- if (!isFieldReference(field)) throw new Error("Binary expression must have FieldReference as first operand");
1292
- const fieldValue = row[field.fieldName];
1293
- switch (filter.operator) {
1294
- case "like": {
1295
- const pattern = value;
1296
- if (typeof fieldValue !== "string") return false;
1297
- return matchLike(fieldValue, pattern, false);
1298
- }
1299
- case "ilike": {
1300
- const pattern = value;
1301
- if (typeof fieldValue !== "string") return false;
1302
- return matchLike(fieldValue, pattern, true);
1303
- }
1304
- case "notLike": {
1305
- const pattern = value;
1306
- if (typeof fieldValue !== "string") return false;
1307
- return !matchLike(fieldValue, pattern, false);
1308
- }
1309
- case "notIlike": {
1310
- const pattern = value;
1311
- if (typeof fieldValue !== "string") return false;
1312
- return !matchLike(fieldValue, pattern, true);
1313
- }
1314
- case "startsWith":
1315
- if (typeof fieldValue !== "string") return false;
1316
- return fieldValue.startsWith(value);
1317
- case "endsWith":
1318
- if (typeof fieldValue !== "string") return false;
1319
- return fieldValue.endsWith(value);
1320
- case "contains":
1321
- if (typeof fieldValue !== "string") return false;
1322
- return fieldValue.includes(value);
1323
- case "eq": return filterValuesEqual(fieldValue, value);
1324
- case "ne": return !filterValuesEqual(fieldValue, value);
1325
- case "gt": return fieldValue > value;
1326
- case "gte": return fieldValue >= value;
1327
- case "lt": return fieldValue < value;
1328
- case "lte": return fieldValue <= value;
1329
- case "inArray": return filterValueInList(fieldValue, value);
1330
- case "notInArray": return !filterValueInList(fieldValue, value);
1331
- case "arrayContains":
1332
- if (!Array.isArray(fieldValue)) return false;
1333
- return value.every((item) => fieldValue.includes(item));
1334
- case "arrayContained": {
1335
- if (!Array.isArray(fieldValue)) return false;
1336
- const arr = value;
1337
- return fieldValue.every((item) => arr.includes(item));
1338
- }
1339
- case "arrayOverlaps": {
1340
- if (!Array.isArray(fieldValue)) return false;
1341
- const arr = value;
1342
- return fieldValue.some((item) => arr.includes(item));
1343
- }
1344
- default: throw new Error(`Unsupported post-fetch operator: ${filter.operator}`);
1345
- }
1346
- }
1347
- if (filter.type === "unary") {
1348
- const [operand] = filter.operands;
1349
- if (isFieldReference(operand)) {
1350
- const fieldValue = row[operand.fieldName];
1351
- switch (filter.operator) {
1352
- case "isNull": return fieldValue === null || fieldValue === void 0;
1353
- case "isNotNull": return fieldValue !== null && fieldValue !== void 0;
1354
- default: throw new Error(`Unsupported unary operator: ${filter.operator}`);
1355
- }
1356
- }
1357
- if (filter.operator === "not") return !evaluateFilter(row, operand);
1358
- throw new Error("Unary expression must have FieldReference or FilterExpression as operand");
1359
- }
1360
- if (filter.type === "logical") {
1361
- if (filter.operator === "and") return filter.operands.every((f) => evaluateFilter(row, f));
1362
- if (filter.operator === "or") return filter.operands.some((f) => evaluateFilter(row, f));
1363
- }
1364
- throw new Error(`Unsupported filter type for post-fetch: ${filter.type}`);
1365
- }
1366
- function triNot(value) {
1367
- return value === "unknown" ? "unknown" : !value;
1368
- }
1369
- function triAnd(values) {
1370
- if (values.some((value) => value === false)) return false;
1371
- if (values.every((value) => value === true)) return true;
1372
- return "unknown";
1373
- }
1374
- function triOr(values) {
1375
- if (values.some((value) => value === true)) return true;
1376
- if (values.every((value) => value === false)) return false;
1377
- return "unknown";
1378
- }
1379
- function evaluateCheckConstraintTriState(row, filter) {
1380
- if (filter.type === "binary") {
1381
- const [field, value] = filter.operands;
1382
- if (!isFieldReference(field)) throw new Error("Binary expression must have FieldReference as first operand");
1383
- const fieldValue = row[field.fieldName];
1384
- const compareValue = isFieldReference(value) ? row[value.fieldName] : value;
1385
- const nullish = (entry) => entry === null || entry === void 0;
1386
- if (nullish(fieldValue) || nullish(compareValue)) return "unknown";
1387
- switch (filter.operator) {
1388
- case "like":
1389
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1390
- return matchLike(fieldValue, compareValue, false);
1391
- case "ilike":
1392
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1393
- return matchLike(fieldValue, compareValue, true);
1394
- case "notLike":
1395
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1396
- return !matchLike(fieldValue, compareValue, false);
1397
- case "notIlike":
1398
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1399
- return !matchLike(fieldValue, compareValue, true);
1400
- case "startsWith":
1401
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1402
- return fieldValue.startsWith(compareValue);
1403
- case "endsWith":
1404
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1405
- return fieldValue.endsWith(compareValue);
1406
- case "contains":
1407
- if (typeof fieldValue !== "string" || typeof compareValue !== "string") return false;
1408
- return fieldValue.includes(compareValue);
1409
- case "eq": return fieldValue === compareValue;
1410
- case "ne": return fieldValue !== compareValue;
1411
- case "gt": return fieldValue > compareValue;
1412
- case "gte": return fieldValue >= compareValue;
1413
- case "lt": return fieldValue < compareValue;
1414
- case "lte": return fieldValue <= compareValue;
1415
- case "inArray": {
1416
- const arr = compareValue;
1417
- if (!Array.isArray(arr)) return false;
1418
- if (arr.includes(fieldValue)) return true;
1419
- return arr.some(nullish) ? "unknown" : false;
1420
- }
1421
- case "notInArray": {
1422
- const arr = compareValue;
1423
- if (!Array.isArray(arr)) return false;
1424
- if (arr.includes(fieldValue)) return false;
1425
- return arr.some(nullish) ? "unknown" : true;
1426
- }
1427
- case "arrayContains": {
1428
- if (!Array.isArray(fieldValue)) return false;
1429
- const arr = compareValue;
1430
- if (!Array.isArray(arr)) return false;
1431
- return arr.every((item) => fieldValue.includes(item));
1432
- }
1433
- case "arrayContained": {
1434
- if (!Array.isArray(fieldValue)) return false;
1435
- const arr = compareValue;
1436
- if (!Array.isArray(arr)) return false;
1437
- return fieldValue.every((item) => arr.includes(item));
1438
- }
1439
- case "arrayOverlaps": {
1440
- if (!Array.isArray(fieldValue)) return false;
1441
- const arr = compareValue;
1442
- if (!Array.isArray(arr)) return false;
1443
- return fieldValue.some((item) => arr.includes(item));
1444
- }
1445
- default: throw new Error(`Unsupported operator: ${filter.operator}`);
1446
- }
1447
- }
1448
- if (filter.type === "unary") {
1449
- const [operand] = filter.operands;
1450
- if (isFieldReference(operand)) {
1451
- const fieldValue = row[operand.fieldName];
1452
- switch (filter.operator) {
1453
- case "isNull": return fieldValue === null || fieldValue === void 0;
1454
- case "isNotNull": return fieldValue !== null && fieldValue !== void 0;
1455
- default: throw new Error(`Unsupported unary operator: ${filter.operator}`);
1456
- }
1457
- }
1458
- if (filter.operator === "not") return triNot(evaluateCheckConstraintTriState(row, operand));
1459
- throw new Error("Unary expression must have FieldReference or FilterExpression as operand");
1460
- }
1461
- if (filter.type === "logical") {
1462
- if (filter.operator === "and") return triAnd(filter.operands.map((operand) => evaluateCheckConstraintTriState(row, operand)));
1463
- if (filter.operator === "or") return triOr(filter.operands.map((operand) => evaluateCheckConstraintTriState(row, operand)));
1464
- }
1465
- throw new Error(`Unsupported filter type for check: ${filter.type}`);
1466
- }
1467
- function enforceCheckConstraints(table, candidate) {
1468
- const checks = getChecks(table);
1469
- if (checks.length === 0) return;
1470
- const tableName = getTableName(table);
1471
- for (const check of checks) if (evaluateCheckConstraintTriState(candidate, check.expression) === false) throw new Error(`Check constraint '${check.name}' violation on '${tableName}'.`);
1472
- }
1473
- function toConvexFilter(expression) {
1474
- const visitor = {
1475
- visitBinary: (expr) => {
1476
- const [field, value] = expr.operands;
1477
- if (!isFieldReference(field)) throw new Error("Binary expression must have FieldReference as first operand");
1478
- const fieldName = field.fieldName;
1479
- switch (expr.operator) {
1480
- case "eq": return (q) => q.eq(q.field(fieldName), value);
1481
- case "ne": return (q) => q.neq(q.field(fieldName), value);
1482
- case "gt": return (q) => q.gt(q.field(fieldName), value);
1483
- case "gte": return (q) => q.gte(q.field(fieldName), value);
1484
- case "lt": return (q) => q.lt(q.field(fieldName), value);
1485
- case "lte": return (q) => q.lte(q.field(fieldName), value);
1486
- case "inArray": {
1487
- const values = value;
1488
- return (q) => {
1489
- if (values.length === 0) return q.eq(q.field("_id"), "__better_convex_never__");
1490
- return values.map((v) => q.eq(q.field(fieldName), v)).reduce((acc, cond) => q.or(acc, cond));
1491
- };
1492
- }
1493
- case "notInArray": {
1494
- const values = value;
1495
- return (q) => {
1496
- return values.map((v) => q.neq(q.field(fieldName), v)).reduce((acc, cond) => q.and(acc, cond));
1497
- };
1498
- }
1499
- case "like":
1500
- case "ilike":
1501
- case "notLike":
1502
- case "notIlike":
1503
- case "startsWith":
1504
- case "endsWith":
1505
- case "contains":
1506
- case "arrayContains":
1507
- case "arrayContained":
1508
- case "arrayOverlaps": return () => true;
1509
- default: throw new Error(`Unsupported binary operator: ${expr.operator}`);
1510
- }
1511
- },
1512
- visitLogical: (expr) => {
1513
- const operandFns = expr.operands.map((op) => op.accept(visitor));
1514
- if (expr.operator === "and") return (q) => {
1515
- let result = operandFns[0](q);
1516
- for (let i = 1; i < operandFns.length; i++) result = q.and(result, operandFns[i](q));
1517
- return result;
1518
- };
1519
- if (expr.operator === "or") return (q) => {
1520
- let result = operandFns[0](q);
1521
- for (let i = 1; i < operandFns.length; i++) result = q.or(result, operandFns[i](q));
1522
- return result;
1523
- };
1524
- throw new Error(`Unsupported logical operator: ${expr.operator}`);
1525
- },
1526
- visitUnary: (expr) => {
1527
- const operand = expr.operands[0];
1528
- if (expr.operator === "not") {
1529
- const operandFn = operand.accept(visitor);
1530
- return (q) => q.not(operandFn(q));
1531
- }
1532
- if (expr.operator === "isNull") {
1533
- if (!isFieldReference(operand)) throw new Error("isNull must operate on a field reference");
1534
- const fieldName = operand.fieldName;
1535
- return (q) => q.eq(q.field(fieldName), null);
1536
- }
1537
- if (expr.operator === "isNotNull") {
1538
- if (!isFieldReference(operand)) throw new Error("isNotNull must operate on a field reference");
1539
- const fieldName = operand.fieldName;
1540
- return (q) => q.neq(q.field(fieldName), null);
1541
- }
1542
- throw new Error(`Unsupported unary operator: ${expr.operator}`);
1543
- }
1544
- };
1545
- return expression.accept(visitor);
1546
- }
1547
-
1548
- //#endregion
1549
- //#region src/orm/extensions.ts
1550
- function defineChainMethod$1(target, key, value) {
1551
- Object.defineProperty(target, key, {
1552
- value,
1553
- enumerable: false,
1554
- configurable: true
1555
- });
1556
- }
1557
- function createSchemaExtensionChain(state, capabilities) {
1558
- const extension = {
1559
- key: state.key,
1560
- tables: state.tables
1561
- };
1562
- Object.defineProperty(extension, OrmSchemaExtensionRelations, {
1563
- value: state.relations,
1564
- enumerable: false,
1565
- configurable: true
1566
- });
1567
- Object.defineProperty(extension, OrmSchemaExtensionTriggers, {
1568
- value: state.triggers,
1569
- enumerable: false,
1570
- configurable: true
1571
- });
1572
- if (capabilities.canRelations) defineChainMethod$1(extension, "relations", (relations) => createSchemaExtensionChain({
1573
- ...state,
1574
- relations
1575
- }, {
1576
- canRelations: false,
1577
- canTriggers: true
1578
- }));
1579
- if (capabilities.canTriggers) defineChainMethod$1(extension, "triggers", (triggers) => createSchemaExtensionChain({
1580
- ...state,
1581
- triggers
1582
- }, {
1583
- canRelations: false,
1584
- canTriggers: false
1585
- }));
1586
- return extension;
1587
- }
1588
- function defineSchemaExtension(key, tables) {
1589
- return createSchemaExtensionChain({
1590
- key,
1591
- tables,
1592
- relations: void 0,
1593
- triggers: void 0
1594
- }, {
1595
- canRelations: true,
1596
- canTriggers: true
1597
- });
1598
- }
1599
-
1600
- //#endregion
1601
- //#region src/orm/aggregate-index/schema.ts
1602
- const AGGREGATE_BUCKET_TABLE = "aggregate_bucket";
1603
- const AGGREGATE_MEMBER_TABLE = "aggregate_member";
1604
- const AGGREGATE_EXTREMA_TABLE = "aggregate_extrema";
1605
- const AGGREGATE_RANK_TREE_TABLE = "aggregate_rank_tree";
1606
- const AGGREGATE_RANK_NODE_TABLE = "aggregate_rank_node";
1607
- const AGGREGATE_STATE_TABLE = "aggregate_state";
1608
- const countBucketTable = convexTable(AGGREGATE_BUCKET_TABLE, {
1609
- tableKey: text().notNull(),
1610
- indexName: text().notNull(),
1611
- keyHash: text().notNull(),
1612
- keyParts: arrayOf(json()).notNull(),
1613
- count: integer().notNull(),
1614
- sumValues: objectOf(integer().notNull()).notNull(),
1615
- nonNullCountValues: objectOf(integer().notNull()).notNull(),
1616
- updatedAt: integer().notNull()
1617
- }, (t) => [index("by_table_index_hash").on(t.tableKey, t.indexName, t.keyHash), index("by_table_index").on(t.tableKey, t.indexName)]);
1618
- const countMemberTable = convexTable(AGGREGATE_MEMBER_TABLE, {
1619
- kind: text().notNull(),
1620
- tableKey: text().notNull(),
1621
- indexName: text().notNull(),
1622
- docId: text().notNull(),
1623
- keyHash: text().notNull(),
1624
- keyParts: arrayOf(json()).notNull(),
1625
- sumValues: objectOf(integer().notNull()).notNull(),
1626
- nonNullCountValues: objectOf(integer().notNull()).notNull(),
1627
- extremaValues: objectOf(json()).notNull(),
1628
- rankNamespace: json(),
1629
- rankKey: json(),
1630
- rankSumValue: integer(),
1631
- updatedAt: integer().notNull()
1632
- }, (t) => [index("by_kind_table_index_doc").on(t.kind, t.tableKey, t.indexName, t.docId), index("by_kind_table_index").on(t.kind, t.tableKey, t.indexName)]);
1633
- const countExtremaTable = convexTable(AGGREGATE_EXTREMA_TABLE, {
1634
- tableKey: text().notNull(),
1635
- indexName: text().notNull(),
1636
- keyHash: text().notNull(),
1637
- fieldName: text().notNull(),
1638
- valueHash: text().notNull(),
1639
- value: json().notNull(),
1640
- sortKey: text().notNull(),
1641
- count: integer().notNull(),
1642
- updatedAt: integer().notNull()
1643
- }, (t) => [
1644
- index("by_table_index").on(t.tableKey, t.indexName),
1645
- index("by_table_index_hash_field_value").on(t.tableKey, t.indexName, t.keyHash, t.fieldName, t.valueHash),
1646
- index("by_table_index_hash_field_sort").on(t.tableKey, t.indexName, t.keyHash, t.fieldName, t.sortKey)
1647
- ]);
1648
- const countStateTable = convexTable(AGGREGATE_STATE_TABLE, {
1649
- kind: text().notNull(),
1650
- tableKey: text().notNull(),
1651
- indexName: text().notNull(),
1652
- keyDefinitionHash: text().notNull(),
1653
- metricDefinitionHash: text().notNull(),
1654
- status: text().notNull(),
1655
- cursor: text(),
1656
- processed: integer().notNull(),
1657
- startedAt: integer().notNull(),
1658
- updatedAt: integer().notNull(),
1659
- completedAt: integer(),
1660
- lastError: text()
1661
- }, (t) => [
1662
- index("by_kind_table_index").on(t.kind, t.tableKey, t.indexName),
1663
- index("by_kind_status").on(t.kind, t.status),
1664
- index("by_table_status").on(t.tableKey, t.status)
1665
- ]);
1666
- const rankTreeTable = convexTable(AGGREGATE_RANK_TREE_TABLE, {
1667
- aggregateName: text().notNull(),
1668
- maxNodeSize: integer().notNull(),
1669
- namespace: json(),
1670
- root: id(AGGREGATE_RANK_NODE_TABLE).notNull()
1671
- }, (tree) => [index("by_namespace").on(tree.namespace), index("by_aggregate_name").on(tree.aggregateName)]);
1672
- const rankNodeTable = convexTable(AGGREGATE_RANK_NODE_TABLE, {
1673
- aggregate: objectOf({
1674
- count: integer().notNull(),
1675
- sum: integer().notNull()
1676
- }),
1677
- items: arrayOf(objectOf({
1678
- k: json(),
1679
- v: json(),
1680
- s: integer().notNull()
1681
- })).notNull(),
1682
- subtrees: arrayOf(text().notNull()).notNull()
1683
- });
1684
- const aggregateStorageTables = {
1685
- [AGGREGATE_BUCKET_TABLE]: countBucketTable,
1686
- [AGGREGATE_MEMBER_TABLE]: countMemberTable,
1687
- [AGGREGATE_EXTREMA_TABLE]: countExtremaTable,
1688
- [AGGREGATE_RANK_TREE_TABLE]: rankTreeTable,
1689
- [AGGREGATE_RANK_NODE_TABLE]: rankNodeTable,
1690
- [AGGREGATE_STATE_TABLE]: countStateTable
1691
- };
1692
- function aggregateExtension() {
1693
- return defineSchemaExtension("aggregate", aggregateStorageTables);
1694
- }
1695
-
1696
- //#endregion
1697
- //#region src/orm/aggregate-index/runtime.ts
1698
- const UNDEFINED_SENTINEL = "__kitcnUndefined";
1699
- const FLOAT64_SIGN_BIT = 1n << 63n;
1700
- const FLOAT64_MASK = (1n << 64n) - 1n;
1701
- const DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$1 = 4096;
1702
- const DEFAULT_AGGREGATE_WORK_BUDGET$1 = 16384;
1703
- const RANGE_PREFIX_WORK_UNIT_BASE = 2;
1704
- const AGGREGATE_BUCKET_READ_CONCURRENCY = 25;
1705
- const PUBLIC_ID_FIELD$1 = "id";
1706
- const INTERNAL_ID_FIELD$1 = "_id";
1707
- const AGGREGATE_STATE_KIND_METRIC = "metric";
1708
- const AGGREGATE_STATE_KIND_RANK = "rank";
1709
- const COUNT_STATUS_BUILDING = "BUILDING";
1710
- const COUNT_STATUS_READY = "READY";
1711
- /** Stored state is being drained before a rebuild, across several mutations. */
1712
- const COUNT_STATUS_CLEARING = "CLEARING";
1713
- const COUNT_ERROR = {
1714
- FILTER_UNSUPPORTED: "COUNT_FILTER_UNSUPPORTED",
1715
- NOT_INDEXED: "COUNT_NOT_INDEXED",
1716
- INDEX_BUILDING: "COUNT_INDEX_BUILDING",
1717
- RLS_UNSUPPORTED: "COUNT_RLS_UNSUPPORTED"
1718
- };
1719
- const AGGREGATE_ERROR = {
1720
- ARGS_UNSUPPORTED: "AGGREGATE_ARGS_UNSUPPORTED",
1721
- FILTER_UNSUPPORTED: "AGGREGATE_FILTER_UNSUPPORTED",
1722
- NOT_INDEXED: "AGGREGATE_NOT_INDEXED",
1723
- INDEX_BUILDING: "AGGREGATE_INDEX_BUILDING",
1724
- RLS_UNSUPPORTED: "AGGREGATE_RLS_UNSUPPORTED"
1725
- };
1726
- const RANGE_WORK_STATE = Symbol("aggregateRangeWorkState");
1727
- const isPlainObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
1728
- const normalizeUndefined = (value) => {
1729
- if (Array.isArray(value)) return value.map((entry) => normalizeUndefined(entry));
1730
- if (isPlainObject(value)) return Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, normalizeUndefined(entry)]));
1731
- if (value === void 0) return { [UNDEFINED_SENTINEL]: true };
1732
- return value;
1733
- };
1734
- const serializeStable = (value) => JSON.stringify(normalizeUndefined(value));
1735
- const toConstraintSet = (values) => {
1736
- const set = /* @__PURE__ */ new Map();
1737
- for (const value of values) set.set(serializeStable(value), value);
1738
- return set;
1739
- };
1740
- const intersectConstraintSets = (left, right) => {
1741
- const result = /* @__PURE__ */ new Map();
1742
- for (const [key, value] of left.entries()) if (right.has(key)) result.set(key, value);
1743
- return result;
1744
- };
1745
- const getColumnNames = (tableConfig) => {
1746
- const names = new Set(Object.keys(tableConfig.table[Columns] ?? {}));
1747
- names.add(INTERNAL_ID_FIELD$1);
1748
- if (usesSystemCreatedAtAlias(tableConfig.table)) names.add(INTERNAL_CREATION_TIME_FIELD);
1749
- return names;
1750
- };
1751
- const getRelationNames = (tableConfig) => new Set(Object.keys(tableConfig.relations ?? {}));
1752
- const normalizeFilterFieldName = (tableConfig, fieldName) => {
1753
- if (fieldName === PUBLIC_ID_FIELD$1) return INTERNAL_ID_FIELD$1;
1754
- if (fieldName === PUBLIC_CREATED_AT_FIELD && usesSystemCreatedAtAlias(tableConfig.table)) return INTERNAL_CREATION_TIME_FIELD;
1755
- return fieldName;
1756
- };
1757
- const createError = (code, message) => /* @__PURE__ */ new Error(`${code}: ${message}`);
1758
- const createFilterError = (codes, methodName, message) => createError(codes.FILTER_UNSUPPORTED, `${methodName} ${message}`);
1759
- const isRangeComparableValue = (value) => {
1760
- if (typeof value === "number") return Number.isFinite(value);
1761
- return typeof value === "string" || typeof value === "boolean";
1762
- };
1763
- const withFieldConstraint = (target, fieldName) => {
1764
- const existing = target.get(fieldName);
1765
- if (existing) return existing;
1766
- const next = {};
1767
- target.set(fieldName, next);
1768
- return next;
1769
- };
1770
- const normalizeAggregateComparableValue = (tableConfig, fieldName, value) => {
1771
- if (fieldName === INTERNAL_CREATION_TIME_FIELD) {
1772
- if (value instanceof Date) return value.getTime();
1773
- if (Array.isArray(value)) return value.map((entry) => entry instanceof Date ? entry.getTime() : entry);
1774
- return value;
1775
- }
1776
- return normalizeTemporalComparableValue(tableConfig.table, fieldName, value);
1777
- };
1778
- const pushConstraint = (target, fieldName, values) => {
1779
- const incoming = toConstraintSet(values);
1780
- const existing = withFieldConstraint(target, fieldName);
1781
- if (!existing.values) {
1782
- existing.values = incoming;
1783
- return;
1784
- }
1785
- existing.values = intersectConstraintSets(existing.values, incoming);
1786
- };
1787
- const pushRangeComparison = (target, fieldName, comparison) => {
1788
- const existing = withFieldConstraint(target, fieldName);
1789
- existing.rangeComparisons ??= [];
1790
- existing.rangeComparisons.push(comparison);
1791
- };
1792
- const matchesRangeComparisons = (value, comparisons) => {
1793
- if (!isRangeComparableValue(value)) return false;
1794
- for (const comparison of comparisons) {
1795
- if (!isRangeComparableValue(comparison.value)) return false;
1796
- const compared = compareSortableValues(value, comparison.value);
1797
- if (comparison.operator === "gt" && !(compared > 0)) return false;
1798
- if (comparison.operator === "gte" && !(compared >= 0)) return false;
1799
- if (comparison.operator === "lt" && !(compared < 0)) return false;
1800
- if (comparison.operator === "lte" && !(compared <= 0)) return false;
1801
- }
1802
- return true;
1803
- };
1804
- const normalizeConstraints = (constraints, codes, methodName) => {
1805
- for (const [fieldName, constraint] of constraints.entries()) {
1806
- if (constraint.rangeComparisons?.length) {
1807
- for (const comparison of constraint.rangeComparisons) if (!isRangeComparableValue(comparison.value)) throw createFilterError(codes, methodName, `range operators on '${fieldName}' only support finite numbers, strings, and booleans.`);
1808
- }
1809
- if (constraint.values && constraint.rangeComparisons?.length) {
1810
- const filtered = /* @__PURE__ */ new Map();
1811
- for (const [stableKey, candidate] of constraint.values.entries()) if (matchesRangeComparisons(candidate, constraint.rangeComparisons)) filtered.set(stableKey, candidate);
1812
- constraint.values = filtered;
1813
- constraint.rangeComparisons = void 0;
1814
- }
1815
- if ((!constraint.values || constraint.values.size === 0) && (!constraint.rangeComparisons || constraint.rangeComparisons.length === 0)) {
1816
- if (constraint.values && constraint.values.size === 0) continue;
1817
- constraints.delete(fieldName);
1818
- }
1819
- }
1820
- };
1821
- const parseFieldFilter = (tableConfig, fieldName, value, target, codes, methodName) => {
1822
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
1823
- pushConstraint(target, fieldName, [normalizeAggregateComparableValue(tableConfig, fieldName, value)]);
1824
- return;
1825
- }
1826
- const filter = value;
1827
- if (Object.hasOwn(filter, "OR") || Object.hasOwn(filter, "NOT")) throw createFilterError(codes, methodName, `does not support OR/NOT for field '${fieldName}'.`);
1828
- if (Object.hasOwn(filter, "AND")) {
1829
- const andEntries = filter.AND;
1830
- if (!Array.isArray(andEntries)) throw createFilterError(codes, methodName, `field-level AND for '${fieldName}' must be an array.`);
1831
- for (const entry of andEntries) parseFieldFilter(tableConfig, fieldName, entry, target, codes, methodName);
1832
- }
1833
- let hasRecognizedOperator = false;
1834
- if (Object.hasOwn(filter, "eq")) {
1835
- hasRecognizedOperator = true;
1836
- pushConstraint(target, fieldName, [normalizeAggregateComparableValue(tableConfig, fieldName, filter.eq)]);
1837
- }
1838
- if (Object.hasOwn(filter, "in")) {
1839
- hasRecognizedOperator = true;
1840
- const inValues = filter.in;
1841
- if (!Array.isArray(inValues)) throw createFilterError(codes, methodName, `field '${fieldName}'.in must be an array.`);
1842
- pushConstraint(target, fieldName, normalizeAggregateComparableValue(tableConfig, fieldName, inValues));
1843
- }
1844
- if (Object.hasOwn(filter, "isNull")) {
1845
- hasRecognizedOperator = true;
1846
- if (filter.isNull !== true) throw createFilterError(codes, methodName, `field '${fieldName}'.isNull only supports true.`);
1847
- pushConstraint(target, fieldName, [null]);
1848
- }
1849
- for (const operator of [
1850
- "gt",
1851
- "gte",
1852
- "lt",
1853
- "lte"
1854
- ]) {
1855
- if (!Object.hasOwn(filter, operator)) continue;
1856
- const boundValue = filter[operator];
1857
- if (boundValue === void 0) continue;
1858
- hasRecognizedOperator = true;
1859
- if (boundValue === null || Array.isArray(boundValue) || isPlainObject(boundValue)) throw createFilterError(codes, methodName, `field '${fieldName}'.${operator} requires a scalar bound.`);
1860
- pushRangeComparison(target, fieldName, {
1861
- operator,
1862
- value: normalizeAggregateComparableValue(tableConfig, fieldName, boundValue)
1863
- });
1864
- }
1865
- const unsupportedOperators = Object.keys(filter).filter((operator) => ![
1866
- "eq",
1867
- "in",
1868
- "isNull",
1869
- "gt",
1870
- "gte",
1871
- "lt",
1872
- "lte",
1873
- "AND"
1874
- ].includes(operator) && filter[operator] !== void 0);
1875
- if (unsupportedOperators.length > 0) throw createFilterError(codes, methodName, `does not support operators [${unsupportedOperators.join(", ")}] on '${fieldName}'.`);
1876
- if (!hasRecognizedOperator && !Object.hasOwn(filter, "AND")) throw createFilterError(codes, methodName, `filter for '${fieldName}' is not supported.`);
1877
- };
1878
- const constraintValuesEqual = (left, right) => {
1879
- if (!left && !right) return true;
1880
- if (!left || !right) return false;
1881
- if (left.size !== right.size) return false;
1882
- for (const key of left.keys()) if (!right.has(key)) return false;
1883
- return true;
1884
- };
1885
- const constraintValuesToFilter = (values) => {
1886
- if (values.size === 1) return values.values().next().value;
1887
- return { in: [...values.values()] };
1888
- };
1889
- const parseFiniteOrBranch = (options) => {
1890
- const { branch, tableConfig, codes, methodName } = options;
1891
- if (Object.hasOwn(branch, "AND") || Object.hasOwn(branch, "OR") || Object.hasOwn(branch, "NOT") || Object.hasOwn(branch, "RAW")) return null;
1892
- const columnNames = getColumnNames(tableConfig);
1893
- const relationNames = getRelationNames(tableConfig);
1894
- const constraints = /* @__PURE__ */ new Map();
1895
- for (const [key, value] of Object.entries(branch)) {
1896
- const normalizedKey = normalizeFilterFieldName(tableConfig, key);
1897
- if (!columnNames.has(normalizedKey)) {
1898
- if (relationNames.has(key)) throw createFilterError(codes, methodName, `does not support relation filters ('${key}') in v1.`);
1899
- throw createFilterError(codes, methodName, `filter field '${key}' is not recognized.`);
1900
- }
1901
- parseFieldFilter(tableConfig, normalizedKey, value, constraints, codes, methodName);
1902
- }
1903
- normalizeConstraints(constraints, codes, methodName);
1904
- const result = /* @__PURE__ */ new Map();
1905
- for (const [field, constraint] of constraints.entries()) {
1906
- if (constraint.rangeComparisons?.length) return null;
1907
- if (!constraint.values) continue;
1908
- result.set(field, new Map(constraint.values.entries()));
1909
- }
1910
- return result;
1911
- };
1912
- const collapseSafeOrBranches = (options) => {
1913
- const { branches, tableConfig, codes, methodName } = options;
1914
- if (branches.length === 0) return null;
1915
- const branchConstraints = branches.map((branch) => parseFiniteOrBranch({
1916
- branch,
1917
- tableConfig,
1918
- codes,
1919
- methodName
1920
- })).filter((entry) => entry !== null);
1921
- if (branchConstraints.length !== branches.length || branchConstraints.length === 0) return null;
1922
- const fields = /* @__PURE__ */ new Set();
1923
- for (const branch of branchConstraints) for (const field of branch.keys()) fields.add(field);
1924
- if (fields.size === 0) return null;
1925
- const varyingFields = [];
1926
- const first = branchConstraints[0];
1927
- for (const field of fields) {
1928
- const firstValues = first.get(field);
1929
- if (branchConstraints.some((branch) => !constraintValuesEqual(firstValues, branch.get(field)))) varyingFields.push(field);
1930
- }
1931
- if (varyingFields.length === 0) {
1932
- const collapsed = {};
1933
- for (const [field, values] of first.entries()) collapsed[field] = constraintValuesToFilter(values);
1934
- return collapsed;
1935
- }
1936
- if (varyingFields.length !== 1) return null;
1937
- const varyingField = varyingFields[0];
1938
- const unionValues = /* @__PURE__ */ new Map();
1939
- for (const branch of branchConstraints) {
1940
- const values = branch.get(varyingField);
1941
- if (!values || values.size === 0) return null;
1942
- for (const [key, value] of values.entries()) unionValues.set(key, value);
1943
- }
1944
- const collapsed = { [varyingField]: constraintValuesToFilter(unionValues) };
1945
- for (const [field, values] of first.entries()) {
1946
- if (field === varyingField) continue;
1947
- collapsed[field] = constraintValuesToFilter(values);
1948
- }
1949
- return collapsed;
1950
- };
1951
- const rewriteSafeOrWhere = (options) => {
1952
- const { where, tableConfig, codes, methodName } = options;
1953
- if (Object.hasOwn(where, "NOT")) throw createFilterError(codes, methodName, "NOT is not supported in no-scan aggregate/count filters.");
1954
- const normalized = {};
1955
- for (const [key, value] of Object.entries(where)) {
1956
- if (key === "AND") {
1957
- if (value === void 0) continue;
1958
- if (!Array.isArray(value)) throw createFilterError(codes, methodName, "AND must be an array of filter objects.");
1959
- normalized.AND = value.map((entry) => {
1960
- if (!isPlainObject(entry)) throw createFilterError(codes, methodName, "AND entries must be objects.");
1961
- return rewriteSafeOrWhere({
1962
- where: entry,
1963
- tableConfig,
1964
- codes,
1965
- methodName
1966
- });
1967
- });
1968
- continue;
1969
- }
1970
- if (key === "OR") continue;
1971
- normalized[key] = value;
1972
- }
1973
- if (!Object.hasOwn(where, "OR")) return normalized;
1974
- const branches = where.OR;
1975
- if (!Array.isArray(branches) || branches.length === 0) throw createFilterError(codes, methodName, "OR must be a non-empty array of filter objects.");
1976
- const collapsedOr = collapseSafeOrBranches({
1977
- branches: branches.map((entry) => {
1978
- if (!isPlainObject(entry)) throw createFilterError(codes, methodName, "OR entries must be objects.");
1979
- return rewriteSafeOrWhere({
1980
- where: entry,
1981
- tableConfig,
1982
- codes,
1983
- methodName
1984
- });
1985
- }),
1986
- tableConfig,
1987
- codes,
1988
- methodName
1989
- });
1990
- if (!collapsedOr) throw createFilterError(codes, methodName, "OR rewrite only supports finite index-plannable branches that differ on one scalar eq/in/isNull field.");
1991
- if (Object.keys(normalized).length === 0) return collapsedOr;
1992
- return { AND: [normalized, collapsedOr] };
1993
- };
1994
- const parseWhereObject = (where, tableConfig, target, codes, methodName) => {
1995
- if (Object.hasOwn(where, "OR") || Object.hasOwn(where, "NOT")) throw createFilterError(codes, methodName, "only supports conjunctions (object fields + AND).");
1996
- if (Object.hasOwn(where, "RAW")) throw createFilterError(codes, methodName, "does not support RAW filters.");
1997
- if (Object.hasOwn(where, "AND")) {
1998
- const andEntries = where.AND;
1999
- if (!Array.isArray(andEntries)) throw createFilterError(codes, methodName, "AND must be an array of filter objects.");
2000
- for (const entry of andEntries) {
2001
- if (!isPlainObject(entry)) throw createFilterError(codes, methodName, "AND entries must be objects.");
2002
- parseWhereObject(entry, tableConfig, target, codes, methodName);
2003
- }
2004
- }
2005
- const columnNames = getColumnNames(tableConfig);
2006
- const relationNames = getRelationNames(tableConfig);
2007
- for (const [key, value] of Object.entries(where)) {
2008
- if (key === "AND") continue;
2009
- const normalizedKey = normalizeFilterFieldName(tableConfig, key);
2010
- if (columnNames.has(normalizedKey)) {
2011
- parseFieldFilter(tableConfig, normalizedKey, value, target, codes, methodName);
2012
- continue;
2013
- }
2014
- if (relationNames.has(key)) throw createFilterError(codes, methodName, `does not support relation filters ('${key}') in v1.`);
2015
- throw createFilterError(codes, methodName, `filter field '${key}' is not recognized.`);
2016
- }
2017
- };
2018
- const constraintsToValueRecord = (constraints) => {
2019
- const record = {};
2020
- for (const [field, constraint] of constraints.entries()) {
2021
- if (!constraint.values) continue;
2022
- record[field] = [...constraint.values.values()];
2023
- }
2024
- return record;
2025
- };
2026
- const getAggregateIndexDefinitions = (tableConfig) => {
2027
- const aggregateIndexes = tableConfig.table.getAggregateIndexes?.();
2028
- if (!Array.isArray(aggregateIndexes)) return [];
2029
- return aggregateIndexes.map((entry) => ({
2030
- name: entry.name,
2031
- fields: entry.fields ?? [],
2032
- countFields: entry.countFields ?? [],
2033
- sumFields: entry.sumFields ?? [],
2034
- avgFields: entry.avgFields ?? [],
2035
- minFields: entry.minFields ?? [],
2036
- maxFields: entry.maxFields ?? []
2037
- }));
2038
- };
2039
- const supportsMetric = (index, metric) => {
2040
- if (metric.kind === "count") return true;
2041
- if (metric.kind === "countField") return index.countFields.includes(metric.field);
2042
- if (metric.kind === "sum") return index.sumFields.includes(metric.field) || index.avgFields.includes(metric.field);
2043
- if (metric.kind === "avg") return index.avgFields.includes(metric.field);
2044
- if (metric.kind === "min") return index.minFields.includes(metric.field);
2045
- return index.maxFields.includes(metric.field);
2046
- };
2047
- const pickAggregateIndex = (indexDefinitions, constrainedFields, metric) => {
2048
- let best = null;
2049
- for (const definition of indexDefinitions) {
2050
- if (!definition.fields.every((field) => constrainedFields.has(field))) continue;
2051
- const indexFieldSet = new Set(definition.fields);
2052
- if ([...constrainedFields].some((field) => !indexFieldSet.has(field))) continue;
2053
- if (!supportsMetric(definition, metric)) continue;
2054
- if (!best || definition.fields.length > best.fields.length) best = definition;
2055
- }
2056
- return best;
2057
- };
2058
- const pickRangeAggregateIndex = (indexDefinitions, constraints, constrainedFields, rangeFieldName, metric) => {
2059
- let best = null;
2060
- for (const definition of indexDefinitions) {
2061
- if (!supportsMetric(definition, metric)) continue;
2062
- const usesImplicitCreationTimeSuffix = rangeFieldName === INTERNAL_CREATION_TIME_FIELD;
2063
- const rangeFieldPosition = usesImplicitCreationTimeSuffix ? definition.fields.length : definition.fields.indexOf(rangeFieldName);
2064
- if (rangeFieldPosition < 0) continue;
2065
- const indexFieldSet = new Set(definition.fields);
2066
- if (usesImplicitCreationTimeSuffix) indexFieldSet.add(INTERNAL_CREATION_TIME_FIELD);
2067
- if ([...constrainedFields].some((field) => !indexFieldSet.has(field))) continue;
2068
- const prefixFields = definition.fields.slice(0, rangeFieldPosition);
2069
- if (prefixFields.some((field) => {
2070
- return !constraints.get(field)?.values;
2071
- })) continue;
2072
- if (prefixFields.some((field) => {
2073
- const fieldConstraint = constraints.get(field);
2074
- return Boolean(fieldConstraint?.rangeComparisons?.length);
2075
- })) continue;
2076
- if (!best || prefixFields.length > best.prefixFields.length || prefixFields.length === best.prefixFields.length && definition.fields.length > best.definition.fields.length) best = {
2077
- definition,
2078
- prefixFields
2079
- };
2080
- }
2081
- return best;
2082
- };
2083
- const buildCandidateKeys = (fields, constraints, index = 0, current = [], output = []) => {
2084
- if (index >= fields.length) {
2085
- output.push([...current]);
2086
- return output;
2087
- }
2088
- const values = constraints[fields[index]] ?? [];
2089
- for (const value of values) {
2090
- current.push(value);
2091
- buildCandidateKeys(fields, constraints, index + 1, current, output);
2092
- current.pop();
2093
- }
2094
- return output;
2095
- };
2096
- const getAggregateCartesianMaxKeys = (tableConfig) => {
2097
- const value = tableConfig.defaults?.aggregateCartesianMaxKeys;
2098
- if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
2099
- return DEFAULT_AGGREGATE_CARTESIAN_MAX_KEYS$1;
2100
- };
2101
- const getAggregateWorkBudget = (tableConfig) => {
2102
- const value = tableConfig.defaults?.aggregateWorkBudget;
2103
- if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
2104
- return DEFAULT_AGGREGATE_WORK_BUDGET$1;
2105
- };
2106
- const countCartesianCombinations = (fields, constraints, maxCount) => {
2107
- if (fields.length === 0) return 1;
2108
- let total = 1;
2109
- for (const field of fields) {
2110
- const values = constraints[field] ?? [];
2111
- if (values.length === 0) return 0;
2112
- const threshold = Math.max(1, maxCount);
2113
- const maxBeforeMultiply = Math.floor(threshold / values.length);
2114
- if (total > maxBeforeMultiply) return threshold + 1;
2115
- total *= values.length;
2116
- }
2117
- return total;
2118
- };
2119
- const enforceCartesianExpansionGuards = (options) => {
2120
- const cartesianMaxKeys = getAggregateCartesianMaxKeys(options.tableConfig);
2121
- const workBudget = getAggregateWorkBudget(options.tableConfig);
2122
- const workUnitsPerCombination = Math.max(1, options.workUnitsPerCombination);
2123
- const maxCombinationsByWork = Math.max(1, Math.floor(workBudget / workUnitsPerCombination));
2124
- const maxTrackedCombinations = Math.max(cartesianMaxKeys, maxCombinationsByWork);
2125
- const combinations = countCartesianCombinations(options.fields, options.fieldValues, maxTrackedCombinations);
2126
- if (combinations === 0) return;
2127
- if (combinations > cartesianMaxKeys) throw createFilterError(options.codes, options.methodName, `expands IN filters to ${combinations} key combinations on aggregateIndex '${options.indexName}', exceeding aggregateCartesianMaxKeys (${cartesianMaxKeys}). Reduce IN list sizes, split the query, add a narrower aggregateIndex, or increase defineSchema(..., { defaults: { aggregateCartesianMaxKeys } }).`);
2128
- const estimatedWork = combinations * workUnitsPerCombination;
2129
- if (estimatedWork > workBudget) throw createFilterError(options.codes, options.methodName, `estimated ${options.workLabel} is ${estimatedWork} units on aggregateIndex '${options.indexName}', exceeding aggregateWorkBudget (${workBudget}). Reduce IN fan-out, split the query, or increase defineSchema(..., { defaults: { aggregateWorkBudget } }).`);
2130
- };
2131
- const hasLogicalOr = (where) => {
2132
- if (Object.hasOwn(where, "OR")) return true;
2133
- const andEntries = where.AND;
2134
- if (!Array.isArray(andEntries)) return false;
2135
- return andEntries.some((entry) => isPlainObject(entry) && hasLogicalOr(entry));
2136
- };
2137
- const combineDnfBranches = (options) => {
2138
- const { left, right, maxBranches, codes, methodName } = options;
2139
- if (left.length === 0 || right.length === 0) return [];
2140
- const threshold = Math.max(1, maxBranches);
2141
- const maxBeforeMultiply = Math.floor(threshold / right.length);
2142
- if (left.length > maxBeforeMultiply) throw createFilterError(codes, methodName, `OR rewrite expands to ${left.length * right.length} branches, exceeding aggregateCartesianMaxKeys (${threshold}). Reduce OR fan-out, split the query, or increase defineSchema(..., { defaults: { aggregateCartesianMaxKeys } }).`);
2143
- const combined = [];
2144
- for (const leftBranch of left) for (const rightBranch of right) combined.push([...leftBranch, ...rightBranch]);
2145
- return combined;
2146
- };
2147
- const expandWhereToFiniteDnf = (options) => {
2148
- const { where, maxBranches, codes, methodName } = options;
2149
- if (Object.hasOwn(where, "NOT")) throw createFilterError(codes, methodName, "NOT is not supported in no-scan aggregate/count filters.");
2150
- if (Object.hasOwn(where, "RAW")) throw createFilterError(codes, methodName, "does not support RAW filters.");
2151
- const scalarClause = {};
2152
- for (const [key, value] of Object.entries(where)) {
2153
- if (key === "AND" || key === "OR") continue;
2154
- scalarClause[key] = value;
2155
- }
2156
- let branches = [[]];
2157
- if (Object.keys(scalarClause).length > 0) branches = branches.map((branch) => [...branch, scalarClause]);
2158
- if (Object.hasOwn(where, "AND")) {
2159
- const andEntries = where.AND;
2160
- if (!Array.isArray(andEntries)) throw createFilterError(codes, methodName, "AND must be an array of filter objects.");
2161
- for (const entry of andEntries) {
2162
- if (!isPlainObject(entry)) throw createFilterError(codes, methodName, "AND entries must be objects.");
2163
- const entryBranches = expandWhereToFiniteDnf({
2164
- where: entry,
2165
- maxBranches,
2166
- codes,
2167
- methodName
2168
- });
2169
- branches = combineDnfBranches({
2170
- left: branches,
2171
- right: entryBranches,
2172
- maxBranches,
2173
- codes,
2174
- methodName
2175
- });
2176
- }
2177
- }
2178
- if (Object.hasOwn(where, "OR")) {
2179
- const orEntries = where.OR;
2180
- if (!Array.isArray(orEntries) || orEntries.length === 0) throw createFilterError(codes, methodName, "OR must be a non-empty array of filter objects.");
2181
- const orBranches = [];
2182
- for (const entry of orEntries) {
2183
- if (!isPlainObject(entry)) throw createFilterError(codes, methodName, "OR entries must be objects.");
2184
- const expanded = expandWhereToFiniteDnf({
2185
- where: entry,
2186
- maxBranches,
2187
- codes,
2188
- methodName
2189
- });
2190
- orBranches.push(...expanded);
2191
- if (orBranches.length > maxBranches) throw createFilterError(codes, methodName, `OR rewrite expands to ${orBranches.length} branches, exceeding aggregateCartesianMaxKeys (${maxBranches}). Reduce OR fan-out, split the query, or increase defineSchema(..., { defaults: { aggregateCartesianMaxKeys } }).`);
2192
- }
2193
- branches = combineDnfBranches({
2194
- left: branches,
2195
- right: orBranches,
2196
- maxBranches,
2197
- codes,
2198
- methodName
2199
- });
2200
- }
2201
- return branches;
2202
- };
2203
- const branchToWhere = (branch) => {
2204
- if (branch.length === 0) return {};
2205
- if (branch.length === 1) return branch[0];
2206
- return { AND: branch };
2207
- };
2208
- const compileFiniteOrUnionPlan = (options) => {
2209
- const { tableConfig, where, metric, aggregateIndexes, codes, methodName, throwNotIndexed } = options;
2210
- if (!hasLogicalOr(where)) return null;
2211
- const cartesianMaxKeys = getAggregateCartesianMaxKeys(tableConfig);
2212
- const workBudget = getAggregateWorkBudget(tableConfig);
2213
- const branches = expandWhereToFiniteDnf({
2214
- where,
2215
- maxBranches: Math.max(1, Math.min(cartesianMaxKeys, workBudget)),
2216
- codes,
2217
- methodName
2218
- });
2219
- if (branches.length === 0) throw createFilterError(codes, methodName, "OR must include at least one branch.");
2220
- const dedupedKeyCandidates = /* @__PURE__ */ new Map();
2221
- let selectedIndexName = null;
2222
- let selectedIndexFields = null;
2223
- let totalBranchCandidates = 0;
2224
- for (const branch of branches) {
2225
- const branchWhere = branchToWhere(branch);
2226
- const constraints = /* @__PURE__ */ new Map();
2227
- parseWhereObject(branchWhere, tableConfig, constraints, codes, methodName);
2228
- normalizeConstraints(constraints, codes, methodName);
2229
- if ([...constraints.entries()].filter(([, constraint]) => (constraint.rangeComparisons?.length ?? 0) > 0).map(([field]) => field).length > 0) throw createFilterError(codes, methodName, "OR rewrite supports finite eq/in/isNull branches only in v1. Range operators inside OR are not supported.");
2230
- const matchedIndex = pickAggregateIndex(aggregateIndexes, new Set(constraints.keys()), metric);
2231
- if (!matchedIndex) throwNotIndexed();
2232
- const exactIndex = matchedIndex;
2233
- if (selectedIndexName === null || selectedIndexFields === null) {
2234
- selectedIndexName = exactIndex.name;
2235
- selectedIndexFields = exactIndex.fields;
2236
- } else if (selectedIndexName !== exactIndex.name || selectedIndexFields.length !== exactIndex.fields.length || selectedIndexFields.some((field, index) => field !== exactIndex.fields[index])) throw createFilterError(codes, methodName, `OR branches must resolve to a single aggregateIndex. Found incompatible branches between '${selectedIndexName}' and '${exactIndex.name}'.`);
2237
- const rawFieldValues = constraintsToValueRecord(constraints);
2238
- const fieldValues = Object.fromEntries(exactIndex.fields.map((field) => [field, rawFieldValues[field] ?? []]));
2239
- enforceCartesianExpansionGuards({
2240
- tableConfig,
2241
- codes,
2242
- methodName,
2243
- indexName: exactIndex.name,
2244
- fields: exactIndex.fields,
2245
- fieldValues,
2246
- workUnitsPerCombination: 1,
2247
- workLabel: "OR-branch key lookup work"
2248
- });
2249
- if (Object.values(fieldValues).some((values) => values.length === 0)) continue;
2250
- const branchCandidates = buildCandidateKeys(exactIndex.fields, fieldValues);
2251
- totalBranchCandidates += branchCandidates.length;
2252
- for (const keyParts of branchCandidates) dedupedKeyCandidates.set(serializeCountKeyParts(keyParts), keyParts);
2253
- }
2254
- if (!selectedIndexName || !selectedIndexFields) throw createFilterError(codes, methodName, "OR rewrite did not resolve to any index-plannable branch.");
2255
- const uniqueKeyCount = dedupedKeyCandidates.size;
2256
- if (uniqueKeyCount > cartesianMaxKeys) throw createFilterError(codes, methodName, `OR rewrite expands to ${uniqueKeyCount} unique key combinations on aggregateIndex '${selectedIndexName}', exceeding aggregateCartesianMaxKeys (${cartesianMaxKeys}). Reduce OR fan-out, split the query, or increase defineSchema(..., { defaults: { aggregateCartesianMaxKeys } }).`);
2257
- const estimatedWork = branches.length + totalBranchCandidates;
2258
- if (estimatedWork > workBudget) throw createFilterError(codes, methodName, `estimated OR rewrite work is ${estimatedWork} units on aggregateIndex '${selectedIndexName}', exceeding aggregateWorkBudget (${workBudget}). Reduce OR fan-out, split the query, or increase defineSchema(..., { defaults: { aggregateWorkBudget } }).`);
2259
- return {
2260
- tableName: tableConfig.name,
2261
- indexName: selectedIndexName,
2262
- indexFields: selectedIndexFields,
2263
- fieldValues: {},
2264
- keyCandidates: [...dedupedKeyCandidates.values()],
2265
- rangeConstraint: null,
2266
- postFieldValues: {},
2267
- metric
2268
- };
2269
- };
2270
- const serializeCountKeyParts = (parts) => serializeStable(parts);
2271
- const compileAggregatePlan = (tableConfig, where, metric, codes, methodName) => {
2272
- const normalizedWhere = where ?? {};
2273
- if (!isPlainObject(normalizedWhere)) throw createFilterError(codes, methodName, "where must be an object filter.");
2274
- const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
2275
- const throwNotIndexed = (fieldList) => {
2276
- if (metric.kind === "count") throw createError(codes.NOT_INDEXED, `No matching aggregateIndex found for count() on '${tableConfig.name}'. Add an aggregateIndex for [${fieldList}] and run aggregateBackfill.`);
2277
- if (metric.kind === "countField") throw createError(codes.NOT_INDEXED, `No matching aggregateIndex found for count(select: { ${metric.field}: true }) on '${tableConfig.name}'. Declare aggregateIndex(...).on(...).count(...) and run aggregateBackfill.`);
2278
- if (metric.kind === "avg") throw createError(codes.NOT_INDEXED, `No matching aggregateIndex found for avg('${metric.field}') on '${tableConfig.name}'. Declare aggregateIndex(...).on(...).avg(...) (or .all() for unfiltered) and run aggregateBackfill.`);
2279
- throw createError(codes.NOT_INDEXED, `No matching aggregateIndex found for ${metric.kind}('${metric.field}') on '${tableConfig.name}'. Declare aggregateIndex(...).on(...).${metric.kind}(...) (or .all() for unfiltered) and run aggregateBackfill.`);
2280
- };
2281
- const finiteOrUnionPlan = compileFiniteOrUnionPlan({
2282
- tableConfig,
2283
- where: normalizedWhere,
2284
- metric,
2285
- aggregateIndexes,
2286
- codes,
2287
- methodName,
2288
- throwNotIndexed: () => throwNotIndexed("OR branches")
2289
- });
2290
- if (finiteOrUnionPlan) return finiteOrUnionPlan;
2291
- const constraints = /* @__PURE__ */ new Map();
2292
- parseWhereObject(rewriteSafeOrWhere({
2293
- where: normalizedWhere,
2294
- tableConfig,
2295
- codes,
2296
- methodName
2297
- }), tableConfig, constraints, codes, methodName);
2298
- normalizeConstraints(constraints, codes, methodName);
2299
- const constrainedFields = new Set(constraints.keys());
2300
- const rangeFields = [...constraints.entries()].filter(([, constraint]) => (constraint.rangeComparisons?.length ?? 0) > 0).map(([field]) => field);
2301
- if (rangeFields.length > 1) throw createFilterError(codes, methodName, `supports range operators on at most one field in v1. Found [${rangeFields.join(", ")}].`);
2302
- const fieldList = [...constrainedFields].join(", ");
2303
- if (rangeFields.length === 0) {
2304
- const matchedIndex = pickAggregateIndex(aggregateIndexes, constrainedFields, metric);
2305
- if (!matchedIndex) throwNotIndexed(fieldList);
2306
- const exactIndex = matchedIndex;
2307
- const rawFieldValues = constraintsToValueRecord(constraints);
2308
- const fieldValues = Object.fromEntries(exactIndex.fields.map((field) => [field, rawFieldValues[field] ?? []]));
2309
- enforceCartesianExpansionGuards({
2310
- tableConfig,
2311
- codes,
2312
- methodName,
2313
- indexName: exactIndex.name,
2314
- fields: exactIndex.fields,
2315
- fieldValues,
2316
- workUnitsPerCombination: 1,
2317
- workLabel: "key lookup work"
2318
- });
2319
- for (const values of Object.values(fieldValues)) if (values.length === 0) return {
2320
- tableName: tableConfig.name,
2321
- indexName: exactIndex.name,
2322
- indexFields: exactIndex.fields,
2323
- fieldValues,
2324
- rangeConstraint: null,
2325
- postFieldValues: {},
2326
- metric
2327
- };
2328
- return {
2329
- tableName: tableConfig.name,
2330
- indexName: exactIndex.name,
2331
- indexFields: exactIndex.fields,
2332
- fieldValues,
2333
- rangeConstraint: null,
2334
- postFieldValues: {},
2335
- metric
2336
- };
2337
- }
2338
- const rangeFieldName = rangeFields[0];
2339
- const rangeComparisons = constraints.get(rangeFieldName)?.rangeComparisons ?? [];
2340
- const matchedRangeIndex = pickRangeAggregateIndex(aggregateIndexes, constraints, constrainedFields, rangeFieldName, metric);
2341
- if (!matchedRangeIndex) throwNotIndexed(fieldList);
2342
- const rangeIndex = matchedRangeIndex;
2343
- const prefixFieldValues = Object.fromEntries(rangeIndex.prefixFields.map((field) => [field, [...constraints.get(field)?.values?.values() ?? []]]));
2344
- const postFieldValues = {};
2345
- for (const field of rangeIndex.definition.fields) {
2346
- if (field === rangeFieldName || rangeIndex.prefixFields.includes(field)) continue;
2347
- const values = constraints.get(field)?.values;
2348
- if (!values) continue;
2349
- postFieldValues[field] = [...values.values()];
2350
- }
2351
- const rangeWorkUnits = RANGE_PREFIX_WORK_UNIT_BASE + Object.keys(postFieldValues).length;
2352
- enforceCartesianExpansionGuards({
2353
- tableConfig,
2354
- codes,
2355
- methodName,
2356
- indexName: rangeIndex.definition.name,
2357
- fields: rangeIndex.prefixFields,
2358
- fieldValues: prefixFieldValues,
2359
- workUnitsPerCombination: rangeWorkUnits,
2360
- workLabel: "range-prefix work"
2361
- });
2362
- return {
2363
- tableName: tableConfig.name,
2364
- indexName: rangeIndex.definition.name,
2365
- indexFields: rangeIndex.definition.fields,
2366
- fieldValues: prefixFieldValues,
2367
- rangeConstraint: {
2368
- fieldName: rangeFieldName,
2369
- comparisons: rangeComparisons,
2370
- prefixFields: rangeIndex.prefixFields,
2371
- workBudget: getAggregateWorkBudget(tableConfig),
2372
- filterErrorCode: codes.FILTER_UNSUPPORTED,
2373
- methodName
2374
- },
2375
- postFieldValues,
2376
- metric
2377
- };
2378
- };
2379
- const compileCountQueryPlan = (tableConfig, where) => {
2380
- const plan = compileAggregatePlan(tableConfig, where, { kind: "count" }, COUNT_ERROR, "count()");
2381
- return {
2382
- tableName: plan.tableName,
2383
- indexName: plan.indexName,
2384
- indexFields: plan.indexFields,
2385
- fieldValues: plan.fieldValues,
2386
- keyCandidates: plan.keyCandidates,
2387
- rangeConstraint: plan.rangeConstraint,
2388
- postFieldValues: plan.postFieldValues
2389
- };
2390
- };
2391
- const compileCountFieldQueryPlan = (tableConfig, where, field) => compileAggregatePlan(tableConfig, where, {
2392
- kind: "countField",
2393
- field
2394
- }, COUNT_ERROR, `count(select: { ${field}: true })`);
2395
- const compileAggregateQueryPlan = (tableConfig, where, metric) => compileAggregatePlan(tableConfig, where, metric, AGGREGATE_ERROR, (() => {
2396
- if (metric.kind === "countField") return `count(select: { ${metric.field}: true })`;
2397
- if (metric.kind === "avg") return `avg('${metric.field}')`;
2398
- return `${metric.kind}()`;
2399
- })());
2400
- const deepEquals = (left, right) => serializeStable(left) === serializeStable(right);
2401
- const normalizeSumValues = (values) => {
2402
- const output = {};
2403
- for (const [field, value] of Object.entries(values)) if (typeof value === "number" && Number.isFinite(value)) output[field] = value;
2404
- return output;
2405
- };
2406
- const normalizeNonNullCountValues = (values) => {
2407
- const output = {};
2408
- for (const [field, value] of Object.entries(values)) if (typeof value === "number" && Number.isFinite(value)) output[field] = value;
2409
- return output;
2410
- };
2411
- const normalizeExtremaValues = (values) => ({ ...values });
2412
- const mergeSumValues = (baseValues, deltaValues) => {
2413
- const merged = { ...baseValues };
2414
- for (const [field, delta] of Object.entries(deltaValues)) {
2415
- const next = (merged[field] ?? 0) + delta;
2416
- if (next === 0) {
2417
- delete merged[field];
2418
- continue;
2419
- }
2420
- merged[field] = next;
2421
- }
2422
- return merged;
2423
- };
2424
- const mergeCountValues = (baseValues, deltaValues) => {
2425
- const merged = { ...baseValues };
2426
- for (const [field, delta] of Object.entries(deltaValues)) {
2427
- const next = (merged[field] ?? 0) + delta;
2428
- if (next === 0) {
2429
- delete merged[field];
2430
- continue;
2431
- }
2432
- merged[field] = next;
2433
- }
2434
- return merged;
2435
- };
2436
- const negateSumValues = (values) => Object.fromEntries(Object.entries(values).map(([field, value]) => [field, -value]));
2437
- const negateCountValues = (values) => Object.fromEntries(Object.entries(values).map(([field, value]) => [field, -value]));
2438
- const listBucketsByHash = async (db, tableName, indexName, keyHash) => await db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index_hash", (q) => q.eq("tableKey", tableName).eq("indexName", indexName).eq("keyHash", keyHash)).collect();
2439
- const getBucketByKey = async (db, tableName, indexName, keyParts) => {
2440
- return (await listBucketsByHash(db, tableName, indexName, serializeCountKeyParts(keyParts))).find((bucket) => deepEquals(bucket.keyParts, keyParts)) ?? null;
2441
- };
2442
- const listBucketsByHashPrefix = async (db, tableName, indexName, scan, limit) => await db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index_hash", (q) => {
2443
- const scoped = q.eq("tableKey", tableName).eq("indexName", indexName);
2444
- return (scan.after === void 0 ? scoped.gte("keyHash", scan.start) : scoped.gt("keyHash", scan.after)).lt("keyHash", scan.end);
2445
- }).take(limit);
2446
- const getExtremaByValue = async (db, tableName, indexName, keyHash, fieldName, value) => {
2447
- const valueHash = serializeStable(value);
2448
- return (await db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index_hash_field_value", (q) => q.eq("tableKey", tableName).eq("indexName", indexName).eq("keyHash", keyHash).eq("fieldName", fieldName).eq("valueHash", valueHash)).collect()).find((row) => deepEquals(row.value, value)) ?? null;
2449
- };
2450
- const takeMembersForIndex = async (db, tableName, indexName, limit) => await db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", AGGREGATE_STATE_KIND_METRIC).eq("tableKey", tableName).eq("indexName", indexName)).take(limit);
2451
- const takeBucketsForIndex = async (db, tableName, indexName, limit) => await db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).take(limit);
2452
- 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);
2453
- const applyBucketDelta = async (db, tableName, indexName, keyParts, deltaCount, deltaSums, deltaNonNullCounts) => {
2454
- if (deltaCount === 0 && Object.keys(deltaSums).length === 0 && Object.keys(deltaNonNullCounts).length === 0) return;
2455
- const existing = await getBucketByKey(db, tableName, indexName, keyParts);
2456
- const now = Date.now();
2457
- if (!existing) {
2458
- if (deltaCount < 0) return;
2459
- await db.insert(AGGREGATE_BUCKET_TABLE, {
2460
- tableKey: tableName,
2461
- indexName,
2462
- keyHash: serializeCountKeyParts(keyParts),
2463
- keyParts,
2464
- count: deltaCount,
2465
- sumValues: deltaSums,
2466
- nonNullCountValues: deltaNonNullCounts,
2467
- updatedAt: now
2468
- });
2469
- return;
2470
- }
2471
- const nextCount = existing.count + deltaCount;
2472
- if (nextCount <= 0) {
2473
- await db.delete(AGGREGATE_BUCKET_TABLE, existing._id);
2474
- return;
2475
- }
2476
- const nextSumValues = mergeSumValues(normalizeSumValues(existing.sumValues), deltaSums);
2477
- const nextNonNullCountValues = mergeCountValues(normalizeNonNullCountValues(existing.nonNullCountValues), deltaNonNullCounts);
2478
- await db.patch(AGGREGATE_BUCKET_TABLE, existing._id, {
2479
- count: nextCount,
2480
- sumValues: nextSumValues,
2481
- nonNullCountValues: nextNonNullCountValues,
2482
- updatedAt: now
2483
- });
2484
- };
2485
- const encodeNumberSortKey = (value) => {
2486
- const view = /* @__PURE__ */ new DataView(/* @__PURE__ */ new ArrayBuffer(8));
2487
- view.setFloat64(0, value);
2488
- let bits = view.getBigUint64(0);
2489
- if ((bits & FLOAT64_SIGN_BIT) !== 0n) bits = ~bits & FLOAT64_MASK;
2490
- else bits |= FLOAT64_SIGN_BIT;
2491
- return bits.toString(16).padStart(16, "0");
2492
- };
2493
- const toComparableSortKey = (value) => {
2494
- if (typeof value === "number") {
2495
- if (!Number.isFinite(value)) throw createError(AGGREGATE_ERROR.FILTER_UNSUPPORTED, `Aggregate min/max only support finite numbers, strings, and booleans. Received ${String(value)}.`);
2496
- return `n:${encodeNumberSortKey(value)}`;
2497
- }
2498
- if (typeof value === "string") return `s:${value}`;
2499
- if (typeof value === "boolean") return `b:${value ? "1" : "0"}`;
2500
- throw createError(AGGREGATE_ERROR.FILTER_UNSUPPORTED, `Aggregate min/max only support finite numbers, strings, and booleans. Received '${typeof value}'.`);
2501
- };
2502
- const compareSortableValues = (left, right) => {
2503
- const leftKey = toComparableSortKey(left);
2504
- const rightKey = toComparableSortKey(right);
2505
- if (leftKey < rightKey) return -1;
2506
- if (leftKey > rightKey) return 1;
2507
- return 0;
2508
- };
2509
- const applyExtremaDelta = async (db, tableName, indexName, keyHash, fieldName, value, deltaCount) => {
2510
- if (deltaCount === 0) return;
2511
- const existing = await getExtremaByValue(db, tableName, indexName, keyHash, fieldName, value);
2512
- const now = Date.now();
2513
- if (!existing) {
2514
- if (deltaCount < 0) return;
2515
- await db.insert(AGGREGATE_EXTREMA_TABLE, {
2516
- tableKey: tableName,
2517
- indexName,
2518
- keyHash,
2519
- fieldName,
2520
- valueHash: serializeStable(value),
2521
- value,
2522
- sortKey: toComparableSortKey(value),
2523
- count: deltaCount,
2524
- updatedAt: now
2525
- });
2526
- return;
2527
- }
2528
- const nextCount = existing.count + deltaCount;
2529
- if (nextCount <= 0) {
2530
- await db.delete(AGGREGATE_EXTREMA_TABLE, existing._id);
2531
- return;
2532
- }
2533
- await db.patch(AGGREGATE_EXTREMA_TABLE, existing._id, {
2534
- count: nextCount,
2535
- updatedAt: now
2536
- });
2537
- };
2538
- const buildKeyHashPrefixBounds = (prefixParts) => {
2539
- if (prefixParts.length === 0) return {
2540
- start: "[",
2541
- end: "[￿"
2542
- };
2543
- const start = `${serializeCountKeyParts(prefixParts).slice(0, -1)},`;
2544
- return {
2545
- start,
2546
- end: `${start}\uffff`
2547
- };
2548
- };
2549
- const matchesKeyPrefix = (keyParts, prefixParts) => prefixParts.every((value, index) => deepEquals(keyParts[index], value));
2550
- /**
2551
- * A prefix scan only bounds the key tuple's leading equality fields, so every
2552
- * bucket it returns still has to clear the range predicate and any equality
2553
- * field that sits after the range field in the index.
2554
- */
2555
- const matchesRangePlanBucket = (options) => {
2556
- const { bucket, indexFields } = options;
2557
- if (!matchesKeyPrefix(bucket.keyParts, options.prefixParts)) return false;
2558
- if (!matchesRangeComparisons(bucket.keyParts[options.rangeFieldIndex], options.comparisons)) return false;
2559
- for (const [field, allowedValues] of Object.entries(options.postFieldSets)) {
2560
- const fieldIndex = indexFields.indexOf(field);
2561
- if (fieldIndex < 0) return false;
2562
- if (!allowedValues.has(serializeStable(bucket.keyParts[fieldIndex]))) return false;
2563
- }
2564
- return true;
2565
- };
2566
- const readPlanBuckets = async (db, plan) => {
2567
- 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);
2568
- const prefixCandidates = buildCandidateKeys(plan.rangeConstraint.prefixFields, plan.fieldValues);
2569
- const rangeFieldIndex = plan.indexFields.indexOf(plan.rangeConstraint.fieldName);
2570
- if (rangeFieldIndex < 0) return [];
2571
- const postFieldSets = Object.fromEntries(Object.entries(plan.postFieldValues).map(([field, values]) => [field, toConstraintSet(values)]));
2572
- const workBudget = plan.rangeConstraint.workBudget ?? DEFAULT_AGGREGATE_WORK_BUDGET$1;
2573
- const scanLimit = workBudget + 1;
2574
- let pending = prefixCandidates.map((prefixParts) => ({
2575
- prefixParts,
2576
- ...buildKeyHashPrefixBounds(prefixParts)
2577
- }));
2578
- const matchedById = /* @__PURE__ */ new Map();
2579
- let scanned = 0;
2580
- while (pending.length > 0 && scanned < scanLimit) {
2581
- const remaining = scanLimit - scanned;
2582
- const batch = pending.slice(0, Math.min(AGGREGATE_BUCKET_READ_CONCURRENCY, remaining));
2583
- const share = Math.floor(remaining / batch.length);
2584
- const pages = await mapWithConcurrency(batch, AGGREGATE_BUCKET_READ_CONCURRENCY, (scan) => listBucketsByHashPrefix(db, plan.tableName, plan.indexName, scan, share));
2585
- const next = pending.slice(batch.length);
2586
- for (let index = 0; index < batch.length; index += 1) {
2587
- const scan = batch[index];
2588
- const page = pages[index];
2589
- scanned += page.length;
2590
- for (const bucket of page) if (matchesRangePlanBucket({
2591
- bucket,
2592
- prefixParts: scan.prefixParts,
2593
- indexFields: plan.indexFields,
2594
- rangeFieldIndex,
2595
- comparisons: plan.rangeConstraint.comparisons,
2596
- postFieldSets
2597
- })) matchedById.set(String(bucket._id), bucket);
2598
- const lastBucket = page.at(-1);
2599
- if (page.length < share || !lastBucket) continue;
2600
- next.push({
2601
- ...scan,
2602
- after: lastBucket.keyHash
2603
- });
2604
- }
2605
- pending = next;
2606
- }
2607
- if (scanned > workBudget) {
2608
- const fanOut = prefixCandidates.length > 1 ? ` across ${prefixCandidates.length} equality-prefix combinations` : "";
2609
- throw createError(plan.rangeConstraint.filterErrorCode ?? AGGREGATE_ERROR.FILTER_UNSUPPORTED, `${plan.rangeConstraint.methodName ?? "aggregate()"} range filter on '${plan.rangeConstraint.fieldName}' scans more than ${workBudget} aggregate buckets${fanOut} on aggregateIndex '${plan.indexName}'. Narrow the equality prefix, shrink the IN lists it expands to, reduce the range field's cardinality, add a narrower aggregateIndex, or increase defineSchema(..., { defaults: { aggregateWorkBudget } }).`);
2610
- }
2611
- const buckets = [...matchedById.values()];
2612
- buckets[RANGE_WORK_STATE] = {
2613
- scannedBuckets: scanned,
2614
- reservedExtremaReads: 0
2615
- };
2616
- return buckets;
2617
- };
2618
- const sortFieldValueRecord = (values) => Object.fromEntries(Object.keys(values).sort().map((field) => [field, values[field] ?? []]));
2619
- const getPlanBucketCacheKey = (plan) => serializeStable({
2620
- tableName: plan.tableName,
2621
- indexName: plan.indexName,
2622
- indexFields: plan.indexFields,
2623
- fieldValues: sortFieldValueRecord(plan.fieldValues),
2624
- keyCandidates: plan.keyCandidates ?? null,
2625
- rangeConstraint: plan.rangeConstraint ? {
2626
- fieldName: plan.rangeConstraint.fieldName,
2627
- comparisons: [...plan.rangeConstraint.comparisons],
2628
- prefixFields: plan.rangeConstraint.prefixFields
2629
- } : null,
2630
- postFieldValues: sortFieldValueRecord(plan.postFieldValues)
2631
- });
2632
- const readPlanBucketsWithCache = async (db, plan, bucketCache) => {
2633
- if (!bucketCache) return await readPlanBuckets(db, plan);
2634
- const cacheKey = getPlanBucketCacheKey(plan);
2635
- const existing = bucketCache.get(cacheKey);
2636
- if (existing) return await existing;
2637
- const pending = readPlanBuckets(db, plan);
2638
- bucketCache.set(cacheKey, pending);
2639
- try {
2640
- return await pending;
2641
- } catch (error) {
2642
- bucketCache.delete(cacheKey);
2643
- throw error;
2644
- }
2645
- };
2646
- const readCountFromBuckets = async (db, plan, bucketCache) => {
2647
- return (await readPlanBucketsWithCache(db, plan, bucketCache)).reduce((sum, bucket) => sum + bucket.count, 0);
2648
- };
2649
- const readSumFromBuckets = async (db, plan, bucketCache) => {
2650
- if (plan.metric.kind !== "sum") throw new Error("readSumFromBuckets() requires a sum aggregate plan.");
2651
- let total = 0;
2652
- let totalNonNull = 0;
2653
- const buckets = await readPlanBucketsWithCache(db, plan, bucketCache);
2654
- for (const bucket of buckets) {
2655
- const sumValues = normalizeSumValues(bucket.sumValues);
2656
- const countValues = normalizeNonNullCountValues(bucket.nonNullCountValues);
2657
- total += sumValues[plan.metric.field] ?? 0;
2658
- totalNonNull += countValues[plan.metric.field] ?? 0;
2659
- }
2660
- if (totalNonNull === 0) return null;
2661
- return total;
2662
- };
2663
- const readCountFieldFromBuckets = async (db, plan, bucketCache) => {
2664
- if (plan.metric.kind !== "countField") throw new Error("readCountFieldFromBuckets() requires a countField aggregate plan.");
2665
- let total = 0;
2666
- const buckets = await readPlanBucketsWithCache(db, plan, bucketCache);
2667
- for (const bucket of buckets) {
2668
- const countValues = normalizeNonNullCountValues(bucket.nonNullCountValues);
2669
- total += countValues[plan.metric.field] ?? 0;
2670
- }
2671
- return total;
2672
- };
2673
- const readAverageFromBuckets = async (db, plan, bucketCache) => {
2674
- if (plan.metric.kind !== "avg") throw new Error("readAverageFromBuckets() requires an avg aggregate plan.");
2675
- let totalSum = 0;
2676
- let totalNonNull = 0;
2677
- const buckets = await readPlanBucketsWithCache(db, plan, bucketCache);
2678
- for (const bucket of buckets) {
2679
- const sumValues = normalizeSumValues(bucket.sumValues);
2680
- const countValues = normalizeNonNullCountValues(bucket.nonNullCountValues);
2681
- totalSum += sumValues[plan.metric.field] ?? 0;
2682
- totalNonNull += countValues[plan.metric.field] ?? 0;
2683
- }
2684
- if (totalNonNull === 0) return null;
2685
- return totalSum / totalNonNull;
2686
- };
2687
- const readKeyExtrema = async (db, params) => {
2688
- let query = db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index_hash_field_sort", (q) => q.eq("tableKey", params.tableName).eq("indexName", params.indexName).eq("keyHash", params.keyHash).eq("fieldName", params.fieldName));
2689
- if (params.kind === "max") query = query.order("desc");
2690
- return (await query.take(1))[0]?.value ?? null;
2691
- };
2692
- const readExtremaFromBuckets = async (db, plan, bucketCache) => {
2693
- if (plan.metric.kind !== "min" && plan.metric.kind !== "max") throw new Error("readExtremaFromBuckets() requires a min/max aggregate plan.");
2694
- let selected = null;
2695
- const buckets = await readPlanBucketsWithCache(db, plan, bucketCache);
2696
- const metric = plan.metric;
2697
- const rangeWork = buckets[RANGE_WORK_STATE];
2698
- if (rangeWork && plan.rangeConstraint) {
2699
- const workBudget = plan.rangeConstraint.workBudget ?? DEFAULT_AGGREGATE_WORK_BUDGET$1;
2700
- const requestedWork = buckets.length;
2701
- const totalWork = rangeWork.scannedBuckets + rangeWork.reservedExtremaReads + requestedWork;
2702
- if (totalWork > workBudget) throw createError(plan.rangeConstraint.filterErrorCode ?? AGGREGATE_ERROR.FILTER_UNSUPPORTED, `${plan.rangeConstraint.methodName ?? "aggregate()"} range filter on '${plan.rangeConstraint.fieldName}' needs ${totalWork} aggregate work units (${rangeWork.scannedBuckets} bucket reads and ${rangeWork.reservedExtremaReads + requestedWork} extrema reads), exceeding aggregateWorkBudget (${workBudget}) on aggregateIndex '${plan.indexName}'. Narrow the range, add a narrower aggregateIndex, or increase defineSchema(..., { defaults: { aggregateWorkBudget } }).`);
2703
- rangeWork.reservedExtremaReads += requestedWork;
2704
- }
2705
- const values = await mapWithConcurrency(buckets, AGGREGATE_BUCKET_READ_CONCURRENCY, (bucket) => readKeyExtrema(db, {
2706
- tableName: plan.tableName,
2707
- indexName: plan.indexName,
2708
- keyHash: bucket.keyHash,
2709
- fieldName: metric.field,
2710
- kind: metric.kind
2711
- }));
2712
- for (const value of values) {
2713
- if (value === null || value === void 0) continue;
2714
- if (selected === null) {
2715
- selected = value;
2716
- continue;
2717
- }
2718
- const comparison = compareSortableValues(value, selected);
2719
- if (plan.metric.kind === "min" && comparison < 0) {
2720
- selected = value;
2721
- continue;
2722
- }
2723
- if (plan.metric.kind === "max" && comparison > 0) selected = value;
2724
- }
2725
- return selected;
2726
- };
2727
- const getMemberByDoc = async (db, tableName, indexName, docId) => {
2728
- return (await db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index_doc", (q) => q.eq("kind", AGGREGATE_STATE_KIND_METRIC).eq("tableKey", tableName).eq("indexName", indexName).eq("docId", docId)).collect())[0] ?? null;
2729
- };
2730
- const isComparableAggregateValue = (value) => {
2731
- if (typeof value === "number") return Number.isFinite(value);
2732
- return typeof value === "string" || typeof value === "boolean";
2733
- };
2734
- const computeAggregateMetricValues = (doc, definition) => {
2735
- const sumValues = {};
2736
- for (const field of [...definition.sumFields, ...definition.avgFields]) {
2737
- const value = doc[field];
2738
- if (value === null || value === void 0) continue;
2739
- if (typeof value !== "number" || !Number.isFinite(value)) throw createError(AGGREGATE_ERROR.FILTER_UNSUPPORTED, `aggregateIndex '${definition.name}' sum('${field}') requires finite number values.`);
2740
- sumValues[field] = value;
2741
- }
2742
- const nonNullCountValues = {};
2743
- for (const field of [
2744
- ...definition.countFields,
2745
- ...definition.sumFields,
2746
- ...definition.avgFields
2747
- ]) {
2748
- const value = doc[field];
2749
- if (value === null || value === void 0) continue;
2750
- nonNullCountValues[field] = 1;
2751
- }
2752
- const extremaFields = new Set([...definition.minFields, ...definition.maxFields]);
2753
- const extremaValues = {};
2754
- for (const field of extremaFields) {
2755
- const value = doc[field];
2756
- if (value === null || value === void 0) continue;
2757
- if (!isComparableAggregateValue(value)) throw createError(AGGREGATE_ERROR.FILTER_UNSUPPORTED, `aggregateIndex '${definition.name}' ${field} must be a finite number, string, or boolean for min/max.`);
2758
- extremaValues[field] = value;
2759
- }
2760
- return {
2761
- sumValues,
2762
- extremaValues,
2763
- nonNullCountValues
2764
- };
2765
- };
2766
- const toExtremaDeltas = (keyHash, values, delta) => Object.entries(values).map(([fieldName, value]) => ({
2767
- keyHash,
2768
- fieldName,
2769
- value,
2770
- delta
2771
- }));
2772
- const accumulateNumberValues = (target, source) => {
2773
- for (const [field, value] of Object.entries(source)) {
2774
- const next = (target[field] ?? 0) + value;
2775
- if (next === 0) {
2776
- delete target[field];
2777
- continue;
2778
- }
2779
- target[field] = next;
2780
- }
2781
- };
2782
- /**
2783
- * Pure half of aggregate reconciliation: works out what a single document's
2784
- * membership change implies, without touching the database. Callers batch these
2785
- * and flush once so a page of documents sharing a key tuple writes its bucket
2786
- * one time instead of once per document.
2787
- */
2788
- const computeMembershipDelta = (existing, params) => {
2789
- const { tableName, indexName, docId, keyParts, metricValues } = params;
2790
- if (!keyParts || !metricValues) {
2791
- if (!existing) return {
2792
- buckets: [],
2793
- extrema: [],
2794
- member: { kind: "none" }
2795
- };
2796
- return {
2797
- buckets: [{
2798
- keyHash: serializeCountKeyParts(existing.keyParts),
2799
- keyParts: existing.keyParts,
2800
- deltaCount: -1,
2801
- deltaSums: negateSumValues(normalizeSumValues(existing.sumValues)),
2802
- deltaNonNullCounts: negateCountValues(normalizeNonNullCountValues(existing.nonNullCountValues))
2803
- }],
2804
- extrema: toExtremaDeltas(existing.keyHash, normalizeExtremaValues(existing.extremaValues), -1),
2805
- member: {
2806
- kind: "delete",
2807
- id: existing._id
2808
- }
2809
- };
2810
- }
2811
- const normalizedKeyParts = keyParts.map((part) => normalizeUndefined(part));
2812
- const keyHash = serializeCountKeyParts(normalizedKeyParts);
2813
- const now = Date.now();
2814
- const normalizedNextSumValues = normalizeSumValues(metricValues.sumValues);
2815
- const normalizedNextNonNullCountValues = normalizeNonNullCountValues(metricValues.nonNullCountValues);
2816
- const normalizedNextExtremaValues = normalizeExtremaValues(metricValues.extremaValues);
2817
- 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 {
2818
- buckets: [],
2819
- extrema: [],
2820
- member: { kind: "none" }
2821
- };
2822
- const buckets = [];
2823
- const extrema = [];
2824
- if (existing) {
2825
- buckets.push({
2826
- keyHash: serializeCountKeyParts(existing.keyParts),
2827
- keyParts: existing.keyParts,
2828
- deltaCount: -1,
2829
- deltaSums: negateSumValues(normalizeSumValues(existing.sumValues)),
2830
- deltaNonNullCounts: negateCountValues(normalizeNonNullCountValues(existing.nonNullCountValues))
2831
- });
2832
- extrema.push(...toExtremaDeltas(existing.keyHash, normalizeExtremaValues(existing.extremaValues), -1));
2833
- }
2834
- buckets.push({
2835
- keyHash,
2836
- keyParts: normalizedKeyParts,
2837
- deltaCount: 1,
2838
- deltaSums: normalizedNextSumValues,
2839
- deltaNonNullCounts: normalizedNextNonNullCountValues
2840
- });
2841
- extrema.push(...toExtremaDeltas(keyHash, normalizedNextExtremaValues, 1));
2842
- const memberFields = {
2843
- kind: AGGREGATE_STATE_KIND_METRIC,
2844
- keyHash,
2845
- keyParts: normalizedKeyParts,
2846
- sumValues: normalizedNextSumValues,
2847
- nonNullCountValues: normalizedNextNonNullCountValues,
2848
- extremaValues: normalizedNextExtremaValues,
2849
- updatedAt: now
2850
- };
2851
- return {
2852
- buckets,
2853
- extrema,
2854
- member: existing ? {
2855
- kind: "patch",
2856
- id: existing._id,
2857
- doc: memberFields
2858
- } : {
2859
- kind: "insert",
2860
- doc: {
2861
- ...memberFields,
2862
- tableKey: tableName,
2863
- indexName,
2864
- docId
2865
- }
2866
- }
2867
- };
2868
- };
2869
- const computeAggregateMembershipDelta = async (db, params) => {
2870
- return computeMembershipDelta(await getMemberByDoc(db, params.tableName, params.indexName, params.docId), params);
2871
- };
2872
- /**
2873
- * Write half of aggregate reconciliation. Folds bucket deltas by key tuple and
2874
- * extrema deltas by (keyHash, field, value) so each storage document is read and
2875
- * written once regardless of how many source documents contributed to it.
2876
- */
2877
- const flushAggregateMembershipDeltas = async (db, tableName, indexName, deltas) => {
2878
- const bucketDeltas = /* @__PURE__ */ new Map();
2879
- const extremaDeltas = /* @__PURE__ */ new Map();
2880
- for (const delta of deltas) {
2881
- for (const bucket of delta.buckets) {
2882
- const current = bucketDeltas.get(bucket.keyHash);
2883
- if (!current) {
2884
- bucketDeltas.set(bucket.keyHash, {
2885
- keyHash: bucket.keyHash,
2886
- keyParts: bucket.keyParts,
2887
- deltaCount: bucket.deltaCount,
2888
- deltaSums: { ...bucket.deltaSums },
2889
- deltaNonNullCounts: { ...bucket.deltaNonNullCounts }
2890
- });
2891
- continue;
2892
- }
2893
- current.deltaCount += bucket.deltaCount;
2894
- accumulateNumberValues(current.deltaSums, bucket.deltaSums);
2895
- accumulateNumberValues(current.deltaNonNullCounts, bucket.deltaNonNullCounts);
2896
- }
2897
- for (const entry of delta.extrema) {
2898
- const cacheKey = serializeStable([
2899
- entry.keyHash,
2900
- entry.fieldName,
2901
- serializeStable(entry.value)
2902
- ]);
2903
- const current = extremaDeltas.get(cacheKey);
2904
- if (!current) {
2905
- extremaDeltas.set(cacheKey, { ...entry });
2906
- continue;
2907
- }
2908
- current.delta += entry.delta;
2909
- }
2910
- }
2911
- for (const bucket of bucketDeltas.values()) await applyBucketDelta(db, tableName, indexName, bucket.keyParts, bucket.deltaCount, bucket.deltaSums, bucket.deltaNonNullCounts);
2912
- for (const entry of extremaDeltas.values()) await applyExtremaDelta(db, tableName, indexName, entry.keyHash, entry.fieldName, entry.value, entry.delta);
2913
- for (const delta of deltas) {
2914
- const member = delta.member;
2915
- if (member.kind === "delete") {
2916
- await db.delete(AGGREGATE_MEMBER_TABLE, member.id);
2917
- continue;
2918
- }
2919
- if (member.kind === "patch") {
2920
- await db.patch(AGGREGATE_MEMBER_TABLE, member.id, member.doc);
2921
- continue;
2922
- }
2923
- if (member.kind === "insert") await db.insert(AGGREGATE_MEMBER_TABLE, member.doc);
2924
- }
2925
- };
2926
- /**
2927
- * Single-document reconciliation. Flushes eagerly so user code reading an
2928
- * aggregate later in the same mutation sees its own writes.
2929
- */
2930
- const reconcileAggregateMembership = async (db, params) => {
2931
- const delta = await computeAggregateMembershipDelta(db, params);
2932
- await flushAggregateMembershipDeltas(db, params.tableName, params.indexName, [delta]);
2933
- };
2934
- const computeCountKeyParts = (doc, fields) => fields.map((field) => normalizeUndefined(doc[field]));
2935
- const applyAggregateIndexesForChange = async (db, tableName, aggregateIndexes, change) => {
2936
- const docId = String(change.id);
2937
- for (const aggregateIndex of aggregateIndexes) {
2938
- const keyParts = change.operation === "delete" ? null : computeCountKeyParts(change.newDoc, aggregateIndex.fields);
2939
- const metricValues = change.operation === "delete" ? null : computeAggregateMetricValues(change.newDoc, aggregateIndex);
2940
- await reconcileAggregateMembership(db, {
2941
- tableName,
2942
- indexName: aggregateIndex.name,
2943
- docId,
2944
- keyParts,
2945
- metricValues
2946
- });
2947
- }
2948
- };
2949
- const getCountState = async (db, tableName, indexName, kind = AGGREGATE_STATE_KIND_METRIC) => {
2950
- const state = (await db.query(AGGREGATE_STATE_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", kind).eq("tableKey", tableName).eq("indexName", indexName)).collect())[0];
2951
- if (!state) return null;
2952
- const { tableKey, ...rest } = state;
2953
- return {
2954
- ...rest,
2955
- tableName: tableKey
2956
- };
2957
- };
2958
- const assertAggregateIndexesWritable = async (db, tableName, metricIndexNames, rankIndexNames) => {
2959
- const clearingStates = await db.query(AGGREGATE_STATE_TABLE).withIndex("by_table_status", (q) => q.eq("tableKey", tableName).eq("status", COUNT_STATUS_CLEARING)).collect();
2960
- const metricNames = new Set(metricIndexNames);
2961
- const rankNames = new Set(rankIndexNames);
2962
- const blockingState = clearingStates.find((state) => state.kind === AGGREGATE_STATE_KIND_RANK ? rankNames.has(state.indexName) : metricNames.has(state.indexName));
2963
- if (!blockingState) return;
2964
- const indexType = blockingState.kind === AGGREGATE_STATE_KIND_RANK ? "rankIndex" : "aggregateIndex";
2965
- throw createError(blockingState.kind === AGGREGATE_STATE_KIND_RANK ? "RANK_INDEX_BUILDING" : AGGREGATE_ERROR.INDEX_BUILDING, `${indexType} '${tableName}.${blockingState.indexName}' is CLEARING. Retry the write after aggregateBackfill reaches BUILDING or READY.`);
2966
- };
2967
- const setCountState = async (db, nextState, kind = AGGREGATE_STATE_KIND_METRIC) => {
2968
- const existing = await getCountState(db, nextState.tableName, nextState.indexName, kind);
2969
- const payload = {
2970
- kind,
2971
- tableKey: nextState.tableName,
2972
- indexName: nextState.indexName,
2973
- keyDefinitionHash: nextState.keyDefinitionHash,
2974
- metricDefinitionHash: nextState.metricDefinitionHash,
2975
- status: nextState.status,
2976
- cursor: nextState.cursor ?? null,
2977
- processed: nextState.processed,
2978
- startedAt: nextState.startedAt,
2979
- updatedAt: nextState.updatedAt,
2980
- completedAt: nextState.completedAt ?? null,
2981
- lastError: nextState.lastError ?? null
2982
- };
2983
- if (!existing) {
2984
- await db.insert(AGGREGATE_STATE_TABLE, payload);
2985
- return;
2986
- }
2987
- await db.patch(AGGREGATE_STATE_TABLE, existing._id, payload);
2988
- };
2989
- const setCountStateError = async (db, tableName, indexName, error, kind = AGGREGATE_STATE_KIND_METRIC) => {
2990
- const now = Date.now();
2991
- const existing = await getCountState(db, tableName, indexName, kind);
2992
- const message = error instanceof Error ? error.message : String(error);
2993
- if (!existing) throw new Error(`Missing count state for '${tableName}.${indexName}' while recording backfill error.`);
2994
- await db.patch(AGGREGATE_STATE_TABLE, existing._id, {
2995
- status: existing.status === COUNT_STATUS_CLEARING ? COUNT_STATUS_CLEARING : COUNT_STATUS_BUILDING,
2996
- updatedAt: now,
2997
- keyDefinitionHash: existing.keyDefinitionHash,
2998
- metricDefinitionHash: existing.metricDefinitionHash,
2999
- lastError: message
3000
- });
3001
- };
3002
- /**
3003
- * Removes at most `batchSize` documents of an aggregate index's stored state and
3004
- * reports whether anything is left. Callers drive it to completion across
3005
- * transactions, so clearing a large index never has to fit in one mutation.
3006
- *
3007
- * Members are removed through the normal delta machinery rather than raw
3008
- * deletes, so buckets and extrema stay consistent with the members that remain
3009
- * at every intermediate step. That keeps concurrent writers correct while the
3010
- * clear drains. Residual bucket/extrema rows (drift with no member behind them)
3011
- * are swept only once no members are left, and the loop re-checks members
3012
- * afterwards.
3013
- */
3014
- const clearCountIndexChunk = async (db, tableName, indexName, batchSize) => {
3015
- const members = await takeMembersForIndex(db, tableName, indexName, batchSize);
3016
- if (members.length > 0) {
3017
- await flushAggregateMembershipDeltas(db, tableName, indexName, members.map((member) => computeMembershipDelta(member, {
3018
- tableName,
3019
- indexName,
3020
- docId: member.docId,
3021
- keyParts: null,
3022
- metricValues: null
3023
- })));
3024
- return {
3025
- done: false,
3026
- processed: members.length
3027
- };
3028
- }
3029
- const buckets = await takeBucketsForIndex(db, tableName, indexName, batchSize);
3030
- if (buckets.length > 0) {
3031
- for (const bucket of buckets) await db.delete(AGGREGATE_BUCKET_TABLE, bucket._id);
3032
- return {
3033
- done: false,
3034
- processed: buckets.length
3035
- };
3036
- }
3037
- const extrema = await takeExtremaForIndex(db, tableName, indexName, batchSize);
3038
- if (extrema.length > 0) {
3039
- for (const entry of extrema) await db.delete(AGGREGATE_EXTREMA_TABLE, entry._id);
3040
- return {
3041
- done: false,
3042
- processed: extrema.length
3043
- };
3044
- }
3045
- return {
3046
- done: true,
3047
- processed: 0
3048
- };
3049
- };
3050
- const createCountError = (code, message) => createError(code, message);
3051
- const createAggregateError = (code, message) => createError(code, message);
3052
- const assertAggregateAllowedForRls = (tableConfig, rlsMode, code, methodName) => {
3053
- if ((typeof tableConfig.table.isRlsEnabled === "function" ? tableConfig.table.isRlsEnabled() : false) && rlsMode !== "skip") throw createError(code, `${methodName} is not available for table '${tableConfig.name}' in RLS-restricted contexts in v1.`);
3054
- };
3055
- const ensureCountAllowedForRls = (tableConfig, rlsMode) => {
3056
- assertAggregateAllowedForRls(tableConfig, rlsMode, COUNT_ERROR.RLS_UNSUPPORTED, "count()");
3057
- };
3058
- const ensureAggregateAllowedForRls = (tableConfig, rlsMode, methodName) => {
3059
- assertAggregateAllowedForRls(tableConfig, rlsMode, AGGREGATE_ERROR.RLS_UNSUPPORTED, methodName);
3060
- };
3061
- const ensureIndexReady = async (db, tableName, indexName, code, indexLabel) => {
3062
- const state = await getCountState(db, tableName, indexName);
3063
- if (!state || state.status !== COUNT_STATUS_READY) throw createError(code, `${indexLabel} '${tableName}.${indexName}' is BUILDING. Run aggregateBackfill until READY.`);
3064
- };
3065
- const ensureCountIndexReady = async (db, tableName, indexName) => ensureIndexReady(db, tableName, indexName, COUNT_ERROR.INDEX_BUILDING, "aggregateIndex");
3066
- const ensureAggregateIndexReady = async (db, tableName, indexName) => ensureIndexReady(db, tableName, indexName, AGGREGATE_ERROR.INDEX_BUILDING, "aggregateIndex");
3067
- const listSchemaAggregateIndexes = (schema) => {
3068
- const entries = [];
3069
- for (const tableConfig of Object.values(schema)) {
3070
- const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
3071
- for (const aggregateIndex of aggregateIndexes) entries.push({
3072
- tableName: tableConfig.name,
3073
- indexName: aggregateIndex.name,
3074
- fields: aggregateIndex.fields,
3075
- countFields: aggregateIndex.countFields,
3076
- sumFields: aggregateIndex.sumFields,
3077
- avgFields: aggregateIndex.avgFields,
3078
- minFields: aggregateIndex.minFields,
3079
- maxFields: aggregateIndex.maxFields
3080
- });
3081
- }
3082
- return entries;
3083
- };
3084
- const isIndexCountZero = (plan) => Array.isArray(plan.keyCandidates) && plan.keyCandidates.length === 0 || [...Object.values(plan.fieldValues), ...Object.values(plan.postFieldValues)].some((values) => values.length === 0);
3085
- const isAggregatePlanZero = (plan) => Array.isArray(plan.keyCandidates) && plan.keyCandidates.length === 0 || [...Object.values(plan.fieldValues), ...Object.values(plan.postFieldValues)].some((values) => values.length === 0);
3086
-
3087
- //#endregion
3088
- //#region src/orm/aggregate-index/rank-runtime.ts
3089
- const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
3090
- const stableEquals = (left, right) => JSON.stringify(left) === JSON.stringify(right);
3091
- const RANK_MEMBER_KIND = AGGREGATE_STATE_KIND_RANK;
3092
- const RANK_ERROR = {
3093
- FILTER_UNSUPPORTED: "RANK_FILTER_UNSUPPORTED",
3094
- NOT_INDEXED: "RANK_NOT_INDEXED",
3095
- INDEX_BUILDING: "RANK_INDEX_BUILDING",
3096
- RLS_UNSUPPORTED: "RANK_RLS_UNSUPPORTED"
3097
- };
3098
- const createRankError = (code, message) => /* @__PURE__ */ new Error(`${code}: ${message}`);
3099
- const rankAggregateName = (tableName, indexName) => `${tableName}.${indexName}`;
3100
- const rankAggregate = (tableName, indexName) => new DirectAggregate({ name: rankAggregateName(tableName, indexName) });
3101
- const toRankValue = (value) => {
3102
- if (value === null || value === void 0) return null;
3103
- if (value instanceof Date) return value.getTime();
3104
- if (typeof value === "number" && Number.isFinite(value)) return value;
3105
- throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, `rankIndex order field values must be finite numbers or dates; received '${typeof value}'.`);
3106
- };
3107
- const decodeRankValue = (value, direction) => {
3108
- if (value === null || value === void 0) return null;
3109
- if (typeof value === "number") return direction === "desc" ? -value : value;
3110
- return value;
3111
- };
3112
- const getRankSortKey = (definition, doc, docId) => {
3113
- const key = [];
3114
- for (const orderField of definition.orderFields) {
3115
- const rawValue = toRankValue(doc[orderField.field]);
3116
- if (rawValue === null) {
3117
- key.push(null);
3118
- continue;
3119
- }
3120
- key.push(orderField.direction === "desc" ? -rawValue : rawValue);
3121
- }
3122
- key.push(docId);
3123
- return key;
3124
- };
3125
- const toPublicRankKey = (definition, key) => {
3126
- if (!Array.isArray(key)) return key;
3127
- const restored = key.slice(0, definition.orderFields.length).map((value, index) => decodeRankValue(value, definition.orderFields[index].direction));
3128
- return restored.length === 1 ? restored[0] : restored;
3129
- };
3130
- const getRankSumValue = (definition, doc) => {
3131
- if (!definition.sumField) return 1;
3132
- const raw = doc[definition.sumField];
3133
- if (raw === null || raw === void 0) return 0;
3134
- if (typeof raw !== "number" || !Number.isFinite(raw)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, `rankIndex '${definition.name}' sum field '${definition.sumField}' must be a finite number.`);
3135
- return raw;
3136
- };
3137
- const validateRankWhereValue = (value) => {
3138
- if (value === null || value === void 0) return null;
3139
- if (isObject(value)) {
3140
- if (Object.hasOwn(value, "eq")) return value.eq ?? null;
3141
- if (value.isNull === true) return null;
3142
- throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where supports only eq and isNull: true on partition fields.");
3143
- }
3144
- if (Array.isArray(value)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where does not support IN arrays in v1.");
3145
- return value;
3146
- };
3147
- const collectWhere = (where, fields, out) => {
3148
- if (Object.hasOwn(where, "OR") || Object.hasOwn(where, "NOT")) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where supports conjunctions only (object fields + AND).");
3149
- if (Object.hasOwn(where, "RAW")) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where does not support RAW filters.");
3150
- const andEntries = where.AND;
3151
- if (andEntries !== void 0) {
3152
- if (!Array.isArray(andEntries)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where AND must be an array.");
3153
- for (const entry of andEntries) {
3154
- if (!isObject(entry)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where AND entries must be objects.");
3155
- collectWhere(entry, fields, out);
3156
- }
3157
- }
3158
- for (const [field, rawValue] of Object.entries(where)) {
3159
- if (field === "AND") continue;
3160
- if (!fields.has(field)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, `rank() where field '${field}' is not a partition field.`);
3161
- const nextValue = validateRankWhereValue(rawValue);
3162
- const prevValue = out.get(field);
3163
- if (prevValue !== void 0 && !stableEquals(prevValue, nextValue)) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, `rank() where has conflicting constraints for '${field}'.`);
3164
- out.set(field, nextValue);
3165
- }
3166
- };
3167
- const buildNamespace = (definition, values) => {
3168
- if (definition.partitionFields.length === 0) return;
3169
- const parts = definition.partitionFields.map((field) => values.get(field));
3170
- return parts.length === 1 ? parts[0] : parts;
3171
- };
3172
- const getRankIndexDefinitions = (tableConfig) => {
3173
- const rankIndexes = tableConfig.table.getRankIndexes?.();
3174
- if (!Array.isArray(rankIndexes)) return [];
3175
- return rankIndexes.map((entry) => ({
3176
- name: entry.name,
3177
- partitionFields: entry.partitionFields ?? [],
3178
- orderFields: entry.orderFields ?? [],
3179
- sumField: entry.sumField
3180
- }));
3181
- };
3182
- const ensureRankAllowedForRls = (tableConfig, rlsMode) => {
3183
- if ((typeof tableConfig.table.isRlsEnabled === "function" ? tableConfig.table.isRlsEnabled() : false) && rlsMode !== "skip") throw createRankError(RANK_ERROR.RLS_UNSUPPORTED, `rank() is not available for table '${tableConfig.name}' in RLS-restricted contexts in v1.`);
3184
- };
3185
- const compileRankPlan = (tableConfig, indexName, where) => {
3186
- const definition = getRankIndexDefinitions(tableConfig).find((entry) => entry.name === indexName);
3187
- if (!definition) throw createRankError(RANK_ERROR.NOT_INDEXED, `No rankIndex '${indexName}' found on '${tableConfig.name}'. Declare rankIndex('${indexName}') and run aggregateBackfill.`);
3188
- if (!isObject(where ?? {})) throw createRankError(RANK_ERROR.FILTER_UNSUPPORTED, "rank() where must be an object filter.");
3189
- const values = /* @__PURE__ */ new Map();
3190
- collectWhere(where ?? {}, new Set(definition.partitionFields), values);
3191
- for (const field of definition.partitionFields) if (!values.has(field)) throw createRankError(RANK_ERROR.NOT_INDEXED, `rank() on '${tableConfig.name}.${indexName}' requires all partition fields constrained: [${definition.partitionFields.join(", ")}].`);
3192
- return {
3193
- tableName: tableConfig.name,
3194
- indexName,
3195
- definition,
3196
- namespace: buildNamespace(definition, values)
3197
- };
3198
- };
3199
- const ensureRankIndexReady = async (db, tableName, indexName) => {
3200
- const state = await getCountState(db, tableName, indexName, AGGREGATE_STATE_KIND_RANK);
3201
- if (!state || state.status !== COUNT_STATUS_READY) throw createRankError(RANK_ERROR.INDEX_BUILDING, `rankIndex '${tableName}.${indexName}' is BUILDING. Run aggregateBackfill until READY.`);
3202
- };
3203
- const getRankMemberByDoc = async (db, tableName, indexName, docId) => {
3204
- return (await db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index_doc", (q) => q.eq("kind", RANK_MEMBER_KIND).eq("tableKey", tableName).eq("indexName", indexName).eq("docId", docId)).collect())[0] ?? null;
3205
- };
3206
- const takeRankMembers = async (db, tableName, indexName, limit) => await db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", RANK_MEMBER_KIND).eq("tableKey", tableName).eq("indexName", indexName)).take(limit);
3207
- const rankCtx = (db) => ({
3208
- db,
3209
- orm: void 0
3210
- });
3211
- /** Trees dropped per invocation once every rank member has been removed. */
3212
- const RANK_TREE_DROP_BATCH = 16;
3213
- /**
3214
- * Removes at most `batchSize` rank members and reports whether anything is
3215
- * left. Each member is removed from the btree before its row is dropped, so the
3216
- * tree stays consistent with the members that remain and a partially drained
3217
- * clear can safely resume in a later mutation.
3218
- */
3219
- const clearRankIndexChunk = async (db, tableName, indexName, batchSize) => {
3220
- const aggregate = rankAggregate(tableName, indexName);
3221
- const ctx = rankCtx(db);
3222
- const members = await takeRankMembers(db, tableName, indexName, batchSize);
3223
- if (members.length > 0) {
3224
- for (const member of members) {
3225
- if (member.rankKey !== void 0) await aggregate.deleteIfExists(ctx, {
3226
- id: member.docId,
3227
- key: member.rankKey,
3228
- namespace: member.rankNamespace
3229
- });
3230
- await db.delete(AGGREGATE_MEMBER_TABLE, member._id);
3231
- }
3232
- return {
3233
- done: false,
3234
- processed: members.length
257
+ this.config = {
258
+ name,
259
+ expression
3235
260
  };
3236
261
  }
3237
- const done = await aggregate.deleteTrees(ctx, RANK_TREE_DROP_BATCH);
3238
- return {
3239
- done,
3240
- processed: done ? 0 : RANK_TREE_DROP_BATCH
3241
- };
3242
- };
3243
- const reconcileRankMembership = async (db, params) => {
3244
- const { tableName, definition, docId, doc } = params;
3245
- const existing = await getRankMemberByDoc(db, tableName, definition.name, docId);
3246
- const aggregate = rankAggregate(tableName, definition.name);
3247
- const ctx = rankCtx(db);
3248
- const now = Date.now();
3249
- if (!doc) {
3250
- if (existing?.rankKey !== void 0) {
3251
- await aggregate.delete(ctx, {
3252
- id: docId,
3253
- key: existing.rankKey,
3254
- namespace: existing.rankNamespace
3255
- });
3256
- await db.delete(AGGREGATE_MEMBER_TABLE, existing._id);
3257
- }
3258
- return;
3259
- }
3260
- const namespace = buildNamespace(definition, new Map(definition.partitionFields.map((field) => [field, doc[field]])));
3261
- const key = getRankSortKey(definition, doc, docId);
3262
- const sumValue = getRankSumValue(definition, doc);
3263
- if (existing && stableEquals(existing.rankNamespace, namespace) && stableEquals(existing.rankKey, key) && (existing.rankSumValue ?? 0) === sumValue) return;
3264
- if (existing?.rankKey !== void 0) {
3265
- await aggregate.replace(ctx, {
3266
- id: docId,
3267
- key: existing.rankKey,
3268
- namespace: existing.rankNamespace
3269
- }, {
3270
- key,
3271
- namespace,
3272
- sumValue
3273
- });
3274
- await db.patch(AGGREGATE_MEMBER_TABLE, existing._id, {
3275
- kind: RANK_MEMBER_KIND,
3276
- keyHash: "__rank__",
3277
- keyParts: [],
3278
- sumValues: {},
3279
- nonNullCountValues: {},
3280
- extremaValues: {},
3281
- rankNamespace: namespace,
3282
- rankKey: key,
3283
- rankSumValue: sumValue,
3284
- updatedAt: now
3285
- });
3286
- return;
3287
- }
3288
- await aggregate.insert(ctx, {
3289
- id: docId,
3290
- key,
3291
- namespace,
3292
- sumValue
3293
- });
3294
- await db.insert(AGGREGATE_MEMBER_TABLE, {
3295
- kind: RANK_MEMBER_KIND,
3296
- tableKey: tableName,
3297
- indexName: definition.name,
3298
- docId,
3299
- keyHash: "__rank__",
3300
- keyParts: [],
3301
- sumValues: {},
3302
- nonNullCountValues: {},
3303
- extremaValues: {},
3304
- rankNamespace: namespace,
3305
- rankKey: key,
3306
- rankSumValue: sumValue,
3307
- updatedAt: now
3308
- });
3309
- };
3310
- const applyRankIndexesForChange = async (db, tableName, rankIndexes, change) => {
3311
- const docId = String(change.id);
3312
- for (const rankIndex of rankIndexes) await reconcileRankMembership(db, {
3313
- tableName,
3314
- definition: rankIndex,
3315
- docId,
3316
- doc: change.operation === "delete" ? null : change.newDoc
3317
- });
3318
- };
3319
- const readRankCount = async (db, plan) => {
3320
- return await rankAggregate(plan.tableName, plan.indexName).count(rankCtx(db), { namespace: plan.namespace });
3321
- };
3322
- const readRankSum = async (db, plan) => {
3323
- return await rankAggregate(plan.tableName, plan.indexName).sum(rankCtx(db), { namespace: plan.namespace });
3324
- };
3325
- const toPublicRankItem = (plan, item) => ({
3326
- id: item.id,
3327
- key: toPublicRankKey(plan.definition, item.key),
3328
- sumValue: item.sumValue
3329
- });
3330
- const readRankAtWithCount = async (db, plan, offset, count) => {
3331
- if (count <= 0) return null;
3332
- const normalizedOffset = offset < 0 ? count + offset : offset;
3333
- if (normalizedOffset < 0 || normalizedOffset >= count) return null;
3334
- return toPublicRankItem(plan, await rankAggregate(plan.tableName, plan.indexName).at(rankCtx(db), offset, { namespace: plan.namespace }));
3335
- };
3336
- const readRankAt = async (db, plan, offset) => await readRankAtWithCount(db, plan, offset, await readRankCount(db, plan));
3337
- const readRankIndexOf = async (db, plan, args) => {
3338
- const member = await getRankMemberByDoc(db, plan.tableName, plan.indexName, args.id);
3339
- if (!member || member.rankKey === void 0) return -1;
3340
- if (!stableEquals(member.rankNamespace, plan.namespace)) return -1;
3341
- return await rankAggregate(plan.tableName, plan.indexName).indexOf(rankCtx(db), member.rankKey, {
3342
- namespace: plan.namespace,
3343
- id: args.id
3344
- });
3345
- };
3346
- const readRankPaginate = async (db, plan, cursor, limit) => {
3347
- const paged = await rankAggregate(plan.tableName, plan.indexName).paginate(rankCtx(db), {
3348
- namespace: plan.namespace,
3349
- cursor: cursor ?? void 0,
3350
- pageSize: limit
3351
- });
3352
- return {
3353
- continueCursor: paged.cursor ?? "",
3354
- isDone: paged.isDone,
3355
- page: paged.page.map((item) => toPublicRankItem(plan, item))
3356
- };
3357
- };
3358
- const readRankMin = async (db, plan) => {
3359
- const item = await rankAggregate(plan.tableName, plan.indexName).min(rankCtx(db), { namespace: plan.namespace });
3360
- return item ? toPublicRankItem(plan, item) : null;
3361
- };
3362
- const readRankMax = async (db, plan) => {
3363
- const item = await rankAggregate(plan.tableName, plan.indexName).max(rankCtx(db), { namespace: plan.namespace });
3364
- return item ? toPublicRankItem(plan, item) : null;
3365
- };
3366
- const readRankRandom = async (db, plan) => {
3367
- const count = await readRankCount(db, plan);
3368
- if (count <= 0) return null;
3369
- return await readRankAtWithCount(db, plan, Math.floor(Math.random() * count), count);
3370
262
  };
263
+ function check(name, expression) {
264
+ return new ConvexCheckBuilder(name, expression);
265
+ }
3371
266
 
3372
267
  //#endregion
3373
- //#region src/orm/aggregate-index/backfill.ts
3374
- const DEFAULT_BACKFILL_BATCH_SIZE = 1e3;
3375
- const getBackfillMode = (candidate) => {
3376
- if (candidate === void 0) return "resume";
3377
- if (candidate === "resume" || candidate === "rebuild" || candidate === "prune") return candidate;
3378
- throw new Error("countBackfill mode must be one of 'resume', 'rebuild', or 'prune'.");
3379
- };
3380
- const getBackfillBatchSize = (candidate) => {
3381
- if (candidate === void 0) return DEFAULT_BACKFILL_BATCH_SIZE;
3382
- if (typeof candidate !== "number" || !Number.isInteger(candidate)) throw new Error("countBackfill batchSize must be a positive integer.");
3383
- if (candidate < 1) throw new Error("countBackfill batchSize must be a positive integer.");
3384
- return candidate;
3385
- };
3386
- const matchesFilter = (target, args) => {
3387
- if (args.tableName && target.tableName !== args.tableName) return false;
3388
- if (args.indexName && target.indexName !== args.indexName) return false;
3389
- return true;
3390
- };
3391
- const getTargets = (schema, args) => {
3392
- const metricTargets = listSchemaAggregateIndexes(schema).map((entry) => ({
3393
- kind: "metric",
3394
- tableName: entry.tableName,
3395
- indexName: entry.indexName,
3396
- fields: entry.fields,
3397
- countFields: entry.countFields,
3398
- sumFields: entry.sumFields,
3399
- avgFields: entry.avgFields,
3400
- minFields: entry.minFields,
3401
- maxFields: entry.maxFields
3402
- }));
3403
- const rankTargets = [];
3404
- for (const tableConfig of Object.values(schema)) {
3405
- const rankIndexes = getRankIndexDefinitions(tableConfig);
3406
- for (const rankIndex of rankIndexes) rankTargets.push({
3407
- kind: "rank",
3408
- tableName: tableConfig.name,
3409
- indexName: rankIndex.name,
3410
- fields: rankIndex.partitionFields,
3411
- orderFields: rankIndex.orderFields,
3412
- rankSumField: rankIndex.sumField
3413
- });
268
+ //#region src/orm/capabilities.ts
269
+ const resolveOrmCapabilities = (capabilities) => {
270
+ const resolved = {};
271
+ for (const capability of capabilities ?? []) {
272
+ if (capability.kind === "aggregate") {
273
+ resolved.aggregate = capability.value;
274
+ continue;
275
+ }
276
+ resolved.migrations = capability.value;
3414
277
  }
3415
- return [...metricTargets, ...rankTargets].filter((entry) => matchesFilter(entry, args));
278
+ return resolved;
3416
279
  };
3417
- const dedupe = (fields) => [...new Set(fields)];
3418
- const computeMetricDefinition = (target) => ({
3419
- countFields: dedupe(target.countFields ?? []),
3420
- sumFields: dedupe(target.sumFields ?? []),
3421
- avgFields: dedupe(target.avgFields ?? []),
3422
- minFields: dedupe(target.minFields ?? []),
3423
- maxFields: dedupe(target.maxFields ?? [])
3424
- });
3425
- const computeKeyDefinitionHash = (target) => JSON.stringify({
3426
- kind: target.kind,
3427
- fields: target.fields,
3428
- orderFields: target.orderFields ?? []
3429
- });
3430
- const computeMetricDefinitionHash = (target) => target.kind === "rank" ? JSON.stringify({ sumField: target.rankSumField ?? null }) : JSON.stringify(computeMetricDefinition(target));
3431
- const parseMetricDefinitionHash = (metricDefinitionHash) => {
3432
- let parsed;
3433
- try {
3434
- parsed = JSON.parse(metricDefinitionHash);
3435
- } catch {
3436
- throw new Error(`Invalid metricDefinitionHash payload: ${metricDefinitionHash}`);
3437
- }
3438
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("Invalid metricDefinitionHash payload shape.");
3439
- const pick = (key) => {
3440
- const candidate = parsed[key];
3441
- if (!Array.isArray(candidate)) throw new Error(`metricDefinitionHash is missing '${key}'.`);
3442
- if (candidate.some((entry) => typeof entry !== "string")) throw new Error(`metricDefinitionHash has non-string value in '${key}'.`);
3443
- return dedupe(candidate);
3444
- };
3445
- return {
3446
- countFields: pick("countFields"),
3447
- sumFields: pick("sumFields"),
3448
- avgFields: pick("avgFields"),
3449
- minFields: pick("minFields"),
3450
- maxFields: pick("maxFields")
3451
- };
280
+ const AGGREGATE_SETUP_HINT = "Generated ORM: declare an aggregateIndex(...)/rankIndex(...) in your schema and rerun `kitcn codegen`, which registers the capability in <functionsDir>/generated/server.ts. Hand-written ORM: import { aggregateCapability } from 'kitcn/orm/aggregate-index' and pass createOrm({ schema, capabilities: [aggregateCapability()] }).";
281
+ const missingAggregateCapabilityError = (usage) => /* @__PURE__ */ new Error(`${usage} requires the aggregate capability. ${AGGREGATE_SETUP_HINT}`);
282
+ const requireAggregateCapability = (capabilities, usage) => {
283
+ const aggregate = capabilities?.aggregate;
284
+ if (!aggregate) throw missingAggregateCapabilityError(usage);
285
+ return aggregate;
3452
286
  };
3453
- const toMetricStorageDefinition = (metricDefinition) => ({
3454
- sumFields: new Set([...metricDefinition.sumFields, ...metricDefinition.avgFields]),
3455
- nonNullCountFields: new Set([...metricDefinition.countFields, ...metricDefinition.avgFields]),
3456
- extremaFields: new Set([...metricDefinition.minFields, ...metricDefinition.maxFields])
3457
- });
3458
- const requiresMetricBackfill = (existingMetricDefinitionHash, targetMetricDefinitionHash) => {
3459
- const existingDefinition = parseMetricDefinitionHash(existingMetricDefinitionHash);
3460
- const targetDefinition = parseMetricDefinitionHash(targetMetricDefinitionHash);
3461
- const existingStorage = toMetricStorageDefinition(existingDefinition);
3462
- const targetStorage = toMetricStorageDefinition(targetDefinition);
3463
- const hasMissing = (target, existing) => {
3464
- for (const field of target) if (!existing.has(field)) return true;
3465
- return false;
3466
- };
3467
- return hasMissing(targetStorage.sumFields, existingStorage.sumFields) || hasMissing(targetStorage.nonNullCountFields, existingStorage.nonNullCountFields) || hasMissing(targetStorage.extremaFields, existingStorage.extremaFields);
287
+ const requireMigrationCapability = (capabilities, usage) => {
288
+ const migrations = capabilities?.migrations;
289
+ if (!migrations) throw new Error(`${usage} requires the migration capability. Generated ORM: add a migration under <functionsDir>/migrations and rerun \`kitcn codegen\`, which registers the capability in <functionsDir>/generated/server.ts. Hand-written ORM: import { migrationCapability } from 'kitcn/orm/migrations' and pass createOrm({ schema, capabilities: [migrationCapability()] }).`);
290
+ return migrations;
3468
291
  };
3469
- function createCountBackfillHandlers(schema, getChunkRef) {
3470
- const serializeKey = (kind, tableName, indexName) => `${kind}\u0000${tableName}\u0000${indexName}`;
3471
- const stateKindFor = (kind) => kind === "rank" ? AGGREGATE_STATE_KIND_RANK : AGGREGATE_STATE_KIND_METRIC;
3472
- const scheduleChunk = async (ctx, tableName, indexName, batchSize) => {
3473
- const chunkRef = getChunkRef?.();
3474
- if (ctx.scheduler && chunkRef) await ctx.scheduler.runAfter(0, chunkRef, {
3475
- tableName,
3476
- indexName,
3477
- batchSize
3478
- });
3479
- };
3480
- /**
3481
- * Drains an index's stored state until it is empty or the shared per-mutation
3482
- * budget runs out. Returns true only when the index is fully cleared, so the
3483
- * caller knows whether to advance the state machine or reschedule.
3484
- */
3485
- const drainIndexClear = async (ctx, kind, tableName, indexName, batchSize, budget) => {
3486
- while (budget.remaining > 0) {
3487
- const limit = Math.min(batchSize, budget.remaining);
3488
- const step = kind === "rank" ? await clearRankIndexChunk(ctx.db, tableName, indexName, limit) : await clearCountIndexChunk(ctx.db, tableName, indexName, limit);
3489
- if (step.done) return true;
3490
- budget.remaining -= Math.max(step.processed, 1);
3491
- }
3492
- return false;
3493
- };
3494
- const pruneRemovedState = async (ctx, args, targets, batchSize, clearBudget) => {
3495
- const targetKeys = new Set(targets.map((target) => serializeKey(target.kind, target.tableName, target.indexName)));
3496
- const states = await ctx.db.query(AGGREGATE_STATE_TABLE).collect();
3497
- const stateByKey = new Map(states.map((state) => [serializeKey(state.kind === AGGREGATE_STATE_KIND_RANK ? "rank" : "metric", state.tableKey, state.indexName), state]));
3498
- const existingKeys = /* @__PURE__ */ new Set();
3499
- for (const state of states) existingKeys.add(serializeKey(state.kind === AGGREGATE_STATE_KIND_RANK ? "rank" : "metric", state.tableKey, state.indexName));
3500
- if (args.tableName && args.indexName) {
3501
- const tableName = args.tableName;
3502
- const indexName = args.indexName;
3503
- const [bucket, extrema, metricMember, rankMember] = await Promise.all([
3504
- ctx.db.query(AGGREGATE_BUCKET_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first(),
3505
- ctx.db.query(AGGREGATE_EXTREMA_TABLE).withIndex("by_table_index", (q) => q.eq("tableKey", tableName).eq("indexName", indexName)).first(),
3506
- ctx.db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", AGGREGATE_STATE_KIND_METRIC).eq("tableKey", tableName).eq("indexName", indexName)).first(),
3507
- ctx.db.query(AGGREGATE_MEMBER_TABLE).withIndex("by_kind_table_index", (q) => q.eq("kind", AGGREGATE_STATE_KIND_RANK).eq("tableKey", tableName).eq("indexName", indexName)).first()
3508
- ]);
3509
- if (bucket || extrema || metricMember) existingKeys.add(serializeKey("metric", tableName, indexName));
3510
- if (rankMember) existingKeys.add(serializeKey("rank", tableName, indexName));
3511
- }
3512
- let pruned = 0;
3513
- let pruning = 0;
3514
- for (const key of existingKeys) {
3515
- const [kind, tableName, indexName] = key.split("\0");
3516
- if (args.tableName && tableName !== args.tableName) continue;
3517
- if (args.indexName && indexName !== args.indexName) continue;
3518
- if (targetKeys.has(key)) continue;
3519
- const state = stateByKey.get(key);
3520
- if (!await drainIndexClear(ctx, kind, tableName, indexName, batchSize, clearBudget)) {
3521
- const now = Date.now();
3522
- await setCountState(ctx.db, {
3523
- tableName,
3524
- indexName,
3525
- kind: stateKindFor(kind),
3526
- keyDefinitionHash: state?.keyDefinitionHash ?? "",
3527
- metricDefinitionHash: state?.metricDefinitionHash ?? "",
3528
- status: COUNT_STATUS_CLEARING,
3529
- cursor: null,
3530
- processed: 0,
3531
- startedAt: now,
3532
- updatedAt: now,
3533
- completedAt: null,
3534
- lastError: null
3535
- }, stateKindFor(kind));
3536
- await scheduleChunk(ctx, tableName, indexName, batchSize);
3537
- pruning += 1;
3538
- continue;
3539
- }
3540
- if (state) await ctx.db.delete(AGGREGATE_STATE_TABLE, state._id);
3541
- pruned += 1;
3542
- }
3543
- return {
3544
- pruned,
3545
- pruning
3546
- };
3547
- };
3548
- /**
3549
- * Finishes clearing an index that no longer exists in the schema. Orphans have
3550
- * no backfill target, so they are driven purely by their CLEARING state row.
3551
- * Returns true when this invocation handled one.
3552
- */
3553
- const drainOrphanClear = async (ctx, args, targets, batchSize) => {
3554
- const targetKeys = new Set(targets.map((target) => serializeKey(target.kind, target.tableName, target.indexName)));
3555
- const states = await ctx.db.query(AGGREGATE_STATE_TABLE).collect();
3556
- for (const state of states) {
3557
- if (state.status !== COUNT_STATUS_CLEARING) continue;
3558
- const kind = state.kind === AGGREGATE_STATE_KIND_RANK ? "rank" : "metric";
3559
- if (targetKeys.has(serializeKey(kind, state.tableKey, state.indexName))) continue;
3560
- if (args.tableName && state.tableKey !== args.tableName) continue;
3561
- if (args.indexName && state.indexName !== args.indexName) continue;
3562
- if (await drainIndexClear(ctx, kind, state.tableKey, state.indexName, batchSize, { remaining: batchSize })) {
3563
- await ctx.db.delete(AGGREGATE_STATE_TABLE, state._id);
3564
- return true;
3565
- }
3566
- await ctx.db.patch(AGGREGATE_STATE_TABLE, state._id, { updatedAt: Date.now() });
3567
- await scheduleChunk(ctx, state.tableKey, state.indexName, batchSize);
3568
- return true;
3569
- }
3570
- return false;
3571
- };
3572
- const kickoff = async (ctx, args) => {
3573
- const targets = getTargets(schema, args);
3574
- const mode = getBackfillMode(args.mode);
3575
- const batchSize = getBackfillBatchSize(args.batchSize);
3576
- const clearBudget = { remaining: batchSize };
3577
- const { pruned, pruning } = await pruneRemovedState(ctx, args, targets, batchSize, clearBudget);
3578
- if (mode === "prune") return {
3579
- targets: targets.length,
3580
- mode,
3581
- scheduled: 0,
3582
- skippedReady: 0,
3583
- needsRebuild: 0,
3584
- pruned,
3585
- pruning,
3586
- status: "ok"
3587
- };
3588
- const now = Date.now();
3589
- let scheduled = 0;
3590
- let skippedReady = 0;
3591
- let needsRebuild = 0;
3592
- for (const target of targets) {
3593
- const stateKind = target.kind === "rank" ? AGGREGATE_STATE_KIND_RANK : AGGREGATE_STATE_KIND_METRIC;
3594
- const existing = await getCountState(ctx.db, target.tableName, target.indexName, stateKind);
3595
- const keyDefinitionHash = computeKeyDefinitionHash(target);
3596
- const metricDefinitionHash = computeMetricDefinitionHash(target);
3597
- const nextStateBase = {
3598
- tableName: target.tableName,
3599
- indexName: target.indexName,
3600
- keyDefinitionHash,
3601
- metricDefinitionHash
3602
- };
3603
- if (mode === "rebuild") {
3604
- const cleared = await drainIndexClear(ctx, target.kind, target.tableName, target.indexName, batchSize, clearBudget);
3605
- await setCountState(ctx.db, {
3606
- ...nextStateBase,
3607
- kind: stateKind,
3608
- status: cleared ? COUNT_STATUS_BUILDING : COUNT_STATUS_CLEARING,
3609
- cursor: null,
3610
- processed: 0,
3611
- startedAt: now,
3612
- updatedAt: now,
3613
- completedAt: null,
3614
- lastError: null
3615
- }, stateKind);
3616
- } else if (existing) {
3617
- if (existing.keyDefinitionHash !== keyDefinitionHash) {
3618
- needsRebuild += 1;
3619
- continue;
3620
- }
3621
- if (existing.metricDefinitionHash !== metricDefinitionHash) {
3622
- if (!(target.kind === "rank" ? true : requiresMetricBackfill(existing.metricDefinitionHash, metricDefinitionHash)) && existing.status === COUNT_STATUS_READY) {
3623
- await setCountState(ctx.db, {
3624
- ...nextStateBase,
3625
- kind: stateKind,
3626
- status: COUNT_STATUS_READY,
3627
- cursor: null,
3628
- processed: existing.processed,
3629
- startedAt: existing.startedAt,
3630
- updatedAt: now,
3631
- completedAt: existing.completedAt ?? now,
3632
- lastError: null
3633
- }, stateKind);
3634
- skippedReady += 1;
3635
- continue;
3636
- }
3637
- await setCountState(ctx.db, {
3638
- ...nextStateBase,
3639
- kind: stateKind,
3640
- status: COUNT_STATUS_BUILDING,
3641
- cursor: null,
3642
- processed: 0,
3643
- startedAt: now,
3644
- updatedAt: now,
3645
- completedAt: null,
3646
- lastError: null
3647
- }, stateKind);
3648
- } else if (existing.status === COUNT_STATUS_READY) {
3649
- skippedReady += 1;
3650
- continue;
3651
- }
3652
- } else await setCountState(ctx.db, {
3653
- ...nextStateBase,
3654
- kind: stateKind,
3655
- status: COUNT_STATUS_BUILDING,
3656
- cursor: null,
3657
- processed: 0,
3658
- startedAt: now,
3659
- updatedAt: now,
3660
- completedAt: null,
3661
- lastError: null
3662
- }, stateKind);
3663
- await scheduleChunk(ctx, target.tableName, target.indexName, batchSize);
3664
- scheduled += 1;
3665
- }
3666
- return {
3667
- targets: targets.length,
3668
- mode,
3669
- scheduled,
3670
- skippedReady,
3671
- needsRebuild,
3672
- pruned,
3673
- pruning,
3674
- status: "ok"
3675
- };
3676
- };
3677
- const chunk = async (ctx, args) => {
3678
- const batchSize = getBackfillBatchSize(args.batchSize);
3679
- const targets = getTargets(schema, args);
3680
- if (!(Boolean(args.tableName && args.indexName) && targets.length === 1) && await drainOrphanClear(ctx, args, targets, batchSize)) return { status: "ok" };
3681
- if (targets.length > 1) {
3682
- for (const target of targets) {
3683
- const stateKind = target.kind === "rank" ? AGGREGATE_STATE_KIND_RANK : AGGREGATE_STATE_KIND_METRIC;
3684
- const state = await getCountState(ctx.db, target.tableName, target.indexName, stateKind);
3685
- if (!state || state.status !== COUNT_STATUS_READY) return chunk(ctx, {
3686
- tableName: target.tableName,
3687
- indexName: target.indexName,
3688
- batchSize
3689
- });
3690
- }
3691
- return { status: "ok" };
3692
- }
3693
- for (const target of targets) {
3694
- const stateKind = target.kind === "rank" ? AGGREGATE_STATE_KIND_RANK : AGGREGATE_STATE_KIND_METRIC;
3695
- try {
3696
- const state = await getCountState(ctx.db, target.tableName, target.indexName, stateKind);
3697
- if (!state || state.status === COUNT_STATUS_READY) continue;
3698
- if (state.status === COUNT_STATUS_CLEARING) {
3699
- const cleared = await drainIndexClear(ctx, target.kind, target.tableName, target.indexName, batchSize, { remaining: batchSize });
3700
- const clearedAt = Date.now();
3701
- await setCountState(ctx.db, {
3702
- tableName: target.tableName,
3703
- indexName: target.indexName,
3704
- kind: stateKind,
3705
- keyDefinitionHash: state.keyDefinitionHash,
3706
- metricDefinitionHash: state.metricDefinitionHash,
3707
- status: cleared ? COUNT_STATUS_BUILDING : COUNT_STATUS_CLEARING,
3708
- cursor: null,
3709
- processed: 0,
3710
- startedAt: state.startedAt,
3711
- updatedAt: clearedAt,
3712
- completedAt: null,
3713
- lastError: null
3714
- }, stateKind);
3715
- await scheduleChunk(ctx, target.tableName, target.indexName, batchSize);
3716
- continue;
3717
- }
3718
- const cursor = state.cursor ?? null;
3719
- const page = await ctx.db.query(target.tableName).withIndex("by_creation_time").paginate({
3720
- cursor,
3721
- numItems: batchSize
3722
- });
3723
- const pendingDeltas = [];
3724
- for (const doc of page.page) if (target.kind === "rank") await reconcileRankMembership(ctx.db, {
3725
- tableName: target.tableName,
3726
- definition: {
3727
- name: target.indexName,
3728
- partitionFields: target.fields,
3729
- orderFields: target.orderFields ?? [],
3730
- sumField: target.rankSumField
3731
- },
3732
- docId: String(doc._id),
3733
- doc
3734
- });
3735
- else pendingDeltas.push(await computeAggregateMembershipDelta(ctx.db, {
3736
- tableName: target.tableName,
3737
- indexName: target.indexName,
3738
- docId: String(doc._id),
3739
- keyParts: computeCountKeyParts(doc, target.fields),
3740
- metricValues: computeAggregateMetricValues(doc, {
3741
- name: target.indexName,
3742
- fields: target.fields,
3743
- countFields: target.countFields ?? [],
3744
- sumFields: target.sumFields ?? [],
3745
- avgFields: target.avgFields ?? [],
3746
- minFields: target.minFields ?? [],
3747
- maxFields: target.maxFields ?? []
3748
- })
3749
- }));
3750
- if (pendingDeltas.length > 0) await flushAggregateMembershipDeltas(ctx.db, target.tableName, target.indexName, pendingDeltas);
3751
- const now = Date.now();
3752
- const nextProcessed = state.processed + page.page.length;
3753
- if (page.isDone) {
3754
- await setCountState(ctx.db, {
3755
- tableName: target.tableName,
3756
- indexName: target.indexName,
3757
- kind: stateKind,
3758
- keyDefinitionHash: state.keyDefinitionHash,
3759
- metricDefinitionHash: state.metricDefinitionHash,
3760
- status: COUNT_STATUS_READY,
3761
- cursor: null,
3762
- processed: nextProcessed,
3763
- startedAt: state.startedAt,
3764
- updatedAt: now,
3765
- completedAt: now,
3766
- lastError: null
3767
- }, stateKind);
3768
- continue;
3769
- }
3770
- await setCountState(ctx.db, {
3771
- tableName: target.tableName,
3772
- indexName: target.indexName,
3773
- kind: stateKind,
3774
- keyDefinitionHash: state.keyDefinitionHash,
3775
- metricDefinitionHash: state.metricDefinitionHash,
3776
- status: COUNT_STATUS_BUILDING,
3777
- cursor: page.continueCursor,
3778
- processed: nextProcessed,
3779
- startedAt: state.startedAt,
3780
- updatedAt: now,
3781
- completedAt: null,
3782
- lastError: null
3783
- }, stateKind);
3784
- await scheduleChunk(ctx, target.tableName, target.indexName, batchSize);
3785
- } catch (error) {
3786
- await setCountStateError(ctx.db, target.tableName, target.indexName, error, stateKind);
3787
- throw error;
3788
- }
3789
- }
3790
- return { status: "ok" };
3791
- };
3792
- const status = async (ctx, args) => {
3793
- const states = await ctx.db.query(AGGREGATE_STATE_TABLE).collect();
3794
- const statesByKey = new Map(states.map((entry) => [`${entry.kind === AGGREGATE_STATE_KIND_RANK ? "rank" : "metric"}:${entry.tableKey}:${entry.indexName}`, entry]));
3795
- return getTargets(schema, args).map((target) => {
3796
- const key = `${target.kind}:${target.tableName}:${target.indexName}`;
3797
- const entry = statesByKey.get(key);
3798
- if (!entry) return {
3799
- kind: target.kind === "rank" ? AGGREGATE_STATE_KIND_RANK : AGGREGATE_STATE_KIND_METRIC,
3800
- tableName: target.tableName,
3801
- indexName: target.indexName,
3802
- keyDefinitionHash: computeKeyDefinitionHash(target),
3803
- metricDefinitionHash: computeMetricDefinitionHash(target),
3804
- status: COUNT_STATUS_BUILDING,
3805
- cursor: null,
3806
- processed: 0,
3807
- startedAt: 0,
3808
- updatedAt: 0,
3809
- completedAt: null,
3810
- lastError: null
3811
- };
3812
- const { tableKey, ...rest } = entry;
3813
- return {
3814
- ...rest,
3815
- tableName: tableKey
3816
- };
3817
- });
3818
- };
3819
- return {
3820
- kickoff,
3821
- chunk,
3822
- status
3823
- };
3824
- }
3825
292
 
3826
293
  //#endregion
3827
294
  //#region src/orm/query-promise.ts
@@ -4977,43 +1444,47 @@ var GelRankQuery = class {
4977
1444
  this.rls = rls;
4978
1445
  }
4979
1446
  async _plan() {
4980
- ensureRankAllowedForRls(this.tableConfig, this.rls?.mode);
4981
- const plan = compileRankPlan(this.tableConfig, this.indexName, this.config.where);
4982
- await ensureRankIndexReady(this.db, this.tableConfig.name, this.indexName);
4983
- return plan;
1447
+ const aggregate = requireAggregateCapability(getOrmContext(this.db)?.capabilities, "rank()");
1448
+ aggregate.ensureRankAllowedForRls(this.tableConfig, this.rls?.mode);
1449
+ const plan = aggregate.compileRankPlan(this.tableConfig, this.indexName, this.config.where);
1450
+ await aggregate.ensureRankIndexReady(this.db, this.tableConfig.name, this.indexName);
1451
+ return {
1452
+ aggregate,
1453
+ plan
1454
+ };
4984
1455
  }
4985
1456
  async count() {
4986
- const plan = await this._plan();
4987
- return await readRankCount(this.db, plan);
1457
+ const { aggregate, plan } = await this._plan();
1458
+ return await aggregate.readRankCount(this.db, plan);
4988
1459
  }
4989
1460
  async sum() {
4990
- const plan = await this._plan();
4991
- return await readRankSum(this.db, plan);
1461
+ const { aggregate, plan } = await this._plan();
1462
+ return await aggregate.readRankSum(this.db, plan);
4992
1463
  }
4993
1464
  async at(offset) {
4994
- const plan = await this._plan();
4995
- return await readRankAt(this.db, plan, offset);
1465
+ const { aggregate, plan } = await this._plan();
1466
+ return await aggregate.readRankAt(this.db, plan, offset);
4996
1467
  }
4997
1468
  async indexOf(args) {
4998
- const plan = await this._plan();
4999
- return await readRankIndexOf(this.db, plan, args);
1469
+ const { aggregate, plan } = await this._plan();
1470
+ return await aggregate.readRankIndexOf(this.db, plan, args);
5000
1471
  }
5001
1472
  async paginate(args) {
5002
1473
  if (!Number.isInteger(args.limit) || args.limit < 1) throw new Error("rank().paginate() requires a positive integer limit.");
5003
- const plan = await this._plan();
5004
- return await readRankPaginate(this.db, plan, args.cursor ?? null, args.limit);
1474
+ const { aggregate, plan } = await this._plan();
1475
+ return await aggregate.readRankPaginate(this.db, plan, args.cursor ?? null, args.limit);
5005
1476
  }
5006
1477
  async min() {
5007
- const plan = await this._plan();
5008
- return await readRankMin(this.db, plan);
1478
+ const { aggregate, plan } = await this._plan();
1479
+ return await aggregate.readRankMin(this.db, plan);
5009
1480
  }
5010
1481
  async max() {
5011
- const plan = await this._plan();
5012
- return await readRankMax(this.db, plan);
1482
+ const { aggregate, plan } = await this._plan();
1483
+ return await aggregate.readRankMax(this.db, plan);
5013
1484
  }
5014
1485
  async random() {
5015
- const plan = await this._plan();
5016
- return await readRankRandom(this.db, plan);
1486
+ const { aggregate, plan } = await this._plan();
1487
+ return await aggregate.readRankRandom(this.db, plan);
5017
1488
  }
5018
1489
  };
5019
1490
  const CONFIGURED_INDEX_RANGE_OPERATORS = new Set([
@@ -5096,6 +1567,16 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
5096
1567
  this.allowFullScan = config.allowFullScan === true;
5097
1568
  this._countIndexReadinessByKey = countIndexReadiness ?? /* @__PURE__ */ new Map();
5098
1569
  }
1570
+ /**
1571
+ * The aggregate-index runtime, registered at `createOrm()`.
1572
+ *
1573
+ * Only `count()`, `aggregate()` and relation counts reach this; plain reads
1574
+ * never touch the aggregate runtime, which is why it is injected rather than
1575
+ * statically imported.
1576
+ */
1577
+ _aggregate(usage) {
1578
+ return requireAggregateCapability(getOrmContext(this.db)?.capabilities, usage);
1579
+ }
5099
1580
  _usesSystemCreatedAtAlias(tableConfig = this.tableConfig) {
5100
1581
  return usesSystemCreatedAtAlias(tableConfig.table);
5101
1582
  }
@@ -6451,7 +2932,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6451
2932
  await existing;
6452
2933
  return;
6453
2934
  }
6454
- const pending = ensureCountIndexReady(this.db, tableName, indexName).catch((error) => {
2935
+ const pending = this._aggregate("count()").ensureCountIndexReady(this.db, tableName, indexName).catch((error) => {
6455
2936
  this._countIndexReadinessByKey.delete(key);
6456
2937
  throw error;
6457
2938
  });
@@ -6465,7 +2946,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6465
2946
  await existing;
6466
2947
  return;
6467
2948
  }
6468
- const pending = ensureAggregateIndexReady(this.db, tableName, indexName).catch((error) => {
2949
+ const pending = this._aggregate("aggregate()").ensureAggregateIndexReady(this.db, tableName, indexName).catch((error) => {
6469
2950
  this._aggregateIndexReadinessByKey.delete(key);
6470
2951
  throw error;
6471
2952
  });
@@ -6491,10 +2972,11 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6491
2972
  if (nativeCount !== null) return nativeCount;
6492
2973
  throw createCountError(COUNT_ERROR.FILTER_UNSUPPORTED, `Native count() syscall unavailable for '${this.tableConfig.name}'.`);
6493
2974
  }
6494
- const plan = compileCountQueryPlan(this.tableConfig, where);
6495
- if (isIndexCountZero(plan)) return 0;
2975
+ const aggregate = this._aggregate("A filtered count()");
2976
+ const plan = aggregate.compileCountQueryPlan(this.tableConfig, where);
2977
+ if (aggregate.isIndexCountZero(plan)) return 0;
6496
2978
  await this._ensureCountIndexReadyOnce(plan.tableName, plan.indexName);
6497
- return await readCountFromBuckets(this.db, plan, bucketCache);
2979
+ return await aggregate.readCountFromBuckets(this.db, plan, bucketCache);
6498
2980
  }
6499
2981
  async _executeCount(config) {
6500
2982
  const windowConfig = this._coerceCountWindowConfig(config);
@@ -6510,11 +2992,13 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6510
2992
  result._all = this._applyCountWindowBounds(total, windowConfig);
6511
2993
  }
6512
2994
  if (windowConfig.hasWindowBounds && select.fields.length > 0) throw createCountError(COUNT_ERROR.FILTER_UNSUPPORTED, "count({ select: { field: true } }) does not support skip/take/cursor in v1. Use count() or count({ select: { _all: true } }).");
2995
+ if (select.fields.length === 0) return result;
2996
+ const aggregate = this._aggregate("count({ select: { field: true } })");
6513
2997
  const fieldEntries = await Promise.all(select.fields.map(async (field) => {
6514
- const plan = compileCountFieldQueryPlan(this.tableConfig, normalizedWhere, field);
6515
- if (isAggregatePlanZero(plan)) return [field, 0];
2998
+ const plan = aggregate.compileCountFieldQueryPlan(this.tableConfig, normalizedWhere, field);
2999
+ if (aggregate.isAggregatePlanZero(plan)) return [field, 0];
6516
3000
  await this._ensureCountIndexReadyOnce(plan.tableName, plan.indexName);
6517
- return [field, await readCountFieldFromBuckets(this.db, plan)];
3001
+ return [field, await aggregate.readCountFieldFromBuckets(this.db, plan)];
6518
3002
  }));
6519
3003
  for (const [field, value] of fieldEntries) result[field] = value;
6520
3004
  return result;
@@ -6585,6 +3069,7 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6585
3069
  }
6586
3070
  async _executeAggregate(config) {
6587
3071
  const normalized = this._coerceAggregateConfig(config);
3072
+ const aggregate = () => this._aggregate("aggregate()");
6588
3073
  ensureAggregateAllowedForRls(this.tableConfig, this.rls?.mode, "aggregate()");
6589
3074
  const result = {};
6590
3075
  const tasks = [];
@@ -6611,16 +3096,16 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6611
3096
  }
6612
3097
  })());
6613
3098
  countTasks.push(...countSelection.fields.map(async (field) => {
6614
- const plan = compileAggregateQueryPlan(this.tableConfig, normalized.where, {
3099
+ const plan = aggregate().compileAggregateQueryPlan(this.tableConfig, normalized.where, {
6615
3100
  kind: "countField",
6616
3101
  field
6617
3102
  });
6618
- if (isAggregatePlanZero(plan)) {
3103
+ if (aggregate().isAggregatePlanZero(plan)) {
6619
3104
  countResult[field] = 0;
6620
3105
  return;
6621
3106
  }
6622
3107
  await this._ensureAggregateIndexReadyOnce(plan.tableName, plan.indexName);
6623
- countResult[field] = await readCountFieldFromBuckets(this.db, plan, bucketReadCache);
3108
+ countResult[field] = await aggregate().readCountFieldFromBuckets(this.db, plan, bucketReadCache);
6624
3109
  }));
6625
3110
  await Promise.all(countTasks);
6626
3111
  result._count = countResult;
@@ -6628,50 +3113,50 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
6628
3113
  }
6629
3114
  if (normalized.sumFields.length > 0) tasks.push((async () => {
6630
3115
  const sumEntries = await Promise.all(normalized.sumFields.map(async (field) => {
6631
- const plan = compileAggregateQueryPlan(this.tableConfig, normalized.where, {
3116
+ const plan = aggregate().compileAggregateQueryPlan(this.tableConfig, normalized.where, {
6632
3117
  kind: "sum",
6633
3118
  field
6634
3119
  });
6635
- if (isAggregatePlanZero(plan)) return [field, null];
3120
+ if (aggregate().isAggregatePlanZero(plan)) return [field, null];
6636
3121
  await this._ensureAggregateIndexReadyOnce(plan.tableName, plan.indexName);
6637
- return [field, await readSumFromBuckets(this.db, plan, bucketReadCache)];
3122
+ return [field, await aggregate().readSumFromBuckets(this.db, plan, bucketReadCache)];
6638
3123
  }));
6639
3124
  result._sum = Object.fromEntries(sumEntries);
6640
3125
  })());
6641
3126
  if (normalized.avgFields.length > 0) tasks.push((async () => {
6642
3127
  const avgEntries = await Promise.all(normalized.avgFields.map(async (field) => {
6643
- const plan = compileAggregateQueryPlan(this.tableConfig, normalized.where, {
3128
+ const plan = aggregate().compileAggregateQueryPlan(this.tableConfig, normalized.where, {
6644
3129
  kind: "avg",
6645
3130
  field
6646
3131
  });
6647
- if (isAggregatePlanZero(plan)) return [field, null];
3132
+ if (aggregate().isAggregatePlanZero(plan)) return [field, null];
6648
3133
  await this._ensureAggregateIndexReadyOnce(plan.tableName, plan.indexName);
6649
- return [field, await readAverageFromBuckets(this.db, plan, bucketReadCache)];
3134
+ return [field, await aggregate().readAverageFromBuckets(this.db, plan, bucketReadCache)];
6650
3135
  }));
6651
3136
  result._avg = Object.fromEntries(avgEntries);
6652
3137
  })());
6653
3138
  if (normalized.minFields.length > 0) tasks.push((async () => {
6654
3139
  const minEntries = await Promise.all(normalized.minFields.map(async (field) => {
6655
- const plan = compileAggregateQueryPlan(this.tableConfig, normalized.where, {
3140
+ const plan = aggregate().compileAggregateQueryPlan(this.tableConfig, normalized.where, {
6656
3141
  kind: "min",
6657
3142
  field
6658
3143
  });
6659
- if (isAggregatePlanZero(plan)) return [field, null];
3144
+ if (aggregate().isAggregatePlanZero(plan)) return [field, null];
6660
3145
  await this._ensureAggregateIndexReadyOnce(plan.tableName, plan.indexName);
6661
- const value = await readExtremaFromBuckets(this.db, plan, bucketReadCache);
3146
+ const value = await aggregate().readExtremaFromBuckets(this.db, plan, bucketReadCache);
6662
3147
  return [field, this._coerceAggregateReturnValue(field, value)];
6663
3148
  }));
6664
3149
  result._min = Object.fromEntries(minEntries);
6665
3150
  })());
6666
3151
  if (normalized.maxFields.length > 0) tasks.push((async () => {
6667
3152
  const maxEntries = await Promise.all(normalized.maxFields.map(async (field) => {
6668
- const plan = compileAggregateQueryPlan(this.tableConfig, normalized.where, {
3153
+ const plan = aggregate().compileAggregateQueryPlan(this.tableConfig, normalized.where, {
6669
3154
  kind: "max",
6670
3155
  field
6671
3156
  });
6672
- if (isAggregatePlanZero(plan)) return [field, null];
3157
+ if (aggregate().isAggregatePlanZero(plan)) return [field, null];
6673
3158
  await this._ensureAggregateIndexReadyOnce(plan.tableName, plan.indexName);
6674
- const value = await readExtremaFromBuckets(this.db, plan, bucketReadCache);
3159
+ const value = await aggregate().readExtremaFromBuckets(this.db, plan, bucketReadCache);
6675
3160
  return [field, this._coerceAggregateReturnValue(field, value)];
6676
3161
  }));
6677
3162
  result._max = Object.fromEntries(maxEntries);
@@ -8065,18 +4550,20 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
8065
4550
  return JSON.stringify(values);
8066
4551
  }
8067
4552
  async _readIndexedRelationCount(tableConfig, where, relationPath, bucketCache) {
4553
+ const aggregate = this._aggregate("_count on a relation");
8068
4554
  ensureCountAllowedForRls(tableConfig, this.rls?.mode);
8069
4555
  try {
8070
- const plan = compileCountQueryPlan(tableConfig, where);
8071
- if (isIndexCountZero(plan)) return 0;
4556
+ const plan = aggregate.compileCountQueryPlan(tableConfig, where);
4557
+ if (aggregate.isIndexCountZero(plan)) return 0;
8072
4558
  await this._ensureCountIndexReadyOnce(plan.tableName, plan.indexName);
8073
- return await readCountFromBuckets(this.db, plan, bucketCache);
4559
+ return await aggregate.readCountFromBuckets(this.db, plan, bucketCache);
8074
4560
  } catch (error) {
8075
4561
  throw this._remapRelationCountError(error, relationPath);
8076
4562
  }
8077
4563
  }
8078
4564
  async _countRelationForRow(row, relationName, edge, where, tableConfig, caches) {
8079
4565
  const relationPath = `${tableConfig.name}.${relationName}`;
4566
+ const aggregate = () => this._aggregate("_count on a relation");
8080
4567
  const bucketCache = caches?.buckets;
8081
4568
  if (edge.through) {
8082
4569
  const throughTableConfig = this._getTableConfigByDbName(edge.through.table);
@@ -8098,8 +4585,8 @@ var GelRelationalQuery = class GelRelationalQuery extends QueryPromise {
8098
4585
  ensureCountAllowedForRls(targetTableConfig, this.rls?.mode);
8099
4586
  const whereRecord = where;
8100
4587
  try {
8101
- const filterPlan = compileCountQueryPlan(targetTableConfig, whereRecord);
8102
- if (isIndexCountZero(filterPlan)) return 0;
4588
+ const filterPlan = aggregate().compileCountQueryPlan(targetTableConfig, whereRecord);
4589
+ if (aggregate().isIndexCountZero(filterPlan)) return 0;
8103
4590
  await this._ensureCountIndexReadyOnce(filterPlan.tableName, filterPlan.indexName);
8104
4591
  } catch (error) {
8105
4592
  throw this._remapRelationCountError(error, relationPath);
@@ -9672,7 +6159,7 @@ const createNoopLifecycle = () => ({
9672
6159
  enabled: false,
9673
6160
  wrapDB: (ctx) => ctx
9674
6161
  });
9675
- function createOrmDbLifecycle(schema, triggerDefinitions) {
6162
+ function createOrmDbLifecycle(schema, triggerDefinitions, capabilities) {
9676
6163
  const tableNameBySchemaKey = /* @__PURE__ */ new Map();
9677
6164
  const tableNames = /* @__PURE__ */ new Set();
9678
6165
  for (const [schemaKey, tableConfig] of Object.entries(schema)) {
@@ -9693,12 +6180,13 @@ function createOrmDbLifecycle(schema, triggerDefinitions) {
9693
6180
  const aggregateIndexes = getAggregateIndexDefinitions(tableConfig);
9694
6181
  const rankIndexes = getRankIndexDefinitions(tableConfig);
9695
6182
  if (aggregateIndexes.length === 0 && rankIndexes.length === 0) continue;
6183
+ const aggregate = requireAggregateCapability(capabilities, `Table '${tableConfig.name}' declares an aggregateIndex/rankIndex, which`);
9696
6184
  const existing = tableHooks.get(tableConfig.name) ?? {};
9697
6185
  const existingChange = existing.change;
9698
6186
  const metricIndexNames = aggregateIndexes.map((entry) => entry.name);
9699
6187
  const rankIndexNames = rankIndexes.map((entry) => entry.name);
9700
6188
  const prependWriteBarrier = (before) => async (data, ctx) => {
9701
- await assertAggregateIndexesWritable(ctx.db, tableConfig.name, metricIndexNames, rankIndexNames);
6189
+ await aggregate.assertAggregateIndexesWritable(ctx.db, tableConfig.name, metricIndexNames, rankIndexNames);
9702
6190
  return before?.(data, ctx);
9703
6191
  };
9704
6192
  tableHooks.set(tableConfig.name, {
@@ -9717,21 +6205,21 @@ function createOrmDbLifecycle(schema, triggerDefinitions) {
9717
6205
  },
9718
6206
  change: async (change, ctx) => {
9719
6207
  if (change.operation === "delete") {
9720
- if (aggregateIndexes.length > 0) await applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
6208
+ if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
9721
6209
  operation: "delete",
9722
6210
  id: change.id
9723
6211
  });
9724
- if (rankIndexes.length > 0) await applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
6212
+ if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
9725
6213
  operation: "delete",
9726
6214
  id: change.id
9727
6215
  });
9728
6216
  } else {
9729
- if (aggregateIndexes.length > 0) await applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
6217
+ if (aggregateIndexes.length > 0) await aggregate.applyAggregateIndexesForChange(ctx.db, tableConfig.name, aggregateIndexes, {
9730
6218
  operation: change.operation,
9731
6219
  id: change.id,
9732
6220
  newDoc: change.newDoc
9733
6221
  });
9734
- if (rankIndexes.length > 0) await applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
6222
+ if (rankIndexes.length > 0) await aggregate.applyRankIndexesForChange(ctx.db, tableConfig.name, rankIndexes, {
9735
6223
  operation: change.operation,
9736
6224
  id: change.id,
9737
6225
  newDoc: change.newDoc
@@ -10310,6 +6798,7 @@ function createDatabase(db, schema, edgeMetadata, options) {
10310
6798
  scheduledMutationBatch: options?.scheduledMutationBatch
10311
6799
  });
10312
6800
  const ormContext = {
6801
+ capabilities: options?.capabilities,
10313
6802
  foreignKeyGraph: getForeignKeyGraph(schema),
10314
6803
  schema,
10315
6804
  edgeMetadata,
@@ -10506,665 +6995,6 @@ function isOne(relation) {
10506
6995
  return relation.relationType === "one";
10507
6996
  }
10508
6997
 
10509
- //#endregion
10510
- //#region src/orm/migrations/definitions.ts
10511
- const MIGRATION_ID_RE = /^[a-zA-Z0-9_:-]+$/;
10512
- const FUNCTION_SOURCE_WHITESPACE_RE = /\s+/g;
10513
- const FUNCTION_SOURCE_PUNCTUATION_SPACE_RE = /\s*([{}();,:])\s*/g;
10514
- function defineMigration(migration) {
10515
- validateMigrationId(migration.id);
10516
- validateMigrationStep("up", migration.up);
10517
- if (migration.down) validateMigrationStep("down", migration.down);
10518
- return migration;
10519
- }
10520
- function defineMigrationSet(migrations) {
10521
- const normalized = [...migrations].map((migration) => {
10522
- const defined = defineMigration(migration);
10523
- return {
10524
- ...defined,
10525
- checksum: defined.checksum ?? computeMigrationChecksum(defined)
10526
- };
10527
- });
10528
- normalized.sort((a, b) => a.id.localeCompare(b.id));
10529
- const byId = {};
10530
- for (const migration of normalized) {
10531
- if (byId[migration.id]) throw new Error(`defineMigrationSet received duplicate migration id '${migration.id}'.`);
10532
- byId[migration.id] = migration;
10533
- }
10534
- return {
10535
- migrations: normalized,
10536
- ids: normalized.map((migration) => migration.id),
10537
- byId
10538
- };
10539
- }
10540
- function detectMigrationDrift(params) {
10541
- const { migrationSet, appliedState } = params;
10542
- const issues = [];
10543
- for (const [migrationId, state] of Object.entries(appliedState)) {
10544
- if (!state.applied) continue;
10545
- const migration = migrationSet.byId[migrationId];
10546
- if (!migration) {
10547
- issues.push({
10548
- kind: "missing_from_manifest",
10549
- migrationId,
10550
- message: `Applied migration '${migrationId}' is missing from the current migration manifest.`
10551
- });
10552
- continue;
10553
- }
10554
- if (state.checksum && state.checksum !== migration.checksum) issues.push({
10555
- kind: "checksum_mismatch",
10556
- migrationId,
10557
- expectedChecksum: migration.checksum,
10558
- actualChecksum: state.checksum,
10559
- message: `Applied migration '${migrationId}' checksum drift detected.`
10560
- });
10561
- }
10562
- return issues;
10563
- }
10564
- function buildMigrationPlan(params) {
10565
- const { direction, migrationSet, appliedState, steps, to } = params;
10566
- if (direction === "up") return {
10567
- direction,
10568
- migrations: migrationSet.migrations.filter((migration) => !appliedState[migration.id]?.applied)
10569
- };
10570
- if (steps !== void 0 && to !== void 0) throw new Error("Use either down steps or down to, not both.");
10571
- const appliedInOrder = migrationSet.migrations.filter((migration) => appliedState[migration.id]?.applied);
10572
- const ensureDownSteps = (selected) => {
10573
- const missingDown = selected.find((migration) => !migration.down);
10574
- if (missingDown) throw new Error(`Cannot execute down migration for '${missingDown.id}': missing down migration handler.`);
10575
- return selected;
10576
- };
10577
- if (to) {
10578
- const targetIndex = migrationSet.ids.indexOf(to);
10579
- if (targetIndex === -1) throw new Error(`Unknown migration id '${to}' for down --to.`);
10580
- return {
10581
- direction,
10582
- migrations: ensureDownSteps(appliedInOrder.filter((migration) => {
10583
- return migrationSet.ids.indexOf(migration.id) > targetIndex;
10584
- })).reverse()
10585
- };
10586
- }
10587
- const resolvedSteps = steps ?? 1;
10588
- if (!Number.isInteger(resolvedSteps) || resolvedSteps < 1) throw new Error("Down steps must be a positive integer.");
10589
- return {
10590
- direction,
10591
- migrations: ensureDownSteps(appliedInOrder.slice(-resolvedSteps).reverse())
10592
- };
10593
- }
10594
- function validateMigrationId(id) {
10595
- if (!id || typeof id !== "string") throw new Error("Migration id must be a non-empty string.");
10596
- if (!MIGRATION_ID_RE.test(id)) throw new Error(`Migration id '${id}' is invalid. Use alphanumeric characters, '_' ':' or '-'.`);
10597
- }
10598
- function validateMigrationStep(direction, step) {
10599
- if (!step || typeof step !== "object") throw new Error(`Migration ${direction} step must be an object.`);
10600
- if (!step.table || typeof step.table !== "string") throw new Error(`Migration ${direction} step.table must be a string.`);
10601
- if (typeof step.migrateOne !== "function") throw new Error(`Migration ${direction} step.migrateOne must be a function.`);
10602
- if (step.batchSize !== void 0 && (!Number.isInteger(step.batchSize) || step.batchSize < 1)) throw new Error(`Migration ${direction} step.batchSize must be a positive integer.`);
10603
- if (step.writeMode !== void 0 && step.writeMode !== "safe_bypass" && step.writeMode !== "normal") throw new Error(`Migration ${direction} step.writeMode must be 'safe_bypass' or 'normal'.`);
10604
- }
10605
- function computeMigrationChecksum(migration) {
10606
- return simpleStableHash(JSON.stringify({
10607
- id: migration.id,
10608
- name: migration.name ?? null,
10609
- description: migration.description ?? null,
10610
- up: serializeStep(migration.up),
10611
- down: migration.down ? serializeStep(migration.down) : null
10612
- }));
10613
- }
10614
- function serializeStep(step) {
10615
- return {
10616
- table: step.table,
10617
- batchSize: step.batchSize ?? null,
10618
- writeMode: step.writeMode ?? "safe_bypass",
10619
- source: normalizeFunctionSource(step.migrateOne)
10620
- };
10621
- }
10622
- function normalizeFunctionSource(fn) {
10623
- return fn.toString().replace(FUNCTION_SOURCE_WHITESPACE_RE, " ").replace(FUNCTION_SOURCE_PUNCTUATION_SPACE_RE, "$1").trim();
10624
- }
10625
- function simpleStableHash(value) {
10626
- let hashA = 2166136261;
10627
- let hashB = 16777619;
10628
- for (let i = 0; i < value.length; i += 1) {
10629
- const code = value.charCodeAt(i);
10630
- hashA ^= code;
10631
- hashA = Math.imul(hashA, 16777619);
10632
- hashB ^= code + i;
10633
- hashB = Math.imul(hashB, 2246822507);
10634
- }
10635
- return `m_${(hashA >>> 0).toString(16).padStart(8, "0")}${(hashB >>> 0).toString(16).padStart(8, "0")}`;
10636
- }
10637
-
10638
- //#endregion
10639
- //#region src/orm/migrations/schema.ts
10640
- const MIGRATION_STATE_TABLE = "migration_state";
10641
- const MIGRATION_RUN_TABLE = "migration_run";
10642
- const migrationStateTable = convexTable(MIGRATION_STATE_TABLE, {
10643
- migrationId: text().notNull(),
10644
- checksum: text().notNull(),
10645
- applied: boolean().notNull(),
10646
- status: text().notNull(),
10647
- direction: text(),
10648
- runId: text(),
10649
- cursor: text(),
10650
- processed: integer().notNull(),
10651
- startedAt: integer(),
10652
- updatedAt: integer().notNull(),
10653
- completedAt: integer(),
10654
- lastError: text(),
10655
- writeMode: text().notNull()
10656
- }, (t) => [index("by_migration_id").on(t.migrationId), index("by_status").on(t.status)]);
10657
- const migrationRunTable = convexTable(MIGRATION_RUN_TABLE, {
10658
- runId: text().notNull(),
10659
- direction: text().notNull(),
10660
- status: text().notNull(),
10661
- dryRun: boolean().notNull(),
10662
- allowDrift: boolean().notNull(),
10663
- migrationIds: custom(v.array(v.string())).notNull(),
10664
- currentIndex: integer().notNull(),
10665
- startedAt: integer().notNull(),
10666
- updatedAt: integer().notNull(),
10667
- completedAt: integer(),
10668
- cancelRequested: boolean().notNull(),
10669
- lastError: text()
10670
- }, (t) => [index("by_run_id").on(t.runId), index("by_status").on(t.status)]);
10671
- const migrationStorageTables = {
10672
- [MIGRATION_STATE_TABLE]: migrationStateTable,
10673
- [MIGRATION_RUN_TABLE]: migrationRunTable
10674
- };
10675
- function migrationExtension() {
10676
- return defineSchemaExtension("migration", migrationStorageTables);
10677
- }
10678
-
10679
- //#endregion
10680
- //#region src/orm/migrations/runtime.ts
10681
- const DEFAULT_BATCH_SIZE = 128;
10682
- function createMigrationHandlers(params) {
10683
- const { schema, migrations, getOrm, getChunkRef } = params;
10684
- const knownTables = new Set(Object.values(schema).map((tableConfig) => tableConfig.name));
10685
- const run = async (ctx, args = {}) => {
10686
- if (!migrations || migrations.migrations.length === 0) return {
10687
- status: "noop",
10688
- reason: "no_migrations_registered"
10689
- };
10690
- const direction = parseDirection(args.direction);
10691
- const dryRun = args.dryRun === true;
10692
- const allowDrift = args.allowDrift === true;
10693
- const restart = args.restart === true;
10694
- const batchSize = parseOptionalPositiveInteger(args.batchSize, "batchSize");
10695
- const steps = parseOptionalPositiveInteger(args.steps, "steps");
10696
- const to = parseOptionalString(args.to, "to");
10697
- const stateRows = await getAllStateRows(ctx.db);
10698
- const appliedState = toAppliedStateMap(stateRows);
10699
- const drift = detectMigrationDrift({
10700
- migrationSet: migrations,
10701
- appliedState
10702
- });
10703
- if (drift.length > 0 && !allowDrift) return {
10704
- status: "drift_blocked",
10705
- direction,
10706
- drift
10707
- };
10708
- const plan = buildMigrationPlan({
10709
- direction,
10710
- migrationSet: migrations,
10711
- appliedState,
10712
- steps,
10713
- to
10714
- });
10715
- if (plan.migrations.length === 0) return {
10716
- status: "noop",
10717
- direction,
10718
- drift,
10719
- plan: []
10720
- };
10721
- for (const migration of plan.migrations) {
10722
- const step = getStepForDirection(migration, direction);
10723
- if (!knownTables.has(step.table)) throw new Error(`Migration '${migration.id}' references unknown table '${step.table}'.`);
10724
- }
10725
- if (dryRun) return {
10726
- status: "dry_run",
10727
- direction,
10728
- drift,
10729
- plan: plan.migrations.map((migration) => migration.id)
10730
- };
10731
- const activeRun = await getActiveRun(ctx.db);
10732
- if (activeRun) return {
10733
- status: "running",
10734
- runId: activeRun.runId
10735
- };
10736
- const now = Date.now();
10737
- const runId = createRunId(now);
10738
- await ctx.db.insert(MIGRATION_RUN_TABLE, {
10739
- runId,
10740
- direction,
10741
- status: "running",
10742
- dryRun: false,
10743
- allowDrift,
10744
- migrationIds: plan.migrations.map((migration) => migration.id),
10745
- currentIndex: 0,
10746
- startedAt: now,
10747
- updatedAt: now,
10748
- cancelRequested: false
10749
- });
10750
- const stateById = new Map(stateRows.map((row) => [row.migrationId, row]));
10751
- for (const migration of plan.migrations) {
10752
- const writeMode = getStepForDirection(migration, direction).writeMode ?? "safe_bypass";
10753
- const existing = stateById.get(migration.id);
10754
- const resetProgress = restart || existing?.direction !== direction;
10755
- if (existing) await ctx.db.patch(existing._id, cleanUndefined({
10756
- checksum: migration.checksum,
10757
- status: "pending",
10758
- direction,
10759
- runId,
10760
- cursor: resetProgress ? null : existing.cursor ?? null,
10761
- processed: resetProgress ? 0 : existing.processed ?? 0,
10762
- startedAt: now,
10763
- updatedAt: now,
10764
- completedAt: null,
10765
- lastError: null,
10766
- writeMode
10767
- }));
10768
- else await ctx.db.insert(MIGRATION_STATE_TABLE, {
10769
- migrationId: migration.id,
10770
- checksum: migration.checksum,
10771
- applied: direction === "down",
10772
- status: "pending",
10773
- direction,
10774
- runId,
10775
- cursor: null,
10776
- processed: 0,
10777
- startedAt: now,
10778
- updatedAt: now,
10779
- completedAt: null,
10780
- lastError: null,
10781
- writeMode
10782
- });
10783
- }
10784
- if (ctx.scheduler) {
10785
- const chunkRef = getChunkRef();
10786
- if (chunkRef) {
10787
- const chunkArgs = { runId };
10788
- if (batchSize !== void 0) chunkArgs.batchSize = batchSize;
10789
- await ctx.scheduler.runAfter(0, chunkRef, chunkArgs);
10790
- return {
10791
- status: "running",
10792
- direction,
10793
- runId,
10794
- plan: plan.migrations.map((migration) => migration.id)
10795
- };
10796
- }
10797
- }
10798
- const inlineResult = await chunk(ctx, {
10799
- runId,
10800
- batchSize
10801
- });
10802
- return {
10803
- status: "running",
10804
- direction,
10805
- runId,
10806
- plan: plan.migrations.map((migration) => migration.id),
10807
- inlineResult
10808
- };
10809
- };
10810
- const chunk = async (ctx, args) => {
10811
- const runId = parseRequiredString(args.runId, "runId");
10812
- const batchSize = parseOptionalPositiveInteger(args.batchSize, "batchSize");
10813
- const runRow = await getRunById(ctx.db, runId);
10814
- if (!runRow) return {
10815
- status: "missing",
10816
- runId
10817
- };
10818
- if (runRow.status !== "running") return {
10819
- status: runRow.status,
10820
- runId
10821
- };
10822
- if (runRow.cancelRequested) {
10823
- await markRunCanceled(ctx.db, runRow);
10824
- return {
10825
- status: "canceled",
10826
- runId
10827
- };
10828
- }
10829
- const migrationId = runRow.migrationIds[runRow.currentIndex];
10830
- if (!migrationId) {
10831
- await markRunCompleted(ctx.db, runRow);
10832
- return {
10833
- status: "completed",
10834
- runId
10835
- };
10836
- }
10837
- const migration = migrations?.byId[migrationId];
10838
- if (!migration) {
10839
- await markRunFailed(ctx.db, runRow, `Migration '${migrationId}' is missing from registry.`);
10840
- return {
10841
- status: "failed",
10842
- runId
10843
- };
10844
- }
10845
- const direction = parseDirection(runRow.direction);
10846
- const step = getStepForDirection(migration, direction);
10847
- const resolvedBatchSize = batchSize ?? step.batchSize ?? DEFAULT_BATCH_SIZE;
10848
- const stateRow = await getOrCreateStateRow(ctx.db, migration, direction, step);
10849
- const cursor = stateRow.cursor ?? null;
10850
- try {
10851
- const page = await ctx.db.query(step.table).paginate({
10852
- cursor,
10853
- numItems: resolvedBatchSize
10854
- });
10855
- const docs = Array.isArray(page?.page) ? page.page : [];
10856
- const orm = getOrm(ctx);
10857
- const writeMode = step.writeMode ?? "safe_bypass";
10858
- let processedInBatch = 0;
10859
- for (const doc of docs) {
10860
- const migrationCtxBase = {
10861
- db: ctx.db,
10862
- migrationId,
10863
- runId,
10864
- direction,
10865
- dryRun: false,
10866
- writeMode
10867
- };
10868
- const result = await runWithWriteMode(orm, writeMode, (resolvedOrm) => step.migrateOne({
10869
- ...migrationCtxBase,
10870
- orm: resolvedOrm
10871
- }, doc));
10872
- if (isPatchPayload(result) && hasDocId(doc)) await ctx.db.patch(doc._id, result);
10873
- processedInBatch += 1;
10874
- }
10875
- const now = Date.now();
10876
- const isDone = Boolean(page?.isDone);
10877
- const nextCursor = isDone ? null : page?.continueCursor ?? null;
10878
- const nextProcessed = (stateRow.processed ?? 0) + processedInBatch;
10879
- if (isDone) {
10880
- await ctx.db.patch(stateRow._id, cleanUndefined({
10881
- status: "completed",
10882
- applied: direction === "up",
10883
- cursor: null,
10884
- processed: nextProcessed,
10885
- completedAt: now,
10886
- updatedAt: now,
10887
- lastError: null,
10888
- runId,
10889
- direction,
10890
- writeMode
10891
- }));
10892
- const nextIndex = runRow.currentIndex + 1;
10893
- const done = nextIndex >= runRow.migrationIds.length;
10894
- await ctx.db.patch(runRow._id, cleanUndefined({
10895
- currentIndex: nextIndex,
10896
- status: done ? "completed" : "running",
10897
- updatedAt: now,
10898
- completedAt: done ? now : null,
10899
- lastError: null
10900
- }));
10901
- if (!done && ctx.scheduler) {
10902
- const chunkRef = getChunkRef();
10903
- if (chunkRef) await ctx.scheduler.runAfter(0, chunkRef, {
10904
- runId,
10905
- batchSize: resolvedBatchSize
10906
- });
10907
- }
10908
- return {
10909
- status: done ? "completed" : "running",
10910
- runId,
10911
- migrationId,
10912
- processedInBatch,
10913
- processed: nextProcessed,
10914
- currentIndex: nextIndex,
10915
- total: runRow.migrationIds.length
10916
- };
10917
- }
10918
- await ctx.db.patch(stateRow._id, cleanUndefined({
10919
- status: "running",
10920
- cursor: nextCursor,
10921
- processed: nextProcessed,
10922
- updatedAt: now,
10923
- runId,
10924
- direction,
10925
- writeMode
10926
- }));
10927
- await ctx.db.patch(runRow._id, cleanUndefined({
10928
- status: "running",
10929
- updatedAt: now
10930
- }));
10931
- if (ctx.scheduler) {
10932
- const chunkRef = getChunkRef();
10933
- if (chunkRef) await ctx.scheduler.runAfter(0, chunkRef, {
10934
- runId,
10935
- batchSize: resolvedBatchSize
10936
- });
10937
- }
10938
- return {
10939
- status: "running",
10940
- runId,
10941
- migrationId,
10942
- processedInBatch,
10943
- processed: nextProcessed,
10944
- cursor: nextCursor
10945
- };
10946
- } catch (error) {
10947
- const message = error.message || String(error);
10948
- await ctx.db.patch(stateRow._id, cleanUndefined({
10949
- status: "failed",
10950
- lastError: message,
10951
- updatedAt: Date.now()
10952
- }));
10953
- await markRunFailed(ctx.db, runRow, message);
10954
- return {
10955
- status: "failed",
10956
- runId,
10957
- migrationId,
10958
- error: message
10959
- };
10960
- }
10961
- };
10962
- const status = async (ctx, args = {}) => {
10963
- if (!migrations) return {
10964
- status: "noop",
10965
- reason: "no_migrations_registered"
10966
- };
10967
- const limit = parseOptionalPositiveInteger(args.limit, "limit") ?? 25;
10968
- const runId = parseOptionalString(args.runId, "runId");
10969
- const stateRows = await getAllStateRows(ctx.db);
10970
- const sortedRuns = [...await getAllRunRows(ctx.db)].sort((left, right) => right.startedAt - left.startedAt);
10971
- const selectedRuns = runId ? sortedRuns.filter((entry) => entry.runId === runId).slice(0, 1) : sortedRuns.slice(0, limit);
10972
- const activeRun = sortedRuns.find((entry) => entry.status === "running") ?? null;
10973
- const appliedState = toAppliedStateMap(stateRows);
10974
- const drift = detectMigrationDrift({
10975
- migrationSet: migrations,
10976
- appliedState
10977
- });
10978
- const pendingUp = buildMigrationPlan({
10979
- direction: "up",
10980
- migrationSet: migrations,
10981
- appliedState
10982
- });
10983
- return {
10984
- status: activeRun ? "running" : "idle",
10985
- activeRun,
10986
- runs: selectedRuns,
10987
- migrations: stateRows.map((row) => ({
10988
- migrationId: row.migrationId,
10989
- checksum: row.checksum,
10990
- applied: row.applied,
10991
- status: row.status,
10992
- direction: row.direction ?? null,
10993
- runId: row.runId ?? null,
10994
- cursor: row.cursor ?? null,
10995
- processed: row.processed,
10996
- updatedAt: row.updatedAt,
10997
- startedAt: row.startedAt ?? null,
10998
- completedAt: row.completedAt ?? null,
10999
- lastError: row.lastError ?? null,
11000
- writeMode: row.writeMode
11001
- })),
11002
- pending: pendingUp.migrations.map((migration) => migration.id),
11003
- drift
11004
- };
11005
- };
11006
- const cancel = async (ctx, args = {}) => {
11007
- if (!migrations) return {
11008
- status: "noop",
11009
- reason: "no_migrations_registered"
11010
- };
11011
- const runId = parseOptionalString(args.runId, "runId");
11012
- const runRow = runId ? await getRunById(ctx.db, runId) : await getActiveRun(ctx.db);
11013
- if (!runRow) return {
11014
- status: "noop",
11015
- reason: "no_active_run"
11016
- };
11017
- if (runRow.status !== "running") return {
11018
- status: "noop",
11019
- reason: "run_not_running",
11020
- runId: runRow.runId,
11021
- runStatus: runRow.status
11022
- };
11023
- const now = Date.now();
11024
- await ctx.db.patch(runRow._id, cleanUndefined({
11025
- cancelRequested: true,
11026
- updatedAt: now
11027
- }));
11028
- if (!(ctx.scheduler ? getChunkRef() : void 0)) {
11029
- await markRunCanceled(ctx.db, runRow);
11030
- return {
11031
- status: "canceled",
11032
- runId: runRow.runId
11033
- };
11034
- }
11035
- return {
11036
- status: "cancel_requested",
11037
- runId: runRow.runId
11038
- };
11039
- };
11040
- return {
11041
- run,
11042
- chunk,
11043
- status,
11044
- cancel
11045
- };
11046
- }
11047
- function getStepForDirection(migration, direction) {
11048
- if (direction === "up") return migration.up;
11049
- if (!migration.down) throw new Error(`Migration '${migration.id}' is missing down migration.`);
11050
- return migration.down;
11051
- }
11052
- function createRunId(now) {
11053
- return `mr_${now}_${Math.random().toString(36).slice(2, 10)}`;
11054
- }
11055
- function parseDirection(value) {
11056
- if (value === void 0 || value === null) return "up";
11057
- if (value === "up" || value === "down") return value;
11058
- throw new Error("Migration direction must be either 'up' or 'down'.");
11059
- }
11060
- function parseOptionalPositiveInteger(value, fieldName) {
11061
- if (value === void 0 || value === null) return;
11062
- if (typeof value !== "number" || !Number.isInteger(value) || value < 1) throw new Error(`Migration ${fieldName} must be a positive integer.`);
11063
- return value;
11064
- }
11065
- function parseOptionalString(value, fieldName) {
11066
- if (value === void 0 || value === null) return;
11067
- if (typeof value !== "string" || value.length === 0) throw new Error(`Migration ${fieldName} must be a non-empty string.`);
11068
- return value;
11069
- }
11070
- function parseRequiredString(value, fieldName) {
11071
- const parsed = parseOptionalString(value, fieldName);
11072
- if (!parsed) throw new Error(`Migration ${fieldName} is required.`);
11073
- return parsed;
11074
- }
11075
- function toAppliedStateMap(stateRows) {
11076
- const entries = {};
11077
- for (const row of stateRows) entries[row.migrationId] = {
11078
- applied: row.applied,
11079
- checksum: row.checksum,
11080
- cursor: row.cursor ?? null,
11081
- processed: row.processed
11082
- };
11083
- return entries;
11084
- }
11085
- async function getAllStateRows(db) {
11086
- return await db.query(MIGRATION_STATE_TABLE).collect();
11087
- }
11088
- async function getAllRunRows(db) {
11089
- return await db.query(MIGRATION_RUN_TABLE).collect();
11090
- }
11091
- async function getRunById(db, runId) {
11092
- return await db.query(MIGRATION_RUN_TABLE).withIndex("by_run_id", (query) => query.eq("runId", runId)).first() ?? null;
11093
- }
11094
- async function getActiveRun(db) {
11095
- return await db.query(MIGRATION_RUN_TABLE).withIndex("by_status", (query) => query.eq("status", "running")).first() ?? null;
11096
- }
11097
- async function getOrCreateStateRow(db, migration, direction, step) {
11098
- const existing = await db.query(MIGRATION_STATE_TABLE).withIndex("by_migration_id", (query) => query.eq("migrationId", migration.id)).first();
11099
- if (existing) return existing;
11100
- const now = Date.now();
11101
- const stateId = await db.insert(MIGRATION_STATE_TABLE, {
11102
- migrationId: migration.id,
11103
- checksum: migration.checksum,
11104
- applied: direction === "down",
11105
- status: "pending",
11106
- direction,
11107
- runId: null,
11108
- cursor: null,
11109
- processed: 0,
11110
- startedAt: now,
11111
- updatedAt: now,
11112
- completedAt: null,
11113
- lastError: null,
11114
- writeMode: step.writeMode ?? "safe_bypass"
11115
- });
11116
- const created = await db.get(stateId);
11117
- if (!created) throw new Error(`Failed to create migration state row for '${migration.id}'.`);
11118
- return created;
11119
- }
11120
- async function markRunCompleted(db, runRow) {
11121
- const now = Date.now();
11122
- await db.patch(runRow._id, cleanUndefined({
11123
- status: "completed",
11124
- updatedAt: now,
11125
- completedAt: now,
11126
- cancelRequested: false,
11127
- lastError: null
11128
- }));
11129
- }
11130
- async function markRunCanceled(db, runRow) {
11131
- const now = Date.now();
11132
- await db.patch(runRow._id, cleanUndefined({
11133
- status: "canceled",
11134
- updatedAt: now,
11135
- completedAt: now,
11136
- cancelRequested: true
11137
- }));
11138
- for (const stateRow of await getAllStateRows(db)) if (stateRow.runId === runRow.runId && (stateRow.status === "running" || stateRow.status === "pending")) await db.patch(stateRow._id, cleanUndefined({
11139
- status: "canceled",
11140
- updatedAt: now
11141
- }));
11142
- }
11143
- async function markRunFailed(db, runRow, message) {
11144
- const now = Date.now();
11145
- await db.patch(runRow._id, cleanUndefined({
11146
- status: "failed",
11147
- updatedAt: now,
11148
- completedAt: now,
11149
- lastError: message
11150
- }));
11151
- }
11152
- async function runWithWriteMode(orm, writeMode, callback) {
11153
- if (writeMode === "normal") return await callback(orm);
11154
- return await orm.skipRules.withoutTriggers(async (noTriggersOrm) => {
11155
- return await callback(noTriggersOrm);
11156
- });
11157
- }
11158
- function isPatchPayload(value) {
11159
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
11160
- }
11161
- function hasDocId(doc) {
11162
- return "_id" in doc && doc._id !== void 0 && doc._id !== null;
11163
- }
11164
- function cleanUndefined(value) {
11165
- return Object.fromEntries(Object.entries(value).filter(([, candidate]) => candidate !== void 0));
11166
- }
11167
-
11168
6998
  //#endregion
11169
6999
  //#region src/orm/relations.ts
11170
7000
  var RelationsBuilderTable = class {
@@ -11950,7 +7780,7 @@ function resolveOrmSchemaConfig(schemaInput) {
11950
7780
  triggers: getSchemaTriggers(schemaInput)
11951
7781
  };
11952
7782
  }
11953
- function createDbFactory(schema, edgeMetadata, dbLifecycle, ormFunctions) {
7783
+ function createDbFactory(schema, edgeMetadata, dbLifecycle, capabilities, ormFunctions) {
11954
7784
  return ((source, options) => {
11955
7785
  const ctxSource = isOrmCtx(source) ? source : void 0;
11956
7786
  const rawDb = ctxSource ? ctxSource.db : source;
@@ -11962,6 +7792,7 @@ function createDbFactory(schema, edgeMetadata, dbLifecycle, ormFunctions) {
11962
7792
  const wrappedCtx = dbLifecycle.wrapDB(lifecycleSource);
11963
7793
  const orm = createDatabase(wrappedCtx.db, schema, edgeMetadata, {
11964
7794
  ...options,
7795
+ capabilities,
11965
7796
  scheduler,
11966
7797
  vectorSearch,
11967
7798
  scheduledDelete,
@@ -11974,9 +7805,10 @@ function createDbFactory(schema, edgeMetadata, dbLifecycle, ormFunctions) {
11974
7805
  }
11975
7806
  function createOrm(config) {
11976
7807
  const { schema: resolvedSchema, triggers } = resolveOrmSchemaConfig(config.schema);
11977
- const dbLifecycle = createOrmDbLifecycle(resolvedSchema, triggers);
7808
+ const capabilities = resolveOrmCapabilities(config.capabilities);
7809
+ const dbLifecycle = createOrmDbLifecycle(resolvedSchema, triggers, capabilities);
11978
7810
  const edgeMetadata = extractRelationsConfig(resolvedSchema);
11979
- const db = createDbFactory(resolvedSchema, edgeMetadata, dbLifecycle, config.ormFunctions);
7811
+ const db = createDbFactory(resolvedSchema, edgeMetadata, dbLifecycle, capabilities, config.ormFunctions);
11980
7812
  const withContext = (ctx, options) => {
11981
7813
  const lifecycleCtx = { ...ctx };
11982
7814
  const wrappedCtx = dbLifecycle.wrapDB(lifecycleCtx);
@@ -11999,8 +7831,10 @@ function createOrm(config) {
11999
7831
  let aggregateBackfillChunkRef = config.ormFunctions.aggregateBackfillChunk;
12000
7832
  let migrationRunChunkRef = config.ormFunctions.migrationRunChunk;
12001
7833
  let resetChunkRef = config.ormFunctions.resetChunk;
12002
- const countBackfillHandlers = createCountBackfillHandlers(resolvedSchema, () => aggregateBackfillChunkRef);
12003
- const migrationHandlers = createMigrationHandlers({
7834
+ let countBackfillHandlersCache;
7835
+ const countBackfillHandlers = () => countBackfillHandlersCache ??= requireAggregateCapability(capabilities, "orm.api() aggregate procedures").createCountBackfillHandlers(resolvedSchema, () => aggregateBackfillChunkRef);
7836
+ let migrationHandlersCache;
7837
+ const migrationHandlers = () => migrationHandlersCache ??= requireMigrationCapability(capabilities, "orm.api() migration procedures").createMigrationHandlers({
12004
7838
  schema: resolvedSchema,
12005
7839
  migrations: config.migrations,
12006
7840
  getOrm: (ctx) => db(ctx),
@@ -12008,12 +7842,12 @@ function createOrm(config) {
12008
7842
  });
12009
7843
  const aggregateBackfillChunk = mutationBuilder({
12010
7844
  args: v.any(),
12011
- handler: countBackfillHandlers.chunk
7845
+ handler: ((ctx, args) => countBackfillHandlers().chunk(ctx, args))
12012
7846
  });
12013
7847
  if (!aggregateBackfillChunkRef) aggregateBackfillChunkRef = aggregateBackfillChunk;
12014
7848
  const migrationRunChunk = mutationBuilder({
12015
7849
  args: v.any(),
12016
- handler: migrationHandlers.chunk
7850
+ handler: ((ctx, args) => migrationHandlers().chunk(ctx, args))
12017
7851
  });
12018
7852
  if (!migrationRunChunkRef) migrationRunChunkRef = migrationRunChunk;
12019
7853
  const resetChunk = mutationBuilder({
@@ -12052,25 +7886,25 @@ function createOrm(config) {
12052
7886
  }),
12053
7887
  aggregateBackfill: mutationBuilder({
12054
7888
  args: v.any(),
12055
- handler: countBackfillHandlers.kickoff
7889
+ handler: ((ctx, args) => countBackfillHandlers().kickoff(ctx, args))
12056
7890
  }),
12057
7891
  aggregateBackfillChunk,
12058
7892
  aggregateBackfillStatus: mutationBuilder({
12059
7893
  args: v.any(),
12060
- handler: countBackfillHandlers.status
7894
+ handler: ((ctx, args) => countBackfillHandlers().status(ctx, args))
12061
7895
  }),
12062
7896
  migrationRun: mutationBuilder({
12063
7897
  args: v.any(),
12064
- handler: migrationHandlers.run
7898
+ handler: ((ctx, args) => migrationHandlers().run(ctx, args))
12065
7899
  }),
12066
7900
  migrationRunChunk,
12067
7901
  migrationStatus: mutationBuilder({
12068
7902
  args: v.any(),
12069
- handler: migrationHandlers.status
7903
+ handler: ((ctx, args) => migrationHandlers().status(ctx, args))
12070
7904
  }),
12071
7905
  migrationCancel: mutationBuilder({
12072
7906
  args: v.any(),
12073
- handler: migrationHandlers.cancel
7907
+ handler: ((ctx, args) => migrationHandlers().cancel(ctx, args))
12074
7908
  }),
12075
7909
  resetChunk,
12076
7910
  reset: internalActionGeneric({