@atscript/db 0.1.147 → 0.1.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/{agg-CV7y8nC6.d.cts → agg-42CSpGDR.d.cts} +2 -2
  2. package/dist/{agg-D5DHsAby.d.mts → agg-Wo-smrYV.d.mts} +3 -3
  3. package/dist/agg.cjs +1 -1
  4. package/dist/agg.d.cts +2 -2
  5. package/dist/agg.d.mts +2 -2
  6. package/dist/agg.mjs +1 -1
  7. package/dist/{aggregate-fns-CtsRbZn9.cjs → aggregate-fns-C-UJRobm.cjs} +46 -7
  8. package/dist/{aggregate-fns-C0BymEqa.mjs → aggregate-fns-CyaZyb9I.mjs} +35 -8
  9. package/dist/aggregate-rules-D_bsCpUI.cjs +26 -0
  10. package/dist/aggregate-rules-jdPrxqWa.mjs +15 -0
  11. package/dist/{buckets-DYopfd2Y.d.mts → buckets-CNdTOnei.d.mts} +312 -18
  12. package/dist/{buckets-CjdPipCC.d.cts → buckets-DJiYlMXc.d.cts} +312 -18
  13. package/dist/{column-diff-Dt7EDJsA.cjs → column-diff-CUU4GvYg.cjs} +681 -87
  14. package/dist/{column-diff-CO06iQvI.mjs → column-diff-Cp6ZoyRE.mjs} +678 -96
  15. package/dist/{db-error-D5uilS_A.mjs → db-error-Az85UhTX.mjs} +32 -1
  16. package/dist/{db-error-DTkkeu5b.cjs → db-error-DRQH4sLY.cjs} +55 -0
  17. package/dist/{fk-diff-BtOmL4OW.d.mts → fk-diff-BQ4krij8.d.cts} +1 -1
  18. package/dist/{fk-diff-DLqwVkKG.d.cts → fk-diff-DsaIijVX.d.mts} +1 -1
  19. package/dist/index.cjs +70 -5
  20. package/dist/index.d.cts +58 -13
  21. package/dist/index.d.mts +58 -13
  22. package/dist/index.mjs +64 -6
  23. package/dist/{nested-writer-SqtYJHF0.cjs → nested-writer-BUQvk6QT.cjs} +68 -2
  24. package/dist/{nested-writer-D35UsDBT.mjs → nested-writer-CUBoq1ZO.mjs} +57 -3
  25. package/dist/numeric-operand-B1jKH7x5.mjs +21 -0
  26. package/dist/numeric-operand-DKfiRLYp.cjs +26 -0
  27. package/dist/ops.cjs +1 -1
  28. package/dist/ops.mjs +1 -1
  29. package/dist/plugin.cjs +36 -9
  30. package/dist/plugin.mjs +36 -9
  31. package/dist/rel.cjs +2 -2
  32. package/dist/rel.d.cts +2 -2
  33. package/dist/rel.d.mts +2 -2
  34. package/dist/rel.mjs +2 -2
  35. package/dist/{relation-helpers-C2bLtE-t.d.mts → relation-helpers-Ba0v49sn.d.mts} +1 -1
  36. package/dist/{relation-helpers-DH3LvwLV.d.cts → relation-helpers-kX7jjgME.d.cts} +1 -1
  37. package/dist/{relation-loader-C0qE8xgR.mjs → relation-loader-ByY1Byrl.mjs} +1 -1
  38. package/dist/{relation-loader-BFhVuJ-B.cjs → relation-loader-D8OrdH-r.cjs} +1 -1
  39. package/dist/shared.cjs +3 -0
  40. package/dist/shared.d.cts +12 -1
  41. package/dist/shared.d.mts +12 -1
  42. package/dist/shared.mjs +2 -1
  43. package/dist/sync.cjs +2 -2
  44. package/dist/sync.d.cts +2 -2
  45. package/dist/sync.d.mts +2 -2
  46. package/dist/sync.mjs +2 -2
  47. package/dist/{validator-Clu2q_7z.mjs → validator-BTiIOTKP.mjs} +9 -3
  48. package/dist/{validator-CVS-onRg.cjs → validator-UcuJxHNT.cjs} +8 -2
  49. package/dist/{validator-CMnTI6r7.d.cts → validator-tBNvM1qc.d.cts} +23 -2
  50. package/dist/{validator-CMnTI6r7.d.mts → validator-tBNvM1qc.d.mts} +23 -2
  51. package/dist/validator.cjs +2 -2
  52. package/dist/validator.d.cts +1 -1
  53. package/dist/validator.d.mts +1 -1
  54. package/dist/validator.mjs +2 -2
  55. package/package.json +8 -8
@@ -1,13 +1,15 @@
1
- import { n as CasMismatchError, r as DbError } from "./db-error-D5uilS_A.mjs";
2
- import { a as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-C0BymEqa.mjs";
3
- import { A as noteRelationFilter, B as tableNameOf, E as createRelationFilterHost, F as findFKEntryForRelation, G as viewExprNullable, H as computedOperands, I as findFKForRelation, K as walkViewExpr, L as findRemoteFK, N as resolveRelationFilterTree, O as hasRelationOp, P as compositeKey, T as containsRelationFilter, V as computeOf, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as sameKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, j as relGuardState, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as pkTupleKey, y as rowMatchesKey, z as slicePerGroup } from "./nested-writer-D35UsDBT.mjs";
1
+ import { a as aggregateExpressionsNotSupported, l as spaceClosedError, n as CasMismatchError, r as DbError } from "./db-error-Az85UhTX.mjs";
2
+ import { a as SOURCE_VALUE_FNS, c as isFirstLast, o as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-CyaZyb9I.mjs";
3
+ import { B as slicePerGroup, D as createRelationFilterHost, E as containsRelationFilter, F as compositeKey, H as computeOf, I as findFKEntryForRelation, J as walkViewExpr, L as findFKForRelation, M as relGuardState, P as resolveRelationFilterTree, R as findRemoteFK, U as computedOperands, V as tableNameOf, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as sameKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, j as noteRelationFilter, k as hasRelationOp, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, q as viewExprNullable, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as pkTupleKey, x as uniqueKeyTuple, y as rowMatchesKey } from "./nested-writer-CUBoq1ZO.mjs";
4
4
  import { i as isJsonLeafType, n as DERIVED_INCOMPATIBLE } from "./derived-rules-0sKn4f5C.mjs";
5
5
  import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, t as deletePath } from "./object-TkiJQ-Dp.mjs";
6
- import { resolveAlias } from "./agg.mjs";
6
+ import { resolveAlias as resolveAlias$1 } from "./agg.mjs";
7
+ import { t as numericTypeProblem } from "./numeric-operand-B1jKH7x5.mjs";
8
+ import { t as NOT_DIMENSION_REASON } from "./aggregate-rules-jdPrxqWa.mjs";
7
9
  import { separateCas, separateFieldOps } from "./ops.mjs";
8
- import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, t as buildDbValidator } from "./validator-Clu2q_7z.mjs";
10
+ import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, t as buildDbValidator } from "./validator-BTiIOTKP.mjs";
9
11
  import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
10
- import { BUCKET_UNITS, isAggregateExpr, isBucketExpr, isPrimitive, isRelationOp, resolveBuckets } from "@uniqu/core";
12
+ import { ARITH_MAX_NODES, BUCKET_UNITS, isAggregateExpr, isAggregateOfExpr, isBucketExpr, isPrimitive, isRelationOp, isSelectArithExpr, resolveAlias, resolveBuckets } from "@uniqu/core";
11
13
  import { AsyncLocalStorage } from "node:async_hooks";
12
14
  //#region src/logger.ts
13
15
  const NoopLogger = {
@@ -51,13 +53,39 @@ const NoopLogger = {
51
53
  * @throws DbError `INVALID_QUERY` carrying every issue (`path` `$select` / `$groupBy`).
52
54
  */
53
55
  function normalizeComputedSelect(controls, fields, aggregate) {
56
+ return resolveComputedSelect(controls, fields, aggregate).buckets;
57
+ }
58
+ /**
59
+ * Everything {@link normalizeComputedSelect} resolves: the calendar buckets,
60
+ * the arithmetic entries (`exprs`: row-level first, then group-level in
61
+ * dependency order — logical names) and the `$rowOrder` keys of `first()` /
62
+ * `last()` (`rowOrder`, as given — the primary key is appended by the
63
+ * mapper). The same normalizer, also covering uniqu's expression and
64
+ * `$rowOrder` rules.
65
+ *
66
+ * @throws DbError `INVALID_QUERY` carrying every issue (`path` `$select` / `$groupBy` / `$rowOrder`).
67
+ * @since 0.1.148
68
+ */
69
+ function resolveComputedSelect(controls, fields, aggregate) {
70
+ const select = controls?.$select;
71
+ const idIssues = [];
72
+ if (Array.isArray(select)) {
73
+ for (const entry of select) if (entry && typeof entry === "object" && entry.$as === "_id") idIssues.push({
74
+ path: "$select",
75
+ message: "Alias \"_id\" is reserved — pick another alias for this $select entry"
76
+ });
77
+ }
54
78
  const res = resolveBuckets(controls, {
55
79
  aggregate,
56
80
  fns: SUPPORTED_AGGREGATE_FNS,
57
81
  isField: (name) => fields.flatMap.has(name) || fields.navFields.has(name) || fields.physicalNames.has(name)
58
82
  });
59
- if (!res.ok) throw new DbError("INVALID_QUERY", res.issues);
60
- return res.buckets;
83
+ if (!res.ok || idIssues.length > 0) throw new DbError("INVALID_QUERY", [...idIssues, ...res.ok ? [] : res.issues]);
84
+ return {
85
+ buckets: res.buckets,
86
+ exprs: res.exprs,
87
+ rowOrder: res.rowOrder
88
+ };
61
89
  }
62
90
  /**
63
91
  * @deprecated since 0.1.136 — renamed {@link normalizeComputedSelect} (it
@@ -1126,6 +1154,194 @@ function computedMeta(rootType, path) {
1126
1154
  } : void 0;
1127
1155
  }
1128
1156
  //#endregion
1157
+ //#region src/query/aggregate-expr.ts
1158
+ /** Most nodes a group-level expression may have once the aliases it names are inlined (SQL repeats them). */
1159
+ const EXPANDED_MAX_NODES = 4 * ARITH_MAX_NODES;
1160
+ /** Most expanded nodes ALL the expression entries of one query may have together. */
1161
+ const QUERY_EXPANDED_MAX_NODES = 1024;
1162
+ /**
1163
+ * Why a field cannot be an arithmetic operand, or `undefined` when it can: it
1164
+ * must be a `number` (not a decimal — those are exact, an expression is IEEE
1165
+ * double — and not a timestamp-tagged number) with plain column storage. The
1166
+ * runtime twin of `@db.compute`'s operand rule, so declared and query-time
1167
+ * arithmetic accept the same fields.
1168
+ *
1169
+ * @since 0.1.148
1170
+ */
1171
+ function numericOperandProblem(fd) {
1172
+ if (fd.encrypted) return "is encrypted";
1173
+ if (isJsonValueField(fd)) return "is not a scalar number";
1174
+ return numericTypeProblem({
1175
+ base: fd.designType,
1176
+ tags: (fd.type?.type)?.tags
1177
+ });
1178
+ }
1179
+ /**
1180
+ * Converts uniqu's JSON arithmetic into the `@db.compute` expression tree
1181
+ * the SQL / MongoDB / memory renderers consume: a name becomes a
1182
+ * `{ field }` leaf, unary `-` becomes `neg`. `rename` maps each name (the
1183
+ * mapper makes row-level names physical; group-level names stay aliases).
1184
+ *
1185
+ * @since 0.1.148
1186
+ */
1187
+ function arithToExprNode(expr, rename = (n) => n) {
1188
+ if (typeof expr === "number") return expr;
1189
+ if (typeof expr === "string") return { field: rename(expr) };
1190
+ const args = expr.$args.map((a) => arithToExprNode(a, rename));
1191
+ if (expr.$op === "-" && args.length === 1) return {
1192
+ op: "neg",
1193
+ args: [args[0]]
1194
+ };
1195
+ if (expr.$op === "coalesce") return {
1196
+ op: "coalesce",
1197
+ args
1198
+ };
1199
+ return {
1200
+ op: expr.$op,
1201
+ args: [args[0], args[1]]
1202
+ };
1203
+ }
1204
+ /**
1205
+ * The names an expression reads, found iteratively and bounded: this runs on
1206
+ * a raw, not yet validated expression (a 10k-deep chain must not overflow the
1207
+ * stack — `normalizeComputedSelect` rejects it afterwards). Stops after
1208
+ * {@link EXPANDED_MAX_NODES} nodes: the shape rules reject anything larger later.
1209
+ * (`@uniqu/core`'s `arithNames` is iterative too but unbounded, so it is not used here.)
1210
+ */
1211
+ function boundedArithNames(expr) {
1212
+ const out = /* @__PURE__ */ new Set();
1213
+ const stack = [expr];
1214
+ for (let visited = 0; stack.length > 0 && visited < EXPANDED_MAX_NODES; visited++) {
1215
+ const e = stack.pop();
1216
+ if (typeof e === "string") out.add(e);
1217
+ else if (e && typeof e === "object" && Array.isArray(e.$args)) {
1218
+ const args = e.$args;
1219
+ for (let i = args.length - 1; i >= 0; i--) stack.push(args[i]);
1220
+ }
1221
+ }
1222
+ return [...out];
1223
+ }
1224
+ /**
1225
+ * The field paths a `$select` entry reads from the table's rows: a plain
1226
+ * field, an aggregate's `$field` (`*` excluded), a row-level expression's
1227
+ * names, a calendar bucket's source. A group-level expression contributes
1228
+ * none (its names are aliases).
1229
+ */
1230
+ function entryFields(item) {
1231
+ if (typeof item === "string") return [item];
1232
+ if (isAggregateExpr(item)) return item.$field === "*" ? [] : [item.$field];
1233
+ if (isAggregateOfExpr(item)) return boundedArithNames(item.$expr);
1234
+ if (isBucketExpr(item)) return [item.$field];
1235
+ return [];
1236
+ }
1237
+ /** Keys of a `$rowOrder` control (the raw object), or `[]`. */
1238
+ function rowOrderKeys(rowOrder) {
1239
+ return rowOrder && typeof rowOrder === "object" ? Object.keys(rowOrder) : [];
1240
+ }
1241
+ /**
1242
+ * Schema-dependent rules of query-time arithmetic and `first` / `last`, after
1243
+ * the shape normalizer and the path guard:
1244
+ *
1245
+ * - every row-level operand and every group-level operand that is a `$groupBy`
1246
+ * field is a numeric field ({@link numericOperandProblem});
1247
+ * - every group-level operand that is a `$select` alias is numeric: counts and
1248
+ * expressions always, `sum` / `avg` / `min` / `max` / `first` / `last` when
1249
+ * their `$field` is;
1250
+ * - a `first` / `last` field is a scalar (no JSON value or array);
1251
+ * - in a strict (dimension / measure) table row-level operands are measures and
1252
+ * `$rowOrder` keys are dimensions or measures (a `first` / `last` field
1253
+ * follows the aggregate rule in `aggregate()`);
1254
+ * - an operand or `first` / `last` field tagged with a quantity ref needs that
1255
+ * ref in `$groupBy`;
1256
+ * - a group-level expression stays within {@link EXPANDED_MAX_NODES} once the
1257
+ * aliases it names are inlined (SQL repeats them at every use), and all the
1258
+ * expression entries of the query together within {@link QUERY_EXPANDED_MAX_NODES}.
1259
+ *
1260
+ * Unknown fields are the path guard's, so they are skipped here.
1261
+ *
1262
+ * @throws DbError `INVALID_QUERY` with every issue.
1263
+ * @since 0.1.148
1264
+ */
1265
+ function checkAggregateExprs(meta, query, exprs, rowOrder) {
1266
+ const select = query.controls.$select;
1267
+ if (!Array.isArray(select)) return;
1268
+ const issues = [];
1269
+ const fdOf = (name) => meta.descriptorByPath.get(name);
1270
+ const numericField = (name) => {
1271
+ const fd = fdOf(name);
1272
+ return fd === void 0 || numericOperandProblem(fd) === void 0;
1273
+ };
1274
+ const operandIssue = (name) => {
1275
+ if (!numericField(name)) issues.push({
1276
+ path: "$select",
1277
+ message: `Field "${name}" is not numeric — arithmetic needs a number field (not decimal, timestamp or text)`
1278
+ });
1279
+ };
1280
+ const numericAlias = /* @__PURE__ */ new Map();
1281
+ for (const item of select) if (isAggregateExpr(item)) {
1282
+ const counts = item.$fn === "count" || item.$fn === "countDistinct";
1283
+ numericAlias.set(resolveAlias(item), counts || numericField(item.$field));
1284
+ } else if (isAggregateOfExpr(item) || isSelectArithExpr(item)) numericAlias.set(item.$as, true);
1285
+ const strict = meta.dimensions.length > 0 || meta.measures.length > 0;
1286
+ const groupBy = new Set(query.controls.$groupBy);
1287
+ const refGroup = (label, field) => {
1288
+ const ref = meta.quantityRefByField.get(field);
1289
+ if (ref && !groupBy.has(ref)) issues.push({
1290
+ path: "$select",
1291
+ message: `${label} requires "${ref}" in $groupBy — quantity-ref-tagged fields must be grouped by their dimension`
1292
+ });
1293
+ };
1294
+ for (const e of exprs) {
1295
+ if (e.level === "row") {
1296
+ for (const name of e.names) {
1297
+ operandIssue(name);
1298
+ if (strict && fdOf(name) && !meta.measures.includes(name)) issues.push({
1299
+ path: "$select",
1300
+ message: `Expression operand "${name}" is not a measure`
1301
+ });
1302
+ refGroup(`Expression "${e.alias}"`, name);
1303
+ }
1304
+ continue;
1305
+ }
1306
+ for (const name of e.names) if (numericAlias.has(name)) {
1307
+ if (numericAlias.get(name) === false) issues.push({
1308
+ path: "$select",
1309
+ message: `"${name}" is not numeric and cannot be used in an expression`
1310
+ });
1311
+ } else operandIssue(name);
1312
+ }
1313
+ for (const item of select) {
1314
+ if (!isFirstLast(item)) continue;
1315
+ const fd = fdOf(item.$field);
1316
+ if (fd && (isJsonValueField(fd) || fd.designType === "array")) issues.push({
1317
+ path: "$select",
1318
+ message: `${item.$fn}("${item.$field}") needs a scalar field — not a JSON value or array`
1319
+ });
1320
+ }
1321
+ for (const key of rowOrder ?? []) if (strict && fdOf(key.field) && !meta.dimensions.includes(key.field) && !meta.measures.includes(key.field)) issues.push({
1322
+ path: "$rowOrder",
1323
+ message: `$rowOrder field "${key.field}" is not a dimension or measure`
1324
+ });
1325
+ const expanded = /* @__PURE__ */ new Map();
1326
+ const countNodes = (expr) => typeof expr === "number" ? 1 : typeof expr === "string" ? expanded.get(expr) ?? 1 : 1 + expr.$args.reduce((sum, arg) => sum + countNodes(arg), 0);
1327
+ let total = 0;
1328
+ const issuesBeforeSize = issues.length;
1329
+ for (const e of exprs) {
1330
+ const size = countNodes(e.expr);
1331
+ expanded.set(e.alias, size);
1332
+ total += size;
1333
+ if (size > EXPANDED_MAX_NODES) issues.push({
1334
+ path: "$select",
1335
+ message: `Expression "${e.alias}" is too large once its aliases are expanded (more than ${EXPANDED_MAX_NODES} nodes)`
1336
+ });
1337
+ }
1338
+ if (total > 1024 && issues.length === issuesBeforeSize) issues.push({
1339
+ path: "$select",
1340
+ message: `The expressions of this query are too large together (more than ${QUERY_EXPANDED_MAX_NODES} nodes once their aliases are expanded)`
1341
+ });
1342
+ if (issues.length) throw new DbError("INVALID_QUERY", issues);
1343
+ }
1344
+ //#endregion
1129
1345
  //#region src/query/uniqu-select.ts
1130
1346
  /**
1131
1347
  * Wraps a raw `$select` value and provides lazy-cached conversions
@@ -1137,8 +1353,9 @@ function computedMeta(rootType, path) {
1137
1353
  * For exclusion → inclusion inversion, pass `allFields` (physical field names).
1138
1354
  *
1139
1355
  * An array `$select` holds plain field names and computed entries —
1140
- * aggregates (`{ $fn, $field }`, {@link aggregates}) and calendar buckets
1141
- * (`{ $bucket, $field }`, {@link buckets}). Entries arrive normalized
1356
+ * aggregates (`{ $fn, $field }`, {@link aggregates}), `first` / `last`
1357
+ * ({@link firstLast}), arithmetic ({@link exprAggregates}, {@link exprs}) and
1358
+ * calendar buckets (`{ $bucket, $field }`, {@link buckets}). Entries arrive normalized
1142
1359
  * (`normalizeComputedSelect` rejects any other shape before translation).
1143
1360
  */
1144
1361
  var UniquSelect = class UniquSelect {
@@ -1147,7 +1364,6 @@ var UniquSelect = class UniquSelect {
1147
1364
  _allFields;
1148
1365
  _array = UniquSelect.UNRESOLVED;
1149
1366
  _projection = UniquSelect.UNRESOLVED;
1150
- _aggregates = UniquSelect.UNRESOLVED;
1151
1367
  /**
1152
1368
  * The calendar buckets of an aggregate `$select`, normalized (canonical
1153
1369
  * zone, week start, alias) with the PHYSICAL source `field` and its
@@ -1157,16 +1373,80 @@ var UniquSelect = class UniquSelect {
1157
1373
  */
1158
1374
  buckets;
1159
1375
  /**
1376
+ * The plain aggregates (`{ $fn, $field }`) of an array-form `$select`;
1377
+ * `undefined` when there are none or the `$select` is object form.
1378
+ * `first` / `last` are listed separately ({@link firstLast}), so an adapter
1379
+ * written before them never meets one here.
1380
+ */
1381
+ aggregates;
1382
+ /** The `first` / `last` entries of an array-form `$select` (since 0.1.148); `undefined` when none. */
1383
+ firstLast;
1384
+ /**
1385
+ * Row-level expression aggregates (`sum(price*qty)`) — leaves are physical
1386
+ * columns; render `fn(<expr>)` per row. `undefined` when there are none.
1387
+ * Only reaches adapters whose `supportsAggregateExpressions()` is true.
1388
+ * Since 0.1.148.
1389
+ */
1390
+ exprAggregates;
1391
+ /**
1392
+ * Group-level expressions in dependency order: each leaf names an alias
1393
+ * defined by an aggregate / `first` / `last` / expression entry or a
1394
+ * grouped column; evaluate after grouping. `undefined` when there are none.
1395
+ * Since 0.1.148.
1396
+ */
1397
+ exprs;
1398
+ /**
1399
+ * The order of the rows inside each group that `first` / `last` read, as
1400
+ * physical columns, with the primary key appended as the final ascending
1401
+ * tie-break. `undefined` without `first` / `last`. Since 0.1.148.
1402
+ */
1403
+ rowOrder;
1404
+ /**
1405
+ * The descriptors of the PHYSICAL columns a `min` / `max` / `first` / `last`
1406
+ * reads — for an engine that cannot aggregate a type directly (PostgreSQL
1407
+ * has no `MIN(boolean)`). Since 0.1.148.
1408
+ */
1409
+ sources;
1410
+ /**
1411
+ * The output alias of every computed entry but the calendar buckets, in the
1412
+ * order adapters emit them: {@link aggregates}, {@link exprAggregates},
1413
+ * {@link firstLast}, then {@link exprs} (which read the others). Since 0.1.148.
1414
+ */
1415
+ computedAliases;
1416
+ /**
1160
1417
  * @param raw - the `$select` value (field paths already physical).
1161
1418
  * @param allFields - physical field names, for exclusion-form inversion.
1162
1419
  * @param buckets - the resolved calendar buckets of the raw `$select`'s
1163
1420
  * `{ $bucket }` entries (the field mappers supply them — physical `field`,
1164
1421
  * source `fd`).
1422
+ * @param computed - the resolved arithmetic and `$rowOrder` parts (physical).
1165
1423
  */
1166
- constructor(raw, allFields, buckets) {
1424
+ constructor(raw, allFields, buckets, computed) {
1167
1425
  this._raw = raw;
1168
1426
  this._allFields = allFields;
1169
1427
  this.buckets = buckets?.length ? buckets : void 0;
1428
+ this.exprAggregates = computed?.exprAggregates?.length ? computed.exprAggregates : void 0;
1429
+ this.exprs = computed?.exprs?.length ? computed.exprs : void 0;
1430
+ this.rowOrder = computed?.rowOrder?.length ? computed.rowOrder : void 0;
1431
+ this.sources = computed?.sources ?? /* @__PURE__ */ new Map();
1432
+ const aggregates = [];
1433
+ const firstLast = [];
1434
+ if (Array.isArray(raw)) {
1435
+ for (const item of raw) if (isFirstLast(item)) firstLast.push({
1436
+ fn: item.$fn,
1437
+ column: item.$field,
1438
+ alias: resolveAlias(item)
1439
+ });
1440
+ else if (isAggregateExpr(item)) aggregates.push(item);
1441
+ }
1442
+ this.aggregates = aggregates.length > 0 ? aggregates : void 0;
1443
+ this.firstLast = firstLast.length > 0 ? firstLast : void 0;
1444
+ this.computedAliases = [
1445
+ ...aggregates.map((expr) => resolveAlias(expr)),
1446
+ ...(this.exprAggregates ?? []).map((e) => e.alias),
1447
+ ...firstLast.map((fl) => fl.alias),
1448
+ ...(this.exprs ?? []).map((e) => e.alias)
1449
+ ];
1170
1450
  }
1171
1451
  /**
1172
1452
  * Resolved inclusion array of plain field names (strings only).
@@ -1215,20 +1495,6 @@ var UniquSelect = class UniquSelect {
1215
1495
  this._projection = result;
1216
1496
  return this._projection;
1217
1497
  }
1218
- /**
1219
- * Extracts AggregateExpr entries from array-form $select.
1220
- * Returns undefined if no aggregates present or if $select is object form.
1221
- */
1222
- get aggregates() {
1223
- if (this._aggregates !== UniquSelect.UNRESOLVED) return this._aggregates;
1224
- if (!Array.isArray(this._raw)) {
1225
- this._aggregates = void 0;
1226
- return;
1227
- }
1228
- const aggs = this._raw.filter(isAggregateExpr);
1229
- this._aggregates = aggs.length > 0 ? aggs : void 0;
1230
- return this._aggregates;
1231
- }
1232
1498
  /** Whether the $select contains any AggregateExpr entries. */
1233
1499
  get hasAggregates() {
1234
1500
  return !!this.aggregates?.length;
@@ -1318,6 +1584,15 @@ function toBool(value) {
1318
1584
  if (value === null || value === void 0) return value;
1319
1585
  return !!value;
1320
1586
  }
1587
+ /**
1588
+ * Coerces the computed aliases of aggregate rows (`min` / `max` / `first` /
1589
+ * `last` of one boolean or decimal field) the way a column of that type is
1590
+ * coerced on read — the row reverse path cannot, as an alias is no column.
1591
+ * @since 0.1.148
1592
+ */
1593
+ function coerceAliasValues(rows, aliases) {
1594
+ for (const row of rows) for (const [alias, fd] of aliases) if (alias in row) row[alias] = fd.designType === "boolean" ? toBool(row[alias]) : toDecimalString(row[alias]);
1595
+ }
1321
1596
  function toDecimalString(value) {
1322
1597
  if (value === null || value === void 0) return value;
1323
1598
  if (typeof value === "string") return value;
@@ -1356,9 +1631,13 @@ var FieldMappingStrategy = class {
1356
1631
  * `buckets` are the query's calendar buckets as the core's normalizer
1357
1632
  * resolved them (`normalizeComputedSelect` — `AtscriptDbReadable.aggregate`
1358
1633
  * runs it before the guards); they reach adapters with `field` made
1359
- * physical and the source descriptor as `fd`.
1634
+ * physical and the source descriptor as `fd`. `exprs` / `rowOrder` are the
1635
+ * arithmetic entries and `$rowOrder` keys of the same normalizer
1636
+ * (`resolveComputedSelect`): they reach adapters as `$select.exprAggregates`
1637
+ * / `.exprs` / `.rowOrder` with physical names (the primary key appended to
1638
+ * the order); `$rowOrder` itself is not forwarded (since 0.1.148).
1360
1639
  */
1361
- translateAggregateQuery(query, meta, buckets) {
1640
+ translateAggregateQuery(query, meta, buckets, exprs = [], rowOrder) {
1362
1641
  const controls = query.controls;
1363
1642
  const aliases = this.computedAliasSet(controls.$select);
1364
1643
  const physicalBuckets = buckets.map((b) => ({
@@ -1367,26 +1646,102 @@ var FieldMappingStrategy = class {
1367
1646
  fd: meta.descriptorByPath.get(b.field)
1368
1647
  }));
1369
1648
  const select = controls.$select && this.physicalSelect(controls.$select, meta);
1649
+ const computed = this.physicalComputed(meta, aliases, exprs, rowOrder, controls.$select);
1370
1650
  return {
1371
1651
  filter: this.translateFilter(query.filter ?? {}, meta),
1372
1652
  controls: {
1373
1653
  ...controls,
1374
1654
  $with: void 0,
1655
+ $rowOrder: void 0,
1375
1656
  $groupBy: this.renamesPaths(meta) ? controls.$groupBy.map((key) => aliases.has(key) ? key : this.physicalPath(key, meta)) : controls.$groupBy,
1376
- $select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets) : void 0,
1657
+ $select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets, computed) : void 0,
1377
1658
  $sort: controls.$sort && this.physicalSort(controls.$sort, meta, aliases),
1378
- $having: controls.$having ? this.translateFilter(controls.$having, meta) : void 0
1659
+ $having: controls.$having ? this.translateHaving(controls.$having, meta, aliases) : void 0
1379
1660
  },
1380
1661
  insights: query.insights
1381
1662
  };
1382
1663
  }
1383
- /** Output aliases of the computed `$select` entries (aggregates and calendar buckets). */
1664
+ /**
1665
+ * `$having` with physical keys — except the computed output `aliases`,
1666
+ * which stay as written (an alias equal to a renamed field's name,
1667
+ * `first(raisedAt):raisedAt`, is the alias, exactly as in `$sort`).
1668
+ */
1669
+ translateHaving(having, meta, aliases) {
1670
+ if (aliases.size === 0 || !having || typeof having !== "object") return this.translateFilter(having, meta);
1671
+ const out = {};
1672
+ const fields = {};
1673
+ for (const [key, value] of Object.entries(having)) if (key === "$and" || key === "$or") out[key] = value.map((f) => this.translateHaving(f, meta, aliases));
1674
+ else if (key === "$not") out[key] = this.translateHaving(value, meta, aliases);
1675
+ else if (aliases.has(key)) out[key] = value;
1676
+ else fields[key] = value;
1677
+ return Object.keys(fields).length > 0 ? {
1678
+ ...out,
1679
+ ...this.translateFilter(fields, meta)
1680
+ } : out;
1681
+ }
1682
+ /** Output aliases of the computed `$select` entries (aggregates, expressions and calendar buckets). */
1384
1683
  computedAliasSet(select) {
1385
1684
  const aliases = /* @__PURE__ */ new Set();
1386
- for (const item of select ?? []) if (isAggregateExpr(item) || isBucketExpr(item)) aliases.add(resolveAlias(item));
1685
+ for (const item of select ?? []) if (isAggregateExpr(item) || isBucketExpr(item)) aliases.add(resolveAlias$1(item));
1686
+ else if (isAggregateOfExpr(item) || isSelectArithExpr(item)) aliases.add(item.$as);
1387
1687
  return aliases;
1388
1688
  }
1389
1689
  /**
1690
+ * The arithmetic and `$rowOrder` parts of a grouped query with PHYSICAL
1691
+ * names: a row-level operand is a column; a group-level operand stays an
1692
+ * alias, or becomes the physical name of a `$groupBy` field. The primary
1693
+ * key is appended to the order as the final ascending tie-break.
1694
+ */
1695
+ physicalComputed(meta, aliases, exprs, rowOrder, select) {
1696
+ const sources = /* @__PURE__ */ new Map();
1697
+ for (const item of select ?? []) {
1698
+ if (!isAggregateExpr(item) || !SOURCE_VALUE_FNS.has(item.$fn)) continue;
1699
+ const fd = meta.descriptorByPath.get(item.$field);
1700
+ if (fd) sources.set(this.physicalPath(item.$field, meta), fd);
1701
+ }
1702
+ const exprAggregates = [];
1703
+ const groupExprs = [];
1704
+ for (const e of exprs) {
1705
+ const names = /* @__PURE__ */ new Set();
1706
+ const node = arithToExprNode(e.expr, (name) => {
1707
+ const resolved = e.level === "group" && aliases.has(name) ? name : this.physicalPath(name, meta);
1708
+ names.add(resolved);
1709
+ return resolved;
1710
+ });
1711
+ if (e.level === "row") exprAggregates.push({
1712
+ fn: e.fn,
1713
+ alias: e.alias,
1714
+ expr: node,
1715
+ names: [...names]
1716
+ });
1717
+ else groupExprs.push({
1718
+ alias: e.alias,
1719
+ expr: node,
1720
+ names: [...names]
1721
+ });
1722
+ }
1723
+ let order;
1724
+ if (rowOrder?.length) {
1725
+ order = rowOrder.map((k) => ({
1726
+ column: this.physicalPath(k.field, meta),
1727
+ desc: k.desc
1728
+ }));
1729
+ for (const pk of meta.primaryKeys) {
1730
+ const column = this.physicalPath(pk, meta);
1731
+ if (!order.some((k) => k.column === column)) order.push({
1732
+ column,
1733
+ desc: false
1734
+ });
1735
+ }
1736
+ }
1737
+ return {
1738
+ exprAggregates,
1739
+ exprs: groupExprs,
1740
+ rowOrder: order,
1741
+ sources
1742
+ };
1743
+ }
1744
+ /**
1390
1745
  * `$select` with its field paths made physical: array-form names and
1391
1746
  * computed `$field`s (`'*'` kept), or the keys of the object
1392
1747
  * (inclusion / exclusion) form. An aggregate's output alias is fixed
@@ -1404,7 +1759,7 @@ var FieldMappingStrategy = class {
1404
1759
  if (physical === item.$field) return item;
1405
1760
  return isAggregateExpr(item) ? {
1406
1761
  ...item,
1407
- $as: resolveAlias(item),
1762
+ $as: resolveAlias$1(item),
1408
1763
  $field: physical
1409
1764
  } : {
1410
1765
  ...item,
@@ -2030,7 +2385,7 @@ function acceptsScalar(kind, value) {
2030
2385
  case "any": return true;
2031
2386
  case "string": return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
2032
2387
  case "number":
2033
- case "decimal": return typeof value === "number" || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
2388
+ case "decimal": return typeof value === "number" && Number.isFinite(value) || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
2034
2389
  case "integer": return typeof value === "number" && Number.isInteger(value) || typeof value === "bigint" || typeof value === "string" && INTEGER_RE.test(value);
2035
2390
  default: return typeof value === "boolean" || value === 0 || value === 1;
2036
2391
  }
@@ -2145,7 +2500,8 @@ function guardFilterValues(meta, filter) {
2145
2500
  }
2146
2501
  /**
2147
2502
  * `$having` values: an aggregate alias is a number (`count`,
2148
- * `countDistinct`, `sum`, `avg`) or its source field's type (`min` / `max`),
2503
+ * `countDistinct`, `sum`, `avg`, an expression) or its source field's type
2504
+ * (`min` / `max`, `first` / `last`),
2149
2505
  * a calendar-bucket alias a string label, any other key a `$groupBy`
2150
2506
  * field's own type.
2151
2507
  */
@@ -2153,11 +2509,12 @@ function guardHavingValues(meta, controls) {
2153
2509
  if (!controls?.$having) return;
2154
2510
  const aliases = /* @__PURE__ */ new Map();
2155
2511
  if (Array.isArray(controls.$select)) {
2156
- for (const item of controls.$select) if (isAggregateExpr(item)) {
2157
- const fd = item.$fn === "min" || item.$fn === "max" ? meta.descriptorByPath.get(item.$field) : void 0;
2512
+ for (const item of controls.$select) if (isAggregateOfExpr(item) || isSelectArithExpr(item)) aliases.set(item.$as, NUMBER);
2513
+ else if (isAggregateExpr(item)) {
2514
+ const fd = SOURCE_VALUE_FNS.has(item.$fn) ? meta.descriptorByPath.get(item.$field) : void 0;
2158
2515
  const counts = item.$fn === "count" || item.$fn === "countDistinct";
2159
- aliases.set(resolveAlias(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
2160
- } else if (isBucketExpr(item)) aliases.set(resolveAlias(item), STRING);
2516
+ aliases.set(resolveAlias$1(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
2517
+ } else if (isBucketExpr(item)) aliases.set(resolveAlias$1(item), STRING);
2161
2518
  }
2162
2519
  walkFilterValues(controls.$having, (key) => {
2163
2520
  const alias = aliases.get(key);
@@ -2420,6 +2777,37 @@ function rejectSource(code, reason) {
2420
2777
  };
2421
2778
  }
2422
2779
  /**
2780
+ * Whether the stored leaf `fd` may be a `$groupBy` key (since 0.1.148) — the
2781
+ * one rule for the core's strict-table check and moost-db's capability index
2782
+ * (`/meta.fields[P].groupable` and the HTTP gate). First failing rule wins:
2783
+ *
2784
+ * 1. `encrypted` — `@db.encrypted`;
2785
+ * 2. `notFilterable` — the adapter cannot filter (so cannot group) the storage;
2786
+ * 3. `notDimension` — a strict table ({@link isStrictTable}) and the field is
2787
+ * not a dimension.
2788
+ *
2789
+ * A calendar-bucket alias is not a field: it groups by its source, whose
2790
+ * dimension rule is {@link bucketSourceVerdict}'s (callers skip aliases).
2791
+ */
2792
+ function groupSourceVerdict(fd, table, adapter) {
2793
+ if (fd.encrypted) return {
2794
+ ok: false,
2795
+ code: "encrypted",
2796
+ reason: ENCRYPTED_REASON
2797
+ };
2798
+ if (!adapter.canFilterField(fd)) return {
2799
+ ok: false,
2800
+ code: "notFilterable",
2801
+ reason: ADAPTER_FILTER_REASON
2802
+ };
2803
+ if (isStrictTable(table) && !table.dimensions.includes(fd.path)) return {
2804
+ ok: false,
2805
+ code: "notDimension",
2806
+ reason: NOT_DIMENSION_REASON
2807
+ };
2808
+ return { ok: true };
2809
+ }
2810
+ /**
2423
2811
  * Whether the stored leaf `fd` may be the source of a calendar bucket
2424
2812
  * (since 0.1.133) — every schema and adapter rule, once, for the core path
2425
2813
  * guard ({@link guardPath} op `bucket`) and moost-db's capability index
@@ -2446,7 +2834,7 @@ function bucketSourceVerdict(fd, table, adapter) {
2446
2834
  if (jsonAncestor !== void 0) return rejectSource("jsonDescendant", `inside JSON-stored column "${jsonAncestor}"`);
2447
2835
  if (!isBucketableField(fd)) return rejectSource("notTimestamp", "not a timestamp field (declare it number.timestamp)");
2448
2836
  if (!adapter.canFilterField(fd)) return rejectSource("notFilterable", ADAPTER_FILTER_REASON);
2449
- if (isStrictTable(table) && !table.dimensions.includes(fd.path)) return rejectSource("notDimension", "not a dimension");
2837
+ if (isStrictTable(table) && !table.dimensions.includes(fd.path)) return rejectSource("notDimension", NOT_DIMENSION_REASON);
2450
2838
  if (adapter.calendarBucketUnits().size === 0) return rejectSource("noBuckets", "adapter has no calendar buckets");
2451
2839
  return { ok: true };
2452
2840
  }
@@ -2478,8 +2866,9 @@ function collectFilterKeys(filter, push, skip, refs) {
2478
2866
  *
2479
2867
  * Aggregate mode is `aggregate` when given, else the presence of `$groupBy`.
2480
2868
  * In aggregate mode `$select` computed entries are collected by kind — an
2481
- * aggregate's `$field` into `aggregate`, a calendar bucket's into `bucket` —
2482
- * and their aliases (`$as`, else uniqu's `resolveAlias`) are exempted from
2869
+ * aggregate's `$field` (a row-level expression's names, a `first` / `last`
2870
+ * field) into `aggregate`, a calendar bucket's into `bucket`, a `$rowOrder`
2871
+ * key into `sort` — and their aliases (`$as`, else uniqu's `resolveAlias`) are exempted from
2483
2872
  * `$sort` / `$having`; a bucket alias is also dropped from `groupBy`, which
2484
2873
  * lists grouped fields only.
2485
2874
  *
@@ -2517,11 +2906,12 @@ function collectQueryPaths(query, aggregate) {
2517
2906
  if (Array.isArray(select)) {
2518
2907
  for (const item of select) if (typeof item === "string") refs.select.push(item);
2519
2908
  else if (!refs.aggregateMode) continue;
2520
- else if (isAggregateExpr(item)) {
2521
- aliases.add(resolveAlias(item));
2522
- if (item.$field !== "*") refs.aggregate.push(item.$field);
2523
- } else if (isBucketExpr(item)) {
2524
- const alias = resolveAlias(item);
2909
+ else if (isAggregateExpr(item) || isAggregateOfExpr(item)) {
2910
+ aliases.add(resolveAlias$1(item));
2911
+ refs.aggregate.push(...entryFields(item));
2912
+ } else if (isSelectArithExpr(item)) aliases.add(item.$as);
2913
+ else if (isBucketExpr(item)) {
2914
+ const alias = resolveAlias$1(item);
2525
2915
  aliases.add(alias);
2526
2916
  bucketAliases.add(alias);
2527
2917
  refs.bucket.push(item.$field);
@@ -2529,6 +2919,7 @@ function collectQueryPaths(query, aggregate) {
2529
2919
  } else if (select && typeof select === "object") refs.select.push(...Object.keys(select));
2530
2920
  refs.groupBy = bucketAliases.size > 0 ? groupBy.filter((name) => !bucketAliases.has(name)) : groupBy;
2531
2921
  for (const name of sortFieldNames(controls.$sort)) if (!aliases.has(name)) refs.sort.push(name);
2922
+ if (refs.aggregateMode) refs.sort.push(...rowOrderKeys(controls.$rowOrder));
2532
2923
  if (refs.aggregateMode) collectFilterKeys(controls.$having, (path) => refs.having.push(path), aliases, refs);
2533
2924
  return refs;
2534
2925
  }
@@ -2704,7 +3095,7 @@ function guardQuery(meta, adapter, query, state) {
2704
3095
  if (!query) return;
2705
3096
  guardFilter(meta, adapter, query.filter);
2706
3097
  guardSort(meta, query.controls?.$sort);
2707
- normalizeComputedSelect(query.controls, meta, false);
3098
+ resolveComputedSelect(query.controls, meta, false);
2708
3099
  guardPaths(meta, adapter, query, false, state);
2709
3100
  }
2710
3101
  /**
@@ -2733,30 +3124,45 @@ function checkHavingKeys(refs) {
2733
3124
  * ({@link checkHavingKeys} — after the path guard so an unknown key still
2734
3125
  * reads `Unknown field`), then the `$having` values (`guardHavingValues`).
2735
3126
  *
2736
- * `buckets` are the query's resolved calendar buckets when the caller already
2737
- * ran `normalizeComputedSelect` (resolved here otherwise).
3127
+ * `resolved` is the query's normalized computed `$select`
3128
+ * (`resolveComputedSelect`) when the caller already ran it (resolved here
3129
+ * otherwise).
2738
3130
  */
2739
3131
  function guardAggregate(meta, adapter, query, resolved) {
2740
- const buckets = resolved ?? normalizeComputedSelect(query.controls, meta, true);
3132
+ const computed = resolved ?? resolveComputedSelect(query.controls, meta, true);
3133
+ const { buckets } = computed;
2741
3134
  guardFilter(meta, adapter, query.filter);
2742
3135
  const controls = query.controls;
2743
3136
  if (meta.encryptedFields.size > 0) {
2744
3137
  for (const field of controls.$groupBy ?? []) if (isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "group by");
2745
- if (controls.$select) for (const item of controls.$select) {
2746
- const field = typeof item === "string" ? item : item.$field;
2747
- if (field !== "*" && isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "aggregate over");
3138
+ if (controls.$select) {
3139
+ for (const item of controls.$select) for (const field of entryFields(item)) if (isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "aggregate over");
2748
3140
  }
3141
+ for (const key of rowOrderKeys(controls.$rowOrder)) if (isEncryptedRef(meta, key)) throw encryptedRefError("ENC_FIELD_SORT", key, "sort by");
2749
3142
  if (controls.$having) guardFilter(meta, adapter, controls.$having, "ENC_FIELD_AGG");
2750
3143
  guardSort(meta, controls.$sort);
2751
3144
  }
2752
3145
  const refs = guardPaths(meta, adapter, query, true);
2753
3146
  guardAggregateFns(adapter, controls.$select);
3147
+ guardAggregateExpressions(adapter, controls.$select);
3148
+ checkAggregateExprs(meta, query, computed.exprs, computed.rowOrder);
2754
3149
  guardBucketUnits(adapter, buckets);
2755
3150
  const having = refs ? checkHavingKeys(refs) : void 0;
2756
3151
  if (having) throw new DbError("INVALID_QUERY", [having]);
2757
3152
  guardHavingValues(meta, controls);
2758
3153
  }
2759
3154
  /**
3155
+ * Rejects an arithmetic `$select` entry (`{ $expr }`, `{ $fn, $expr }`) on an
3156
+ * adapter whose `supportsAggregateExpressions()` is false with
3157
+ * `AGG_EXPR_NOT_SUPPORTED`, before anything is translated.
3158
+ *
3159
+ * @since 0.1.148
3160
+ */
3161
+ function guardAggregateExpressions(adapter, select) {
3162
+ if (!Array.isArray(select) || adapter.supportsAggregateExpressions()) return;
3163
+ for (const item of select) if (isAggregateOfExpr(item) || isSelectArithExpr(item)) throw aggregateExpressionsNotSupported();
3164
+ }
3165
+ /**
2760
3166
  * Rejects an aggregate whose (known — the normalizer checked the name)
2761
3167
  * function this adapter does not render (`aggregateFns()`) with
2762
3168
  * `AGG_FN_NOT_SUPPORTED`, before anything is translated — an adapter written
@@ -2937,8 +3343,11 @@ var AtscriptDbReadable = class {
2937
3343
  setEncryption(encryption) {
2938
3344
  this._encryption = encryption;
2939
3345
  }
3346
+ /** @internal Set by the owning `DbSpace` when it closes. */
3347
+ _spaceClosed = false;
2940
3348
  /** Ensures metadata is built. Called before any metadata access. */
2941
3349
  _ensureBuilt() {
3350
+ if (this._spaceClosed) throw spaceClosedError();
2942
3351
  if (!this._meta.isBuilt) {
2943
3352
  this._meta.build(this.type, this.adapter, this.logger);
2944
3353
  if (this._meta.navFields.size > 0 && this._tableResolver) {
@@ -3104,6 +3513,22 @@ var AtscriptDbReadable = class {
3104
3513
  this._ensureBuilt();
3105
3514
  return this._meta.primaryKeys;
3106
3515
  }
3516
+ /**
3517
+ * Physical column lists that must be unique: the primary key (when declared)
3518
+ * followed by every unique index and every adapter-contributed unique field. Used by conflict-ignoring inserts.
3519
+ * @since 0.1.148
3520
+ */
3521
+ get uniqueKeySets() {
3522
+ this._ensureBuilt();
3523
+ const sets = [];
3524
+ if (this._meta.primaryKeys.length > 0) sets.push(this._meta.primaryKeys.map((f) => this._meta.physicalPath(f)));
3525
+ for (const index of this._meta.indexes.values()) if (index.type === "unique") sets.push(index.fields.map((f) => f.name));
3526
+ for (const prop of this._meta.uniqueProps) {
3527
+ const column = this._meta.physicalPath(prop);
3528
+ if (!sets.some((s) => s.length === 1 && s[0] === column)) sets.push([column]);
3529
+ }
3530
+ return sets;
3531
+ }
3107
3532
  /** Preferred row identifier field names. Defaults to primary keys. */
3108
3533
  get preferredId() {
3109
3534
  this._ensureBuilt();
@@ -3303,8 +3728,10 @@ var AtscriptDbReadable = class {
3303
3728
  * work (the derived read plan on document adapters) is done once for all
3304
3729
  * of them. Every read path funnels through here.
3305
3730
  */
3306
- _fromRead(rows, controls) {
3307
- return this._fieldMapper.reconstructRows(rows, this._meta, controls);
3731
+ _fromRead(rows, controls, aliasFields) {
3732
+ const out = this._fieldMapper.reconstructRows(rows, this._meta, controls);
3733
+ if (aliasFields?.size) coerceAliasValues(out, aliasFields);
3734
+ return out;
3308
3735
  }
3309
3736
  /**
3310
3737
  * Translates a read query for the adapter. A `$select` that leaves out a
@@ -3564,7 +3991,8 @@ var AtscriptDbReadable = class {
3564
3991
  async aggregate(query) {
3565
3992
  this._ensureBuilt();
3566
3993
  const { $groupBy, $select } = query.controls;
3567
- const buckets = normalizeComputedSelect(query.controls, this._meta, true);
3994
+ const computed = resolveComputedSelect(query.controls, this._meta, true);
3995
+ const { buckets } = computed;
3568
3996
  if ($select) {
3569
3997
  const groupBySet = new Set($groupBy);
3570
3998
  for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new DbError("INVALID_QUERY", [{
@@ -3577,13 +4005,19 @@ var AtscriptDbReadable = class {
3577
4005
  const dimSet = new Set(dimensions);
3578
4006
  const measSet = new Set(measures);
3579
4007
  const bucketAliases = new Set(buckets.map((b) => b.alias));
3580
- for (const field of $groupBy) if (!dimSet.has(field) && !bucketAliases.has(field)) throw new DbError("INVALID_QUERY", [{
3581
- path: "$groupBy",
3582
- message: `Field "${field}" is not a dimension`
3583
- }]);
4008
+ const descriptors = new Map(this._meta.fieldDescriptors.map((fd) => [fd.path, fd]));
4009
+ for (const field of $groupBy) {
4010
+ if (bucketAliases.has(field)) continue;
4011
+ const fd = descriptors.get(field);
4012
+ const verdict = fd && groupSourceVerdict(fd, this._meta, this.adapter);
4013
+ if (verdict ? !verdict.ok && verdict.code === "notDimension" : !dimSet.has(field)) throw new DbError("INVALID_QUERY", [{
4014
+ path: "$groupBy",
4015
+ message: `Field "${field}" is not a dimension`
4016
+ }]);
4017
+ }
3584
4018
  if ($select) for (const item of $select) {
3585
4019
  if (!isAggregateExpr(item) || item.$field === "*" || measSet.has(item.$field)) continue;
3586
- if (item.$fn === "countDistinct") {
4020
+ if (item.$fn === "countDistinct" || isFirstLast(item)) {
3587
4021
  if (dimSet.has(item.$field)) continue;
3588
4022
  throw new DbError("INVALID_QUERY", [{
3589
4023
  path: "$select",
@@ -3613,10 +4047,24 @@ var AtscriptDbReadable = class {
3613
4047
  this._ensureSearchable();
3614
4048
  if (!searchTerm.trim()) return query.controls.$count ? [{ count: 0 }] : [];
3615
4049
  }
3616
- guardAggregate(this._meta, this.adapter, query, buckets);
3617
- const dbQuery = this._fieldMapper.translateAggregateQuery(query, this._meta, buckets);
4050
+ guardAggregate(this._meta, this.adapter, query, computed);
4051
+ const dbQuery = this._fieldMapper.translateAggregateQuery(query, this._meta, buckets, computed.exprs, computed.rowOrder);
3618
4052
  const results = await this.adapter.aggregate(dbQuery);
3619
- return this._fromRead(results, query.controls);
4053
+ return this._fromRead(results, query.controls, this._aliasFields($select));
4054
+ }
4055
+ /**
4056
+ * The computed aliases of one source field that is a boolean or a decimal —
4057
+ * `min` / `max` / `first` / `last` — with that field's descriptor: the
4058
+ * aggregate row's value is coerced like the column's own on read.
4059
+ */
4060
+ _aliasFields(select) {
4061
+ const out = /* @__PURE__ */ new Map();
4062
+ for (const item of select ?? []) {
4063
+ if (!isAggregateExpr(item) || !SOURCE_VALUE_FNS.has(item.$fn)) continue;
4064
+ const fd = this._meta.descriptorByPath.get(item.$field);
4065
+ if (fd && (fd.designType === "boolean" || fd.designType === "decimal")) out.set(resolveAlias(item), fd);
4066
+ }
4067
+ return out;
3620
4068
  }
3621
4069
  /** Whether the underlying adapter supports text search. */
3622
4070
  isSearchable() {
@@ -3630,6 +4078,10 @@ var AtscriptDbReadable = class {
3630
4078
  calendarBucketUnits() {
3631
4079
  return this.adapter.calendarBucketUnits();
3632
4080
  }
4081
+ /** Whether the adapter renders aggregate arithmetic (proxies adapter capability). @since 0.1.148 */
4082
+ supportsAggregateExpressions() {
4083
+ return this.adapter.supportsAggregateExpressions();
4084
+ }
3633
4085
  /** Aggregate functions the adapter renders (proxies adapter capability). @since 0.1.136 */
3634
4086
  aggregateFns() {
3635
4087
  return this.adapter.aggregateFns();
@@ -4038,7 +4490,7 @@ var AtscriptDbReadable = class {
4038
4490
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
4039
4491
  */
4040
4492
  async loadRelations(rows, withRelations) {
4041
- const { loadRelationsImpl } = await import("./relation-loader-C0qE8xgR.mjs").then((n) => n.n);
4493
+ const { loadRelationsImpl } = await import("./relation-loader-ByY1Byrl.mjs").then((n) => n.n);
4042
4494
  return loadRelationsImpl(rows, withRelations, this);
4043
4495
  }
4044
4496
  /**
@@ -4398,6 +4850,22 @@ var BaseDbAdapter = class {
4398
4850
  return BASE_AGGREGATE_FNS;
4399
4851
  }
4400
4852
  /**
4853
+ * Whether this adapter renders arithmetic in an aggregate `$select`
4854
+ * (`{ $expr }`, `{ $fn, $expr }`) — IEEE double, NULL propagating, `/` by
4855
+ * zero NULL, the same semantics as `@db.compute`. Default `false`
4856
+ * (fail-closed): the core rejects such a query with `AGG_EXPR_NOT_SUPPORTED`
4857
+ * before dispatch. An adapter returning `true` also receives
4858
+ * `controls.$select.exprAggregates` / `.exprs` (see `UniquSelect`), and
4859
+ * `first` / `last` (`aggregateFns()`) with `.firstLast` and `.rowOrder`;
4860
+ * the shared `evaluateExpr` evaluates an expression tree in process.
4861
+ * moost-db advertises it as `/meta.aggregateExpressions`.
4862
+ *
4863
+ * @since 0.1.148
4864
+ */
4865
+ supportsAggregateExpressions() {
4866
+ return false;
4867
+ }
4868
+ /**
4401
4869
  * Revision of how this adapter renders a managed view (its SQL / pipeline)
4402
4870
  * from an unchanged view definition. Stored in each managed view's sync
4403
4871
  * snapshot when defined, so bumping it recreates every managed view of the
@@ -4779,6 +5247,28 @@ var BaseDbAdapter = class {
4779
5247
  throw new Error("Aggregation not supported by this adapter");
4780
5248
  }
4781
5249
  /**
5250
+ * Conflict-ignoring batch insert (`insertMany(rows, { onConflict: 'ignore' })`).
5251
+ * Returns ONE SLOT PER INPUT ROW, in order: `{ insertedId }` for an inserted
5252
+ * row, `null` for a row skipped because it collided with a STORED row on the
5253
+ * primary key or a unique index. (The core already removed duplicates inside
5254
+ * the batch.) Only uniqueness collisions are skipped — NOT NULL, FK, check
5255
+ * and every other error must throw — and a skipped row must never abort the
5256
+ * surrounding transaction. Fail-closed by default: throws
5257
+ * `DbError("ON_CONFLICT_NOT_SUPPORTED")`; also override
5258
+ * {@link supportsInsertIgnore}.
5259
+ * @since 0.1.148
5260
+ */
5261
+ insertManyIgnore(_data) {
5262
+ return Promise.reject(new DbError("ON_CONFLICT_NOT_SUPPORTED", [{
5263
+ path: "",
5264
+ message: `The ${this.constructor.name} adapter does not support onConflict "ignore"`
5265
+ }]));
5266
+ }
5267
+ /** Whether {@link insertManyIgnore} is implemented (drives `crud.insert: ["onConflict"]` in `/meta`). @since 0.1.148 */
5268
+ supportsInsertIgnore() {
5269
+ return false;
5270
+ }
5271
+ /**
4782
5272
  * When true, the adapter can handle column type changes in-place
4783
5273
  * (e.g. MySQL's ALTER TABLE MODIFY COLUMN) without requiring table recreation.
4784
5274
  * The generic sync layer will delegate type changes to {@link syncColumns}
@@ -5561,43 +6051,40 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5561
6051
  }
5562
6052
  return this.validators.get(purpose);
5563
6053
  }
5564
- /**
5565
- * Inserts a single record. Delegates to {@link insertMany} for unified
5566
- * nested creation support.
5567
- */
5568
6054
  async insertOne(payload, opts) {
5569
- return { insertedId: (await this.insertMany([payload], {
6055
+ const result = await this._insertMany([payload], {
5570
6056
  ...opts,
5571
6057
  _action: "insert"
5572
- })).insertedIds[0] };
6058
+ });
6059
+ if ("conflicts" in result) {
6060
+ const conflict = result.conflicts.length > 0;
6061
+ return conflict ? { conflict } : {
6062
+ insertedId: result.insertedIds[0],
6063
+ conflict
6064
+ };
6065
+ }
6066
+ return { insertedId: result.insertedIds[0] };
5573
6067
  }
5574
- /**
5575
- * Inserts multiple records with batch-optimized nested creation.
5576
- *
5577
- * Supports **nested creation**: if payloads include data for navigation
5578
- * fields (`@db.rel.to` / `@db.rel.from`), related records are created
5579
- * automatically in batches. TO dependencies are batch-created first
5580
- * (their PKs become our FKs), FROM dependents are batch-created after
5581
- * (they receive our PKs as their FKs). Fully recursive — nested records
5582
- * with their own nav data trigger further batch inserts at each level.
5583
- * Recursive up to `maxDepth` (default 3).
5584
- *
5585
- * `opts.guard` (since 0.1.128) runs once inside the transaction, after
5586
- * defaults + validation, with the prepared rows; `opts.check` (since
5587
- * 0.1.143) runs once after every phase with the inserted rows' primary-key
5588
- * filters — see {@link TWriteOptions}.
5589
- */
5590
6068
  async insertMany(payloads, opts) {
6069
+ return this._insertMany(payloads, opts);
6070
+ }
6071
+ async _insertMany(payloads, opts) {
5591
6072
  this._ensureBuilt();
5592
- const { _depth, _action, maxDepth: userMax, guard, check } = opts ?? {};
6073
+ const { _depth, _action, maxDepth: userMax, guard, check, onConflict } = opts ?? {};
6074
+ if (onConflict !== void 0 && onConflict !== "error" && onConflict !== "ignore") throw new DbError("INVALID_QUERY", [{
6075
+ path: "onConflict",
6076
+ message: `Unknown onConflict mode "${String(onConflict)}"`
6077
+ }]);
6078
+ const ignore = onConflict === "ignore";
5593
6079
  const maxDepth = userMax ?? 3;
5594
6080
  const depth = _depth ?? 0;
5595
6081
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
5596
6082
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
5597
6083
  return enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
5598
6084
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
5599
- const items = payloads.map((p) => this._applyDefaults(clone(p)));
5600
- const originals = canNest ? items.map((item) => ({ ...item })) : [];
6085
+ let items = payloads.map((p) => this._applyDefaults(clone(p)));
6086
+ if (ignore) this._linkToByKeyInIgnoreMode(items);
6087
+ let originals = canNest ? items.map((item) => ({ ...item })) : [];
5601
6088
  const validator = this.getValidator("insert");
5602
6089
  const ctx = {
5603
6090
  mode: "insert",
@@ -5610,18 +6097,28 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5610
6097
  validateBatch(validator, items, ctx);
5611
6098
  }
5612
6099
  await this._encryptItems(items, "write");
6100
+ if (ignore) this._rejectNestedToInIgnoreMode(items);
5613
6101
  const host = this;
5614
6102
  if (canNest) await batchInsertNestedTo(host, items, maxDepth, depth);
5615
- const prepared = [];
6103
+ let prepared = [];
5616
6104
  for (const data of items) {
5617
6105
  for (const navField of this._meta.navFields) delete data[navField];
5618
6106
  prepared.push(this._fieldMapper.prepareForWrite(data, this._meta, this.adapter));
5619
6107
  }
5620
6108
  await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
5621
6109
  if (canNest) await preValidateNestedFrom(host, originals);
5622
- const result = await this.adapter.insertMany(prepared);
5623
- if (canNest) await batchInsertNestedFrom(host, originals, result.insertedIds, maxDepth, depth);
5624
- if (canNest) await batchInsertNestedVia(host, originals, result.insertedIds, maxDepth, depth);
6110
+ let ignored;
6111
+ if (ignore) {
6112
+ ignored = await this._insertIgnoring(prepared);
6113
+ const keep = ignored.inserted;
6114
+ items = keep.map((i) => items[i]);
6115
+ originals = canNest ? keep.map((i) => originals[i]) : originals;
6116
+ prepared = keep.map((i) => prepared[i]);
6117
+ }
6118
+ const result = ignored ?? await this.adapter.insertMany(prepared);
6119
+ const parentIds = result.insertedIds;
6120
+ if (canNest) await batchInsertNestedFrom(host, originals, parentIds, maxDepth, depth);
6121
+ if (canNest) await batchInsertNestedVia(host, originals, parentIds, maxDepth, depth);
5625
6122
  if (check) await this._runWriteCheck(check, _action ?? "insertMany", this._insertedPkFilters(items, prepared, result.insertedIds));
5626
6123
  return result;
5627
6124
  }));
@@ -6182,6 +6679,91 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
6182
6679
  });
6183
6680
  }
6184
6681
  /**
6682
+ * Ignore mode never writes a related parent — it would be orphaned when the
6683
+ * row is skipped. A TO object that names only the target's key fields
6684
+ * (`{ org: { id: 9 } }`) creates nothing: it is a reference to an existing
6685
+ * parent, so it becomes the row's foreign key (the existence check follows
6686
+ * with the other FK validation). Runs before validation (which would demand
6687
+ * the parent's required fields); any other TO object is left for
6688
+ * {@link _rejectNestedToInIgnoreMode}.
6689
+ */
6690
+ _linkToByKeyInIgnoreMode(items) {
6691
+ for (const [navField, relation] of this._meta.relations) {
6692
+ if (relation.direction !== "to") continue;
6693
+ const fk = this._findFKForRelation(relation);
6694
+ if (fk === void 0) continue;
6695
+ for (const item of items) {
6696
+ const nav = item[navField];
6697
+ if (nav === void 0 || nav === null || typeof nav !== "object" || Array.isArray(nav)) continue;
6698
+ const target = nav;
6699
+ const keys = Object.keys(target);
6700
+ if (!(keys.length === fk.targetFields.length && fk.targetFields.every((f) => keys.includes(f) && target[f] != null))) continue;
6701
+ fk.localFields.forEach((local, i) => {
6702
+ const value = target[fk.targetFields[i]];
6703
+ if (item[local] !== void 0 && item[local] !== null && !sameKey(item[local], value)) throw new DbError("INVALID_QUERY", [{
6704
+ path: navField,
6705
+ message: `"${navField}" and "${local}" name different parents`
6706
+ }]);
6707
+ item[local] = value;
6708
+ });
6709
+ delete item[navField];
6710
+ }
6711
+ }
6712
+ }
6713
+ /** Ignore mode: whatever TO object is left would create a related parent (see {@link _linkToByKeyInIgnoreMode}). */
6714
+ _rejectNestedToInIgnoreMode(items) {
6715
+ for (const [navField, relation] of this._meta.relations) {
6716
+ if (relation.direction !== "to") continue;
6717
+ if (items.some((item) => item[navField] !== void 0 && item[navField] !== null)) throw new DbError("INVALID_QUERY", [{
6718
+ path: navField,
6719
+ message: "onConflict \"ignore\" cannot create a related parent record — insert it first or reference it by key"
6720
+ }]);
6721
+ }
6722
+ }
6723
+ /**
6724
+ * Conflict-ignoring main insert: marks rows repeating an earlier row's
6725
+ * primary / unique key tuple (NULL components never collide), sends the rest
6726
+ * to the adapter and assembles one slot per input row.
6727
+ */
6728
+ async _insertIgnoring(prepared) {
6729
+ const keySets = this.uniqueKeySets;
6730
+ const seen = keySets.map(() => /* @__PURE__ */ new Set());
6731
+ const conflictAt = /* @__PURE__ */ new Set();
6732
+ const send = [];
6733
+ for (let i = 0; i < prepared.length; i++) {
6734
+ const row = prepared[i];
6735
+ const tuples = keySets.map((fields) => uniqueKeyTuple(row, fields));
6736
+ if (tuples.some((t, k) => t !== void 0 && seen[k].has(t))) {
6737
+ conflictAt.add(i);
6738
+ continue;
6739
+ }
6740
+ tuples.forEach((t, k) => {
6741
+ if (t !== void 0) seen[k].add(t);
6742
+ });
6743
+ send.push(i);
6744
+ }
6745
+ const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i])) : [];
6746
+ if (slots.length !== send.length) throw new DbError("INVALID_QUERY", [{
6747
+ path: "",
6748
+ message: "Adapter insertManyIgnore must return one slot per input row"
6749
+ }]);
6750
+ const insertedIds = [];
6751
+ const inserted = [];
6752
+ send.forEach((rowIndex, k) => {
6753
+ const slot = slots[k];
6754
+ if (slot) {
6755
+ inserted.push(rowIndex);
6756
+ insertedIds.push(slot.insertedId);
6757
+ } else conflictAt.add(rowIndex);
6758
+ });
6759
+ return {
6760
+ insertedCount: insertedIds.length,
6761
+ insertedIds,
6762
+ inserted,
6763
+ conflicts: [...conflictAt].toSorted((a, b) => a - b)
6764
+ };
6765
+ }
6766
+ /**
6185
6767
  * The exact primary-key filter of each inserted row: the logical key from
6186
6768
  * the row (SDK defaults applied), else the stored key the adapter wrote into
6187
6769
  * the prepared row (e.g. a driver-assigned `_id`), else — single-field keys
@@ -7265,4 +7847,4 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
7265
7847
  return diff;
7266
7848
  }
7267
7849
  //#endregion
7268
- export { UniquSelect as $, geoIndexNotFoundMessage as A, classifyQueryPath as B, ALL_VIEW_CAPABILITIES as C, NativeIntegrity as D, IntegrityStrategy as E, acceptedOperatorsHint as F, guardPaths as G, guardAggregate as H, assertGeoPoint as I, sortFieldNames as J, guardQuery as K, bucketSourceVerdict as L, vectorIndexNotFoundMessage as M, ADAPTER_FILTER_REASON as N, AtscriptDbReadable as O, ENCRYPTED_REASON as P, FieldMappingStrategy as Q, canFilterLeaf as R, ALL_BUCKET_UNITS as S, createFailureCollector as T, guardFilter as U, collectQueryPaths as V, guardPath as W, RelationalFieldMapper as X, unsupportedOperatorMessage as Y, DocumentFieldMapper as Z, isViewType as _, computeTableHash as a, isJsonValueField as at, decomposePatch as b, snapshotToExistingColumns as c, resolveCalendarBuckets as ct, computeForeignKeyDiff as d, TableMetadata as et, fkColumns as f, isAtscriptDbView as g, AtscriptDbView as h, computeSchemaHash as i, isBucketableField as it, searchIndexNotFoundMessage as j, resolveDesignType as k, snapshotToExistingTableOptions as l, NoopLogger as lt, hasForeignKeyChanges as m, isColumnTypeChanged as n, isGeoPointType as nt, computeTableSnapshot as o, jsonValueAncestor as ot, fkKey as p, narrowerFilterOps as q, canonicalizeQueryNode as r, aliasTargetOf as rt, computeViewSnapshot as s, normalizeComputedSelect as st, computeColumnDiff as t, isGeoIndexableType as tt, viewSnapshotSources as u, AtscriptDbTable as v, BaseDbAdapter as w, ApplicationIntegrity as x, assertNoVersionWrites as y, checkHavingKeys as z };
7850
+ export { FieldMappingStrategy as $, geoIndexNotFoundMessage as A, classifyQueryPath as B, ALL_VIEW_CAPABILITIES as C, NativeIntegrity as D, IntegrityStrategy as E, acceptedOperatorsHint as F, guardPath as G, groupSourceVerdict as H, assertGeoPoint as I, narrowerFilterOps as J, guardPaths as K, bucketSourceVerdict as L, vectorIndexNotFoundMessage as M, ADAPTER_FILTER_REASON as N, AtscriptDbReadable as O, ENCRYPTED_REASON as P, DocumentFieldMapper as Q, canFilterLeaf as R, ALL_BUCKET_UNITS as S, createFailureCollector as T, guardAggregate as U, collectQueryPaths as V, guardFilter as W, unsupportedOperatorMessage as X, sortFieldNames as Y, RelationalFieldMapper as Z, isViewType as _, computeTableHash as a, aliasTargetOf as at, decomposePatch as b, snapshotToExistingColumns as c, jsonValueAncestor as ct, computeForeignKeyDiff as d, NoopLogger as dt, UniquSelect as et, fkColumns as f, isAtscriptDbView as g, AtscriptDbView as h, computeSchemaHash as i, isGeoPointType as it, searchIndexNotFoundMessage as j, resolveDesignType as k, snapshotToExistingTableOptions as l, normalizeComputedSelect as lt, hasForeignKeyChanges as m, isColumnTypeChanged as n, TableMetadata as nt, computeTableSnapshot as o, isBucketableField as ot, fkKey as p, guardQuery as q, canonicalizeQueryNode as r, isGeoIndexableType as rt, computeViewSnapshot as s, isJsonValueField as st, computeColumnDiff as t, numericOperandProblem as tt, viewSnapshotSources as u, resolveCalendarBuckets as ut, AtscriptDbTable as v, BaseDbAdapter as w, ApplicationIntegrity as x, assertNoVersionWrites as y, checkHavingKeys as z };