@atscript/db 0.1.146 → 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 (59) 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-CGBv3E8S.cjs → aggregate-fns-C-UJRobm.cjs} +52 -5
  8. package/dist/{aggregate-fns-CfsveE1w.mjs → aggregate-fns-CyaZyb9I.mjs} +35 -6
  9. package/dist/aggregate-rules-D_bsCpUI.cjs +26 -0
  10. package/dist/aggregate-rules-jdPrxqWa.mjs +15 -0
  11. package/dist/{buckets-DRycmhOW.d.mts → buckets-CNdTOnei.d.mts} +832 -33
  12. package/dist/{buckets-DYFu0eZ8.d.cts → buckets-DJiYlMXc.d.cts} +832 -33
  13. package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-CUU4GvYg.cjs} +1546 -193
  14. package/dist/{column-diff-e2oHc71_.mjs → column-diff-Cp6ZoyRE.mjs} +1529 -200
  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/{column-diff-Q9UmWn5x.d.mts → fk-diff-BQ4krij8.d.cts} +48 -2
  18. package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DsaIijVX.d.mts} +48 -2
  19. package/dist/index.cjs +95 -35
  20. package/dist/index.d.cts +92 -22
  21. package/dist/index.d.mts +92 -22
  22. package/dist/index.mjs +65 -35
  23. package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-BUQvk6QT.cjs} +734 -2
  24. package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-CUBoq1ZO.mjs} +598 -4
  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 +293 -5
  30. package/dist/plugin.mjs +294 -6
  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-B-0NRKat.d.mts → relation-helpers-Ba0v49sn.d.mts} +1 -1
  36. package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-kX7jjgME.d.cts} +1 -1
  37. package/dist/relation-loader-ByY1Byrl.mjs +370 -0
  38. package/dist/relation-loader-D8OrdH-r.cjs +369 -0
  39. package/dist/shared.cjs +4 -1
  40. package/dist/shared.d.cts +16 -4
  41. package/dist/shared.d.mts +16 -4
  42. package/dist/shared.mjs +3 -2
  43. package/dist/sync.cjs +11 -7
  44. package/dist/sync.d.cts +2 -25
  45. package/dist/sync.d.mts +2 -25
  46. package/dist/sync.mjs +11 -7
  47. package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
  48. package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
  49. package/dist/{validator-Clu2q_7z.mjs → validator-BTiIOTKP.mjs} +9 -3
  50. package/dist/{validator-CVS-onRg.cjs → validator-UcuJxHNT.cjs} +8 -2
  51. package/dist/{validator-Bw6ks9Hy.d.mts → validator-tBNvM1qc.d.cts} +31 -7
  52. package/dist/{validator-Bw6ks9Hy.d.cts → validator-tBNvM1qc.d.mts} +31 -7
  53. package/dist/validator.cjs +2 -2
  54. package/dist/validator.d.cts +1 -1
  55. package/dist/validator.d.mts +1 -1
  56. package/dist/validator.mjs +2 -2
  57. package/package.json +8 -8
  58. package/dist/relation-loader-CBPY6kM7.cjs +0 -461
  59. package/dist/relation-loader-D9XuXaMv.mjs +0 -462
@@ -1,13 +1,15 @@
1
- import { n as CasMismatchError, r as DbError } from "./db-error-D5uilS_A.mjs";
2
- import { i as SUPPORTED_AGGREGATE_FNS, n as BASE_AGGREGATE_FNS } from "./aggregate-fns-CfsveE1w.mjs";
3
- import { C as findRemoteFK, S as findFKForRelation, T 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, 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, x as findFKEntryForRelation, y as rowMatchesKey } from "./nested-writer-CnOOAehr.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, 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
@@ -471,6 +499,14 @@ var TableMetadata = class {
471
499
  jsonValueParents = /* @__PURE__ */ new Set();
472
500
  /** Every field descriptor's `physicalName` — reserved names a bucket alias may not take. */
473
501
  physicalNames = /* @__PURE__ */ new Set();
502
+ /**
503
+ * Resolves / guards relational filter predicates (`{ nav: { $some: … } }`)
504
+ * against the related tables — installed by the owning readable when the
505
+ * table has navigation fields and a table resolver (a `DbSpace`). The field
506
+ * mappers and the path guard reach the related tables through it.
507
+ * @since 0.1.147
508
+ */
509
+ relationFilters;
474
510
  _built = false;
475
511
  _identifications;
476
512
  _alwaysAddressable;
@@ -552,7 +588,7 @@ var TableMetadata = class {
552
588
  if (!this.nestedObjects) this._classifyFields();
553
589
  const overrides = adapter.getMetadataOverrides?.(this);
554
590
  if (overrides) this._applyOverrides(overrides);
555
- this._buildFieldDescriptors(adapter);
591
+ this._buildFieldDescriptors(adapter, type);
556
592
  this._buildGuardIndexes();
557
593
  if (!this.nestedObjects) this._buildLeafIndexes();
558
594
  this._buildIdentifications();
@@ -615,12 +651,15 @@ var TableMetadata = class {
615
651
  const isArr = fieldType.type.kind === "array";
616
652
  const elementType = isArr ? fieldType.type.of : fieldType;
617
653
  const resolveTarget = () => elementType?.ref?.type() ?? elementType;
654
+ const relFilter = metadata.get("db.rel.filter");
618
655
  this.relations.set(fieldName, {
619
656
  direction,
620
657
  alias,
621
658
  targetType: resolveTarget,
622
659
  isArray: isArr,
623
- ...direction === "via" ? { viaType: raw } : {}
660
+ ...direction === "via" ? { viaType: raw } : {},
661
+ ...metadata.has("db.rel.filterable") ? { filterable: true } : {},
662
+ ...relFilter ? { filter: relFilter } : {}
624
663
  });
625
664
  }
626
665
  if (metadata.has("db.rel.FK")) {
@@ -878,7 +917,7 @@ var TableMetadata = class {
878
917
  * Called once during build() — everything it needs
879
918
  * (flatMap, indexes, columnMap, etc.) is already populated.
880
919
  */
881
- _buildFieldDescriptors(adapter) {
920
+ _buildFieldDescriptors(adapter, rootType) {
882
921
  const descriptors = [];
883
922
  const skipFlattening = this.nestedObjects;
884
923
  const indexedFields = new Set([...this.primaryKeys, ...this.uniqueProps]);
@@ -929,10 +968,21 @@ var TableMetadata = class {
929
968
  unitRefField,
930
969
  encrypted: isEncrypted || underEncrypted || void 0,
931
970
  isGeoPoint: isGeoPointType(type) || void 0,
932
- derived: this.derivedFields.get(path)
971
+ derived: this.derivedFields.get(path),
972
+ computed: computedMeta(rootType, path)
933
973
  });
934
974
  }
935
- this._resolveFkTargetFields(descriptors);
975
+ const flatCache = /* @__PURE__ */ new Map();
976
+ const flatOf = (type) => {
977
+ let flat = flatCache.get(type);
978
+ if (!flat) {
979
+ flat = flattenAnnotatedType(type);
980
+ flatCache.set(type, flat);
981
+ }
982
+ return flat;
983
+ };
984
+ this._resolveFkTargetFields(descriptors, flatOf);
985
+ this._resolveFkPhysicalFields(flatOf);
936
986
  Object.freeze(descriptors);
937
987
  this.fieldDescriptors = descriptors;
938
988
  this.columnDescriptors = Object.freeze(descriptors.filter((fd) => !fd.ignored && !(skipFlattening && fd.derived)));
@@ -956,9 +1006,37 @@ var TableMetadata = class {
956
1006
  }
957
1007
  }
958
1008
  /**
1009
+ * Fills `physicalFields` / `physicalTargetFields` on every FK: the local
1010
+ * side from this table's path maps, the target side from the referenced
1011
+ * type's own `@db.column` renames (same storage rules as this table — a
1012
+ * dotted target path is a flattened column on relational storage, a
1013
+ * renamed top-level key on document storage).
1014
+ */
1015
+ _resolveFkPhysicalFields(flatOf) {
1016
+ const targetPhysical = (fk, field) => {
1017
+ const targetType = fk.targetTypeRef?.();
1018
+ if (!targetType) return field;
1019
+ const flat = flatOf(targetType);
1020
+ const columnOf = (path) => flat.get(path)?.metadata?.get("db.column");
1021
+ if (this.nestedObjects) {
1022
+ const dot = field.indexOf(".");
1023
+ const top = dot === -1 ? field : field.slice(0, dot);
1024
+ const renamed = columnOf(top);
1025
+ return renamed === void 0 ? field : renamed + field.slice(top.length);
1026
+ }
1027
+ return relationalColumnName(field, columnOf(field), field.includes("."));
1028
+ };
1029
+ for (const fk of this.foreignKeys.values()) {
1030
+ fk.physicalFields = fk.fields.map((f) => this.physicalPath(f));
1031
+ fk.physicalTargetFields = fk.targetFields.map((f) => targetPhysical(fk, f));
1032
+ const targetSchema = fk.targetTypeRef?.()?.metadata?.get("db.schema");
1033
+ if (targetSchema) fk.targetSchema = targetSchema;
1034
+ }
1035
+ }
1036
+ /**
959
1037
  * Resolves `fkTargetField` for FK fields in field descriptors.
960
1038
  */
961
- _resolveFkTargetFields(descriptors) {
1039
+ _resolveFkTargetFields(descriptors, flatOf) {
962
1040
  if (this.foreignKeys.size === 0) return;
963
1041
  const fkFieldToTarget = /* @__PURE__ */ new Map();
964
1042
  for (const fk of this.foreignKeys.values()) {
@@ -969,18 +1047,12 @@ var TableMetadata = class {
969
1047
  });
970
1048
  }
971
1049
  if (fkFieldToTarget.size === 0) return;
972
- const flatCache = /* @__PURE__ */ new Map();
973
1050
  for (const descriptor of descriptors) {
974
1051
  const target = fkFieldToTarget.get(descriptor.path);
975
1052
  if (!target) continue;
976
1053
  const targetType = target.targetTypeRef();
977
1054
  if (!targetType) continue;
978
- let targetFlatMap = flatCache.get(targetType);
979
- if (!targetFlatMap) {
980
- targetFlatMap = flattenAnnotatedType(targetType);
981
- flatCache.set(targetType, targetFlatMap);
982
- }
983
- const targetFieldType = targetFlatMap.get(target.targetField);
1055
+ const targetFieldType = flatOf(targetType).get(target.targetField);
984
1056
  if (!targetFieldType) continue;
985
1057
  const targetMetadata = targetFieldType.metadata;
986
1058
  if (targetMetadata?.has("db.encrypted")) throw new Error(`FK field "${descriptor.path}" references encrypted field "${target.targetField}" — joins are impossible over ciphertext`);
@@ -1072,6 +1144,203 @@ var TableMetadata = class {
1072
1144
  this.preferredId = selected ? [...selected.fields] : [...this.primaryKeys];
1073
1145
  }
1074
1146
  };
1147
+ /** `TDbFieldMeta.computed` of a top-level `@db.compute` view field (since 0.1.147). */
1148
+ function computedMeta(rootType, path) {
1149
+ if (path.includes(".")) return void 0;
1150
+ const computed = computedOperands(rootType, path);
1151
+ return computed ? {
1152
+ operands: Object.freeze(computed.operands),
1153
+ via: Object.freeze(computed.via)
1154
+ } : void 0;
1155
+ }
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
+ }
1075
1344
  //#endregion
1076
1345
  //#region src/query/uniqu-select.ts
1077
1346
  /**
@@ -1084,8 +1353,9 @@ var TableMetadata = class {
1084
1353
  * For exclusion → inclusion inversion, pass `allFields` (physical field names).
1085
1354
  *
1086
1355
  * An array `$select` holds plain field names and computed entries —
1087
- * aggregates (`{ $fn, $field }`, {@link aggregates}) and calendar buckets
1088
- * (`{ $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
1089
1359
  * (`normalizeComputedSelect` rejects any other shape before translation).
1090
1360
  */
1091
1361
  var UniquSelect = class UniquSelect {
@@ -1094,7 +1364,6 @@ var UniquSelect = class UniquSelect {
1094
1364
  _allFields;
1095
1365
  _array = UniquSelect.UNRESOLVED;
1096
1366
  _projection = UniquSelect.UNRESOLVED;
1097
- _aggregates = UniquSelect.UNRESOLVED;
1098
1367
  /**
1099
1368
  * The calendar buckets of an aggregate `$select`, normalized (canonical
1100
1369
  * zone, week start, alias) with the PHYSICAL source `field` and its
@@ -1104,16 +1373,80 @@ var UniquSelect = class UniquSelect {
1104
1373
  */
1105
1374
  buckets;
1106
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
+ /**
1107
1417
  * @param raw - the `$select` value (field paths already physical).
1108
1418
  * @param allFields - physical field names, for exclusion-form inversion.
1109
1419
  * @param buckets - the resolved calendar buckets of the raw `$select`'s
1110
1420
  * `{ $bucket }` entries (the field mappers supply them — physical `field`,
1111
1421
  * source `fd`).
1422
+ * @param computed - the resolved arithmetic and `$rowOrder` parts (physical).
1112
1423
  */
1113
- constructor(raw, allFields, buckets) {
1424
+ constructor(raw, allFields, buckets, computed) {
1114
1425
  this._raw = raw;
1115
1426
  this._allFields = allFields;
1116
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
+ ];
1117
1450
  }
1118
1451
  /**
1119
1452
  * Resolved inclusion array of plain field names (strings only).
@@ -1162,20 +1495,6 @@ var UniquSelect = class UniquSelect {
1162
1495
  this._projection = result;
1163
1496
  return this._projection;
1164
1497
  }
1165
- /**
1166
- * Extracts AggregateExpr entries from array-form $select.
1167
- * Returns undefined if no aggregates present or if $select is object form.
1168
- */
1169
- get aggregates() {
1170
- if (this._aggregates !== UniquSelect.UNRESOLVED) return this._aggregates;
1171
- if (!Array.isArray(this._raw)) {
1172
- this._aggregates = void 0;
1173
- return;
1174
- }
1175
- const aggs = this._raw.filter(isAggregateExpr);
1176
- this._aggregates = aggs.length > 0 ? aggs : void 0;
1177
- return this._aggregates;
1178
- }
1179
1498
  /** Whether the $select contains any AggregateExpr entries. */
1180
1499
  get hasAggregates() {
1181
1500
  return !!this.aggregates?.length;
@@ -1265,6 +1584,15 @@ function toBool(value) {
1265
1584
  if (value === null || value === void 0) return value;
1266
1585
  return !!value;
1267
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
+ }
1268
1596
  function toDecimalString(value) {
1269
1597
  if (value === null || value === void 0) return value;
1270
1598
  if (typeof value === "string") return value;
@@ -1303,9 +1631,13 @@ var FieldMappingStrategy = class {
1303
1631
  * `buckets` are the query's calendar buckets as the core's normalizer
1304
1632
  * resolved them (`normalizeComputedSelect` — `AtscriptDbReadable.aggregate`
1305
1633
  * runs it before the guards); they reach adapters with `field` made
1306
- * 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).
1307
1639
  */
1308
- translateAggregateQuery(query, meta, buckets) {
1640
+ translateAggregateQuery(query, meta, buckets, exprs = [], rowOrder) {
1309
1641
  const controls = query.controls;
1310
1642
  const aliases = this.computedAliasSet(controls.$select);
1311
1643
  const physicalBuckets = buckets.map((b) => ({
@@ -1314,26 +1646,102 @@ var FieldMappingStrategy = class {
1314
1646
  fd: meta.descriptorByPath.get(b.field)
1315
1647
  }));
1316
1648
  const select = controls.$select && this.physicalSelect(controls.$select, meta);
1649
+ const computed = this.physicalComputed(meta, aliases, exprs, rowOrder, controls.$select);
1317
1650
  return {
1318
1651
  filter: this.translateFilter(query.filter ?? {}, meta),
1319
1652
  controls: {
1320
1653
  ...controls,
1321
1654
  $with: void 0,
1655
+ $rowOrder: void 0,
1322
1656
  $groupBy: this.renamesPaths(meta) ? controls.$groupBy.map((key) => aliases.has(key) ? key : this.physicalPath(key, meta)) : controls.$groupBy,
1323
- $select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets) : void 0,
1657
+ $select: select ? new UniquSelect(select, meta.allPhysicalFields, physicalBuckets, computed) : void 0,
1324
1658
  $sort: controls.$sort && this.physicalSort(controls.$sort, meta, aliases),
1325
- $having: controls.$having ? this.translateFilter(controls.$having, meta) : void 0
1659
+ $having: controls.$having ? this.translateHaving(controls.$having, meta, aliases) : void 0
1326
1660
  },
1327
1661
  insights: query.insights
1328
1662
  };
1329
1663
  }
1330
- /** 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). */
1331
1683
  computedAliasSet(select) {
1332
1684
  const aliases = /* @__PURE__ */ new Set();
1333
- 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);
1334
1687
  return aliases;
1335
1688
  }
1336
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
+ /**
1337
1745
  * `$select` with its field paths made physical: array-form names and
1338
1746
  * computed `$field`s (`'*'` kept), or the keys of the object
1339
1747
  * (inclusion / exclusion) form. An aggregate's output alias is fixed
@@ -1351,7 +1759,7 @@ var FieldMappingStrategy = class {
1351
1759
  if (physical === item.$field) return item;
1352
1760
  return isAggregateExpr(item) ? {
1353
1761
  ...item,
1354
- $as: resolveAlias(item),
1762
+ $as: resolveAlias$1(item),
1355
1763
  $field: physical
1356
1764
  } : {
1357
1765
  ...item,
@@ -1372,19 +1780,43 @@ var FieldMappingStrategy = class {
1372
1780
  return translated;
1373
1781
  }
1374
1782
  /**
1375
- * Recursively walks a filter expression, applying `@db.column` key renames
1376
- * (document paths — {@link TableMetadata.documentPath}) and adapter-specific
1377
- * value formatting via `formatFilterValue`.
1783
+ * Translates a logical filter for the adapter: relational predicates are
1784
+ * resolved first (`resolveRelationFilterTree`), then every key and value
1785
+ * goes through {@link translateResolvedFilter}. `depth` is the predicate
1786
+ * level of `filter` itself (0 for a query's own filter; the related tables
1787
+ * translate predicate operands at deeper levels).
1788
+ */
1789
+ translateFilter(filter, meta, depth = 0) {
1790
+ const has = containsRelationFilter(filter);
1791
+ const resolved = has ? resolveRelationFilterTree(filter, meta, depth) : filter;
1792
+ return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
1793
+ }
1794
+ /**
1795
+ * `out` — the translation of the caller's `filter` — with its pre-scan
1796
+ * result (`has`) cached for the adapter's repeated `containsRelationFilter`
1797
+ * checks; only when the core built it (never the caller's own object).
1798
+ */
1799
+ noteTranslated(filter, out, has) {
1800
+ if (out !== filter) noteRelationFilter(out, has);
1801
+ return out;
1802
+ }
1803
+ /**
1804
+ * Recursively walks a filter expression (predicates already resolved),
1805
+ * applying `@db.column` key renames (document paths —
1806
+ * {@link TableMetadata.documentPath}) and adapter-specific value formatting
1807
+ * via `formatFilterValue`. A resolved predicate passes through under its
1808
+ * navigation-field key.
1378
1809
  *
1379
1810
  * The relational mapper overrides this to use `leafByLogical` for deeper
1380
1811
  * key resolution (flattened nested paths).
1381
1812
  */
1382
- translateFilter(filter, meta) {
1813
+ translateResolvedFilter(filter, meta) {
1383
1814
  if (!filter || typeof filter !== "object") return filter;
1384
1815
  if (!meta.toStorageFormatters && meta.columnMap.size === 0 && meta.derivedFields.size === 0) return filter;
1385
1816
  const result = {};
1386
- for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilter(f, meta));
1387
- else if (key === "$not") result[key] = this.translateFilter(value, meta);
1817
+ for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateResolvedFilter(f, meta));
1818
+ else if (key === "$not") result[key] = this.translateResolvedFilter(value, meta);
1819
+ else if (meta.navFields.has(key)) result[key] = value;
1388
1820
  else if (key.startsWith("$")) result[key] = value;
1389
1821
  else {
1390
1822
  const physical = this.physicalPath(key, meta);
@@ -1686,10 +2118,13 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1686
2118
  return result;
1687
2119
  }
1688
2120
  translateQuery(query, meta) {
2121
+ const logical = query.filter;
2122
+ const has = containsRelationFilter(logical);
2123
+ const filter = has ? resolveRelationFilterTree(logical, meta, 0) : logical;
1689
2124
  if (!meta.requiresMappings) {
1690
2125
  const controls = query.controls;
1691
2126
  return {
1692
- filter: meta.toStorageFormatters ? this.translateFilter(query.filter, meta) : query.filter,
2127
+ filter: meta.toStorageFormatters ? this.noteTranslated(logical, this.translateResolvedFilter(filter, meta), has) : filter,
1693
2128
  controls: {
1694
2129
  ...controls,
1695
2130
  $with: void 0,
@@ -1699,7 +2134,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1699
2134
  };
1700
2135
  }
1701
2136
  return {
1702
- filter: this.translateFilterWithRename(query.filter, meta),
2137
+ filter: this.noteTranslated(logical, this.translateFilterWithRename(filter, meta), has),
1703
2138
  controls: query.controls ? this.translateControls(query.controls, meta) : {},
1704
2139
  insights: query.insights
1705
2140
  };
@@ -1709,10 +2144,10 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1709
2144
  return meta.leafByLogical.get(logical)?.physicalName ?? logical;
1710
2145
  }
1711
2146
  /**
1712
- * Overrides the base `translateFilter` to use `leafByLogical` for key resolution
2147
+ * Overrides the base `translateResolvedFilter` to use `leafByLogical` for key resolution
1713
2148
  * (handles flattened nested paths like `contact.email` → `contact__email`).
1714
2149
  */
1715
- translateFilter(filter, meta) {
2150
+ translateResolvedFilter(filter, meta) {
1716
2151
  if (!filter || typeof filter !== "object") return filter;
1717
2152
  if (!meta.requiresMappings && !meta.toStorageFormatters) return filter;
1718
2153
  return this.translateFilterWithRename(filter, meta);
@@ -1720,13 +2155,16 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1720
2155
  /**
1721
2156
  * Translates filter with key renaming from logical to physical names.
1722
2157
  * Used by the relational query path where field paths must be mapped
1723
- * to `__`-separated column names.
2158
+ * to `__`-separated column names. Relational predicates must already be
2159
+ * resolved (`translateFilter` / `translateQuery` do it); a resolved
2160
+ * predicate passes through under its navigation-field key.
1724
2161
  */
1725
2162
  translateFilterWithRename(filter, meta) {
1726
2163
  if (!filter || typeof filter !== "object") return filter;
1727
2164
  const result = {};
1728
2165
  for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilterWithRename(f, meta));
1729
2166
  else if (key === "$not") result[key] = this.translateFilterWithRename(value, meta);
2167
+ else if (meta.navFields.has(key)) result[key] = value;
1730
2168
  else if (key.startsWith("$")) result[key] = value;
1731
2169
  else {
1732
2170
  const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
@@ -1856,6 +2294,236 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1856
2294
  }
1857
2295
  };
1858
2296
  //#endregion
2297
+ //#region src/query/filter-values.ts
2298
+ const OPAQUE = {
2299
+ kinds: new Set(["any"]),
2300
+ timestamp: false
2301
+ };
2302
+ const NUMBER = {
2303
+ kinds: new Set(["number"]),
2304
+ timestamp: false
2305
+ };
2306
+ const INTEGER = {
2307
+ kinds: new Set(["integer"]),
2308
+ timestamp: false
2309
+ };
2310
+ const STRING = {
2311
+ kinds: new Set(["string"]),
2312
+ timestamp: false
2313
+ };
2314
+ function collectKinds(type, kinds, out, depth = 0) {
2315
+ const def = type?.type;
2316
+ const metadata = type?.metadata;
2317
+ if (!def || depth > 8) {
2318
+ kinds.add("any");
2319
+ return;
2320
+ }
2321
+ switch (def.kind) {
2322
+ case "": switch (def.designType) {
2323
+ case "string":
2324
+ kinds.add("string");
2325
+ return;
2326
+ case "number": {
2327
+ const timestamp = def.tags?.has("timestamp") === true;
2328
+ if (timestamp) out.timestamp = true;
2329
+ const integer = timestamp || def.tags?.has("int") === true || metadata?.has?.("expect.int") === true;
2330
+ kinds.add(integer ? "integer" : "number");
2331
+ return;
2332
+ }
2333
+ case "decimal":
2334
+ kinds.add("decimal");
2335
+ return;
2336
+ case "boolean":
2337
+ kinds.add("boolean");
2338
+ return;
2339
+ case "null":
2340
+ case "undefined":
2341
+ case "never": return;
2342
+ default:
2343
+ kinds.add("any");
2344
+ return;
2345
+ }
2346
+ case "union":
2347
+ for (const item of def.items ?? []) collectKinds(item, kinds, out, depth + 1);
2348
+ return;
2349
+ case "array":
2350
+ collectKinds(def.of, kinds, out, depth + 1);
2351
+ return;
2352
+ default: kinds.add("any");
2353
+ }
2354
+ }
2355
+ const typeCache = /* @__PURE__ */ new WeakMap();
2356
+ /**
2357
+ * The value kinds a filter on `fd` accepts (cached per descriptor). A leaf
2358
+ * inside a JSON value (a `@db.json` object or an array, addressable on
2359
+ * nested-object adapters) is opaque: its contents are not schema-enforced.
2360
+ */
2361
+ function valueTypeOf(meta, fd) {
2362
+ let vt = typeCache.get(fd);
2363
+ if (vt) return vt;
2364
+ if (jsonValueAncestor(fd.path, meta.jsonValueParents) !== void 0 || fd.encrypted || fd.isGeoPoint || fd.designType === "json" || fd.designType === "object" || !fd.type) vt = OPAQUE;
2365
+ else {
2366
+ const kinds = /* @__PURE__ */ new Set();
2367
+ const out = { timestamp: false };
2368
+ collectKinds(fd.type, kinds, out);
2369
+ const metadata = fd.type.metadata;
2370
+ if ((fd.defaultValue?.kind === "fn" && fd.defaultValue.fn !== "uuid" || metadata?.has?.("db.agg.count") === true || metadata?.has?.("db.agg.countDistinct") === true) && kinds.delete("number")) kinds.add("integer");
2371
+ vt = kinds.size === 0 ? OPAQUE : {
2372
+ kinds,
2373
+ timestamp: out.timestamp
2374
+ };
2375
+ }
2376
+ typeCache.set(fd, vt);
2377
+ return vt;
2378
+ }
2379
+ /** A decimal literal (`5`, `-1.5`, `.5`, `1e3`), surrounding blanks allowed — no hex, no `Infinity`. */
2380
+ const NUMERIC_RE = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
2381
+ /** An integer literal (`5`, `-12`), surrounding blanks allowed. */
2382
+ const INTEGER_RE = /^\s*[+-]?\d+\s*$/;
2383
+ function acceptsScalar(kind, value) {
2384
+ switch (kind) {
2385
+ case "any": return true;
2386
+ case "string": return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
2387
+ case "number":
2388
+ case "decimal": return typeof value === "number" && Number.isFinite(value) || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
2389
+ case "integer": return typeof value === "number" && Number.isInteger(value) || typeof value === "bigint" || typeof value === "string" && INTEGER_RE.test(value);
2390
+ default: return typeof value === "boolean" || value === 0 || value === 1;
2391
+ }
2392
+ }
2393
+ /** The first element of `value` (itself when not an array) `vt` rejects, or `undefined`. */
2394
+ function rejectedValue(vt, value) {
2395
+ if (vt.kinds.has("any") || value === null || value === void 0) return void 0;
2396
+ if (Array.isArray(value)) {
2397
+ for (const item of value) {
2398
+ const bad = rejectedValue(vt, item);
2399
+ if (bad) return bad;
2400
+ }
2401
+ return;
2402
+ }
2403
+ if (typeof value === "object" && !isPlainObject(value)) return void 0;
2404
+ for (const kind of vt.kinds) if (acceptsScalar(kind, value)) return void 0;
2405
+ return { value };
2406
+ }
2407
+ const KIND_LABEL = {
2408
+ string: "a string",
2409
+ number: "a number",
2410
+ integer: "an integer",
2411
+ decimal: "a decimal (number or numeric string)",
2412
+ boolean: "a boolean"
2413
+ };
2414
+ function expectedOf(vt) {
2415
+ const labels = [];
2416
+ for (const kind of vt.kinds) {
2417
+ if (kind === "any") continue;
2418
+ labels.push(kind === "integer" && vt.timestamp ? "an integer (epoch milliseconds)" : KIND_LABEL[kind]);
2419
+ }
2420
+ return labels.join(" or ");
2421
+ }
2422
+ function describeValue(value) {
2423
+ if (typeof value === "string") return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);
2424
+ if (isPlainObject(value)) return "an object";
2425
+ return String(value);
2426
+ }
2427
+ function valueError(path, op, message) {
2428
+ return new DbError("INVALID_QUERY", [{
2429
+ path,
2430
+ message: `Invalid filter value for "${path}"${op ? ` (${op})` : ""}: ${message}`
2431
+ }]);
2432
+ }
2433
+ function holdsStrings(vt) {
2434
+ return vt.kinds.has("string") || vt.kinds.has("any");
2435
+ }
2436
+ function checkRegex(path, vt, op, pattern) {
2437
+ if (!holdsStrings(vt)) throw valueError(path, op, `a pattern match needs a string field, "${path}" holds ${expectedOf(vt)}`);
2438
+ if (typeof pattern !== "string" && !(pattern instanceof RegExp)) throw valueError(path, op, `expected a regular expression, got ${describeValue(pattern)}`);
2439
+ }
2440
+ function checkValue(path, vt, op, value) {
2441
+ if (value instanceof RegExp) {
2442
+ checkRegex(path, vt, op ?? "RegExp", value);
2443
+ return;
2444
+ }
2445
+ const bad = rejectedValue(vt, value);
2446
+ if (bad) throw valueError(path, op, `expected ${expectedOf(vt)}, got ${describeValue(bad.value)}`);
2447
+ }
2448
+ /** Operators whose operand is compared with the field's values. */
2449
+ const COMPARE_OPS = new Set([
2450
+ "$eq",
2451
+ "$ne",
2452
+ "$gt",
2453
+ "$gte",
2454
+ "$lt",
2455
+ "$lte",
2456
+ "$in",
2457
+ "$nin"
2458
+ ]);
2459
+ /** Checks one filter entry's value (a bare value or an operator map) against `vt`. */
2460
+ function checkEntry(path, vt, value) {
2461
+ if (vt.kinds.has("any")) return;
2462
+ if (!isPlainObject(value)) {
2463
+ checkValue(path, vt, void 0, value);
2464
+ return;
2465
+ }
2466
+ for (const [op, operand] of Object.entries(value)) if (op === "$regex") checkRegex(path, vt, op, operand);
2467
+ else if (COMPARE_OPS.has(op)) checkValue(path, vt, op, operand);
2468
+ }
2469
+ /**
2470
+ * Walks `filter` (through `$and` / `$or` / `$not`; relational predicates are
2471
+ * the related table's) and checks every entry whose key `typeOf` knows.
2472
+ */
2473
+ function walkFilterValues(filter, typeOf) {
2474
+ if (!isPlainObject(filter)) return;
2475
+ for (const [key, value] of Object.entries(filter)) {
2476
+ if (key === "$and" || key === "$or") {
2477
+ if (Array.isArray(value)) for (const child of value) walkFilterValues(child, typeOf);
2478
+ continue;
2479
+ }
2480
+ if (key === "$not") {
2481
+ walkFilterValues(value, typeOf);
2482
+ continue;
2483
+ }
2484
+ if (key.startsWith("$") || hasRelationOp(value)) continue;
2485
+ const vt = typeOf(key);
2486
+ if (vt) checkEntry(key, vt, value);
2487
+ }
2488
+ }
2489
+ /**
2490
+ * Rejects (`INVALID_QUERY`, `path` = the field) a filter value that cannot
2491
+ * denote its field's declared type — see the module notes for the accepted
2492
+ * forms. Unknown paths are skipped (the path guard owns them).
2493
+ */
2494
+ function guardFilterValues(meta, filter) {
2495
+ if (!filter) return;
2496
+ walkFilterValues(filter, (key) => {
2497
+ const fd = meta.descriptorByPath.get(key);
2498
+ return fd ? valueTypeOf(meta, fd) : void 0;
2499
+ });
2500
+ }
2501
+ /**
2502
+ * `$having` values: an aggregate alias is a number (`count`,
2503
+ * `countDistinct`, `sum`, `avg`, an expression) or its source field's type
2504
+ * (`min` / `max`, `first` / `last`),
2505
+ * a calendar-bucket alias a string label, any other key a `$groupBy`
2506
+ * field's own type.
2507
+ */
2508
+ function guardHavingValues(meta, controls) {
2509
+ if (!controls?.$having) return;
2510
+ const aliases = /* @__PURE__ */ new Map();
2511
+ if (Array.isArray(controls.$select)) {
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;
2515
+ const counts = item.$fn === "count" || item.$fn === "countDistinct";
2516
+ aliases.set(resolveAlias$1(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
2517
+ } else if (isBucketExpr(item)) aliases.set(resolveAlias$1(item), STRING);
2518
+ }
2519
+ walkFilterValues(controls.$having, (key) => {
2520
+ const alias = aliases.get(key);
2521
+ if (alias) return alias;
2522
+ const fd = meta.descriptorByPath.get(key);
2523
+ return fd ? valueTypeOf(meta, fd) : void 0;
2524
+ });
2525
+ }
2526
+ //#endregion
1859
2527
  //#region src/query/query-guards.ts
1860
2528
  /**
1861
2529
  * Engine-agnostic query-time guards, applied in the core layer BEFORE filter
@@ -1882,6 +2550,9 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
1882
2550
  * {@link canFilterLeaf}) needs → `INVALID_QUERY` (see {@link guardPaths}).
1883
2551
  * Runs after the checks above so `ENC_*` codes keep firing first for
1884
2552
  * encrypted subtrees.
2553
+ * - every filter / `$having` comparison value must be able to denote its
2554
+ * field's declared type (`?n='x'` on a number) → `INVALID_QUERY`, after
2555
+ * the path guard (see `guardFilterValues`, since 0.1.147).
1885
2556
  */
1886
2557
  /** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
1887
2558
  function assertGeoPoint(point, path) {
@@ -1938,6 +2609,10 @@ function guardFilter(meta, adapter, filter, encCode = "ENC_FIELD_FILTER") {
1938
2609
  continue;
1939
2610
  }
1940
2611
  if (key.startsWith("$")) continue;
2612
+ if (hasRelationOp(value)) {
2613
+ guardRelationOperands(key, value);
2614
+ continue;
2615
+ }
1941
2616
  if (hasEncrypted && isEncryptedRef(meta, key)) throw encryptedRefError(encCode, key, "filter on");
1942
2617
  if (!isPrimitive(value)) {
1943
2618
  for (const [op, opValue] of Object.entries(value)) if (op === "$geoWithin") guardGeoWithin(meta, adapter, key, opValue);
@@ -1948,6 +2623,23 @@ function guardFilter(meta, adapter, filter, encCode = "ENC_FIELD_FILTER") {
1948
2623
  }
1949
2624
  }
1950
2625
  }
2626
+ /**
2627
+ * Shape rules of a relational predicate's operator map: only `$some` /
2628
+ * `$none` (never mixed with comparison operators), each operand a filter
2629
+ * object.
2630
+ */
2631
+ function guardRelationOperands(key, ops) {
2632
+ for (const [op, operand] of Object.entries(ops)) {
2633
+ if (!isRelationOp(op)) throw new DbError("INVALID_QUERY", [{
2634
+ path: key,
2635
+ message: `Cannot mix "$some" / "$none" with "${op}" on "${key}"`
2636
+ }]);
2637
+ if (!isPlainObject(operand)) throw new DbError("INVALID_QUERY", [{
2638
+ path: key,
2639
+ message: `"${op}" on "${key}" expects a filter object`
2640
+ }]);
2641
+ }
2642
+ }
1951
2643
  /** Rejects `$sort` keys referencing encrypted fields. */
1952
2644
  function guardSort(meta, sort) {
1953
2645
  if (!sort || typeof sort !== "object" || meta.encryptedFields.size === 0) return;
@@ -2021,8 +2713,18 @@ function filterPredicateOf(value) {
2021
2713
  const ops = value;
2022
2714
  if ("$geoWithin" in ops) return "geo";
2023
2715
  const keys = Object.keys(ops);
2716
+ if (keys.length > 0 && keys.every(isRelationOp)) return "relation";
2024
2717
  return keys.length === 1 && keys[0] === "$exists" ? "exists" : "compare";
2025
2718
  }
2719
+ /** The `{ op, filter }` list of a `relation` entry's operator map. */
2720
+ function relationOpsOf(value) {
2721
+ const out = [];
2722
+ for (const [op, filter] of Object.entries(value)) if (isRelationOp(op)) out.push({
2723
+ op,
2724
+ filter
2725
+ });
2726
+ return out;
2727
+ }
2026
2728
  /**
2027
2729
  * Whether a stored leaf physically supports a filter predicate of this class
2028
2730
  * — the one rule the core path guard and moost-db's HTTP capability index
@@ -2039,6 +2741,7 @@ function filterPredicateOf(value) {
2039
2741
  function canFilterLeaf(fd, predicate, adapter) {
2040
2742
  if (fd.encrypted) return false;
2041
2743
  switch (predicate) {
2744
+ case "relation": return false;
2042
2745
  case "exists": return true;
2043
2746
  case "geo": return fd.isGeoPoint === true && adapter.isGeoSearchable();
2044
2747
  default: return adapter.canFilterField(fd);
@@ -2074,6 +2777,37 @@ function rejectSource(code, reason) {
2074
2777
  };
2075
2778
  }
2076
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
+ /**
2077
2811
  * Whether the stored leaf `fd` may be the source of a calendar bucket
2078
2812
  * (since 0.1.133) — every schema and adapter rule, once, for the core path
2079
2813
  * guard ({@link guardPath} op `bucket`) and moost-db's capability index
@@ -2100,7 +2834,7 @@ function bucketSourceVerdict(fd, table, adapter) {
2100
2834
  if (jsonAncestor !== void 0) return rejectSource("jsonDescendant", `inside JSON-stored column "${jsonAncestor}"`);
2101
2835
  if (!isBucketableField(fd)) return rejectSource("notTimestamp", "not a timestamp field (declare it number.timestamp)");
2102
2836
  if (!adapter.canFilterField(fd)) return rejectSource("notFilterable", ADAPTER_FILTER_REASON);
2103
- 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);
2104
2838
  if (adapter.calendarBucketUnits().size === 0) return rejectSource("noBuckets", "adapter has no calendar buckets");
2105
2839
  return { ok: true };
2106
2840
  }
@@ -2132,8 +2866,9 @@ function collectFilterKeys(filter, push, skip, refs) {
2132
2866
  *
2133
2867
  * Aggregate mode is `aggregate` when given, else the presence of `$groupBy`.
2134
2868
  * In aggregate mode `$select` computed entries are collected by kind — an
2135
- * aggregate's `$field` into `aggregate`, a calendar bucket's into `bucket` —
2136
- * 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
2137
2872
  * `$sort` / `$having`; a bucket alias is also dropped from `groupBy`, which
2138
2873
  * lists grouped fields only.
2139
2874
  *
@@ -2150,10 +2885,17 @@ function collectQueryPaths(query, aggregate) {
2150
2885
  bucket: [],
2151
2886
  aggregateMode: false
2152
2887
  };
2153
- collectFilterKeys(query.filter, (path, value) => refs.filter.push({
2154
- path,
2155
- predicate: filterPredicateOf(value)
2156
- }), void 0, refs);
2888
+ collectFilterKeys(query.filter, (path, value) => {
2889
+ const predicate = filterPredicateOf(value);
2890
+ refs.filter.push(predicate === "relation" ? {
2891
+ path,
2892
+ predicate,
2893
+ relation: relationOpsOf(value)
2894
+ } : {
2895
+ path,
2896
+ predicate
2897
+ });
2898
+ }, void 0, refs);
2157
2899
  const controls = query.controls ?? {};
2158
2900
  const rawGroupBy = controls.$groupBy;
2159
2901
  const groupBy = Array.isArray(rawGroupBy) ? rawGroupBy.filter((f) => typeof f === "string") : typeof rawGroupBy === "string" ? [rawGroupBy] : [];
@@ -2164,11 +2906,12 @@ function collectQueryPaths(query, aggregate) {
2164
2906
  if (Array.isArray(select)) {
2165
2907
  for (const item of select) if (typeof item === "string") refs.select.push(item);
2166
2908
  else if (!refs.aggregateMode) continue;
2167
- else if (isAggregateExpr(item)) {
2168
- aliases.add(resolveAlias(item));
2169
- if (item.$field !== "*") refs.aggregate.push(item.$field);
2170
- } else if (isBucketExpr(item)) {
2171
- 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);
2172
2915
  aliases.add(alias);
2173
2916
  bucketAliases.add(alias);
2174
2917
  refs.bucket.push(item.$field);
@@ -2176,6 +2919,7 @@ function collectQueryPaths(query, aggregate) {
2176
2919
  } else if (select && typeof select === "object") refs.select.push(...Object.keys(select));
2177
2920
  refs.groupBy = bucketAliases.size > 0 ? groupBy.filter((name) => !bucketAliases.has(name)) : groupBy;
2178
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));
2179
2923
  if (refs.aggregateMode) collectFilterKeys(controls.$having, (path) => refs.having.push(path), aliases, refs);
2180
2924
  return refs;
2181
2925
  }
@@ -2245,8 +2989,11 @@ function pathSourceOf(meta) {
2245
2989
  function guardPath(meta, adapter, path, op, predicate = "compare") {
2246
2990
  const verb = OP_VERB[op];
2247
2991
  const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
2992
+ if (predicate === "relation" && kind !== "nav" && kind !== "unknown") throw pathError(path, `"$some" / "$none" are only valid on a navigation relation — "${path}" is not one`);
2248
2993
  switch (kind) {
2249
- case "nav": throw pathError(path, `Cannot ${verb} "${path}" — navigation path`);
2994
+ case "nav":
2995
+ if (op === "filter" && predicate === "relation" && parent === void 0) return;
2996
+ throw pathError(path, navPathMessage(path, verb, op, parent));
2250
2997
  case "leaf": {
2251
2998
  if (op === "select") return;
2252
2999
  const fd = meta.descriptorByPath.get(path);
@@ -2271,6 +3018,35 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
2271
3018
  }
2272
3019
  }
2273
3020
  /**
3021
+ * The rejection of a navigation path in a non-predicate position. A filter
3022
+ * names the predicate that expresses it: `ticket.status` →
3023
+ * `{ ticket: { $some: { status: … } } }`.
3024
+ */
3025
+ function navPathMessage(path, verb, op, parent) {
3026
+ const base = `Cannot ${verb} "${path}" — navigation path`;
3027
+ if (op !== "filter") return base;
3028
+ return `${base}; use { ${parent ?? path}: { $some: ${parent === void 0 ? "…" : `{ ${path.slice(parent.length + 1)}: … }`} } }`;
3029
+ }
3030
+ /**
3031
+ * A `relation` filter entry: the adapter must render predicates in this
3032
+ * mode, the table must be wired to its related tables (a `DbSpace`), and
3033
+ * each operand is guarded by the related table (depth / count caps, the
3034
+ * related table's own path rules) — see `TRelationFilterHost.guard`.
3035
+ */
3036
+ function guardRelationRef(meta, adapter, ref, state) {
3037
+ const path = state.path ? `${state.path}.${ref.path}` : ref.path;
3038
+ const mode = state.write ? "write" : "read";
3039
+ if (!adapter.supportsRelationFilters(mode)) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
3040
+ path,
3041
+ message: `Relational predicates ($some / $none) are not supported by this adapter${state.write ? " in mutation filters" : ""}`
3042
+ }]);
3043
+ if (!meta.relationFilters) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
3044
+ path,
3045
+ message: `Relational predicate on "${path}" needs the table to come from a DbSpace (no table resolver)`
3046
+ }]);
3047
+ for (const { op, filter } of ref.relation ?? []) meta.relationFilters.guard(ref.path, op, filter, state);
3048
+ }
3049
+ /**
2274
3050
  * Core backstop for every read / aggregate / mutation-filter entry point:
2275
3051
  * each referenced path (see {@link collectQueryPaths}) must exist on THIS
2276
3052
  * adapter with the physical capability the position needs (see
@@ -2285,20 +3061,29 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
2285
3061
  * `bucket`), `$groupBy` fields are checked, and computed aliases (`$as` or
2286
3062
  * the default) are exempt in `$sort` / `$having`.
2287
3063
  *
3064
+ * Then every filter comparison value must be able to denote its field's
3065
+ * declared type (`guardFilterValues`, since 0.1.147) — after the paths, so
3066
+ * an unknown or unfilterable field keeps its own rejection.
3067
+ *
2288
3068
  * Returns the collected refs so callers can run further structural rules
2289
3069
  * (see {@link checkHavingKeys}) without walking the query again.
2290
3070
  */
2291
- function guardPaths(meta, adapter, query, aggregate = false) {
3071
+ function guardPaths(meta, adapter, query, aggregate = false, state) {
2292
3072
  if (!query) return;
2293
3073
  const refs = collectQueryPaths(query, aggregate);
2294
3074
  if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
2295
- for (const ref of refs.filter) guardPath(meta, adapter, ref.path, "filter", ref.predicate);
3075
+ let relState = state;
3076
+ for (const ref of refs.filter) {
3077
+ guardPath(meta, adapter, ref.path, "filter", ref.predicate);
3078
+ if (ref.predicate === "relation") guardRelationRef(meta, adapter, ref, relState ??= relGuardState());
3079
+ }
2296
3080
  for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
2297
3081
  for (const path of refs.select) guardPath(meta, adapter, path, "select");
2298
3082
  for (const path of refs.aggregate) guardPath(meta, adapter, path, "aggregate");
2299
3083
  for (const path of refs.bucket) guardPath(meta, adapter, path, "bucket");
2300
3084
  for (const path of refs.groupBy) guardPath(meta, adapter, path, "groupBy");
2301
3085
  for (const path of refs.having) guardPath(meta, adapter, path, "having");
3086
+ guardFilterValues(meta, query.filter);
2302
3087
  return refs;
2303
3088
  }
2304
3089
  /**
@@ -2306,12 +3091,12 @@ function guardPaths(meta, adapter, query, aggregate = false) {
2306
3091
  * normalizer (a calendar bucket is invalid outside a grouped query), then
2307
3092
  * the path guard.
2308
3093
  */
2309
- function guardQuery(meta, adapter, query) {
3094
+ function guardQuery(meta, adapter, query, state) {
2310
3095
  if (!query) return;
2311
3096
  guardFilter(meta, adapter, query.filter);
2312
3097
  guardSort(meta, query.controls?.$sort);
2313
- normalizeComputedSelect(query.controls, meta, false);
2314
- guardPaths(meta, adapter, query);
3098
+ resolveComputedSelect(query.controls, meta, false);
3099
+ guardPaths(meta, adapter, query, false, state);
2315
3100
  }
2316
3101
  /**
2317
3102
  * `$having` is a post-aggregation filter, so a key is either a computed
@@ -2337,29 +3122,45 @@ function checkHavingKeys(refs) {
2337
3122
  * (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
2338
3123
  * (`BUCKET_NOT_SUPPORTED`), then the `$having` key rule
2339
3124
  * ({@link checkHavingKeys} — after the path guard so an unknown key still
2340
- * reads `Unknown field`).
3125
+ * reads `Unknown field`), then the `$having` values (`guardHavingValues`).
2341
3126
  *
2342
- * `buckets` are the query's resolved calendar buckets when the caller already
2343
- * 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).
2344
3130
  */
2345
3131
  function guardAggregate(meta, adapter, query, resolved) {
2346
- const buckets = resolved ?? normalizeComputedSelect(query.controls, meta, true);
3132
+ const computed = resolved ?? resolveComputedSelect(query.controls, meta, true);
3133
+ const { buckets } = computed;
2347
3134
  guardFilter(meta, adapter, query.filter);
2348
3135
  const controls = query.controls;
2349
3136
  if (meta.encryptedFields.size > 0) {
2350
3137
  for (const field of controls.$groupBy ?? []) if (isEncryptedRef(meta, field)) throw encryptedRefError("ENC_FIELD_AGG", field, "group by");
2351
- if (controls.$select) for (const item of controls.$select) {
2352
- const field = typeof item === "string" ? item : item.$field;
2353
- 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");
2354
3140
  }
3141
+ for (const key of rowOrderKeys(controls.$rowOrder)) if (isEncryptedRef(meta, key)) throw encryptedRefError("ENC_FIELD_SORT", key, "sort by");
2355
3142
  if (controls.$having) guardFilter(meta, adapter, controls.$having, "ENC_FIELD_AGG");
2356
3143
  guardSort(meta, controls.$sort);
2357
3144
  }
2358
3145
  const refs = guardPaths(meta, adapter, query, true);
2359
3146
  guardAggregateFns(adapter, controls.$select);
3147
+ guardAggregateExpressions(adapter, controls.$select);
3148
+ checkAggregateExprs(meta, query, computed.exprs, computed.rowOrder);
2360
3149
  guardBucketUnits(adapter, buckets);
2361
3150
  const having = refs ? checkHavingKeys(refs) : void 0;
2362
3151
  if (having) throw new DbError("INVALID_QUERY", [having]);
3152
+ guardHavingValues(meta, controls);
3153
+ }
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();
2363
3164
  }
2364
3165
  /**
2365
3166
  * Rejects an aggregate whose (known — the normalizer checked the name)
@@ -2542,9 +3343,18 @@ var AtscriptDbReadable = class {
2542
3343
  setEncryption(encryption) {
2543
3344
  this._encryption = encryption;
2544
3345
  }
3346
+ /** @internal Set by the owning `DbSpace` when it closes. */
3347
+ _spaceClosed = false;
2545
3348
  /** Ensures metadata is built. Called before any metadata access. */
2546
3349
  _ensureBuilt() {
2547
- if (!this._meta.isBuilt) this._meta.build(this.type, this.adapter, this.logger);
3350
+ if (this._spaceClosed) throw spaceClosedError();
3351
+ if (!this._meta.isBuilt) {
3352
+ this._meta.build(this.type, this.adapter, this.logger);
3353
+ if (this._meta.navFields.size > 0 && this._tableResolver) {
3354
+ const resolver = this._tableResolver;
3355
+ this._meta.relationFilters = createRelationFilterHost(this, (type) => resolver(type));
3356
+ }
3357
+ }
2548
3358
  if (this._meta.encryptedFields.size > 0 && !this._encryption) throw new DbError("ENC_CONFIG_MISSING", [{
2549
3359
  path: "",
2550
3360
  message: `Table "${this.tableName}" declares @db.encrypted fields but the DbSpace has no encryption configuration — pass { encryption: { defaultKeyId, keys } } to the DbSpace options`
@@ -2571,6 +3381,56 @@ var AtscriptDbReadable = class {
2571
3381
  _guardQuery(query) {
2572
3382
  guardQuery(this._meta, this.adapter, query);
2573
3383
  }
3384
+ /**
3385
+ * Guards a relational predicate operand against THIS table — the filter
3386
+ * guard and the path guard with the predicate's shared `state` (depth,
3387
+ * count, read/write mode). Called by the source table's relation host.
3388
+ *
3389
+ * @internal Core wiring for relational predicates; not consumer API.
3390
+ */
3391
+ _guardRelationOperand(filter, state) {
3392
+ this._ensureBuilt();
3393
+ guardFilter(this._meta, this.adapter, filter);
3394
+ guardPaths(this._meta, this.adapter, { filter }, false, state);
3395
+ }
3396
+ /**
3397
+ * Translates a relational predicate operand for THIS table's adapter (its
3398
+ * own field mapper; nested predicates resolved at `depth + 1`).
3399
+ *
3400
+ * @internal Core wiring for relational predicates; not consumer API.
3401
+ */
3402
+ _resolveRelationOperand(filter, depth) {
3403
+ this._ensureBuilt();
3404
+ return this._fieldMapper.translateFilter(filter, this._meta, depth);
3405
+ }
3406
+ /**
3407
+ * Translates a logical query (filter + controls) for THIS table's adapter
3408
+ * after the read guards — exactly what `findMany` hands the adapter.
3409
+ * For adapters that load `$with` relations natively and must address the
3410
+ * related table's physical names.
3411
+ *
3412
+ * @internal Adapter-facing surface; not part of the consumer API.
3413
+ * @since 0.1.147
3414
+ */
3415
+ _translateForAdapter(query) {
3416
+ this._ensureBuilt();
3417
+ this._guardQuery(query);
3418
+ return this._fieldMapper.translateQuery(query, this._meta);
3419
+ }
3420
+ /**
3421
+ * Physical rows of THIS table → logical rows (field mapping, value
3422
+ * formatters, decryption) — what every read does before `$with` loading.
3423
+ * `controls` are the logical read controls the rows were read with.
3424
+ *
3425
+ * @internal Adapter-facing surface; not part of the consumer API.
3426
+ * @since 0.1.147
3427
+ */
3428
+ async _rowsFromAdapter(rows, controls) {
3429
+ this._ensureBuilt();
3430
+ const out = this._fromRead(rows, controls);
3431
+ await this._decryptRows(out);
3432
+ return out;
3433
+ }
2574
3434
  _encryptedPathsCache;
2575
3435
  /** Pre-split `encryptedFields` paths — computed once, reused on every read/write. */
2576
3436
  get _encryptedPaths() {
@@ -2653,6 +3513,22 @@ var AtscriptDbReadable = class {
2653
3513
  this._ensureBuilt();
2654
3514
  return this._meta.primaryKeys;
2655
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
+ }
2656
3532
  /** Preferred row identifier field names. Defaults to primary keys. */
2657
3533
  get preferredId() {
2658
3534
  this._ensureBuilt();
@@ -2825,6 +3701,15 @@ var AtscriptDbReadable = class {
2825
3701
  this._ensureBuilt();
2826
3702
  return this._meta.pathToPhysical;
2827
3703
  }
3704
+ /**
3705
+ * Physical column (or document path) of a logical field path —
3706
+ * `@db.column` renames and flattening applied.
3707
+ * @since 0.1.147
3708
+ */
3709
+ physicalPath(logical) {
3710
+ this._ensureBuilt();
3711
+ return this._meta.physicalPath(logical);
3712
+ }
2828
3713
  /** Precomputed physical column name → logical dot-path map (inverse). */
2829
3714
  get physicalToPath() {
2830
3715
  this._ensureBuilt();
@@ -2843,8 +3728,10 @@ var AtscriptDbReadable = class {
2843
3728
  * work (the derived read plan on document adapters) is done once for all
2844
3729
  * of them. Every read path funnels through here.
2845
3730
  */
2846
- _fromRead(rows, controls) {
2847
- 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;
2848
3735
  }
2849
3736
  /**
2850
3737
  * Translates a read query for the adapter. A `$select` that leaves out a
@@ -2879,10 +3766,14 @@ var AtscriptDbReadable = class {
2879
3766
  widened
2880
3767
  };
2881
3768
  }
2882
- /** Reconstructs + decrypts a read's rows, loads its `$with` relations and strips widened keys. */
2883
- async _finishRead(results, read) {
2884
- const rows = this._fromRead(results, read.controls);
2885
- await this._decryptRows(rows);
3769
+ /**
3770
+ * Reconstructs + decrypts a read's rows, keeps the ones `pick` selects (all
3771
+ * by default), loads their `$with` relations and strips widened keys.
3772
+ */
3773
+ async _finishRead(results, read, pick) {
3774
+ const all = this._fromRead(results, read.controls);
3775
+ await this._decryptRows(all);
3776
+ const rows = pick ? pick(all) : all;
2886
3777
  if (read.withRelations?.length) {
2887
3778
  await this.loadRelations(rows, read.withRelations);
2888
3779
  for (const key of read.widened) for (const row of rows) deletePath(row, key);
@@ -2991,11 +3882,12 @@ var AtscriptDbReadable = class {
2991
3882
  * table. Defense-in-depth for query-path validation: `flattenAnnotatedType`
2992
3883
  * still truncates real self-referential cycles, so paths like
2993
3884
  * `parent.parent.name` on a self-ref schema would miss `flatMap.has` but
2994
- * remain valid field references on the target.
3885
+ * remain valid field references on the target — a path may cross the same
3886
+ * relation any number of times (callers cap the depth).
2995
3887
  *
2996
- * Cycle-safe via a visited set keyed on `<tableName>:<navField>`.
3888
+ * Terminates on cyclic schemas: every hop consumes one path segment.
2997
3889
  */
2998
- isValidFieldPath(path, _visited) {
3890
+ isValidFieldPath(path) {
2999
3891
  if (this.flatMap.has(path)) return true;
3000
3892
  const dotIdx = path.indexOf(".");
3001
3893
  if (dotIdx === -1) return false;
@@ -3003,11 +3895,7 @@ var AtscriptDbReadable = class {
3003
3895
  const tail = path.slice(dotIdx + 1);
3004
3896
  const targetTable = this.relatedTable(head);
3005
3897
  if (!targetTable || typeof targetTable.isValidFieldPath !== "function") return false;
3006
- const visited = _visited ?? /* @__PURE__ */ new Set();
3007
- const cycleKey = `${this.tableName}:${head}`;
3008
- if (visited.has(cycleKey)) return false;
3009
- visited.add(cycleKey);
3010
- return targetTable.isValidFieldPath(tail, visited);
3898
+ return targetTable.isValidFieldPath(tail);
3011
3899
  }
3012
3900
  /**
3013
3901
  * Creates a new validator with custom options.
@@ -3041,6 +3929,22 @@ var AtscriptDbReadable = class {
3041
3929
  return await this._finishRead(await this.adapter.findMany(read.translated), read);
3042
3930
  }
3043
3931
  /**
3932
+ * `findMany` for the generic `$with` loader. With `partitionBy` (logical
3933
+ * fields), `$skip` / `$limit` apply per group of rows sharing those fields'
3934
+ * values (`BaseDbAdapter.findManyPerPartition`); `pick` chooses which of the
3935
+ * read rows to keep before their own `$with` relations load.
3936
+ *
3937
+ * @internal Relation-loader surface; not part of the consumer API.
3938
+ * @since 0.1.147
3939
+ */
3940
+ async _findManyForRelation(query, opts) {
3941
+ this._ensureBuilt();
3942
+ this._guardQuery(query);
3943
+ const read = this._translateRead(query);
3944
+ const results = opts.partitionBy ? await this.adapter.findManyPerPartition(read.translated, opts.partitionBy.map((field) => this._meta.physicalPath(field))) : await this.adapter.findMany(read.translated);
3945
+ return this._finishRead(results, read, opts.pick);
3946
+ }
3947
+ /**
3044
3948
  * Counts records matching the query.
3045
3949
  */
3046
3950
  async count(query) {
@@ -3087,7 +3991,8 @@ var AtscriptDbReadable = class {
3087
3991
  async aggregate(query) {
3088
3992
  this._ensureBuilt();
3089
3993
  const { $groupBy, $select } = query.controls;
3090
- const buckets = normalizeComputedSelect(query.controls, this._meta, true);
3994
+ const computed = resolveComputedSelect(query.controls, this._meta, true);
3995
+ const { buckets } = computed;
3091
3996
  if ($select) {
3092
3997
  const groupBySet = new Set($groupBy);
3093
3998
  for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new DbError("INVALID_QUERY", [{
@@ -3100,13 +4005,19 @@ var AtscriptDbReadable = class {
3100
4005
  const dimSet = new Set(dimensions);
3101
4006
  const measSet = new Set(measures);
3102
4007
  const bucketAliases = new Set(buckets.map((b) => b.alias));
3103
- for (const field of $groupBy) if (!dimSet.has(field) && !bucketAliases.has(field)) throw new DbError("INVALID_QUERY", [{
3104
- path: "$groupBy",
3105
- message: `Field "${field}" is not a dimension`
3106
- }]);
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
+ }
3107
4018
  if ($select) for (const item of $select) {
3108
4019
  if (!isAggregateExpr(item) || item.$field === "*" || measSet.has(item.$field)) continue;
3109
- if (item.$fn === "countDistinct") {
4020
+ if (item.$fn === "countDistinct" || isFirstLast(item)) {
3110
4021
  if (dimSet.has(item.$field)) continue;
3111
4022
  throw new DbError("INVALID_QUERY", [{
3112
4023
  path: "$select",
@@ -3136,10 +4047,24 @@ var AtscriptDbReadable = class {
3136
4047
  this._ensureSearchable();
3137
4048
  if (!searchTerm.trim()) return query.controls.$count ? [{ count: 0 }] : [];
3138
4049
  }
3139
- guardAggregate(this._meta, this.adapter, query, buckets);
3140
- 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);
3141
4052
  const results = await this.adapter.aggregate(dbQuery);
3142
- 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;
3143
4068
  }
3144
4069
  /** Whether the underlying adapter supports text search. */
3145
4070
  isSearchable() {
@@ -3153,6 +4078,10 @@ var AtscriptDbReadable = class {
3153
4078
  calendarBucketUnits() {
3154
4079
  return this.adapter.calendarBucketUnits();
3155
4080
  }
4081
+ /** Whether the adapter renders aggregate arithmetic (proxies adapter capability). @since 0.1.148 */
4082
+ supportsAggregateExpressions() {
4083
+ return this.adapter.supportsAggregateExpressions();
4084
+ }
3156
4085
  /** Aggregate functions the adapter renders (proxies adapter capability). @since 0.1.136 */
3157
4086
  aggregateFns() {
3158
4087
  return this.adapter.aggregateFns();
@@ -3561,7 +4490,7 @@ var AtscriptDbReadable = class {
3561
4490
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
3562
4491
  */
3563
4492
  async loadRelations(rows, withRelations) {
3564
- const { loadRelationsImpl } = await import("./relation-loader-D9XuXaMv.mjs").then((n) => n.n);
4493
+ const { loadRelationsImpl } = await import("./relation-loader-ByY1Byrl.mjs").then((n) => n.n);
3565
4494
  return loadRelationsImpl(rows, withRelations, this);
3566
4495
  }
3567
4496
  /**
@@ -3627,8 +4556,14 @@ function createFailureCollector(what) {
3627
4556
  //#region src/base-adapter.ts
3628
4557
  const EMPTY_DEFAULT_FNS = /* @__PURE__ */ new Set();
3629
4558
  const EMPTY_BUCKET_UNITS = /* @__PURE__ */ new Set();
3630
- /** Every calendar-bucket unit — what an adapter that renders them all returns from `calendarBucketUnits()`. */
4559
+ /**
4560
+ * Every calendar-bucket unit — what an adapter that renders them all returns
4561
+ * from `calendarBucketUnits()`. Includes `'hour'` since 0.1.147.
4562
+ */
3631
4563
  const ALL_BUCKET_UNITS = new Set(BUCKET_UNITS);
4564
+ const NO_VIEW_CAPABILITIES = /* @__PURE__ */ new Set();
4565
+ /** Every view capability — what the bundled adapters return from `viewCapabilities()`. @since 0.1.147 */
4566
+ const ALL_VIEW_CAPABILITIES = new Set(["compute", "firstJoin"]);
3632
4567
  const txStorage = new AsyncLocalStorage();
3633
4568
  /** The innermost open transaction of `owner` in the current async chain. */
3634
4569
  function findTxContext(owner) {
@@ -3688,6 +4623,26 @@ var BaseDbAdapter = class {
3688
4623
  registerReadable(readable, logger) {
3689
4624
  this._table = readable;
3690
4625
  if (logger) this.logger = logger;
4626
+ if (readable.isView) this._guardViewCapabilities(readable);
4627
+ }
4628
+ /**
4629
+ * Makes `ensureTable()` of a managed view fail closed (since 0.1.147): it
4630
+ * throws before the adapter renders a computed column / first-row join its
4631
+ * {@link viewCapabilities} does not list — schema sync refuses such a view
4632
+ * up front, a direct `ensureTable()` call must not render it as a plain
4633
+ * (row-multiplying) join either. Wraps the subclass's own implementation, so
4634
+ * third-party adapters get the guard without code changes.
4635
+ */
4636
+ _guardViewCapabilities(readable) {
4637
+ if (Object.prototype.hasOwnProperty.call(this, "ensureTable")) return;
4638
+ const view = readable;
4639
+ if (typeof view.viewCapabilityProblems !== "function") return;
4640
+ const ensureTable = this.ensureTable.bind(this);
4641
+ this.ensureTable = (opts) => {
4642
+ const problems = view.viewCapabilityProblems();
4643
+ if (problems.length > 0) return Promise.reject(new Error(problems.join("; ")));
4644
+ return ensureTable(opts);
4645
+ };
3691
4646
  }
3692
4647
  /**
3693
4648
  * Called by {@link DbSpace} right after its factory builds this adapter —
@@ -3872,8 +4827,10 @@ var BaseDbAdapter = class {
3872
4827
  * be adopted adapter by adapter. An adapter that returns a unit must group
3873
4828
  * by the bucket alias in `$groupBy` — see `controls.$select.buckets`
3874
4829
  * (`TResolvedBucket`: physical `field`, source `fd`) — and return the
3875
- * `YYYY-MM-DD` label of the bucket's first local day (null for a null or
3876
- * out-of-range source, uniqu's `bucketLabel` semantics). Since 0.1.132.
4830
+ * `YYYY-MM-DD` label of the bucket's first local day, or for `'hour'` the
4831
+ * local wall-clock hour `YYYY-MM-DDTHH:00` (null for a null or out-of-range
4832
+ * source, uniqu's `bucketLabel` semantics). Since 0.1.132; `'hour'` since
4833
+ * 0.1.147 — an adapter returning {@link ALL_BUCKET_UNITS} must render it.
3877
4834
  */
3878
4835
  calendarBucketUnits() {
3879
4836
  return EMPTY_BUCKET_UNITS;
@@ -3893,6 +4850,22 @@ var BaseDbAdapter = class {
3893
4850
  return BASE_AGGREGATE_FNS;
3894
4851
  }
3895
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
+ /**
3896
4869
  * Revision of how this adapter renders a managed view (its SQL / pipeline)
3897
4870
  * from an unchanged view definition. Stored in each managed view's sync
3898
4871
  * snapshot when defined, so bumping it recreates every managed view of the
@@ -3904,6 +4877,18 @@ var BaseDbAdapter = class {
3904
4877
  */
3905
4878
  viewRenderRevision() {}
3906
4879
  /**
4880
+ * The managed-view features this adapter renders: `compute` — computed
4881
+ * columns (`@db.compute`); `firstJoin` — first-row joins (the ordered 4th
4882
+ * argument of `@db.view.joins`). Schema sync refuses a view using a feature
4883
+ * not listed. The default is EMPTY (fail-closed): a third-party adapter
4884
+ * opts in once it renders them.
4885
+ *
4886
+ * @since 0.1.147
4887
+ */
4888
+ viewCapabilities() {
4889
+ return NO_VIEW_CAPABILITIES;
4890
+ }
4891
+ /**
3907
4892
  * Whether this adapter enforces foreign key constraints natively.
3908
4893
  * When `true`, the generic layer skips application-level cascade/setNull
3909
4894
  * on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
@@ -3961,6 +4946,39 @@ var BaseDbAdapter = class {
3961
4946
  return false;
3962
4947
  }
3963
4948
  /**
4949
+ * Whether this adapter renders relational filter predicates
4950
+ * (`{ nav: { $some | $none: … } }`) in `mode` — `read` for find / count /
4951
+ * search / aggregate filters, `write` for mutation filters
4952
+ * (`updateMany`, `deleteMany`, …). Default `false`: the core rejects such
4953
+ * filters with `REL_FILTER_NOT_SUPPORTED` before they reach the adapter.
4954
+ *
4955
+ * An adapter returning `true` receives each predicate already resolved by
4956
+ * the core: the filter visitor's `relation(field, op, operand)` callback
4957
+ * gets a `ResolvedRelationFilter` operand (`kind`, physical
4958
+ * correlation `pairs`, `target` / `junction` tables with their adapters,
4959
+ * and the inner `filter` already translated to the target's physical
4960
+ * names, nested predicates resolved too). Keep `relation` on every
4961
+ * `walkFilter` visitor that may meet such a filter.
4962
+ *
4963
+ * @since 0.1.147
4964
+ */
4965
+ supportsRelationFilters(_mode) {
4966
+ return false;
4967
+ }
4968
+ /**
4969
+ * Whether `other` serves a table of the SAME store as this adapter, so one
4970
+ * statement / pipeline can correlate both (a relational predicate renders
4971
+ * the related table inside this table's query). Default: same adapter class
4972
+ * and same {@link _transactionOwner} (the driver / pool / client the
4973
+ * adapter was built with). Override when the owner is shared across
4974
+ * separate databases (e.g. one Mongo client over several databases).
4975
+ *
4976
+ * @since 0.1.147
4977
+ */
4978
+ sharesStoreWith(other) {
4979
+ return other.constructor === this.constructor && other._transactionOwner() === this._transactionOwner();
4980
+ }
4981
+ /**
3964
4982
  * Loads relations onto result rows using adapter-native operations.
3965
4983
  * Only called when {@link supportsNativeRelations} returns `true`.
3966
4984
  *
@@ -4197,6 +5215,31 @@ var BaseDbAdapter = class {
4197
5215
  };
4198
5216
  }
4199
5217
  /**
5218
+ * Reads like {@link findMany}, except that `$skip` / `$limit` apply to each
5219
+ * partition — the rows sharing the values of the `partitionBy` columns
5220
+ * (physical names) — instead of to the whole result. The generic `$with`
5221
+ * loader reads the related rows of many parent rows at once this way, so a
5222
+ * relation's `$skip` / `$limit` page each parent row's related rows.
5223
+ * `$sort` orders the rows within a partition; how partitions interleave is
5224
+ * unspecified.
5225
+ *
5226
+ * Default: one {@link findMany} without `$skip` / `$limit`, paged per
5227
+ * partition in memory. The SQL adapters override it with a `ROW_NUMBER()`
5228
+ * window, so only the kept rows are read.
5229
+ *
5230
+ * @since 0.1.147
5231
+ */
5232
+ async findManyPerPartition(query, partitionBy) {
5233
+ const { $skip, $limit, ...controls } = query.controls;
5234
+ return slicePerGroup(await this.findMany({
5235
+ ...query,
5236
+ controls
5237
+ }), (row) => compositeKey(partitionBy, row, getPath), {
5238
+ skip: $skip ?? void 0,
5239
+ limit: $limit ?? void 0
5240
+ });
5241
+ }
5242
+ /**
4200
5243
  * Executes an aggregate query (GROUP BY + aggregate functions).
4201
5244
  * Default throws — override in adapters that support aggregation.
4202
5245
  */
@@ -4204,6 +5247,28 @@ var BaseDbAdapter = class {
4204
5247
  throw new Error("Aggregation not supported by this adapter");
4205
5248
  }
4206
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
+ /**
4207
5272
  * When true, the adapter can handle column type changes in-place
4208
5273
  * (e.g. MySQL's ALTER TABLE MODIFY COLUMN) without requiring table recreation.
4209
5274
  * The generic sync layer will delegate type changes to {@link syncColumns}
@@ -4267,6 +5332,8 @@ var BaseDbAdapter = class {
4267
5332
  //#endregion
4268
5333
  //#region src/strategies/application-integrity.ts
4269
5334
  const MAX_CASCADE_DEPTH = 100;
5335
+ /** Rows per pinned-key delete batch (keeps `IN (…)` / `$or` lists bounded). */
5336
+ const PIN_BATCH = 1e3;
4270
5337
  const cascadeStorage = new AsyncLocalStorage();
4271
5338
  /**
4272
5339
  * Integrity strategy for adapters without native FK support (e.g. MongoDB).
@@ -4335,6 +5402,12 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4335
5402
  * - `restrict`: throws if any children exist
4336
5403
  * - `cascade`: recursively deletes child records
4337
5404
  * - `setNull`: sets FK fields to null
5405
+ *
5406
+ * When `filter` holds a relational predicate, returns the matched rows
5407
+ * PINNED by primary key (see {@link TCascadePin}): the caller deletes those
5408
+ * rows, never re-evaluating `filter` on data the cascade just changed (a
5409
+ * `$some` over a cascaded child relation would otherwise stop matching and
5410
+ * leave the parent behind). Any other filter → `undefined`.
4338
5411
  */
4339
5412
  async cascadeBeforeDelete(filter, tableName, meta, cascadeResolver, translateFilter, adapter) {
4340
5413
  const parentCtx = cascadeStorage.getStore();
@@ -4346,6 +5419,11 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4346
5419
  }]);
4347
5420
  const targets = cascadeResolver(tableName);
4348
5421
  if (targets.length === 0) return;
5422
+ const pkPhysical = containsRelationFilter(filter) ? meta.primaryKeys.map((pk) => meta.physicalPath(pk)) : void 0;
5423
+ if (pkPhysical?.length === 0) throw new DbError("REL_FILTER_NOT_SUPPORTED", [{
5424
+ path: "",
5425
+ message: "Cannot delete by a relational predicate with application-level cascades: the table has no primary key"
5426
+ }]);
4349
5427
  const neededLogical = /* @__PURE__ */ new Set();
4350
5428
  for (const t of targets) for (const tf of t.fk.targetFields) neededLogical.add(tf);
4351
5429
  for (const pk of meta.primaryKeys) neededLogical.add(pk);
@@ -4360,7 +5438,8 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4360
5438
  filter: translateFilter(filter),
4361
5439
  controls: { $select: new UniquSelect(physicalFields) }
4362
5440
  });
4363
- if (rawRecords.length === 0) return;
5441
+ const pin = pkPhysical ? pinByPrimaryKey(rawRecords, pkPhysical) : void 0;
5442
+ if (rawRecords.length === 0) return pin;
4364
5443
  const allRecords = rawRecords.map((r) => {
4365
5444
  const mapped = {};
4366
5445
  for (const [key, val] of Object.entries(r)) mapped[physicalToLogical.get(key) ?? key] = val;
@@ -4376,7 +5455,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4376
5455
  addedKeys.push(key);
4377
5456
  records.push(record);
4378
5457
  }
4379
- if (records.length === 0) return;
5458
+ if (records.length === 0) return pin;
4380
5459
  try {
4381
5460
  await cascadeStorage.run({
4382
5461
  visited,
@@ -4416,6 +5495,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4416
5495
  } finally {
4417
5496
  for (const key of addedKeys) visited.delete(key);
4418
5497
  }
5498
+ return pin;
4419
5499
  }
4420
5500
  needsCascade(cascadeResolver) {
4421
5501
  return !!cascadeResolver;
@@ -4456,6 +5536,23 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
4456
5536
  return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
4457
5537
  }
4458
5538
  };
5539
+ /**
5540
+ * Adapter-ready filters addressing exactly `rows` by their (physical) primary
5541
+ * key, in batches of {@link PIN_BATCH}: `{ pk: { $in } }` for a single key,
5542
+ * `{ $or: [{ a, b }, …] }` for a composite one. Values are the raw stored
5543
+ * values the adapter returned, so no value formatting is re-applied.
5544
+ */
5545
+ function pinByPrimaryKey(rows, pk) {
5546
+ const out = [];
5547
+ for (let i = 0; i < rows.length; i += PIN_BATCH) {
5548
+ const batch = rows.slice(i, i + PIN_BATCH);
5549
+ if (pk.length === 1) {
5550
+ const field = pk[0];
5551
+ out.push({ [field]: { $in: batch.map((r) => r[field]) } });
5552
+ } else out.push({ $or: batch.map((r) => Object.fromEntries(pk.map((f) => [f, r[f]]))) });
5553
+ }
5554
+ return out;
5555
+ }
4459
5556
  //#endregion
4460
5557
  //#region src/patch/array-ops-resolver.ts
4461
5558
  /**
@@ -4954,43 +6051,40 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
4954
6051
  }
4955
6052
  return this.validators.get(purpose);
4956
6053
  }
4957
- /**
4958
- * Inserts a single record. Delegates to {@link insertMany} for unified
4959
- * nested creation support.
4960
- */
4961
6054
  async insertOne(payload, opts) {
4962
- return { insertedId: (await this.insertMany([payload], {
6055
+ const result = await this._insertMany([payload], {
4963
6056
  ...opts,
4964
6057
  _action: "insert"
4965
- })).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] };
4966
6067
  }
4967
- /**
4968
- * Inserts multiple records with batch-optimized nested creation.
4969
- *
4970
- * Supports **nested creation**: if payloads include data for navigation
4971
- * fields (`@db.rel.to` / `@db.rel.from`), related records are created
4972
- * automatically in batches. TO dependencies are batch-created first
4973
- * (their PKs become our FKs), FROM dependents are batch-created after
4974
- * (they receive our PKs as their FKs). Fully recursive — nested records
4975
- * with their own nav data trigger further batch inserts at each level.
4976
- * Recursive up to `maxDepth` (default 3).
4977
- *
4978
- * `opts.guard` (since 0.1.128) runs once inside the transaction, after
4979
- * defaults + validation, with the prepared rows; `opts.check` (since
4980
- * 0.1.143) runs once after every phase with the inserted rows' primary-key
4981
- * filters — see {@link TWriteOptions}.
4982
- */
4983
6068
  async insertMany(payloads, opts) {
6069
+ return this._insertMany(payloads, opts);
6070
+ }
6071
+ async _insertMany(payloads, opts) {
4984
6072
  this._ensureBuilt();
4985
- 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";
4986
6079
  const maxDepth = userMax ?? 3;
4987
6080
  const depth = _depth ?? 0;
4988
6081
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
4989
6082
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
4990
6083
  return enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
4991
6084
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
4992
- const items = payloads.map((p) => this._applyDefaults(clone(p)));
4993
- 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 })) : [];
4994
6088
  const validator = this.getValidator("insert");
4995
6089
  const ctx = {
4996
6090
  mode: "insert",
@@ -5003,18 +6097,28 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5003
6097
  validateBatch(validator, items, ctx);
5004
6098
  }
5005
6099
  await this._encryptItems(items, "write");
6100
+ if (ignore) this._rejectNestedToInIgnoreMode(items);
5006
6101
  const host = this;
5007
6102
  if (canNest) await batchInsertNestedTo(host, items, maxDepth, depth);
5008
- const prepared = [];
6103
+ let prepared = [];
5009
6104
  for (const data of items) {
5010
6105
  for (const navField of this._meta.navFields) delete data[navField];
5011
6106
  prepared.push(this._fieldMapper.prepareForWrite(data, this._meta, this.adapter));
5012
6107
  }
5013
6108
  await this._integrity.validateForeignKeys(items, this._meta, this._fkLookupResolver, this._writeTableResolver);
5014
6109
  if (canNest) await preValidateNestedFrom(host, originals);
5015
- const result = await this.adapter.insertMany(prepared);
5016
- if (canNest) await batchInsertNestedFrom(host, originals, result.insertedIds, maxDepth, depth);
5017
- 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);
5018
6122
  if (check) await this._runWriteCheck(check, _action ?? "insertMany", this._insertedPkFilters(items, prepared, result.insertedIds));
5019
6123
  return result;
5020
6124
  }));
@@ -5331,7 +6435,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5331
6435
  const filter = this._andScope(pinned, opts?.scope);
5332
6436
  const translated = this._fieldMapper.translateFilter(filter, this._meta);
5333
6437
  if (guard) await guard(new RemoveGuardContext(id, filter, this));
5334
- if (needsCascade) await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
6438
+ if (needsCascade) {
6439
+ const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
6440
+ if (pin) return pin.length > 0 ? this.adapter.deleteOne(pin[0]) : { deletedCount: 0 };
6441
+ }
5335
6442
  return this.adapter.deleteOne(translated);
5336
6443
  };
5337
6444
  return remapDeleteFkViolation(this.tableName, () => guard || needsCascade || candidates.length > 1 ? this.adapter.withTransaction(run) : run());
@@ -5375,7 +6482,12 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5375
6482
  this._ensureBuilt();
5376
6483
  this._guardMutationFilter(filter);
5377
6484
  if (this._integrity.needsCascade(this._cascadeResolver)) return remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
5378
- await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
6485
+ const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
6486
+ if (pin) {
6487
+ let deletedCount = 0;
6488
+ for (const batch of pin) deletedCount += (await this.adapter.deleteMany(batch)).deletedCount;
6489
+ return { deletedCount };
6490
+ }
5379
6491
  return this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta));
5380
6492
  }));
5381
6493
  return remapDeleteFkViolation(this.tableName, () => this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta)));
@@ -5397,7 +6509,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5397
6509
  /** Engine-agnostic guard for user-supplied mutation filters (updateMany/deleteMany/…). */
5398
6510
  _guardMutationFilter(filter) {
5399
6511
  guardFilter(this._meta, this.adapter, filter);
5400
- guardPaths(this._meta, this.adapter, { filter });
6512
+ guardPaths(this._meta, this.adapter, { filter }, false, relGuardState(true));
5401
6513
  }
5402
6514
  /**
5403
6515
  * Encrypts `@db.encrypted` field values in place on (already validated)
@@ -5567,6 +6679,91 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5567
6679
  });
5568
6680
  }
5569
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
+ /**
5570
6767
  * The exact primary-key filter of each inserted row: the logical key from
5571
6768
  * the row (SDK defaults applied), else the stored key the adapter wrote into
5572
6769
  * the prepared row (e.g. a driver-assigned `_id`), else — single-field keys
@@ -5794,8 +6991,16 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
5794
6991
  };
5795
6992
  //#endregion
5796
6993
  //#region src/table/db-view.ts
5797
- /** The `@db.agg.*` annotations, one per supported aggregate function. */
5798
- const AGG_KEYS = SUPPORTED_AGGREGATE_FNS.map((fn) => `db.agg.${fn}`);
6994
+ /**
6995
+ * The single `@meta.id` field of a first-row join target (its source type,
6996
+ * past `@db.alias`) — the anchor of the join's correlated subquery.
6997
+ * @throws when the target declares no or several primary-key fields.
6998
+ */
6999
+ function firstJoinKey(view, type, scope) {
7000
+ const ids = type.type.kind === "object" ? [...type.type.props.entries()].filter(([, p]) => p.metadata.has("meta.id")) : [];
7001
+ if (ids.length !== 1) throw new Error(`View "${view}": the first-row join on "${scope}" needs a target with exactly one @meta.id field (found ${ids.length})`);
7002
+ return ids[0][0];
7003
+ }
5799
7004
  /**
5800
7005
  * Reads a view field's `@db.agg.*` annotation. The compiled value is
5801
7006
  * `{ field?, condition? }`; models compiled by older versions carry `true`
@@ -5803,7 +7008,7 @@ const AGG_KEYS = SUPPORTED_AGGREGATE_FNS.map((fn) => `db.agg.${fn}`);
5803
7008
  * A missing field or `true` is `'*'` (COUNT(*)).
5804
7009
  */
5805
7010
  function readViewAgg(metadata) {
5806
- for (const key of AGG_KEYS) {
7011
+ for (const key of AGG_ANNOTATIONS) {
5807
7012
  const val = metadata?.get(key);
5808
7013
  if (val === void 0) continue;
5809
7014
  const aggFn = key.slice(7);
@@ -5881,7 +7086,9 @@ function inheritViewFieldSeals(viewType) {
5881
7086
  try {
5882
7087
  const forRef = viewType.metadata.get("db.view.for");
5883
7088
  const entryType = forRef && refType(forRef);
5884
- for (const [fieldName, fieldType] of viewType.type.props.entries()) {
7089
+ const props = viewType.type.props;
7090
+ for (const [fieldName, fieldType] of props.entries()) {
7091
+ if (computeOf(fieldType) !== void 0) continue;
5885
7092
  const { agg, sourceType, sourcePath } = viewFieldSource(fieldName, fieldType, entryType);
5886
7093
  if (agg?.aggField === "*" || !sourceType) continue;
5887
7094
  const source = viewSourceOf(sourceType).type;
@@ -5892,6 +7099,14 @@ function inheritViewFieldSeals(viewType) {
5892
7099
  if (agg) throw new Error(`View "${tableNameOf(viewType)}" field "${fieldName}": @db.agg.${agg.aggFn} over the @db.encrypted field "${sourcePath}" — ciphertext cannot be aggregated`);
5893
7100
  fieldType.metadata.set("db.encrypted", true);
5894
7101
  }
7102
+ for (const [fieldName, fieldType] of props.entries()) {
7103
+ const computed = computedOperands(viewType, fieldName);
7104
+ if (!computed) continue;
7105
+ const { operands, via } = computed;
7106
+ const encrypted = operands.find((p) => props.get(p)?.metadata.has("db.encrypted"));
7107
+ if (encrypted) throw new Error(`View "${tableNameOf(viewType)}": @db.compute over the @db.encrypted field "${encrypted}" — ciphertext cannot be computed`);
7108
+ if ([...operands, ...via].some((p) => props.get(p)?.metadata.has("db.writeOnly"))) fieldType.metadata.set("db.writeOnly", true);
7109
+ }
5895
7110
  } catch (error) {
5896
7111
  sealedViews.delete(viewType);
5897
7112
  throw error;
@@ -5954,13 +7169,39 @@ var AtscriptDbView = class extends AtscriptDbReadable {
5954
7169
  if (rawJoins) for (const join of rawJoins) {
5955
7170
  const targetType = refType(join.target);
5956
7171
  const target = viewSourceOf(targetType());
5957
- joins.push({
7172
+ const viewJoin = {
5958
7173
  targetType,
5959
7174
  targetTable: target.table,
5960
7175
  scope: target.name,
5961
7176
  condition: join.condition,
5962
7177
  kind: join.kind === "left" ? "left" : "inner"
5963
- });
7178
+ };
7179
+ if (join.order?.length) {
7180
+ const key = firstJoinKey(this.tableName, target.type, target.name);
7181
+ const order = join.order.map((item) => {
7182
+ const seals = sourceFieldSeals(target.type, item.ref.field);
7183
+ if (seals.encrypted || seals.writeOnly) throw new Error(`View "${this.tableName}": the first-row join on "${target.name}" cannot order by the ${seals.encrypted ? "@db.encrypted" : "@db.writeOnly"} field "${item.ref.field}"`);
7184
+ return {
7185
+ ref: {
7186
+ type: targetType,
7187
+ field: item.ref.field
7188
+ },
7189
+ desc: item.desc === true
7190
+ };
7191
+ });
7192
+ if (!order.some((item) => item.ref.field === key)) order.push({
7193
+ ref: {
7194
+ type: targetType,
7195
+ field: key
7196
+ },
7197
+ desc: false
7198
+ });
7199
+ viewJoin.first = {
7200
+ order,
7201
+ key
7202
+ };
7203
+ }
7204
+ joins.push(viewJoin);
5964
7205
  }
5965
7206
  const filter = metadata.get("db.view.filter");
5966
7207
  const having = metadata.get("db.view.having");
@@ -6037,6 +7278,26 @@ var AtscriptDbView = class extends AtscriptDbReadable {
6037
7278
  this._columnMappings ??= this._buildColumnMappings();
6038
7279
  return this._columnMappings;
6039
7280
  }
7281
+ /**
7282
+ * Why the bound adapter cannot render this managed view: one message per
7283
+ * computed column / first-row join whose feature its `viewCapabilities()`
7284
+ * does not list. Empty for an external view or when every feature is
7285
+ * rendered. Schema sync refuses such a view; the adapter's `ensureTable()`
7286
+ * throws for it (fail-closed for adapters that predate the features).
7287
+ * @since 0.1.147
7288
+ */
7289
+ viewCapabilityProblems() {
7290
+ if (this.isExternal) return [];
7291
+ const caps = this.dbAdapter.viewCapabilities();
7292
+ const problems = [];
7293
+ if (!caps.has("compute")) {
7294
+ for (const col of this.getViewColumnMappings()) if (col.expr !== void 0) problems.push(`View "${this.tableName}" field "${col.viewPath}": computed columns (@db.compute) are not supported by this adapter (viewCapabilities())`);
7295
+ }
7296
+ if (!caps.has("firstJoin")) {
7297
+ for (const join of this.viewPlan.joins) if (join.first) problems.push(`View "${this.tableName}" join "${join.scope}": first-row joins are not supported by this adapter (viewCapabilities())`);
7298
+ }
7299
+ return problems;
7300
+ }
6040
7301
  _buildColumnMappings() {
6041
7302
  const plan = this.viewPlan;
6042
7303
  const mappings = [];
@@ -6051,6 +7312,17 @@ var AtscriptDbView = class extends AtscriptDbReadable {
6051
7312
  const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.scope));
6052
7313
  for (const [fieldName, fieldType] of this._type.type.props.entries()) {
6053
7314
  if (ignored.has(fieldName)) continue;
7315
+ const expr = computeOf(fieldType);
7316
+ if (expr !== void 0) {
7317
+ mappings.push({
7318
+ viewColumn: viewName(fieldName) ?? fieldName,
7319
+ viewPath: fieldName,
7320
+ sourceTable: plan.entryTable,
7321
+ sourceColumn: "",
7322
+ expr
7323
+ });
7324
+ continue;
7325
+ }
6054
7326
  const { agg, chained, sourceType, sourcePath } = viewFieldSource(fieldName, fieldType, plan.entryType);
6055
7327
  const aggField = agg?.aggField;
6056
7328
  if (aggField === "*" && agg?.aggFn !== "count") fail(fieldName, `aggregate "${agg?.aggFn}" needs a field — only count accepts *`);
@@ -6087,8 +7359,37 @@ var AtscriptDbView = class extends AtscriptDbReadable {
6087
7359
  ...agg
6088
7360
  } : mapping);
6089
7361
  }
7362
+ this._checkComputed(mappings, fail);
6090
7363
  return mappings;
6091
7364
  }
7365
+ /**
7366
+ * The runtime twin of the compile-time `@db.compute` rules the renderers
7367
+ * rely on: every leaf names a (non-ignored) view column, computed columns
7368
+ * form no cycle; sets each computed mapping's `nullable`.
7369
+ */
7370
+ _checkComputed(mappings, fail) {
7371
+ const computed = mappings.filter((m) => m.expr !== void 0);
7372
+ if (computed.length === 0) return;
7373
+ const byPath = new Map(mappings.map((m) => [m.viewPath, m]));
7374
+ const props = this._type.type.kind === "object" ? this._type.type.props : void 0;
7375
+ const state = /* @__PURE__ */ new Map();
7376
+ const visit = (m, chain) => {
7377
+ if (state.get(m.viewPath) === "done") return;
7378
+ if (state.get(m.viewPath) === "visiting") fail(m.viewPath, `@db.compute depends on itself: ${[...chain, m.viewPath].join(" → ")}`);
7379
+ state.set(m.viewPath, "visiting");
7380
+ walkViewExpr(m.expr, (path) => {
7381
+ const leaf = byPath.get(path);
7382
+ if (!leaf) fail(m.viewPath, `@db.compute references "${path}", which is not a column of the view`);
7383
+ if (leaf.expr !== void 0) visit(leaf, [...chain, m.viewPath]);
7384
+ });
7385
+ state.set(m.viewPath, "done");
7386
+ if (viewExprNullable(m.expr, (path) => {
7387
+ const leaf = byPath.get(path);
7388
+ return leaf.expr === void 0 ? props?.get(path)?.optional === true : leaf.nullable === true;
7389
+ })) m.nullable = true;
7390
+ };
7391
+ for (const m of computed) visit(m, []);
7392
+ }
6092
7393
  /** One view column over one physical source (a column or a JSON leaf). */
6093
7394
  _leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable) {
6094
7395
  const mapping = {
@@ -6121,6 +7422,62 @@ function isAtscriptDbView(readable) {
6121
7422
  return readable.isView;
6122
7423
  }
6123
7424
  //#endregion
7425
+ //#region src/schema/fk-diff.ts
7426
+ /** Canonical key for an FK: sorted local field names, comma-joined. */
7427
+ function fkKey(fields) {
7428
+ return [...fields].toSorted().join(",");
7429
+ }
7430
+ /**
7431
+ * Physical local / target column names of a desired FK (`@db.column` renames
7432
+ * applied) — what DDL, constraint sync, the FK diff and the snapshot compare.
7433
+ */
7434
+ function fkColumns(fk) {
7435
+ return {
7436
+ fields: fk.physicalFields ?? fk.fields,
7437
+ targetFields: fk.physicalTargetFields ?? fk.targetFields
7438
+ };
7439
+ }
7440
+ /**
7441
+ * Compares desired FK constraints against stored snapshot to detect
7442
+ * additions, removals, and property changes (target table, target fields,
7443
+ * onDelete, onUpdate).
7444
+ */
7445
+ function computeForeignKeyDiff(desired, existingSnapshot) {
7446
+ const added = [];
7447
+ const removed = [];
7448
+ const changed = [];
7449
+ const existingByKey = /* @__PURE__ */ new Map();
7450
+ for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
7451
+ const desiredKeys = /* @__PURE__ */ new Set();
7452
+ for (const fk of desired.values()) {
7453
+ const key = fkKey(fkColumns(fk).fields);
7454
+ desiredKeys.add(key);
7455
+ const existing = existingByKey.get(key);
7456
+ if (!existing) added.push(fk);
7457
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
7458
+ desired: fk,
7459
+ existing
7460
+ });
7461
+ }
7462
+ for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
7463
+ return {
7464
+ added,
7465
+ removed,
7466
+ changed
7467
+ };
7468
+ }
7469
+ /** Whether the FK diff contains any changes. */
7470
+ function hasForeignKeyChanges(diff) {
7471
+ return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
7472
+ }
7473
+ function fkPropertiesDiffer(desired, existing) {
7474
+ if (desired.targetTable !== existing.targetTable) return true;
7475
+ if (fkKey(fkColumns(desired).targetFields) !== fkKey(existing.targetFields)) return true;
7476
+ if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
7477
+ if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
7478
+ return false;
7479
+ }
7480
+ //#endregion
6124
7481
  //#region src/schema/schema-hash.ts
6125
7482
  /**
6126
7483
  * The physical sources a stored view snapshot reads: its entry table and the
@@ -6187,9 +7544,9 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
6187
7544
  }))
6188
7545
  })).toSorted((a, b) => a.key.localeCompare(b.key));
6189
7546
  const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
6190
- fields: [...fk.fields].toSorted(),
7547
+ fields: [...fkColumns(fk).fields].toSorted(),
6191
7548
  targetTable: fk.targetTable,
6192
- targetFields: [...fk.targetFields].toSorted(),
7549
+ targetFields: [...fkColumns(fk).targetFields].toSorted(),
6193
7550
  onDelete: fk.onDelete,
6194
7551
  onUpdate: fk.onUpdate
6195
7552
  })).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
@@ -6217,7 +7574,9 @@ function computeViewSnapshot(view) {
6217
7574
  const plan = view.viewPlan;
6218
7575
  const qualify = (ref) => view.resolveFieldRef(ref, (n) => n);
6219
7576
  const canonical = (node) => JSON.stringify(canonicalizeQueryNode(node, qualify));
6220
- const columns = view.getViewColumnMappings().map((m) => {
7577
+ const mappings = view.getViewColumnMappings();
7578
+ const columnOf = new Map(mappings.map((m) => [m.viewPath, m.viewColumn]));
7579
+ const columns = mappings.map((m) => {
6221
7580
  const col = {
6222
7581
  column: m.viewColumn,
6223
7582
  sourceTable: m.sourceTable,
@@ -6230,6 +7589,7 @@ function computeViewSnapshot(view) {
6230
7589
  if (m.aggFn) col.aggFn = m.aggFn;
6231
7590
  if (m.aggField) col.aggField = m.aggField;
6232
7591
  if (m.aggFilter) col.aggFilter = canonical(m.aggFilter);
7592
+ if (m.expr !== void 0) col.expr = JSON.stringify(canonicalizeViewExpr(m.expr, (path) => columnOf.get(path) ?? path));
6233
7593
  return col;
6234
7594
  }).toSorted((a, b) => a.column < b.column ? -1 : a.column > b.column ? 1 : 0);
6235
7595
  const result = {
@@ -6243,6 +7603,7 @@ function computeViewSnapshot(view) {
6243
7603
  condition: canonical(j.condition)
6244
7604
  };
6245
7605
  if (j.kind === "left") join.kind = "left";
7606
+ if (j.first) join.order = JSON.stringify(j.first.order.map((item) => [qualify(item.ref), item.desc ? -1 : 1]));
6246
7607
  return join;
6247
7608
  }),
6248
7609
  columns
@@ -6281,6 +7642,20 @@ function canonicalizeQueryNode(node, qualify) {
6281
7642
  return out;
6282
7643
  }
6283
7644
  /**
7645
+ * Converts a `@db.compute` expression into a serializable structure whose
7646
+ * JSON is a stable function of its meaning: leaves become the view column
7647
+ * they read (`column(path)`), literals `{ n }`, operations `{ op, a }`.
7648
+ * @since 0.1.147
7649
+ */
7650
+ function canonicalizeViewExpr(expr, column) {
7651
+ if (typeof expr === "number") return { n: expr };
7652
+ if ("field" in expr) return { c: column(expr.field) };
7653
+ return {
7654
+ op: expr.op,
7655
+ a: expr.args.map((arg) => canonicalizeViewExpr(arg, column))
7656
+ };
7657
+ }
7658
+ /**
6284
7659
  * Computes a deterministic hash string from multiple table snapshots.
6285
7660
  * Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
6286
7661
  */
@@ -6344,52 +7719,6 @@ function fnv1a(str) {
6344
7719
  return Math.trunc(hash).toString(16).padStart(8, "0");
6345
7720
  }
6346
7721
  //#endregion
6347
- //#region src/schema/fk-diff.ts
6348
- /** Canonical key for an FK: sorted local field names, comma-joined. */
6349
- function fkKey(fields) {
6350
- return [...fields].toSorted().join(",");
6351
- }
6352
- /**
6353
- * Compares desired FK constraints against stored snapshot to detect
6354
- * additions, removals, and property changes (target table, target fields,
6355
- * onDelete, onUpdate).
6356
- */
6357
- function computeForeignKeyDiff(desired, existingSnapshot) {
6358
- const added = [];
6359
- const removed = [];
6360
- const changed = [];
6361
- const existingByKey = /* @__PURE__ */ new Map();
6362
- for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
6363
- const desiredKeys = /* @__PURE__ */ new Set();
6364
- for (const fk of desired.values()) {
6365
- const key = fkKey(fk.fields);
6366
- desiredKeys.add(key);
6367
- const existing = existingByKey.get(key);
6368
- if (!existing) added.push(fk);
6369
- else if (fkPropertiesDiffer(fk, existing)) changed.push({
6370
- desired: fk,
6371
- existing
6372
- });
6373
- }
6374
- for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
6375
- return {
6376
- added,
6377
- removed,
6378
- changed
6379
- };
6380
- }
6381
- /** Whether the FK diff contains any changes. */
6382
- function hasForeignKeyChanges(diff) {
6383
- return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
6384
- }
6385
- function fkPropertiesDiffer(desired, existing) {
6386
- if (desired.targetTable !== existing.targetTable) return true;
6387
- if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
6388
- if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
6389
- if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
6390
- return false;
6391
- }
6392
- //#endregion
6393
7722
  //#region src/schema/column-diff.ts
6394
7723
  /**
6395
7724
  * Why a derived column (desired, live, or both) must be dropped and re-added,
@@ -6518,4 +7847,4 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
6518
7847
  return diff;
6519
7848
  }
6520
7849
  //#endregion
6521
- export { isGeoIndexableType as $, vectorIndexNotFoundMessage as A, guardAggregate as B, createFailureCollector as C, resolveDesignType as D, AtscriptDbReadable as E, bucketSourceVerdict as F, narrowerFilterOps as G, guardPath as H, canFilterLeaf as I, RelationalFieldMapper as J, sortFieldNames as K, checkHavingKeys as L, ENCRYPTED_REASON as M, acceptedOperatorsHint as N, geoIndexNotFoundMessage as O, assertGeoPoint as P, TableMetadata as Q, classifyQueryPath as R, BaseDbAdapter as S, NativeIntegrity as T, guardPaths as U, guardFilter as V, guardQuery as W, FieldMappingStrategy as X, DocumentFieldMapper as Y, UniquSelect as Z, AtscriptDbTable as _, hasForeignKeyChanges as a, normalizeComputedSelect as at, ApplicationIntegrity as b, computeTableHash as c, snapshotToExistingColumns as d, isGeoPointType as et, snapshotToExistingTableOptions as f, isViewType as g, isAtscriptDbView as h, fkKey as i, jsonValueAncestor as it, ADAPTER_FILTER_REASON as j, searchIndexNotFoundMessage as k, computeTableSnapshot as l, AtscriptDbView as m, isColumnTypeChanged as n, isBucketableField as nt, canonicalizeQueryNode as o, resolveCalendarBuckets as ot, viewSnapshotSources as p, unsupportedOperatorMessage as q, computeForeignKeyDiff as r, isJsonValueField as rt, computeSchemaHash as s, NoopLogger as st, computeColumnDiff as t, aliasTargetOf as tt, computeViewSnapshot as u, assertNoVersionWrites as v, IntegrityStrategy as w, ALL_BUCKET_UNITS as x, decomposePatch as y, collectQueryPaths 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 };