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.
- package/dist/aggregate/index.d.ts +11 -1
- package/dist/aggregate/index.js +1 -1
- package/dist/auth/generated/index.d.ts +1 -1
- package/dist/auth/generated/index.js +1 -1
- package/dist/auth/http/index.d.ts +1 -1
- package/dist/auth/index.d.ts +4 -4
- package/dist/auth/index.js +10 -9
- package/dist/auth/nextjs/index.d.ts +2 -2
- package/dist/auth/nextjs/index.js +3 -3
- package/dist/auth/start/server/index.d.ts +1 -1
- package/dist/auth/start/server/index.js +1 -1
- package/dist/{builder-CsxVc5xC.js → builder-1Vx-tEnS.js} +3 -3
- package/dist/{caller-factory-CWa0ELLD.js → caller-factory-Dd3H7j3V.js} +3 -3
- package/dist/{where-clause-compiler-B_H3oio5.d.ts → capabilities-Bem7xvGK.d.ts} +2958 -4145
- package/dist/cli.mjs +155 -60
- package/dist/{convex-plugin-D8B0oCFq.js → convex-plugin-DfOhBU9g.js} +1 -1
- package/dist/{create-schema-orm-B3f2Kc8O.js → create-schema-orm-Bu2JCiBS.js} +1 -1
- package/dist/crpc/index.d.ts +3 -3
- package/dist/crpc/index.js +4 -4
- package/dist/{customFunctions-DxEEO4Dq.js → customFunctions-BbhgdRGl.js} +2 -2
- package/dist/definitions-D9vhJ6OV.js +215 -0
- package/dist/extensions-DzjCJXYl.js +55 -0
- package/dist/filter-expression-Dydt8wS0.js +402 -0
- package/dist/{generated-contract-disabled-B-nmd7Ne.d.ts → generated-contract-disabled-BEc4d98x.d.ts} +2 -2
- package/dist/{http-types-BoSDAh4Y.d.ts → http-types-90mapMP9.d.ts} +33 -3
- package/dist/id-Bibg34Yb.js +37 -0
- package/dist/{local-env-yFKub75x.mjs → local-env-Du24tdbp.mjs} +2243 -2113
- package/dist/{middleware-Br5zubu7.d.ts → middleware-CE_MC6Gi.d.ts} +1 -1
- package/dist/{middleware-DUd1Sj39.js → middleware-cJDRHTRY.js} +1 -1
- package/dist/orm/aggregate-index/index.d.ts +20 -0
- package/dist/orm/aggregate-index/index.js +2136 -0
- package/dist/orm/index.d.ts +5 -4
- package/dist/orm/index.js +126 -4292
- package/dist/orm/migrations/index.d.ts +3 -0
- package/dist/orm/migrations/index.js +506 -0
- package/dist/plugins/index.d.ts +1 -1
- package/dist/plugins/index.js +1 -1
- package/dist/{procedure-caller-DQxnLBS_.js → procedure-caller-DIzXfX2E.js} +2 -2
- package/dist/{procedure-name-C55TynK3.d.ts → procedure-name-B09-cf1-.d.ts} +3 -3
- package/dist/{query-context-BzihIpnM.js → query-context-DOm5Xm3H.js} +1 -402
- package/dist/{query-options-C_eBSIXG.js → query-options-DUiOj01a.js} +33 -1
- package/dist/ratelimit/index.d.ts +47 -11
- package/dist/ratelimit/index.js +256 -102
- package/dist/react/index.d.ts +9 -6
- package/dist/react/index.js +36 -17
- package/dist/rsc/index.d.ts +6 -9
- package/dist/rsc/index.js +7 -11
- package/dist/runtime-DfGsZtx3.js +1074 -0
- package/dist/schema-Besl-rpv.js +1411 -0
- package/dist/server/index.d.ts +4 -4
- package/dist/server/index.js +4 -4
- package/dist/solid/index.d.ts +9 -6
- package/dist/solid/index.js +36 -17
- package/dist/{runtime-6DJxpDfb.js → table-rmJm5Qio.js} +3 -1095
- package/dist/{token-xlpENMVn.js → token-DcV_0fkF.js} +1 -1
- package/dist/{types-C0Xl7P8K.d.ts → types-In-ZZOsd.d.ts} +1 -1
- package/dist/{validators-C7LelqTN.js → validators-Dmbhsd3U.js} +1 -1
- package/dist/watcher.mjs +1 -1
- package/dist/where-clause-compiler-mohWFiG0.d.ts +2042 -0
- package/package.json +3 -2
- package/skills/kitcn/SKILL.md +19 -18
- package/skills/kitcn/references/features/aggregates.md +21 -0
- package/skills/kitcn/references/features/auth-organizations.md +3 -3
- package/skills/kitcn/references/features/auth-polar.md +2 -1
- package/skills/kitcn/references/features/auth.md +8 -1
- package/skills/kitcn/references/features/http.md +26 -1
- package/skills/kitcn/references/features/ratelimit.md +11 -0
- package/skills/kitcn/references/features/react.md +1 -1
- package/skills/kitcn/references/setup/auth.md +52 -42
- package/skills/kitcn/references/setup/index.md +1 -1
- package/skills/kitcn/references/setup/server.md +37 -6
- /package/dist/{api-entry-N3nBOlI2.js → api-entry-CkDpGYVg.js} +0 -0
- /package/dist/{context-utils-BBUtBqjN.d.ts → context-utils-DwZ3Cam1.d.ts} +0 -0
- /package/dist/{create-schema-BXrKE2YY.js → create-schema-BRFjdgRr.js} +0 -0
- /package/dist/{error-Bvo7YEhk.js → error-CMLeCadS.js} +0 -0
- /package/dist/{generated-contract-disabled-BXaz7JCE.js → generated-contract-disabled-CZa0iyV0.js} +0 -0
- /package/dist/{meta-utils-D9K4fICl.js → meta-utils-D2lqN2Lg.js} +0 -0
- /package/dist/{query-context-CNo9ffvI.d.ts → query-context-DJONf8X5.d.ts} +0 -0
- /package/dist/{token-B9Bjcqug.d.ts → token-kQaqFby4.d.ts} +0 -0
- /package/dist/{transformer-yZuBWo8v.js → transformer-D8wO-kEj.js} +0 -0
- /package/dist/{types-BCl8gfGy.d.ts → types-Bf3XQex5.d.ts} +0 -0
- /package/dist/{types-DF2cg_w0.d.ts → types-MHYXboyy.d.ts} +0 -0
- /package/dist/{types-CnTpHR1F.d.ts → types-eNzQmbqM.d.ts} +0 -0
- /package/dist/{upstream-BR6sBLg3.js → upstream-BCgGZX6q.js} +0 -0
- /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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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/
|
|
3374
|
-
const
|
|
3375
|
-
const
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
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
|
|
278
|
+
return resolved;
|
|
3416
279
|
};
|
|
3417
|
-
const
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
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
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
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
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
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
|
|
6495
|
-
|
|
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
|
|
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
|
-
|
|
12003
|
-
const
|
|
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({
|