@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.
- 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-CGBv3E8S.cjs → aggregate-fns-C-UJRobm.cjs} +52 -5
- package/dist/{aggregate-fns-CfsveE1w.mjs → aggregate-fns-CyaZyb9I.mjs} +35 -6
- package/dist/aggregate-rules-D_bsCpUI.cjs +26 -0
- package/dist/aggregate-rules-jdPrxqWa.mjs +15 -0
- package/dist/{buckets-DRycmhOW.d.mts → buckets-CNdTOnei.d.mts} +832 -33
- package/dist/{buckets-DYFu0eZ8.d.cts → buckets-DJiYlMXc.d.cts} +832 -33
- package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-CUU4GvYg.cjs} +1546 -193
- package/dist/{column-diff-e2oHc71_.mjs → column-diff-Cp6ZoyRE.mjs} +1529 -200
- 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/{column-diff-Q9UmWn5x.d.mts → fk-diff-BQ4krij8.d.cts} +48 -2
- package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DsaIijVX.d.mts} +48 -2
- package/dist/index.cjs +95 -35
- package/dist/index.d.cts +92 -22
- package/dist/index.d.mts +92 -22
- package/dist/index.mjs +65 -35
- package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-BUQvk6QT.cjs} +734 -2
- package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-CUBoq1ZO.mjs} +598 -4
- 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 +293 -5
- package/dist/plugin.mjs +294 -6
- 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-B-0NRKat.d.mts → relation-helpers-Ba0v49sn.d.mts} +1 -1
- package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-kX7jjgME.d.cts} +1 -1
- package/dist/relation-loader-ByY1Byrl.mjs +370 -0
- package/dist/relation-loader-D8OrdH-r.cjs +369 -0
- package/dist/shared.cjs +4 -1
- package/dist/shared.d.cts +16 -4
- package/dist/shared.d.mts +16 -4
- package/dist/shared.mjs +3 -2
- package/dist/sync.cjs +11 -7
- package/dist/sync.d.cts +2 -25
- package/dist/sync.d.mts +2 -25
- package/dist/sync.mjs +11 -7
- package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
- package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
- 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-Bw6ks9Hy.d.mts → validator-tBNvM1qc.d.cts} +31 -7
- package/dist/{validator-Bw6ks9Hy.d.cts → validator-tBNvM1qc.d.mts} +31 -7
- 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
- package/dist/relation-loader-CBPY6kM7.cjs +0 -461
- package/dist/relation-loader-D9XuXaMv.mjs +0 -462
|
@@ -1,6 +1,134 @@
|
|
|
1
|
-
import { i as DepthLimitExceededError, r as DbError } from "./db-error-
|
|
2
|
-
import { r as getPath } from "./object-TkiJQ-Dp.mjs";
|
|
1
|
+
import { i as DepthLimitExceededError, r as DbError } from "./db-error-Az85UhTX.mjs";
|
|
2
|
+
import { a as isPlainObject, r as getPath } from "./object-TkiJQ-Dp.mjs";
|
|
3
3
|
import { ValidatorError } from "@atscript/typescript/utils";
|
|
4
|
+
import { isRelationOp } from "@uniqu/core";
|
|
5
|
+
//#region src/query/query-tree.ts
|
|
6
|
+
/**
|
|
7
|
+
* `true` when a query-tree operand is a field reference (`{ field, type? }`)
|
|
8
|
+
* rather than a literal — e.g. the right side of a field-to-field comparison.
|
|
9
|
+
* @since 0.1.136
|
|
10
|
+
*/
|
|
11
|
+
function isFieldRef(value) {
|
|
12
|
+
return value !== null && typeof value === "object" && "field" in value;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Translates a JS-emitted query tree into a FilterExpr.
|
|
16
|
+
* Resolves field references (type + field path) to physical column names
|
|
17
|
+
* via the provided resolver function.
|
|
18
|
+
*/
|
|
19
|
+
function translateQueryTree(node, resolveField) {
|
|
20
|
+
if ("$and" in node) return { $and: node.$and.map((n) => translateQueryTree(n, resolveField)) };
|
|
21
|
+
if ("$or" in node) return { $or: node.$or.map((n) => translateQueryTree(n, resolveField)) };
|
|
22
|
+
if ("$not" in node) return { $not: translateQueryTree(node.$not, resolveField) };
|
|
23
|
+
const comp = node;
|
|
24
|
+
const leftField = resolveField(comp.left);
|
|
25
|
+
if (isFieldRef(comp.right)) {
|
|
26
|
+
const rightField = resolveField(comp.right);
|
|
27
|
+
return { [leftField]: { [comp.op]: { $field: rightField } } };
|
|
28
|
+
}
|
|
29
|
+
if (comp.op === "$exists") return { [leftField]: { $exists: comp.right !== false } };
|
|
30
|
+
return { [leftField]: { [comp.op]: comp.right } };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Calls `leaf` with the field path of every field-reference leaf of a
|
|
34
|
+
* computed-column expression (`@db.compute`), in source order — leaves name
|
|
35
|
+
* the view's own fields.
|
|
36
|
+
* @since 0.1.147
|
|
37
|
+
*/
|
|
38
|
+
function walkViewExpr(expr, leaf) {
|
|
39
|
+
if (typeof expr === "number") return;
|
|
40
|
+
if ("field" in expr) {
|
|
41
|
+
leaf(expr.field);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
for (const arg of expr.args) walkViewExpr(arg, leaf);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Evaluates a computed expression in process (the memory adapter, a test, a
|
|
48
|
+
* third-party adapter) with the semantics every adapter shares: IEEE double,
|
|
49
|
+
* a leaf is `Number()`-ed, NULL / undefined propagates as `null`, `/` by zero
|
|
50
|
+
* is `null`, `coalesce` returns its first non-null value.
|
|
51
|
+
* @since 0.1.148
|
|
52
|
+
*/
|
|
53
|
+
function evaluateExpr(expr, leaf) {
|
|
54
|
+
if (typeof expr === "number") return expr;
|
|
55
|
+
if ("field" in expr) {
|
|
56
|
+
const v = leaf(expr.field);
|
|
57
|
+
if (v === null || v === void 0) return null;
|
|
58
|
+
const n = Number(v);
|
|
59
|
+
return Number.isNaN(n) ? null : n;
|
|
60
|
+
}
|
|
61
|
+
if (expr.op === "coalesce") {
|
|
62
|
+
for (const arg of expr.args) {
|
|
63
|
+
const v = evaluateExpr(arg, leaf);
|
|
64
|
+
if (v !== null) return v;
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const a = evaluateExpr(expr.args[0], leaf);
|
|
69
|
+
if (a === null) return null;
|
|
70
|
+
if (expr.op === "neg") return -a;
|
|
71
|
+
const b = evaluateExpr(expr.args[1], leaf);
|
|
72
|
+
if (b === null) return null;
|
|
73
|
+
switch (expr.op) {
|
|
74
|
+
case "+": return a + b;
|
|
75
|
+
case "-": return a - b;
|
|
76
|
+
case "*": return a * b;
|
|
77
|
+
default: return b === 0 ? null : a / b;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Whether a computed-column expression may yield NULL: a `/` (division by
|
|
82
|
+
* zero is NULL), a leaf for which `nullableLeaf` holds, or an operation over
|
|
83
|
+
* a nullable operand — `coalesce` only when every argument is nullable.
|
|
84
|
+
* @since 0.1.147
|
|
85
|
+
*/
|
|
86
|
+
function viewExprNullable(expr, nullableLeaf) {
|
|
87
|
+
if (typeof expr === "number") return false;
|
|
88
|
+
if ("field" in expr) return nullableLeaf(expr.field);
|
|
89
|
+
if (expr.op === "/") return true;
|
|
90
|
+
if (expr.op === "coalesce") return expr.args.every((a) => viewExprNullable(a, nullableLeaf));
|
|
91
|
+
return expr.args.some((a) => viewExprNullable(a, nullableLeaf));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The `@db.compute` expression of a view field, if any.
|
|
95
|
+
* @since 0.1.147
|
|
96
|
+
*/
|
|
97
|
+
function computeOf(fieldType) {
|
|
98
|
+
return fieldType?.metadata.get("db.compute");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The transitive non-computed operands of the `@db.compute` field `field` of
|
|
102
|
+
* `viewType`: its leaves, a computed leaf replaced by its own operands, and
|
|
103
|
+
* (`via`) the computed fields passed through. `undefined` when `field` is not
|
|
104
|
+
* computed. Cycles are not followed (they are rejected where the view's
|
|
105
|
+
* columns are built).
|
|
106
|
+
* @since 0.1.147
|
|
107
|
+
*/
|
|
108
|
+
function computedOperands(viewType, field) {
|
|
109
|
+
const props = viewType.type.kind === "object" ? viewType.type.props : void 0;
|
|
110
|
+
const exprOf = (name) => computeOf(props?.get(name));
|
|
111
|
+
const root = exprOf(field);
|
|
112
|
+
if (root === void 0) return void 0;
|
|
113
|
+
const out = /* @__PURE__ */ new Set();
|
|
114
|
+
const via = [];
|
|
115
|
+
const seen = new Set([field]);
|
|
116
|
+
const visit = (expr) => walkViewExpr(expr, (path) => {
|
|
117
|
+
const nested = exprOf(path);
|
|
118
|
+
if (nested === void 0) out.add(path);
|
|
119
|
+
else if (!seen.has(path)) {
|
|
120
|
+
seen.add(path);
|
|
121
|
+
via.push(path);
|
|
122
|
+
visit(nested);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
visit(root);
|
|
126
|
+
return {
|
|
127
|
+
operands: [...out],
|
|
128
|
+
via
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
4
132
|
//#region src/rel/relation-helpers.ts
|
|
5
133
|
/**
|
|
6
134
|
* Finds the FK entry that connects a `@db.rel.to` relation to its target.
|
|
@@ -52,10 +180,462 @@ function tableNameOf(type) {
|
|
|
52
180
|
function resolveRelationTargetTable(relation) {
|
|
53
181
|
return tableNameOf(relation.targetType());
|
|
54
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* A string key of `fields`' values on `obj` — equal for rows that agree on
|
|
185
|
+
* every field (`null` / `undefined` key distinctly from any string). `read`
|
|
186
|
+
* reads one field (default: a top-level property).
|
|
187
|
+
*/
|
|
188
|
+
function compositeKey(fields, obj, read = (o, f) => o[f]) {
|
|
189
|
+
let key = "";
|
|
190
|
+
for (let i = 0; i < fields.length; i++) {
|
|
191
|
+
if (i > 0) key += "\0\0";
|
|
192
|
+
const v = read(obj, fields[i]);
|
|
193
|
+
key += v === null || v === void 0 ? "\0" : String(v);
|
|
194
|
+
}
|
|
195
|
+
return key;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Keeps, of each group of `rows` sharing `keyOf(row)`, the rows `page`
|
|
199
|
+
* selects — `$skip` / `$limit` applied per group, in the order of `rows`.
|
|
200
|
+
* The order of the kept rows is preserved.
|
|
201
|
+
*/
|
|
202
|
+
function slicePerGroup(rows, keyOf, page) {
|
|
203
|
+
const skip = page.skip ?? 0;
|
|
204
|
+
const end = page.limit === void 0 ? Infinity : skip + page.limit;
|
|
205
|
+
const seen = /* @__PURE__ */ new Map();
|
|
206
|
+
const kept = [];
|
|
207
|
+
for (const row of rows) {
|
|
208
|
+
const key = keyOf(row);
|
|
209
|
+
const index = seen.get(key) ?? 0;
|
|
210
|
+
seen.set(key, index + 1);
|
|
211
|
+
if (index >= skip && index < end) kept.push(row);
|
|
212
|
+
}
|
|
213
|
+
return kept;
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/query/relation-filter.ts
|
|
217
|
+
/**
|
|
218
|
+
* Relational filter predicates (`{ nav: { $some | $none: <filter on the
|
|
219
|
+
* related table> } }`) — resolution against the related tables.
|
|
220
|
+
*
|
|
221
|
+
* Semantics: `rel(r, nav)` is exactly the set of related rows `$with=nav`
|
|
222
|
+
* loads for row `r` (same foreign-key pairing, the relation's
|
|
223
|
+
* `@db.rel.filter` included). `$some: F` holds when one of them matches `F`,
|
|
224
|
+
* `$none: F` when none does; a NULL foreign-key component means "no related
|
|
225
|
+
* row". The core resolves each predicate into a {@link ResolvedRelationFilter}
|
|
226
|
+
* (physical names on every side, the inner filter translated by the related
|
|
227
|
+
* table's own field mapper) before the adapter sees the filter — adapters
|
|
228
|
+
* only render it.
|
|
229
|
+
*
|
|
230
|
+
* @since 0.1.147
|
|
231
|
+
*/
|
|
232
|
+
/**
|
|
233
|
+
* Maximum nesting of relational predicates in one filter, server-added ones
|
|
234
|
+
* included (a predicate inside a predicate's operand counts one level). The
|
|
235
|
+
* core backstop for every caller; higher than moost-db's per-client limit
|
|
236
|
+
* (`REL_FILTER_CLIENT_MAX_DEPTH`) so server overlays (row scopes,
|
|
237
|
+
* `transformRelationFilter`) have headroom above what a client may send.
|
|
238
|
+
*/
|
|
239
|
+
const REL_FILTER_MAX_DEPTH = 4;
|
|
240
|
+
/** Maximum number of relational predicates in one filter (nested and server-added ones included). See {@link REL_FILTER_MAX_DEPTH}. */
|
|
241
|
+
const REL_FILTER_MAX_NODES = 16;
|
|
242
|
+
/**
|
|
243
|
+
* Cross-realm brand of {@link ResolvedRelationFilter}: `instanceof` fails when
|
|
244
|
+
* two copies of `@atscript/db` are loaded (ESM + CJS, or nested installs) and
|
|
245
|
+
* an adapter from one sees nodes built by the other.
|
|
246
|
+
*/
|
|
247
|
+
const RESOLVED_BRAND = Symbol.for("@atscript/db:ResolvedRelationFilter");
|
|
248
|
+
/**
|
|
249
|
+
* A relational predicate as adapters receive it — the operand of
|
|
250
|
+
* `FilterVisitor.relation(field, op, operand)` once the core translated the
|
|
251
|
+
* filter. Every name is physical:
|
|
252
|
+
*
|
|
253
|
+
* - `to` / `from`: `pairs` correlate a SOURCE column with a TARGET column
|
|
254
|
+
* (`target.<pair.target> = source.<pair.source>`, one pair per composite
|
|
255
|
+
* key part);
|
|
256
|
+
* - `via`: `pairs` is empty — `junction.toSource` correlates the junction
|
|
257
|
+
* with the source row, `junction.toTarget` with the target row.
|
|
258
|
+
*
|
|
259
|
+
* `filter` is the inner filter on the TARGET (already translated by the
|
|
260
|
+
* target's field mapper: renames, flattening, value formatters; nested
|
|
261
|
+
* predicates resolved the same way), conjoined with the target part of the
|
|
262
|
+
* relation's `@db.rel.filter`. `{}` matches every related row.
|
|
263
|
+
*
|
|
264
|
+
* @since 0.1.147
|
|
265
|
+
*/
|
|
266
|
+
var ResolvedRelationFilter = class {
|
|
267
|
+
/** @internal cross-realm brand (see {@link isResolvedRelationFilter}). */
|
|
268
|
+
[RESOLVED_BRAND] = true;
|
|
269
|
+
kind;
|
|
270
|
+
/** Logical navigation field name on the source table. */
|
|
271
|
+
nav;
|
|
272
|
+
source;
|
|
273
|
+
target;
|
|
274
|
+
pairs;
|
|
275
|
+
junction;
|
|
276
|
+
filter;
|
|
277
|
+
constructor(init) {
|
|
278
|
+
this.kind = init.kind;
|
|
279
|
+
this.nav = init.nav;
|
|
280
|
+
this.source = init.source;
|
|
281
|
+
this.target = init.target;
|
|
282
|
+
this.pairs = init.pairs;
|
|
283
|
+
this.junction = init.junction;
|
|
284
|
+
this.filter = init.filter;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
/** `true` for a {@link ResolvedRelationFilter} (the resolved operand of a predicate). */
|
|
288
|
+
function isResolvedRelationFilter(value) {
|
|
289
|
+
return typeof value === "object" && value !== null && value[RESOLVED_BRAND] === true;
|
|
290
|
+
}
|
|
291
|
+
/** `true` when `value` (a filter entry's value) is an operator map with a `$some` / `$none` key. */
|
|
292
|
+
function hasRelationOp(value) {
|
|
293
|
+
if (!isPlainObject(value)) return false;
|
|
294
|
+
for (const key in value) if (isRelationOp(key)) return true;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Pre-scan results of filters the core BUILT (translated / resolved
|
|
299
|
+
* filters, never mutated after) — adapters scan the same translated filter
|
|
300
|
+
* several times per operation. Caller-owned filters are never cached: they
|
|
301
|
+
* may be mutated between two queries.
|
|
302
|
+
*/
|
|
303
|
+
const relationFilterCache = /* @__PURE__ */ new WeakMap();
|
|
304
|
+
/** @internal Records the pre-scan result of a filter the core built. */
|
|
305
|
+
function noteRelationFilter(filter, has) {
|
|
306
|
+
if (filter && typeof filter === "object") relationFilterCache.set(filter, has);
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* `true` when `filter` holds a relational predicate anywhere outside
|
|
310
|
+
* predicate operands (through `$and` / `$or` / `$not`) — the cheap pre-scan
|
|
311
|
+
* the field mappers and renderers use to keep predicate-free filters on
|
|
312
|
+
* their fast paths. Results for filters the core translated are cached.
|
|
313
|
+
*/
|
|
314
|
+
function containsRelationFilter(filter) {
|
|
315
|
+
if (!filter || typeof filter !== "object") return false;
|
|
316
|
+
const cached = relationFilterCache.get(filter);
|
|
317
|
+
if (cached !== void 0) return cached;
|
|
318
|
+
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") {
|
|
319
|
+
if (Array.isArray(value) && value.some((child) => containsRelationFilter(child))) return true;
|
|
320
|
+
} else if (key === "$not") {
|
|
321
|
+
if (containsRelationFilter(value)) return true;
|
|
322
|
+
} else if (!key.startsWith("$") && hasRelationOp(value)) return true;
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Calls `visit` for every resolved predicate of a TRANSLATED filter,
|
|
327
|
+
* depth-first: the top-level ones, then (with `nested`) those inside each
|
|
328
|
+
* operand — target filters and junction filters alike. Adapters use it to
|
|
329
|
+
* prepare per-predicate data (memory snapshots, self-referencing checks).
|
|
330
|
+
*/
|
|
331
|
+
function forEachResolvedRelation(filter, visit, nested = true) {
|
|
332
|
+
if (!filter || typeof filter !== "object") return;
|
|
333
|
+
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") {
|
|
334
|
+
if (Array.isArray(value)) for (const child of value) forEachResolvedRelation(child, visit, nested);
|
|
335
|
+
} else if (key === "$not") forEachResolvedRelation(value, visit, nested);
|
|
336
|
+
else if (!key.startsWith("$") && isPlainObject(value)) for (const [op, operand] of Object.entries(value)) {
|
|
337
|
+
if (!isRelationOp(op) || !isResolvedRelationFilter(operand)) continue;
|
|
338
|
+
visit(operand, op);
|
|
339
|
+
if (nested) {
|
|
340
|
+
forEachResolvedRelation(operand.filter, visit, nested);
|
|
341
|
+
forEachResolvedRelation(operand.junction?.filter, visit, nested);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** A fresh {@link TRelGuardState} for one query. */
|
|
346
|
+
function relGuardState(write = false) {
|
|
347
|
+
return {
|
|
348
|
+
depth: 0,
|
|
349
|
+
counter: { nodes: 0 },
|
|
350
|
+
write,
|
|
351
|
+
path: ""
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function invalid(path, message) {
|
|
355
|
+
return new DbError("INVALID_QUERY", [{
|
|
356
|
+
path,
|
|
357
|
+
message
|
|
358
|
+
}]);
|
|
359
|
+
}
|
|
360
|
+
function notSupported(path, message) {
|
|
361
|
+
return new DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
362
|
+
path,
|
|
363
|
+
message
|
|
364
|
+
}]);
|
|
365
|
+
}
|
|
366
|
+
/** The present, non-empty `parts` ANDed (`{}` when none, a single one as is). */
|
|
367
|
+
function andFilters(...parts) {
|
|
368
|
+
const present = parts.filter((p) => p != null && Object.keys(p).length > 0);
|
|
369
|
+
if (present.length === 0) return {};
|
|
370
|
+
if (present.length === 1) return present[0];
|
|
371
|
+
return { $and: present };
|
|
372
|
+
}
|
|
373
|
+
const NO_STATIC_FILTER = {};
|
|
374
|
+
const staticFilterCache = /* @__PURE__ */ new WeakMap();
|
|
375
|
+
/**
|
|
376
|
+
* A relation's `@db.rel.filter` as logical filters per side — what `$with`
|
|
377
|
+
* loading and relational predicates both AND into the related rows (the
|
|
378
|
+
* filter is part of the relation's meaning). Top-level `and` conditions are
|
|
379
|
+
* split by side: an unqualified field and the related type's fields go to
|
|
380
|
+
* `target`, the `@db.rel.via` junction's to `junction`. A single condition
|
|
381
|
+
* that reads both sides (e.g. an `or` across them) or compares two fields is
|
|
382
|
+
* rejected with `INVALID_QUERY` — `name` is the navigation field (error path).
|
|
383
|
+
*
|
|
384
|
+
* @since 0.1.147
|
|
385
|
+
*/
|
|
386
|
+
function relationStaticFilter(relation, name = "") {
|
|
387
|
+
if (!relation.filter) return NO_STATIC_FILTER;
|
|
388
|
+
const cached = staticFilterCache.get(relation);
|
|
389
|
+
if (cached) return cached;
|
|
390
|
+
const targetType = relation.targetType();
|
|
391
|
+
const junctionType = relation.viaType?.();
|
|
392
|
+
const targetName = tableNameOf(targetType);
|
|
393
|
+
const junctionName = junctionType ? tableNameOf(junctionType) : void 0;
|
|
394
|
+
const sideOf = (ref) => {
|
|
395
|
+
const type = ref.type?.();
|
|
396
|
+
if (!type || type === targetType || tableNameOf(type) === targetName) return "target";
|
|
397
|
+
if (junctionType && (type === junctionType || tableNameOf(type) === junctionName)) return "junction";
|
|
398
|
+
throw invalid(name, `@db.rel.filter on "${name}" references a type other than the related type` + (junctionType ? " and the junction" : "") + " — only those can be referenced");
|
|
399
|
+
};
|
|
400
|
+
const target = [];
|
|
401
|
+
const junction = [];
|
|
402
|
+
const conjuncts = "$and" in relation.filter ? relation.filter.$and : [relation.filter];
|
|
403
|
+
for (const conjunct of conjuncts) {
|
|
404
|
+
const sides = /* @__PURE__ */ new Set();
|
|
405
|
+
collectSides(conjunct, (ref) => sides.add(sideOf(ref)), name);
|
|
406
|
+
if (sides.size > 1) throw invalid(name, `@db.rel.filter on "${name}" has a condition reading both the junction and the related type — split it into separate "and" conditions`);
|
|
407
|
+
const filter = translateQueryTree(conjunct, (ref) => ref.field);
|
|
408
|
+
(sides.has("junction") ? junction : target).push(filter);
|
|
409
|
+
}
|
|
410
|
+
const result = {
|
|
411
|
+
target: target.length > 0 ? andFilters(...target) : void 0,
|
|
412
|
+
junction: junction.length > 0 ? andFilters(...junction) : void 0
|
|
413
|
+
};
|
|
414
|
+
staticFilterCache.set(relation, result);
|
|
415
|
+
return result;
|
|
416
|
+
}
|
|
417
|
+
function collectSides(node, visit, name) {
|
|
418
|
+
if ("$and" in node) {
|
|
419
|
+
for (const child of node.$and) collectSides(child, visit, name);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if ("$or" in node) {
|
|
423
|
+
for (const child of node.$or) collectSides(child, visit, name);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if ("$not" in node) {
|
|
427
|
+
collectSides(node.$not, visit, name);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
visit(node.left);
|
|
431
|
+
if (isFieldRef(node.right)) throw invalid(name, `@db.rel.filter on "${name}" compares two fields — only field-to-value conditions are supported`);
|
|
432
|
+
}
|
|
433
|
+
function zip(left, right, a, b) {
|
|
434
|
+
return left.map((value, i) => ({
|
|
435
|
+
[a]: value,
|
|
436
|
+
[b]: right[i]
|
|
437
|
+
}));
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* The {@link TRelationFilterHost} of a readable: resolves a navigation
|
|
441
|
+
* field's related table (and junction) through `resolve` (the readable's
|
|
442
|
+
* table resolver), pairing foreign keys exactly like `$with` loading does
|
|
443
|
+
* (`findFKEntryForRelation` / `findRemoteFK`).
|
|
444
|
+
*/
|
|
445
|
+
function createRelationFilterHost(owner, resolve) {
|
|
446
|
+
const plans = /* @__PURE__ */ new Map();
|
|
447
|
+
const sameAdapter = (other, path, what) => {
|
|
448
|
+
if (!owner.getAdapter().sharesStoreWith(other.getAdapter())) throw notSupported(path, `Relational predicate on "${path}": the ${what} table lives in a different database or adapter`);
|
|
449
|
+
};
|
|
450
|
+
const planOf = (nav, path) => {
|
|
451
|
+
const cached = plans.get(nav);
|
|
452
|
+
if (cached) return cached;
|
|
453
|
+
const meta = owner.getMetadata();
|
|
454
|
+
const relation = meta.relations.get(nav);
|
|
455
|
+
if (!relation) throw invalid(path, `"$some" / "$none" are only valid on a navigation relation — "${nav}" is not one`);
|
|
456
|
+
const target = resolve(relation.targetType());
|
|
457
|
+
if (!target) throw notSupported(path, `Relational predicate on "${path}": the related table is not available`);
|
|
458
|
+
sameAdapter(target, path, "related");
|
|
459
|
+
let plan;
|
|
460
|
+
if (relation.direction === "to") {
|
|
461
|
+
const fk = findFKEntryForRelation(relation, meta.foreignKeys);
|
|
462
|
+
if (!fk) throw invalid(path, `Relation "${nav}" has no foreign key to filter by`);
|
|
463
|
+
plan = {
|
|
464
|
+
kind: "to",
|
|
465
|
+
relation,
|
|
466
|
+
target,
|
|
467
|
+
pairs: zip(fk.fields, fk.targetFields, "source", "target")
|
|
468
|
+
};
|
|
469
|
+
} else if (relation.direction === "from") {
|
|
470
|
+
const fk = findRemoteFK(target.getMetadata(), owner.tableName, relation.alias);
|
|
471
|
+
if (!fk) throw invalid(path, `Relation "${nav}" has no foreign key to filter by`);
|
|
472
|
+
plan = {
|
|
473
|
+
kind: "from",
|
|
474
|
+
relation,
|
|
475
|
+
target,
|
|
476
|
+
pairs: zip(fk.targetFields, fk.fields, "source", "target")
|
|
477
|
+
};
|
|
478
|
+
} else {
|
|
479
|
+
const junctionType = relation.viaType?.();
|
|
480
|
+
const junction = junctionType ? resolve(junctionType) : void 0;
|
|
481
|
+
if (!junction) throw notSupported(path, `Relational predicate on "${path}": the junction table is not available`);
|
|
482
|
+
sameAdapter(junction, path, "junction");
|
|
483
|
+
const junctionMeta = junction.getMetadata();
|
|
484
|
+
const fkToThis = findRemoteFK(junctionMeta, owner.tableName);
|
|
485
|
+
const fkToTarget = findRemoteFK(junctionMeta, tableNameOf(relation.targetType()));
|
|
486
|
+
if (!fkToThis || !fkToTarget) throw invalid(path, `Relation "${nav}" has no junction foreign keys to filter by`);
|
|
487
|
+
if (fkToThis === fkToTarget) throw invalid(path, `Relation "${nav}" is a self-referencing many-to-many — relational predicates cannot tell its two junction keys apart`);
|
|
488
|
+
plan = {
|
|
489
|
+
kind: "via",
|
|
490
|
+
relation,
|
|
491
|
+
target,
|
|
492
|
+
pairs: [],
|
|
493
|
+
junction: {
|
|
494
|
+
owner: junction,
|
|
495
|
+
toSource: zip(fkToThis.fields, fkToThis.targetFields, "junction", "source"),
|
|
496
|
+
toTarget: zip(fkToTarget.fields, fkToTarget.targetFields, "junction", "target")
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
plans.set(nav, plan);
|
|
501
|
+
return plan;
|
|
502
|
+
};
|
|
503
|
+
const tableOf = (o) => {
|
|
504
|
+
const adapter = o.getAdapter();
|
|
505
|
+
return {
|
|
506
|
+
table: adapter.resolveTableName(),
|
|
507
|
+
name: adapter.resolveTableName(false),
|
|
508
|
+
adapter
|
|
509
|
+
};
|
|
510
|
+
};
|
|
511
|
+
return {
|
|
512
|
+
guard(nav, op, inner, state) {
|
|
513
|
+
const path = state.path ? `${state.path}.${nav}` : nav;
|
|
514
|
+
const depth = state.depth + 1;
|
|
515
|
+
if (depth > 4) throw invalid("", `Relational predicates nest at most 4 levels deep`);
|
|
516
|
+
if (++state.counter.nodes > 16) throw invalid("", `At most 16 relational predicates per query`);
|
|
517
|
+
if (!isPlainObject(inner)) throw invalid(path, `"${op}" on "${path}" expects a filter object`);
|
|
518
|
+
const plan = planOf(nav, path);
|
|
519
|
+
relationStaticFilter(plan.relation, path);
|
|
520
|
+
try {
|
|
521
|
+
plan.target._guardRelationOperand(inner, {
|
|
522
|
+
...state,
|
|
523
|
+
depth,
|
|
524
|
+
path
|
|
525
|
+
});
|
|
526
|
+
} catch (error) {
|
|
527
|
+
throw prefixError(error, path);
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
resolve(nav, _op, inner, depth) {
|
|
531
|
+
if (depth > 4) throw invalid("", `Relational predicates nest at most 4 levels deep`);
|
|
532
|
+
const plan = planOf(nav, nav);
|
|
533
|
+
const ownerMeta = owner.getMetadata();
|
|
534
|
+
const targetMeta = plan.target.getMetadata();
|
|
535
|
+
const statics = relationStaticFilter(plan.relation, nav);
|
|
536
|
+
const filter = plan.target._resolveRelationOperand(andFilters(statics.target, inner), depth);
|
|
537
|
+
let junction;
|
|
538
|
+
if (plan.junction) {
|
|
539
|
+
const junctionMeta = plan.junction.owner.getMetadata();
|
|
540
|
+
junction = {
|
|
541
|
+
...tableOf(plan.junction.owner),
|
|
542
|
+
toSource: plan.junction.toSource.map((p) => ({
|
|
543
|
+
junction: junctionMeta.physicalPath(p.junction),
|
|
544
|
+
source: ownerMeta.physicalPath(p.source)
|
|
545
|
+
})),
|
|
546
|
+
toTarget: plan.junction.toTarget.map((p) => ({
|
|
547
|
+
junction: junctionMeta.physicalPath(p.junction),
|
|
548
|
+
target: targetMeta.physicalPath(p.target)
|
|
549
|
+
})),
|
|
550
|
+
...statics.junction ? { filter: plan.junction.owner._resolveRelationOperand(statics.junction, depth) } : {}
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return new ResolvedRelationFilter({
|
|
554
|
+
kind: plan.kind,
|
|
555
|
+
nav,
|
|
556
|
+
source: tableOf(owner),
|
|
557
|
+
target: tableOf(plan.target),
|
|
558
|
+
pairs: plan.pairs.map((p) => ({
|
|
559
|
+
source: ownerMeta.physicalPath(p.source),
|
|
560
|
+
target: targetMeta.physicalPath(p.target)
|
|
561
|
+
})),
|
|
562
|
+
junction,
|
|
563
|
+
filter
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
/** Re-throws a related table's guard error with its paths under the navigation chain. */
|
|
569
|
+
function prefixError(error, path) {
|
|
570
|
+
if (!(error instanceof DbError)) return error;
|
|
571
|
+
if (error.errors.every((e) => !e.path)) return error;
|
|
572
|
+
if (error.errors.every((e) => e.path === path || e.path.startsWith(`${path}.`))) return error;
|
|
573
|
+
const errors = error.errors.map((e) => ({
|
|
574
|
+
path: e.path ? `${path}.${e.path}` : path,
|
|
575
|
+
message: e.message
|
|
576
|
+
}));
|
|
577
|
+
return new DbError(error.code, errors, `${errors[0]?.message ?? error.message} (in "${path}")`);
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Resolves every relational predicate of a (logical) filter through
|
|
581
|
+
* `meta.relationFilters` — the step every field-mapper entry point runs
|
|
582
|
+
* first. Predicates at the top of `filter` are at level `depth + 1`.
|
|
583
|
+
* Already-resolved operands pass through, so translating twice is safe.
|
|
584
|
+
* One pass: unchanged subtrees (and `filter` itself, when nothing needed
|
|
585
|
+
* resolving) are returned as they are.
|
|
586
|
+
*/
|
|
587
|
+
function resolveRelationFilterTree(filter, meta, depth) {
|
|
588
|
+
let out;
|
|
589
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
590
|
+
let next = value;
|
|
591
|
+
if (key === "$and" || key === "$or") {
|
|
592
|
+
if (Array.isArray(value)) {
|
|
593
|
+
let children;
|
|
594
|
+
for (let i = 0; i < value.length; i++) {
|
|
595
|
+
const child = value[i];
|
|
596
|
+
if (!child || typeof child !== "object") continue;
|
|
597
|
+
const resolved = resolveRelationFilterTree(child, meta, depth);
|
|
598
|
+
if (resolved === child) continue;
|
|
599
|
+
children ??= [...value];
|
|
600
|
+
children[i] = resolved;
|
|
601
|
+
}
|
|
602
|
+
if (children) next = children;
|
|
603
|
+
}
|
|
604
|
+
} else if (key === "$not") {
|
|
605
|
+
if (value && typeof value === "object") next = resolveRelationFilterTree(value, meta, depth);
|
|
606
|
+
} else if (!key.startsWith("$") && hasRelationOp(value)) next = resolvePredicateMap(key, value, meta, depth);
|
|
607
|
+
if (next === value) continue;
|
|
608
|
+
out ??= { ...filter };
|
|
609
|
+
out[key] = next;
|
|
610
|
+
}
|
|
611
|
+
if (!out) return filter;
|
|
612
|
+
noteRelationFilter(out, true);
|
|
613
|
+
return out;
|
|
614
|
+
}
|
|
615
|
+
/** One `{ $some | $none: … }` map of `key` resolved (operands already resolved pass through). */
|
|
616
|
+
function resolvePredicateMap(key, value, meta, depth) {
|
|
617
|
+
const host = meta.relationFilters;
|
|
618
|
+
if (!host) {
|
|
619
|
+
if (!meta.navFields.has(key)) throw invalid(key, `"$some" / "$none" are only valid on a navigation relation — "${key}" is not one`);
|
|
620
|
+
throw notSupported(key, `Relational predicate on "${key}" needs the table to come from a DbSpace (no table resolver)`);
|
|
621
|
+
}
|
|
622
|
+
const ops = {};
|
|
623
|
+
for (const [op, operand] of Object.entries(value)) {
|
|
624
|
+
if (!isRelationOp(op)) throw invalid(key, `Cannot mix "$some" / "$none" with "${op}" on "${key}"`);
|
|
625
|
+
ops[op] = isResolvedRelationFilter(operand) ? operand : host.resolve(key, op, operand, depth + 1);
|
|
626
|
+
}
|
|
627
|
+
return ops;
|
|
628
|
+
}
|
|
55
629
|
//#endregion
|
|
56
630
|
//#region src/shared/keys.ts
|
|
57
|
-
/**
|
|
631
|
+
/**
|
|
632
|
+
* String form of a key value, equal across driver representations: an
|
|
633
|
+
* ObjectId stringifies to its hex, a bigint to its digits, a Date to its ISO
|
|
634
|
+
* instant, a Buffer / Uint8Array to hex.
|
|
635
|
+
*/
|
|
58
636
|
function keyString(value) {
|
|
637
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
638
|
+
if (value instanceof Uint8Array) return Array.from(value, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
59
639
|
return String(value);
|
|
60
640
|
}
|
|
61
641
|
/** Key equality across driver representations (number vs numeric string, ObjectId vs hex). */
|
|
@@ -82,6 +662,20 @@ function rowMatchesKey(row, key) {
|
|
|
82
662
|
for (const field in key) if (!sameKey(getPath(row, field), key[field])) return false;
|
|
83
663
|
return true;
|
|
84
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* Identity of the key tuple `fields` of `row` for uniqueness checks, or
|
|
667
|
+
* `undefined` when any component is null / missing (a NULL never collides).
|
|
668
|
+
* Equal across driver representations (see {@link keyString}).
|
|
669
|
+
*/
|
|
670
|
+
function uniqueKeyTuple(row, fields) {
|
|
671
|
+
const parts = [];
|
|
672
|
+
for (const field of fields) {
|
|
673
|
+
const value = getPath(row, field);
|
|
674
|
+
if (value === void 0 || value === null) return void 0;
|
|
675
|
+
parts.push(keyString(value));
|
|
676
|
+
}
|
|
677
|
+
return JSON.stringify(parts);
|
|
678
|
+
}
|
|
85
679
|
//#endregion
|
|
86
680
|
//#region src/table/error-utils.ts
|
|
87
681
|
/**
|
|
@@ -877,4 +1471,4 @@ async function viaReplace(via, targets, parentPK, maxDepth, depth) {
|
|
|
877
1471
|
await viaLinkTargets(via, targets, parentPK, maxDepth, depth);
|
|
878
1472
|
}
|
|
879
1473
|
//#endregion
|
|
880
|
-
export {
|
|
1474
|
+
export { isResolvedRelationFilter as A, slicePerGroup as B, REL_FILTER_MAX_NODES as C, createRelationFilterHost as D, containsRelationFilter as E, compositeKey as F, isFieldRef as G, computeOf as H, findFKEntryForRelation as I, walkViewExpr as J, translateQueryTree as K, findFKForRelation as L, relGuardState as M, relationStaticFilter as N, forEachResolvedRelation as O, resolveRelationFilterTree as P, findRemoteFK as R, REL_FILTER_MAX_DEPTH as S, andFilters as T, computedOperands as U, tableNameOf as V, evaluateExpr as W, remapDeleteFkViolation as _, batchPatchNestedFrom as a, sameKey as b, batchReplaceNestedFrom as c, checkDepthOverflow as d, planNestedFromVia as f, enrichFkViolation as g, validateBatch as h, batchInsertNestedVia as i, noteRelationFilter as j, hasRelationOp as k, batchReplaceNestedTo as l, preValidateNestedFrom as m, batchInsertNestedFrom as n, batchPatchNestedTo as o, planPatchNestedTo as p, viewExprNullable as q, batchInsertNestedTo as r, batchPatchNestedVia as s, applyPatchNestedTo as t, batchReplaceNestedVia as u, pkTupleKey as v, ResolvedRelationFilter as w, uniqueKeyTuple as x, rowMatchesKey as y, resolveRelationTargetTable as z };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/shared/numeric-operand.ts
|
|
2
|
+
/** Primitive tags of timestamps (MySQL may store them as native `TIMESTAMP`). */
|
|
3
|
+
const TIMESTAMP_TAGS = [
|
|
4
|
+
"timestamp",
|
|
5
|
+
"created",
|
|
6
|
+
"updated"
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Why a field cannot be an arithmetic operand, or `undefined` when it can: it
|
|
10
|
+
* must be a `number` — not a decimal (exact; an expression is IEEE double) and
|
|
11
|
+
* not a timestamp-tagged number. The one rule behind `@db.compute` (declared,
|
|
12
|
+
* from the `.as` source) and query-time arithmetic (from the runtime
|
|
13
|
+
* descriptor), so the two cannot diverge. Storage rules (`@db.ignore`,
|
|
14
|
+
* encrypted, JSON) stay with the callers.
|
|
15
|
+
*/
|
|
16
|
+
function numericTypeProblem({ base, tags }) {
|
|
17
|
+
if (base !== "number" && base !== "integer") return base === "decimal" ? "is a decimal" : `is a ${base}`;
|
|
18
|
+
if (tags && TIMESTAMP_TAGS.some((t) => tags.has(t))) return "is a timestamp";
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { numericTypeProblem as t };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/shared/numeric-operand.ts
|
|
2
|
+
/** Primitive tags of timestamps (MySQL may store them as native `TIMESTAMP`). */
|
|
3
|
+
const TIMESTAMP_TAGS = [
|
|
4
|
+
"timestamp",
|
|
5
|
+
"created",
|
|
6
|
+
"updated"
|
|
7
|
+
];
|
|
8
|
+
/**
|
|
9
|
+
* Why a field cannot be an arithmetic operand, or `undefined` when it can: it
|
|
10
|
+
* must be a `number` — not a decimal (exact; an expression is IEEE double) and
|
|
11
|
+
* not a timestamp-tagged number. The one rule behind `@db.compute` (declared,
|
|
12
|
+
* from the `.as` source) and query-time arithmetic (from the runtime
|
|
13
|
+
* descriptor), so the two cannot diverge. Storage rules (`@db.ignore`,
|
|
14
|
+
* encrypted, JSON) stay with the callers.
|
|
15
|
+
*/
|
|
16
|
+
function numericTypeProblem({ base, tags }) {
|
|
17
|
+
if (base !== "number" && base !== "integer") return base === "decimal" ? "is a decimal" : `is a ${base}`;
|
|
18
|
+
if (tags && TIMESTAMP_TAGS.some((t) => tags.has(t))) return "is a timestamp";
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
Object.defineProperty(exports, "numericTypeProblem", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
get: function() {
|
|
24
|
+
return numericTypeProblem;
|
|
25
|
+
}
|
|
26
|
+
});
|
package/dist/ops.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_db_error = require("./db-error-
|
|
2
|
+
const require_db_error = require("./db-error-DRQH4sLY.cjs");
|
|
3
3
|
//#region src/ops.ts
|
|
4
4
|
/** Increment a numeric field by `value` (default 1). */
|
|
5
5
|
function $inc(value = 1) {
|
package/dist/ops.mjs
CHANGED