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