@atscript/db 0.1.135 → 0.1.136
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-DKuf_v2L.d.cts → agg-CV7y8nC6.d.cts} +13 -3
- package/dist/{agg-BtlGeRfj.d.mts → agg-D5DHsAby.d.mts} +13 -3
- package/dist/agg.cjs +35 -3
- package/dist/agg.d.cts +2 -2
- package/dist/agg.d.mts +2 -2
- package/dist/agg.mjs +34 -1
- package/dist/aggregate-fns-CGBv3E8S.cjs +87 -0
- package/dist/aggregate-fns-CfsveE1w.mjs +58 -0
- package/dist/{buckets-D6PBXKRJ.d.cts → buckets-kKY0RKYN.d.cts} +60 -17
- package/dist/{buckets-GruoVxH5.d.mts → buckets-y4EzoX7D.d.mts} +60 -17
- package/dist/{db-space-9CH5neN7.d.mts → db-space-BX4K3vb6.d.mts} +126 -7
- package/dist/{db-space-BdtBNTeH.d.cts → db-space-ClG41vJh.d.cts} +126 -7
- package/dist/{db-view-Dm7MpUGB.cjs → db-view-BAzhEUxB.cjs} +391 -92
- package/dist/{db-view-Doh8vPg3.mjs → db-view-BrAtbaOx.mjs} +385 -92
- package/dist/index.cjs +17 -3
- package/dist/index.d.cts +12 -10
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +14 -4
- package/dist/{nested-writer-wk1EFUNY.cjs → nested-writer-BZNCuqI6.cjs} +16 -2
- package/dist/{nested-writer-CqL24ojl.mjs → nested-writer-FWD5oOYh.mjs} +11 -3
- package/dist/plugin.cjs +215 -75
- package/dist/plugin.mjs +215 -76
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -20
- package/dist/rel.d.mts +2 -20
- package/dist/rel.mjs +2 -2
- package/dist/relation-helpers-BVjQflL_.d.cts +30 -0
- package/dist/relation-helpers-D4haLo1n.d.mts +30 -0
- package/dist/{relation-loader-BD4xANQJ.cjs → relation-loader-6ZB_5KFq.cjs} +1 -1
- package/dist/{relation-loader-B68R1LET.mjs → relation-loader-CTFaZpVa.mjs} +1 -1
- package/dist/shared.cjs +5 -1
- package/dist/shared.d.cts +16 -3
- package/dist/shared.d.mts +16 -3
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +37 -10
- package/dist/sync.d.cts +24 -4
- package/dist/sync.d.mts +24 -4
- package/dist/sync.mjs +37 -10
- package/dist/{validation-utils-MWOP1Ts4.mjs → validation-utils-B4h-GW4d.mjs} +29 -7
- package/dist/{validation-utils-B7SXPkm7.cjs → validation-utils-Dg0hW6dn.cjs} +52 -6
- package/dist/{validator-CewfnGZj.d.mts → validator-Drb2N-YL.d.cts} +1 -1
- package/dist/{validator-CewfnGZj.d.cts → validator-Drb2N-YL.d.mts} +1 -1
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +1 -1
- package/dist/agg-CvXDGnKi.mjs +0 -61
- package/dist/agg-EcIbAFnJ.cjs +0 -78
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { n as CasMismatchError, r as DbError } from "./db-error-D5uilS_A.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { i as SUPPORTED_AGGREGATE_FNS, n as BASE_AGGREGATE_FNS } from "./aggregate-fns-CfsveE1w.mjs";
|
|
3
|
+
import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, g as findRemoteFK, h as findFKForRelation, i as batchPatchNestedFrom, l as batchReplaceNestedVia, m as remapDeleteFkViolation, n as batchInsertNestedTo, o as batchPatchNestedVia, p as enrichFkViolation, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow, v as tableNameOf } from "./nested-writer-FWD5oOYh.mjs";
|
|
4
|
+
import { resolveAlias } from "./agg.mjs";
|
|
3
5
|
import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, l as isEmptyObject, n as buildPatchPartial, t as buildDbValidator, u as isPlainObject } from "./validator-Ch7UIQl9.mjs";
|
|
4
|
-
import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, g as findRemoteFK, h as findFKForRelation, i as batchPatchNestedFrom, l as batchReplaceNestedVia, m as remapDeleteFkViolation, n as batchInsertNestedTo, o as batchPatchNestedVia, p as enrichFkViolation, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CqL24ojl.mjs";
|
|
5
6
|
import { separateCas, separateFieldOps } from "./ops.mjs";
|
|
6
7
|
import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
|
|
7
8
|
import { BUCKET_UNITS, isAggregateExpr, isBucketExpr, isPrimitive, resolveBuckets } from "@uniqu/core";
|
|
@@ -17,14 +18,15 @@ const NoopLogger = {
|
|
|
17
18
|
//#endregion
|
|
18
19
|
//#region src/query/buckets.ts
|
|
19
20
|
/**
|
|
20
|
-
* The one normalizer of `$select` computed entries
|
|
21
|
+
* The one normalizer of `$select` computed entries — uniqu's
|
|
21
22
|
* `resolveBuckets` (entry shapes, unit, time zone canonicalization, week
|
|
22
23
|
* start, alias syntax and uniqueness, "grouped queries only", "must also
|
|
23
24
|
* appear in $groupBy", string `$groupBy` entries) with the table's names as
|
|
24
25
|
* the collision set: a bucket alias may not equal a logical path, a physical
|
|
25
26
|
* column or a navigation field, so a label is never reverse-mapped as a
|
|
26
27
|
* column. Aggregate entries are checked against `SUPPORTED_AGGREGATE_FNS`
|
|
27
|
-
* (and `'*'` is `count`'s only)
|
|
28
|
+
* (and `'*'` is `count`'s only); whether the adapter renders a function is
|
|
29
|
+
* `guardAggregate`'s (`aggregateFns()` → `AGG_FN_NOT_SUPPORTED`).
|
|
28
30
|
*
|
|
29
31
|
* Which layer validates what:
|
|
30
32
|
* - **Shapes** (this normalizer) run FIRST at every entry point — the core's
|
|
@@ -46,7 +48,7 @@ const NoopLogger = {
|
|
|
46
48
|
*
|
|
47
49
|
* @throws DbError `INVALID_QUERY` carrying every issue (`path` `$select` / `$groupBy`).
|
|
48
50
|
*/
|
|
49
|
-
function
|
|
51
|
+
function normalizeComputedSelect(controls, fields, aggregate) {
|
|
50
52
|
const res = resolveBuckets(controls, {
|
|
51
53
|
aggregate,
|
|
52
54
|
fns: SUPPORTED_AGGREGATE_FNS,
|
|
@@ -56,6 +58,11 @@ function resolveCalendarBuckets(controls, fields, aggregate) {
|
|
|
56
58
|
return res.buckets;
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
61
|
+
* @deprecated since 0.1.136 — renamed {@link normalizeComputedSelect} (it
|
|
62
|
+
* normalizes every computed `$select` entry, aggregates included).
|
|
63
|
+
*/
|
|
64
|
+
const resolveCalendarBuckets = normalizeComputedSelect;
|
|
65
|
+
/**
|
|
59
66
|
* Whether a field's TYPE allows it to be the source of a calendar bucket: a
|
|
60
67
|
* `number` / `integer` leaf carrying the `timestamp` tag
|
|
61
68
|
* (`number.timestamp`, `.created`, `.updated`) that is not
|
|
@@ -111,6 +118,37 @@ function findAncestorInSet(path, set) {
|
|
|
111
118
|
if (set.has(ancestor)) return ancestor;
|
|
112
119
|
}
|
|
113
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Logical field path → its physical path in document storage (nested
|
|
123
|
+
* objects kept inline). `@db.column` renames (`columnMap`) apply to the
|
|
124
|
+
* annotated key, and a document renames the TOP-LEVEL key only — nested keys
|
|
125
|
+
* are stored as-is — so a dotted path under a renamed top-level object
|
|
126
|
+
* renames its first segment: `profile.bio` under `@db.column 'prof'` →
|
|
127
|
+
* `prof.bio`.
|
|
128
|
+
*/
|
|
129
|
+
function documentPath(columnMap, path) {
|
|
130
|
+
const direct = columnMap.get(path);
|
|
131
|
+
if (direct !== void 0) return direct;
|
|
132
|
+
const dot = path.indexOf(".");
|
|
133
|
+
if (dot === -1) return path;
|
|
134
|
+
const top = columnMap.get(path.slice(0, dot));
|
|
135
|
+
return top === void 0 ? path : top + path.slice(dot);
|
|
136
|
+
}
|
|
137
|
+
/** The `__`-separated parent prefix of a dotted path (`a.b.c` → `a__b__`), `""` for a top-level path. */
|
|
138
|
+
function flattenedPrefix(path) {
|
|
139
|
+
const lastDot = path.lastIndexOf(".");
|
|
140
|
+
return lastDot >= 0 ? `${path.slice(0, lastDot).replace(/\./g, "__")}__` : "";
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Relational column of a column-stored path: a top-level field is its
|
|
144
|
+
* `@db.column` (`override`) or its name; a leaf of a `flattened` object is
|
|
145
|
+
* its parent segments joined with `__` plus its `@db.column` or own segment
|
|
146
|
+
* (`address.zip` with `@db.column 'zip_code'` → `address__zip_code`).
|
|
147
|
+
*/
|
|
148
|
+
function relationalColumnName(path, override, flattened) {
|
|
149
|
+
if (override) return flattened ? flattenedPrefix(path) + override : override;
|
|
150
|
+
return flattened ? path.replace(/\./g, "__") : path;
|
|
151
|
+
}
|
|
114
152
|
/** Returns true if `metadata` indicates a navigation relation field. */
|
|
115
153
|
function isNavRelation(metadata) {
|
|
116
154
|
return metadata.has("db.rel.to") || metadata.has("db.rel.from") || metadata.has("db.rel.via");
|
|
@@ -231,12 +269,7 @@ var TableMetadata = class {
|
|
|
231
269
|
* first segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
|
|
232
270
|
*/
|
|
233
271
|
documentPath(path) {
|
|
234
|
-
|
|
235
|
-
if (direct !== void 0) return direct;
|
|
236
|
-
const dot = path.indexOf(".");
|
|
237
|
-
if (dot === -1) return path;
|
|
238
|
-
const top = this.columnMap.get(path.slice(0, dot));
|
|
239
|
-
return top === void 0 ? path : top + path.slice(dot);
|
|
272
|
+
return documentPath(this.columnMap, path);
|
|
240
273
|
}
|
|
241
274
|
/**
|
|
242
275
|
* Runs the full metadata compilation pipeline. Called once by
|
|
@@ -354,8 +387,7 @@ var TableMetadata = class {
|
|
|
354
387
|
const raw = metadata.get("db.rel.FK");
|
|
355
388
|
const alias = raw === true ? void 0 : raw;
|
|
356
389
|
if (fieldType.ref) {
|
|
357
|
-
const
|
|
358
|
-
const targetTable = refTarget?.metadata?.get("db.table") || refTarget?.id || "";
|
|
390
|
+
const targetTable = tableNameOf(fieldType.ref.type());
|
|
359
391
|
const targetField = fieldType.ref.field;
|
|
360
392
|
const key = alias || `__auto_${fieldName}`;
|
|
361
393
|
const existing = this.foreignKeys.get(key);
|
|
@@ -498,10 +530,7 @@ var TableMetadata = class {
|
|
|
498
530
|
if (findAncestorInSet(path, this.jsonFields) !== void 0) continue;
|
|
499
531
|
if (findAncestorInSet(path, this.encryptedFields) !== void 0) continue;
|
|
500
532
|
const isFlattened = findAncestorInSet(path, this.flattenedParents) !== void 0;
|
|
501
|
-
const
|
|
502
|
-
let physicalName;
|
|
503
|
-
if (columnOverride) physicalName = isFlattened ? this._flattenedPrefix(path) + columnOverride : columnOverride;
|
|
504
|
-
else physicalName = isFlattened ? path.replace(/\./g, "__") : path;
|
|
533
|
+
const physicalName = relationalColumnName(path, this.columnMap.get(path), isFlattened);
|
|
505
534
|
this.pathToPhysical.set(path, physicalName);
|
|
506
535
|
this.physicalToPath.set(physicalName, path);
|
|
507
536
|
const fieldType = this.flatMap.get(path);
|
|
@@ -520,11 +549,6 @@ var TableMetadata = class {
|
|
|
520
549
|
this.onlyColumnRenames = this.columnMap.size > 0 && this.flattenedParents.size === 0 && this.jsonFields.size === 0;
|
|
521
550
|
this.requiresMappings = this.flattenedParents.size > 0 || this.jsonFields.size > 0 || this.onlyColumnRenames;
|
|
522
551
|
}
|
|
523
|
-
/** Returns the `__`-separated parent prefix for a dot-separated path, or empty string for top-level paths. */
|
|
524
|
-
_flattenedPrefix(path) {
|
|
525
|
-
const lastDot = path.lastIndexOf(".");
|
|
526
|
-
return lastDot >= 0 ? `${path.slice(0, lastDot).replace(/\./g, "__")}__` : "";
|
|
527
|
-
}
|
|
528
552
|
/** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
|
|
529
553
|
/**
|
|
530
554
|
* Indexes non-ignored descriptors by logical path and retains the JSON-parent
|
|
@@ -599,7 +623,7 @@ var TableMetadata = class {
|
|
|
599
623
|
const physicalName = skipFlattening ? this.columnMap.get(path) ?? path : this.pathToPhysical.get(path) ?? this.columnMap.get(path) ?? path;
|
|
600
624
|
const fromLocal = this._columnFromMap.get(path);
|
|
601
625
|
let renamedFrom;
|
|
602
|
-
if (fromLocal) renamedFrom = isFlattened ?
|
|
626
|
+
if (fromLocal) renamedFrom = isFlattened ? flattenedPrefix(path) + fromLocal : fromLocal;
|
|
603
627
|
const currencyCode = type.metadata.get("db.amount.currency");
|
|
604
628
|
const currencyRefField = type.metadata.get("db.amount.currency.ref");
|
|
605
629
|
const unitCode = type.metadata.get("db.unit");
|
|
@@ -780,7 +804,7 @@ var TableMetadata = class {
|
|
|
780
804
|
* An array `$select` holds plain field names and computed entries —
|
|
781
805
|
* aggregates (`{ $fn, $field }`, {@link aggregates}) and calendar buckets
|
|
782
806
|
* (`{ $bucket, $field }`, {@link buckets}). Entries arrive normalized
|
|
783
|
-
* (`
|
|
807
|
+
* (`normalizeComputedSelect` rejects any other shape before translation).
|
|
784
808
|
*/
|
|
785
809
|
var UniquSelect = class UniquSelect {
|
|
786
810
|
static UNRESOLVED = Symbol("unresolved");
|
|
@@ -913,7 +937,7 @@ var FieldMappingStrategy = class {
|
|
|
913
937
|
* equals a field name).
|
|
914
938
|
*
|
|
915
939
|
* `buckets` are the query's calendar buckets as the core's normalizer
|
|
916
|
-
* resolved them (`
|
|
940
|
+
* resolved them (`normalizeComputedSelect` — `AtscriptDbReadable.aggregate`
|
|
917
941
|
* runs it before the guards); they reach adapters with `field` made
|
|
918
942
|
* physical and the source descriptor as `fd`.
|
|
919
943
|
*/
|
|
@@ -948,16 +972,28 @@ var FieldMappingStrategy = class {
|
|
|
948
972
|
/**
|
|
949
973
|
* `$select` with its field paths made physical: array-form names and
|
|
950
974
|
* computed `$field`s (`'*'` kept), or the keys of the object
|
|
951
|
-
* (inclusion / exclusion) form.
|
|
975
|
+
* (inclusion / exclusion) form. An aggregate's output alias is fixed
|
|
976
|
+
* (`$as`) from its LOGICAL field first, so a default alias never leaks a
|
|
977
|
+
* physical name (`sum(amount)` over `@db.column 'amount_cents'` stays
|
|
978
|
+
* `sum_amount`); a bucket's alias is already resolved.
|
|
952
979
|
*/
|
|
953
980
|
physicalSelect(select, meta) {
|
|
954
981
|
if (!this.renamesPaths(meta)) return select;
|
|
955
982
|
if (Array.isArray(select)) return select.map((item) => {
|
|
956
983
|
if (typeof item === "string") return this.physicalPath(item, meta);
|
|
957
|
-
if (isAggregateExpr(item) || isBucketExpr(item))
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
984
|
+
if (isAggregateExpr(item) || isBucketExpr(item)) {
|
|
985
|
+
if (item.$field === "*") return item;
|
|
986
|
+
const physical = this.physicalPath(item.$field, meta);
|
|
987
|
+
if (physical === item.$field) return item;
|
|
988
|
+
return isAggregateExpr(item) ? {
|
|
989
|
+
...item,
|
|
990
|
+
$as: resolveAlias(item),
|
|
991
|
+
$field: physical
|
|
992
|
+
} : {
|
|
993
|
+
...item,
|
|
994
|
+
$field: physical
|
|
995
|
+
};
|
|
996
|
+
}
|
|
961
997
|
return item;
|
|
962
998
|
});
|
|
963
999
|
const translated = {};
|
|
@@ -1382,7 +1418,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1382
1418
|
* - `$exists` with a non-boolean operand → `INVALID_QUERY`
|
|
1383
1419
|
* - malformed `$select` computed entries / `$groupBy` entries, calendar
|
|
1384
1420
|
* buckets outside grouped queries or with a bad unit / zone / alias →
|
|
1385
|
-
* `INVALID_QUERY` (the shared normalizer, `
|
|
1421
|
+
* `INVALID_QUERY` (the shared normalizer, `normalizeComputedSelect`)
|
|
1386
1422
|
* - a calendar bucket over a field that is not a bucket source
|
|
1387
1423
|
* (`bucketSourceVerdict`) → `INVALID_QUERY`, or `BUCKET_NOT_SUPPORTED`
|
|
1388
1424
|
* when the adapter has no calendar buckets at all; a unit the adapter's
|
|
@@ -1648,7 +1684,7 @@ function collectFilterKeys(filter, push, skip, refs) {
|
|
|
1648
1684
|
* `$sort` / `$having`; a bucket alias is also dropped from `groupBy`, which
|
|
1649
1685
|
* lists grouped fields only.
|
|
1650
1686
|
*
|
|
1651
|
-
* Entry shapes are not checked here — see `
|
|
1687
|
+
* Entry shapes are not checked here — see `normalizeComputedSelect`.
|
|
1652
1688
|
*/
|
|
1653
1689
|
function collectQueryPaths(query, aggregate) {
|
|
1654
1690
|
const refs = {
|
|
@@ -1788,7 +1824,7 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
|
1788
1824
|
* {@link guardPath}). Adapters may therefore assume every path they receive
|
|
1789
1825
|
* is physical.
|
|
1790
1826
|
*
|
|
1791
|
-
* Entry shapes are the caller's to normalize first (`
|
|
1827
|
+
* Entry shapes are the caller's to normalize first (`normalizeComputedSelect`
|
|
1792
1828
|
* — {@link guardQuery} / {@link guardAggregate} do).
|
|
1793
1829
|
*
|
|
1794
1830
|
* In aggregate mode (`aggregate = true`) `$select` computed entries have
|
|
@@ -1821,7 +1857,7 @@ function guardQuery(meta, adapter, query) {
|
|
|
1821
1857
|
if (!query) return;
|
|
1822
1858
|
guardFilter(meta, adapter, query.filter);
|
|
1823
1859
|
guardSort(meta, query.controls?.$sort);
|
|
1824
|
-
|
|
1860
|
+
normalizeComputedSelect(query.controls, meta, false);
|
|
1825
1861
|
guardPaths(meta, adapter, query);
|
|
1826
1862
|
}
|
|
1827
1863
|
/**
|
|
@@ -1844,16 +1880,17 @@ function checkHavingKeys(refs) {
|
|
|
1844
1880
|
}
|
|
1845
1881
|
/**
|
|
1846
1882
|
* Aggregate-path guard: $groupBy / $select / $having encryption refs + filter
|
|
1847
|
-
* + $sort, then the path guard, then the adapter's
|
|
1883
|
+
* + $sort, then the path guard, then the adapter's aggregate functions
|
|
1884
|
+
* (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
|
|
1848
1885
|
* (`BUCKET_NOT_SUPPORTED`), then the `$having` key rule
|
|
1849
1886
|
* ({@link checkHavingKeys} — after the path guard so an unknown key still
|
|
1850
1887
|
* reads `Unknown field`).
|
|
1851
1888
|
*
|
|
1852
1889
|
* `buckets` are the query's resolved calendar buckets when the caller already
|
|
1853
|
-
* ran `
|
|
1890
|
+
* ran `normalizeComputedSelect` (resolved here otherwise).
|
|
1854
1891
|
*/
|
|
1855
1892
|
function guardAggregate(meta, adapter, query, resolved) {
|
|
1856
|
-
const buckets = resolved ??
|
|
1893
|
+
const buckets = resolved ?? normalizeComputedSelect(query.controls, meta, true);
|
|
1857
1894
|
guardFilter(meta, adapter, query.filter);
|
|
1858
1895
|
const controls = query.controls;
|
|
1859
1896
|
if (meta.encryptedFields.size > 0) {
|
|
@@ -1866,11 +1903,26 @@ function guardAggregate(meta, adapter, query, resolved) {
|
|
|
1866
1903
|
guardSort(meta, controls.$sort);
|
|
1867
1904
|
}
|
|
1868
1905
|
const refs = guardPaths(meta, adapter, query, true);
|
|
1906
|
+
guardAggregateFns(adapter, controls.$select);
|
|
1869
1907
|
guardBucketUnits(adapter, buckets);
|
|
1870
1908
|
const having = refs ? checkHavingKeys(refs) : void 0;
|
|
1871
1909
|
if (having) throw new DbError("INVALID_QUERY", [having]);
|
|
1872
1910
|
}
|
|
1873
1911
|
/**
|
|
1912
|
+
* Rejects an aggregate whose (known — the normalizer checked the name)
|
|
1913
|
+
* function this adapter does not render (`aggregateFns()`) with
|
|
1914
|
+
* `AGG_FN_NOT_SUPPORTED`, before anything is translated — an adapter written
|
|
1915
|
+
* before a function existed never receives it.
|
|
1916
|
+
*/
|
|
1917
|
+
function guardAggregateFns(adapter, select) {
|
|
1918
|
+
if (!select) return;
|
|
1919
|
+
const fns = adapter.aggregateFns();
|
|
1920
|
+
for (const item of select) if (isAggregateExpr(item) && !fns.has(item.$fn)) throw new DbError("AGG_FN_NOT_SUPPORTED", [{
|
|
1921
|
+
path: "$select",
|
|
1922
|
+
message: `Aggregate function "${item.$fn}" is not supported by this adapter`
|
|
1923
|
+
}]);
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1874
1926
|
* Rejects a calendar bucket whose unit this adapter cannot group by
|
|
1875
1927
|
* (`calendarBucketUnits()`; an adapter with none was already answered per
|
|
1876
1928
|
* source by `bucketSourceVerdict`) with `BUCKET_NOT_SUPPORTED`,
|
|
@@ -2377,13 +2429,15 @@ var AtscriptDbReadable = class {
|
|
|
2377
2429
|
*
|
|
2378
2430
|
* Validates:
|
|
2379
2431
|
* - `$select` computed entries and calendar buckets (the shared normalizer,
|
|
2380
|
-
* `
|
|
2432
|
+
* `normalizeComputedSelect`: shapes, unit, zone, alias, grouping)
|
|
2381
2433
|
* - Plain fields in $select are a subset of $groupBy
|
|
2382
2434
|
* - When dimensions/measures are defined (strict mode): $groupBy fields
|
|
2383
|
-
* must be dimensions, aggregate $field values must be measures (or '*'
|
|
2435
|
+
* must be dimensions, aggregate $field values must be measures (or '*';
|
|
2436
|
+
* a `countDistinct` field may also be a dimension)
|
|
2384
2437
|
* - the path guard (a bucket source must pass `bucketSourceVerdict` —
|
|
2385
2438
|
* timestamp type, no JSON ancestor, a dimension in strict mode, an
|
|
2386
|
-
* adapter with calendar buckets)
|
|
2439
|
+
* adapter with calendar buckets), the adapter's aggregate functions
|
|
2440
|
+
* (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
|
|
2387
2441
|
* (`BUCKET_NOT_SUPPORTED`)
|
|
2388
2442
|
*
|
|
2389
2443
|
* Translates field names, delegates to adapter.aggregate(),
|
|
@@ -2392,7 +2446,7 @@ var AtscriptDbReadable = class {
|
|
|
2392
2446
|
async aggregate(query) {
|
|
2393
2447
|
this._ensureBuilt();
|
|
2394
2448
|
const { $groupBy, $select } = query.controls;
|
|
2395
|
-
const buckets =
|
|
2449
|
+
const buckets = normalizeComputedSelect(query.controls, this._meta, true);
|
|
2396
2450
|
if ($select) {
|
|
2397
2451
|
const groupBySet = new Set($groupBy);
|
|
2398
2452
|
for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new DbError("INVALID_QUERY", [{
|
|
@@ -2409,8 +2463,16 @@ var AtscriptDbReadable = class {
|
|
|
2409
2463
|
path: "$groupBy",
|
|
2410
2464
|
message: `Field "${field}" is not a dimension`
|
|
2411
2465
|
}]);
|
|
2412
|
-
if ($select) {
|
|
2413
|
-
|
|
2466
|
+
if ($select) for (const item of $select) {
|
|
2467
|
+
if (!isAggregateExpr(item) || item.$field === "*" || measSet.has(item.$field)) continue;
|
|
2468
|
+
if (item.$fn === "countDistinct") {
|
|
2469
|
+
if (dimSet.has(item.$field)) continue;
|
|
2470
|
+
throw new DbError("INVALID_QUERY", [{
|
|
2471
|
+
path: "$select",
|
|
2472
|
+
message: `Aggregate field "${item.$field}" is not a dimension or measure`
|
|
2473
|
+
}]);
|
|
2474
|
+
}
|
|
2475
|
+
throw new DbError("INVALID_QUERY", [{
|
|
2414
2476
|
path: "$select",
|
|
2415
2477
|
message: `Aggregate field "${item.$field}" is not a measure`
|
|
2416
2478
|
}]);
|
|
@@ -2449,6 +2511,10 @@ var AtscriptDbReadable = class {
|
|
|
2449
2511
|
calendarBucketUnits() {
|
|
2450
2512
|
return this.adapter.calendarBucketUnits();
|
|
2451
2513
|
}
|
|
2514
|
+
/** Aggregate functions the adapter renders (proxies adapter capability). @since 0.1.136 */
|
|
2515
|
+
aggregateFns() {
|
|
2516
|
+
return this.adapter.aggregateFns();
|
|
2517
|
+
}
|
|
2452
2518
|
/** Whether the adapter can sort by a given field (proxies adapter capability). */
|
|
2453
2519
|
canSortField(fd) {
|
|
2454
2520
|
return this.adapter.canSortField(fd);
|
|
@@ -2739,7 +2805,7 @@ var AtscriptDbReadable = class {
|
|
|
2739
2805
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
2740
2806
|
*/
|
|
2741
2807
|
async loadRelations(rows, withRelations) {
|
|
2742
|
-
const { loadRelationsImpl } = await import("./relation-loader-
|
|
2808
|
+
const { loadRelationsImpl } = await import("./relation-loader-CTFaZpVa.mjs").then((n) => n.n);
|
|
2743
2809
|
return loadRelationsImpl(rows, withRelations, this);
|
|
2744
2810
|
}
|
|
2745
2811
|
/**
|
|
@@ -3038,6 +3104,20 @@ var BaseDbAdapter = class {
|
|
|
3038
3104
|
return EMPTY_BUCKET_UNITS;
|
|
3039
3105
|
}
|
|
3040
3106
|
/**
|
|
3107
|
+
* Aggregate functions (`{ $fn, $field }` in an aggregate `$select`) this
|
|
3108
|
+
* adapter renders. The default is `sum`, `count`, `avg`, `min` and `max`;
|
|
3109
|
+
* an adapter that also implements `countDistinct` (distinct non-null
|
|
3110
|
+
* values) returns `ALL_AGGREGATE_FNS`. The core rejects a known function
|
|
3111
|
+
* missing from this set with `AGG_FN_NOT_SUPPORTED` before dispatch, so
|
|
3112
|
+
* `aggregate()` only ever receives functions listed here; moost-db's
|
|
3113
|
+
* `/meta` advertises the set as `aggregateFns`.
|
|
3114
|
+
*
|
|
3115
|
+
* @since 0.1.136
|
|
3116
|
+
*/
|
|
3117
|
+
aggregateFns() {
|
|
3118
|
+
return BASE_AGGREGATE_FNS;
|
|
3119
|
+
}
|
|
3120
|
+
/**
|
|
3041
3121
|
* Whether this adapter enforces foreign key constraints natively.
|
|
3042
3122
|
* When `true`, the generic layer skips application-level cascade/setNull
|
|
3043
3123
|
* on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
|
|
@@ -4607,7 +4687,150 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4607
4687
|
}
|
|
4608
4688
|
};
|
|
4609
4689
|
//#endregion
|
|
4690
|
+
//#region src/table/view-source.ts
|
|
4691
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
4692
|
+
function sourceIndex(type) {
|
|
4693
|
+
let idx = indexCache.get(type);
|
|
4694
|
+
if (idx) return idx;
|
|
4695
|
+
idx = {
|
|
4696
|
+
flatMap: /* @__PURE__ */ new Map(),
|
|
4697
|
+
columnMap: /* @__PURE__ */ new Map(),
|
|
4698
|
+
unstored: /* @__PURE__ */ new Set(),
|
|
4699
|
+
jsonRoots: /* @__PURE__ */ new Set(),
|
|
4700
|
+
encrypted: /* @__PURE__ */ new Set(),
|
|
4701
|
+
optional: /* @__PURE__ */ new Set()
|
|
4702
|
+
};
|
|
4703
|
+
if (type.type.kind === "object") {
|
|
4704
|
+
const navFields = /* @__PURE__ */ new Set();
|
|
4705
|
+
const collected = [];
|
|
4706
|
+
idx.flatMap = flattenAnnotatedType(type, {
|
|
4707
|
+
excludePhantomTypes: true,
|
|
4708
|
+
onField: (path, fieldType, metadata) => {
|
|
4709
|
+
if (isNavRelation(metadata)) navFields.add(path);
|
|
4710
|
+
collected.push([
|
|
4711
|
+
path,
|
|
4712
|
+
fieldType,
|
|
4713
|
+
metadata
|
|
4714
|
+
]);
|
|
4715
|
+
}
|
|
4716
|
+
});
|
|
4717
|
+
const storage = [];
|
|
4718
|
+
for (const [path, fieldType, metadata] of collected) {
|
|
4719
|
+
if (!path || findAncestorInSet(path, navFields) !== void 0) continue;
|
|
4720
|
+
const column = metadata.get("db.column");
|
|
4721
|
+
if (column) idx.columnMap.set(path, column);
|
|
4722
|
+
if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
|
|
4723
|
+
if (metadata.has("db.encrypted")) storage.push([path, false]);
|
|
4724
|
+
else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
|
|
4725
|
+
}
|
|
4726
|
+
storage.sort(([a], [b]) => a.split(".").length - b.split(".").length);
|
|
4727
|
+
for (const [path, json] of storage) if (findAncestorInSet(path, idx.jsonRoots) === void 0 && findAncestorInSet(path, idx.encrypted) === void 0) (json ? idx.jsonRoots : idx.encrypted).add(path);
|
|
4728
|
+
for (const [path, node] of idx.flatMap) if (path && node.optional) idx.optional.add(path);
|
|
4729
|
+
}
|
|
4730
|
+
indexCache.set(type, idx);
|
|
4731
|
+
return idx;
|
|
4732
|
+
}
|
|
4733
|
+
/** `path` itself if it is in `set`, else its nearest ancestor in `set`. */
|
|
4734
|
+
function selfOrAncestor(path, set) {
|
|
4735
|
+
return set.has(path) ? path : findAncestorInSet(path, set);
|
|
4736
|
+
}
|
|
4737
|
+
/**
|
|
4738
|
+
* Resolves a LOGICAL path of a source table (a view field's chain ref, an
|
|
4739
|
+
* aggregate's field, a predicate operand) to where it is physically stored.
|
|
4740
|
+
* Internal — `AtscriptDbView.resolveRefSource` is the public entry.
|
|
4741
|
+
*
|
|
4742
|
+
* Relational rules (`TableMetadata`'s): the outermost `@db.json` or array
|
|
4743
|
+
* node with segments remaining is the column and the rest becomes
|
|
4744
|
+
* {@link TViewSource.jsonPath}; a flattened leaf is its parent segments
|
|
4745
|
+
* joined with `__` plus its `@db.column` (or segment); a top-level field is
|
|
4746
|
+
* its `@db.column` or name. Nested-object adapters use the document path
|
|
4747
|
+
* (only a top-level key is renamed) and never a JSON path.
|
|
4748
|
+
*
|
|
4749
|
+
* A path the type does not declare resolves to itself — the database
|
|
4750
|
+
* reports the unknown column, as before.
|
|
4751
|
+
* @throws for a path without storage (`@db.ignore`, a navigation relation,
|
|
4752
|
+
* or inside one), and — relational — for a path inside an `@db.encrypted` field.
|
|
4753
|
+
*/
|
|
4754
|
+
function resolveViewSource(sourceType, logicalPath, nestedObjects) {
|
|
4755
|
+
const idx = sourceIndex(sourceType);
|
|
4756
|
+
const node = idx.flatMap.get(logicalPath);
|
|
4757
|
+
const jsonRoot = selfOrAncestor(logicalPath, idx.jsonRoots);
|
|
4758
|
+
const optional = selfOrAncestor(logicalPath, idx.optional) !== void 0 || jsonRoot !== void 0 && jsonRoot !== logicalPath;
|
|
4759
|
+
if (!node) return {
|
|
4760
|
+
column: logicalPath,
|
|
4761
|
+
designType: "unknown",
|
|
4762
|
+
optional
|
|
4763
|
+
};
|
|
4764
|
+
const unstored = selfOrAncestor(logicalPath, idx.unstored);
|
|
4765
|
+
if (unstored !== void 0) throw new Error(`"${logicalPath}" has no column — "${unstored}" is @db.ignore or a navigation relation`);
|
|
4766
|
+
const designType = resolveDesignType(node);
|
|
4767
|
+
if (nestedObjects) return {
|
|
4768
|
+
column: documentPath(idx.columnMap, logicalPath),
|
|
4769
|
+
designType,
|
|
4770
|
+
optional
|
|
4771
|
+
};
|
|
4772
|
+
const encrypted = findAncestorInSet(logicalPath, idx.encrypted);
|
|
4773
|
+
if (encrypted !== void 0) throw new Error(`"${logicalPath}" is inside the @db.encrypted field "${encrypted}" — reference "${encrypted}" itself`);
|
|
4774
|
+
if (jsonRoot !== void 0) {
|
|
4775
|
+
const column = relationalColumnName(jsonRoot, idx.columnMap.get(jsonRoot), jsonRoot.includes("."));
|
|
4776
|
+
return jsonRoot === logicalPath ? {
|
|
4777
|
+
column,
|
|
4778
|
+
designType,
|
|
4779
|
+
optional
|
|
4780
|
+
} : {
|
|
4781
|
+
column,
|
|
4782
|
+
jsonPath: logicalPath.slice(jsonRoot.length + 1).split("."),
|
|
4783
|
+
designType,
|
|
4784
|
+
optional
|
|
4785
|
+
};
|
|
4786
|
+
}
|
|
4787
|
+
if (designType === "object" && !idx.encrypted.has(logicalPath)) return {
|
|
4788
|
+
column: logicalPath.replace(/\./g, "__"),
|
|
4789
|
+
designType,
|
|
4790
|
+
flattened: true,
|
|
4791
|
+
optional
|
|
4792
|
+
};
|
|
4793
|
+
return {
|
|
4794
|
+
column: relationalColumnName(logicalPath, idx.columnMap.get(logicalPath), logicalPath.includes(".")),
|
|
4795
|
+
designType,
|
|
4796
|
+
optional
|
|
4797
|
+
};
|
|
4798
|
+
}
|
|
4799
|
+
//#endregion
|
|
4610
4800
|
//#region src/table/db-view.ts
|
|
4801
|
+
/** The `@db.agg.*` annotations, one per supported aggregate function. */
|
|
4802
|
+
const AGG_KEYS = SUPPORTED_AGGREGATE_FNS.map((fn) => `db.agg.${fn}`);
|
|
4803
|
+
/**
|
|
4804
|
+
* Reads a view field's `@db.agg.*` annotation. The compiled value is
|
|
4805
|
+
* `{ field?, condition? }`; models compiled by older versions carry `true`
|
|
4806
|
+
* (bare `@db.agg.count`) or a string (the field), which are read the same way.
|
|
4807
|
+
* A missing field or `true` is `'*'` (COUNT(*)).
|
|
4808
|
+
*/
|
|
4809
|
+
function readViewAgg(metadata) {
|
|
4810
|
+
for (const key of AGG_KEYS) {
|
|
4811
|
+
const val = metadata?.get(key);
|
|
4812
|
+
if (val === void 0) continue;
|
|
4813
|
+
const aggFn = key.slice(7);
|
|
4814
|
+
if (typeof val === "string") return {
|
|
4815
|
+
aggFn,
|
|
4816
|
+
aggField: val
|
|
4817
|
+
};
|
|
4818
|
+
if (val === null || typeof val !== "object") return {
|
|
4819
|
+
aggFn,
|
|
4820
|
+
aggField: "*"
|
|
4821
|
+
};
|
|
4822
|
+
return {
|
|
4823
|
+
aggFn,
|
|
4824
|
+
aggField: val.field ?? "*",
|
|
4825
|
+
...val.condition ? { aggFilter: val.condition } : {}
|
|
4826
|
+
};
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
const JSON_LEAF_TYPES = new Set([
|
|
4830
|
+
"string",
|
|
4831
|
+
"number",
|
|
4832
|
+
"boolean"
|
|
4833
|
+
]);
|
|
4611
4834
|
/**
|
|
4612
4835
|
* Database view abstraction driven by Atscript `@db.view.*` annotations.
|
|
4613
4836
|
*
|
|
@@ -4623,6 +4846,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4623
4846
|
*/
|
|
4624
4847
|
var AtscriptDbView = class extends AtscriptDbReadable {
|
|
4625
4848
|
_viewPlan;
|
|
4849
|
+
_columnMappings;
|
|
4626
4850
|
get isView() {
|
|
4627
4851
|
return true;
|
|
4628
4852
|
}
|
|
@@ -4648,19 +4872,18 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
4648
4872
|
const metadata = this._type.metadata;
|
|
4649
4873
|
const forRef = metadata.get("db.view.for");
|
|
4650
4874
|
const entryType = typeof forRef === "function" ? forRef : forRef.type;
|
|
4651
|
-
const
|
|
4652
|
-
const entryTable = entryTypeResolved?.metadata?.get("db.table") || entryTypeResolved?.id || "";
|
|
4875
|
+
const entryTable = tableNameOf(entryType());
|
|
4653
4876
|
const rawJoins = metadata.get("db.view.joins");
|
|
4654
4877
|
const joins = [];
|
|
4655
4878
|
if (rawJoins) for (const join of rawJoins) {
|
|
4656
4879
|
const targetRef = join.target;
|
|
4657
4880
|
const targetType = typeof targetRef === "function" ? targetRef : targetRef.type;
|
|
4658
4881
|
const targetTypeResolved = targetType();
|
|
4659
|
-
const targetTable = targetTypeResolved?.metadata?.get("db.table") || targetTypeResolved?.id || "";
|
|
4660
4882
|
joins.push({
|
|
4661
4883
|
targetType,
|
|
4662
|
-
targetTable,
|
|
4663
|
-
condition: join.condition
|
|
4884
|
+
targetTable: tableNameOf(targetTypeResolved),
|
|
4885
|
+
condition: join.condition,
|
|
4886
|
+
kind: join.kind === "left" ? "left" : "inner"
|
|
4664
4887
|
});
|
|
4665
4888
|
}
|
|
4666
4889
|
const filter = metadata.get("db.view.filter");
|
|
@@ -4676,73 +4899,143 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
4676
4899
|
};
|
|
4677
4900
|
return this._viewPlan;
|
|
4678
4901
|
}
|
|
4902
|
+
/** Whether the adapter stores nested objects natively (document paths, no JSON columns). */
|
|
4903
|
+
get _nested() {
|
|
4904
|
+
return this.adapter.supportsNestedObjects();
|
|
4905
|
+
}
|
|
4906
|
+
/**
|
|
4907
|
+
* Resolves a view query field ref (join condition, `@db.view.filter`,
|
|
4908
|
+
* conditional-aggregate predicate) to its table name and PHYSICAL source on
|
|
4909
|
+
* this view's adapter — the column (or document path) with `TableMetadata`'s
|
|
4910
|
+
* layout rules, the path inside a JSON column, and whether the value may be
|
|
4911
|
+
* absent. An unqualified ref resolves against the entry table.
|
|
4912
|
+
* @throws for a ref without storage (`@db.ignore`, navigation relation) or
|
|
4913
|
+
* inside an `@db.encrypted` field (relational adapters).
|
|
4914
|
+
* @since 0.1.136
|
|
4915
|
+
*/
|
|
4916
|
+
resolveRefSource(ref) {
|
|
4917
|
+
const type = ref.type ? ref.type() : this.viewPlan.entryType();
|
|
4918
|
+
return {
|
|
4919
|
+
table: tableNameOf(type),
|
|
4920
|
+
source: resolveViewSource(type, ref.field, this._nested)
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4679
4923
|
/**
|
|
4680
|
-
* Resolves a query field ref
|
|
4924
|
+
* Resolves a query field ref (join condition, `@db.view.filter`) to a
|
|
4925
|
+
* quoted `table.column` SQL fragment — the PHYSICAL column (flattened
|
|
4926
|
+
* `__` name, `@db.column` rename). An unqualified ref
|
|
4927
|
+
* resolves against the entry table.
|
|
4681
4928
|
*
|
|
4682
4929
|
* @param ref - The field reference from the query tree.
|
|
4683
4930
|
* @param qi - Identifier quoting function (e.g. backtick for MySQL, double-quote for SQLite).
|
|
4684
4931
|
* Defaults to double-quote wrapping for backwards compatibility.
|
|
4932
|
+
* @throws when the ref reads inside a JSON column (not supported in view conditions).
|
|
4685
4933
|
*/
|
|
4686
4934
|
resolveFieldRef(ref, qi = (n) => `"${n}"`) {
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
}
|
|
4691
|
-
const resolved = ref.type();
|
|
4692
|
-
return `${qi(resolved?.metadata?.get("db.table") || resolved?.id || "")}.${qi(ref.field)}`;
|
|
4935
|
+
const { table, source } = this.resolveRefSource(ref);
|
|
4936
|
+
if (source.jsonPath) throw new Error(`View "${this.tableName}": "${ref.field}" reads inside a JSON column — JSON paths are not supported in view conditions`);
|
|
4937
|
+
return `${qi(table)}.${qi(source.column)}`;
|
|
4693
4938
|
}
|
|
4694
4939
|
/**
|
|
4695
|
-
* Maps each view
|
|
4696
|
-
*
|
|
4940
|
+
* Maps each view column to its source table and PHYSICAL source column.
|
|
4941
|
+
*
|
|
4942
|
+
* View fields resolve through their chain ref; fields without a ref read
|
|
4943
|
+
* the entry table under the same name; aggregates read their `@db.agg.*`
|
|
4944
|
+
* field from the entry table (or their ref). Source names are
|
|
4945
|
+
* physical (flattened `__` names, `@db.column`, document paths), `viewColumn`
|
|
4946
|
+
* is the view's own physical name, an object field whose source is a
|
|
4947
|
+
* flattened object expands to one mapping per leaf, and a primitive leaf
|
|
4948
|
+
* inside a JSON column carries `json` (rendered by adapters that support
|
|
4949
|
+
* JSON extraction).
|
|
4950
|
+
*
|
|
4951
|
+
* Computed once per view (the plan and the type are immutable).
|
|
4952
|
+
*
|
|
4953
|
+
* @throws for an object field over a JSON column without `@db.json` on the
|
|
4954
|
+
* view field, a JSON leaf that is not a string / number / boolean, or an
|
|
4955
|
+
* aggregate other than `count` over `'*'`.
|
|
4697
4956
|
*/
|
|
4698
4957
|
getViewColumnMappings() {
|
|
4958
|
+
this._columnMappings ??= this._buildColumnMappings();
|
|
4959
|
+
return this._columnMappings;
|
|
4960
|
+
}
|
|
4961
|
+
_buildColumnMappings() {
|
|
4699
4962
|
const plan = this.viewPlan;
|
|
4700
4963
|
const mappings = [];
|
|
4701
4964
|
if (this._type.type.kind !== "object") return mappings;
|
|
4702
|
-
const aggKeys = [
|
|
4703
|
-
"db.agg.sum",
|
|
4704
|
-
"db.agg.avg",
|
|
4705
|
-
"db.agg.count",
|
|
4706
|
-
"db.agg.min",
|
|
4707
|
-
"db.agg.max"
|
|
4708
|
-
];
|
|
4709
4965
|
const ignored = this.ignoredFields;
|
|
4966
|
+
const meta = this.getMetadata();
|
|
4967
|
+
const nested = this._nested;
|
|
4968
|
+
const viewName = (path) => nested ? meta.documentPath(path) : meta.pathToPhysical.get(path);
|
|
4969
|
+
const fail = (field, message) => {
|
|
4970
|
+
throw new Error(`View "${this.tableName}" field "${field}": ${message}`);
|
|
4971
|
+
};
|
|
4972
|
+
const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.targetTable));
|
|
4710
4973
|
for (const [fieldName, fieldType] of this._type.type.props.entries()) {
|
|
4711
4974
|
if (ignored.has(fieldName)) continue;
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
aggFn = key.split(".")[2];
|
|
4718
|
-
aggField = typeof val === "string" ? val : "*";
|
|
4719
|
-
break;
|
|
4720
|
-
}
|
|
4721
|
-
}
|
|
4975
|
+
const agg = readViewAgg(fieldType.metadata);
|
|
4976
|
+
const aggField = agg?.aggField;
|
|
4977
|
+
if (aggField === "*" && agg?.aggFn !== "count") fail(fieldName, `aggregate "${agg?.aggFn}" needs a field — only count accepts *`);
|
|
4978
|
+
let sourceType;
|
|
4979
|
+
let sourcePath;
|
|
4722
4980
|
if (fieldType.ref) {
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
const sourceColumn = fieldType.ref.field || fieldName;
|
|
4726
|
-
mappings.push({
|
|
4727
|
-
viewColumn: fieldName,
|
|
4728
|
-
sourceTable,
|
|
4729
|
-
sourceColumn,
|
|
4730
|
-
aggFn,
|
|
4731
|
-
aggField
|
|
4732
|
-
});
|
|
4981
|
+
sourceType = fieldType.ref.type();
|
|
4982
|
+
sourcePath = fieldType.ref.field || fieldName;
|
|
4733
4983
|
} else {
|
|
4734
|
-
|
|
4984
|
+
sourceType = plan.entryType();
|
|
4985
|
+
sourcePath = aggField && aggField !== "*" ? aggField : fieldName;
|
|
4986
|
+
}
|
|
4987
|
+
const sourceTable = tableNameOf(sourceType);
|
|
4988
|
+
const joinNullable = leftJoined.has(sourceTable);
|
|
4989
|
+
if (aggField === "*" && !fieldType.ref) {
|
|
4735
4990
|
mappings.push({
|
|
4736
|
-
viewColumn: fieldName,
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4991
|
+
viewColumn: viewName(fieldName) ?? fieldName,
|
|
4992
|
+
viewPath: fieldName,
|
|
4993
|
+
sourceTable,
|
|
4994
|
+
sourceColumn: "*",
|
|
4995
|
+
...agg
|
|
4741
4996
|
});
|
|
4997
|
+
continue;
|
|
4998
|
+
}
|
|
4999
|
+
const source = resolveViewSource(sourceType, sourcePath, nested);
|
|
5000
|
+
const ownColumn = viewName(fieldName);
|
|
5001
|
+
if (ownColumn === void 0 && !nested) {
|
|
5002
|
+
if (!source.flattened) fail(fieldName, "source is a JSON column — add @db.json to the view field");
|
|
5003
|
+
const prefix = `${fieldName}.`;
|
|
5004
|
+
for (const [viewPath, viewColumn] of meta.pathToPhysical) {
|
|
5005
|
+
if (!viewPath.startsWith(prefix) || ignored.has(viewPath)) continue;
|
|
5006
|
+
const leafPath = `${sourcePath}.${viewPath.slice(prefix.length)}`;
|
|
5007
|
+
const leaf = resolveViewSource(sourceType, leafPath, nested);
|
|
5008
|
+
mappings.push(this._leafMapping(viewPath, viewColumn, sourceTable, leaf, joinNullable));
|
|
5009
|
+
}
|
|
5010
|
+
continue;
|
|
4742
5011
|
}
|
|
5012
|
+
if (source.flattened) fail(fieldName, "source is a flattened object — remove @db.json from the view field");
|
|
5013
|
+
const mapping = this._leafMapping(fieldName, ownColumn ?? fieldName, sourceTable, source, joinNullable);
|
|
5014
|
+
mappings.push(agg ? {
|
|
5015
|
+
...mapping,
|
|
5016
|
+
...agg
|
|
5017
|
+
} : mapping);
|
|
4743
5018
|
}
|
|
4744
5019
|
return mappings;
|
|
4745
5020
|
}
|
|
5021
|
+
/** One view column over one physical source (a column or a JSON leaf). */
|
|
5022
|
+
_leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable) {
|
|
5023
|
+
const mapping = {
|
|
5024
|
+
viewColumn,
|
|
5025
|
+
viewPath,
|
|
5026
|
+
sourceTable,
|
|
5027
|
+
sourceColumn: source.column
|
|
5028
|
+
};
|
|
5029
|
+
if (joinNullable || source.optional || source.designType === "unknown") mapping.nullable = true;
|
|
5030
|
+
if (source.jsonPath) {
|
|
5031
|
+
if (!JSON_LEAF_TYPES.has(source.designType)) throw new Error(`View "${this.tableName}" field "${viewPath}": JSON extraction supports string, number and boolean leaves only`);
|
|
5032
|
+
mapping.json = {
|
|
5033
|
+
path: source.jsonPath,
|
|
5034
|
+
type: source.designType
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
5037
|
+
return mapping;
|
|
5038
|
+
}
|
|
4746
5039
|
};
|
|
4747
5040
|
/**
|
|
4748
5041
|
* Structural type guard for views: `true` when the readable reports
|
|
@@ -4757,4 +5050,4 @@ function isAtscriptDbView(readable) {
|
|
|
4757
5050
|
return readable.isView;
|
|
4758
5051
|
}
|
|
4759
5052
|
//#endregion
|
|
4760
|
-
export { unsupportedOperatorMessage as A, isJsonValueField as B, guardAggregate as C, guardQuery as D, guardPaths as E, TableMetadata as F,
|
|
5053
|
+
export { unsupportedOperatorMessage as A, isJsonValueField as B, guardAggregate as C, guardQuery as D, guardPaths as E, TableMetadata as F, normalizeComputedSelect as H, findAncestorInSet as I, isGeoIndexableType as L, DocumentFieldMapper as M, FieldMappingStrategy as N, narrowerFilterOps as O, UniquSelect as P, isGeoPointType as R, collectQueryPaths as S, guardPath as T, resolveCalendarBuckets as U, jsonValueAncestor as V, NoopLogger as W, assertGeoPoint as _, decomposePatch as a, checkHavingKeys as b, BaseDbAdapter as c, NativeIntegrity as d, AtscriptDbReadable as f, acceptedOperatorsHint as g, ENCRYPTED_REASON as h, assertNoVersionWrites as i, RelationalFieldMapper as j, sortFieldNames as k, createFailureCollector as l, ADAPTER_FILTER_REASON as m, isAtscriptDbView as n, ApplicationIntegrity as o, resolveDesignType as p, AtscriptDbTable as r, ALL_BUCKET_UNITS as s, AtscriptDbView as t, IntegrityStrategy as u, bucketSourceVerdict as v, guardFilter as w, classifyQueryPath as x, canFilterLeaf as y, isBucketableField as z };
|