@atscript/db 0.1.135 → 0.1.137
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-GruoVxH5.d.mts → buckets-BFG2RYRW.d.mts} +662 -31
- package/dist/{buckets-D6PBXKRJ.d.cts → buckets-C-27xmtq.d.cts} +662 -31
- package/dist/column-diff-BmqvgBWw.d.cts +24 -0
- package/dist/{db-view-Doh8vPg3.mjs → column-diff-BwOA5101.mjs} +795 -125
- package/dist/{db-view-Dm7MpUGB.cjs → column-diff-CgxgFKzx.cjs} +873 -125
- package/dist/column-diff-DPkbZIVE.d.mts +24 -0
- package/dist/index.cjs +79 -55
- package/dist/index.d.cts +12 -10
- package/dist/index.d.mts +12 -10
- package/dist/index.mjs +30 -11
- 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 +217 -77
- package/dist/plugin.mjs +217 -78
- 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-D3Zu0Mta.d.mts +30 -0
- package/dist/relation-helpers-DxrvS6ar.d.cts +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 +38 -321
- package/dist/sync.d.cts +29 -17
- package/dist/sync.d.mts +29 -17
- package/dist/sync.mjs +6 -289
- 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
- package/dist/db-space-9CH5neN7.d.mts +0 -439
- package/dist/db-space-BdtBNTeH.d.cts +0 -439
|
@@ -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,43 @@ function findAncestorInSet(path, set) {
|
|
|
111
118
|
if (set.has(ancestor)) return ancestor;
|
|
112
119
|
}
|
|
113
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Whether a `@db.column` / `@db.column.renamed` on `path` applies: always on
|
|
123
|
+
* relational storage; on document storage (`nestedObjects`) for top-level
|
|
124
|
+
* fields only — nested keys are stored as-is. Column maps are built with it.
|
|
125
|
+
*/
|
|
126
|
+
function columnOverrideApplies(path, nestedObjects) {
|
|
127
|
+
return !nestedObjects || !path.includes(".");
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Logical field path → its physical path in document storage (nested
|
|
131
|
+
* objects kept inline). A document renames the TOP-LEVEL key only — nested
|
|
132
|
+
* keys are stored as-is, so a `@db.column` on a nested leaf renames nothing
|
|
133
|
+
* (`address.zip` stays `address.zip`; see {@link columnOverrideApplies}) —
|
|
134
|
+
* and a dotted path under a renamed top-level object renames its first
|
|
135
|
+
* segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
|
|
136
|
+
*/
|
|
137
|
+
function documentPath(columnMap, path) {
|
|
138
|
+
const dot = path.indexOf(".");
|
|
139
|
+
if (dot === -1) return columnMap.get(path) ?? path;
|
|
140
|
+
const top = columnMap.get(path.slice(0, dot));
|
|
141
|
+
return top === void 0 ? path : top + path.slice(dot);
|
|
142
|
+
}
|
|
143
|
+
/** The `__`-separated parent prefix of a dotted path (`a.b.c` → `a__b__`), `""` for a top-level path. */
|
|
144
|
+
function flattenedPrefix(path) {
|
|
145
|
+
const lastDot = path.lastIndexOf(".");
|
|
146
|
+
return lastDot >= 0 ? `${path.slice(0, lastDot).replace(/\./g, "__")}__` : "";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Relational column of a column-stored path: a top-level field is its
|
|
150
|
+
* `@db.column` (`override`) or its name; a leaf of a `flattened` object is
|
|
151
|
+
* its parent segments joined with `__` plus its `@db.column` or own segment
|
|
152
|
+
* (`address.zip` with `@db.column 'zip_code'` → `address__zip_code`).
|
|
153
|
+
*/
|
|
154
|
+
function relationalColumnName(path, override, flattened) {
|
|
155
|
+
if (override) return flattened ? flattenedPrefix(path) + override : override;
|
|
156
|
+
return flattened ? path.replace(/\./g, "__") : path;
|
|
157
|
+
}
|
|
114
158
|
/** Returns true if `metadata` indicates a navigation relation field. */
|
|
115
159
|
function isNavRelation(metadata) {
|
|
116
160
|
return metadata.has("db.rel.to") || metadata.has("db.rel.from") || metadata.has("db.rel.via");
|
|
@@ -158,6 +202,7 @@ var TableMetadata = class {
|
|
|
158
202
|
ignoredFields = /* @__PURE__ */ new Set();
|
|
159
203
|
uniqueProps = /* @__PURE__ */ new Set();
|
|
160
204
|
defaults = /* @__PURE__ */ new Map();
|
|
205
|
+
/** Logical path → `@db.column` override (top-level keys only on document storage). */
|
|
161
206
|
columnMap = /* @__PURE__ */ new Map();
|
|
162
207
|
dimensions = [];
|
|
163
208
|
measures = [];
|
|
@@ -223,20 +268,17 @@ var TableMetadata = class {
|
|
|
223
268
|
get isBuilt() {
|
|
224
269
|
return this._built;
|
|
225
270
|
}
|
|
271
|
+
/** {@link documentPath} over this table's `columnMap`. */
|
|
272
|
+
documentPath(path) {
|
|
273
|
+
return documentPath(this.columnMap, path);
|
|
274
|
+
}
|
|
226
275
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
* as-is — so a dotted path under a renamed top-level object renames its
|
|
231
|
-
* first segment: `profile.bio` under `@db.column 'prof'` → `prof.bio`.
|
|
276
|
+
* Physical name of a logical path: the document path on nested-object
|
|
277
|
+
* adapters, else the relational column (`pathToPhysical`, then the
|
|
278
|
+
* `@db.column` override).
|
|
232
279
|
*/
|
|
233
|
-
|
|
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);
|
|
280
|
+
physicalPath(logical) {
|
|
281
|
+
return this.nestedObjects ? this.documentPath(logical) : this.pathToPhysical.get(logical) ?? this.columnMap.get(logical) ?? logical;
|
|
240
282
|
}
|
|
241
283
|
/**
|
|
242
284
|
* Runs the full metadata compilation pipeline. Called once by
|
|
@@ -326,10 +368,11 @@ var TableMetadata = class {
|
|
|
326
368
|
this.primaryKeys.push(fieldName);
|
|
327
369
|
this.originalMetaIdFields.push(fieldName);
|
|
328
370
|
}
|
|
371
|
+
const renamable = columnOverrideApplies(fieldName, this.nestedObjects);
|
|
329
372
|
const column = metadata.get("db.column");
|
|
330
|
-
if (column) this.columnMap.set(fieldName, column);
|
|
373
|
+
if (column && renamable) this.columnMap.set(fieldName, column);
|
|
331
374
|
const columnFrom = metadata.get("db.column.renamed");
|
|
332
|
-
if (columnFrom) this._columnFromMap.set(fieldName, columnFrom);
|
|
375
|
+
if (columnFrom && renamable) this._columnFromMap.set(fieldName, columnFrom);
|
|
333
376
|
const resolvedDefault = resolveDefaultFromMetadata(metadata);
|
|
334
377
|
if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
|
|
335
378
|
if (metadata.has("db.ignore")) this.ignoredFields.add(fieldName);
|
|
@@ -354,8 +397,7 @@ var TableMetadata = class {
|
|
|
354
397
|
const raw = metadata.get("db.rel.FK");
|
|
355
398
|
const alias = raw === true ? void 0 : raw;
|
|
356
399
|
if (fieldType.ref) {
|
|
357
|
-
const
|
|
358
|
-
const targetTable = refTarget?.metadata?.get("db.table") || refTarget?.id || "";
|
|
400
|
+
const targetTable = tableNameOf(fieldType.ref.type());
|
|
359
401
|
const targetField = fieldType.ref.field;
|
|
360
402
|
const key = alias || `__auto_${fieldName}`;
|
|
361
403
|
const existing = this.foreignKeys.get(key);
|
|
@@ -498,10 +540,7 @@ var TableMetadata = class {
|
|
|
498
540
|
if (findAncestorInSet(path, this.jsonFields) !== void 0) continue;
|
|
499
541
|
if (findAncestorInSet(path, this.encryptedFields) !== void 0) continue;
|
|
500
542
|
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;
|
|
543
|
+
const physicalName = relationalColumnName(path, this.columnMap.get(path), isFlattened);
|
|
505
544
|
this.pathToPhysical.set(path, physicalName);
|
|
506
545
|
this.physicalToPath.set(physicalName, path);
|
|
507
546
|
const fieldType = this.flatMap.get(path);
|
|
@@ -520,11 +559,6 @@ var TableMetadata = class {
|
|
|
520
559
|
this.onlyColumnRenames = this.columnMap.size > 0 && this.flattenedParents.size === 0 && this.jsonFields.size === 0;
|
|
521
560
|
this.requiresMappings = this.flattenedParents.size > 0 || this.jsonFields.size > 0 || this.onlyColumnRenames;
|
|
522
561
|
}
|
|
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
562
|
/** Nearest `@db.encrypted` ancestor of `path` (exclusive), or `undefined`. */
|
|
529
563
|
/**
|
|
530
564
|
* Indexes non-ignored descriptors by logical path and retains the JSON-parent
|
|
@@ -596,10 +630,10 @@ var TableMetadata = class {
|
|
|
596
630
|
else if (isJson) storage = "json";
|
|
597
631
|
else if (isFlattened) storage = "flattened";
|
|
598
632
|
else storage = "column";
|
|
599
|
-
const physicalName =
|
|
633
|
+
const physicalName = this.physicalPath(path);
|
|
600
634
|
const fromLocal = this._columnFromMap.get(path);
|
|
601
635
|
let renamedFrom;
|
|
602
|
-
if (fromLocal) renamedFrom = isFlattened ?
|
|
636
|
+
if (fromLocal) renamedFrom = isFlattened ? flattenedPrefix(path) + fromLocal : fromLocal;
|
|
603
637
|
const currencyCode = type.metadata.get("db.amount.currency");
|
|
604
638
|
const currencyRefField = type.metadata.get("db.amount.currency.ref");
|
|
605
639
|
const unitCode = type.metadata.get("db.unit");
|
|
@@ -700,7 +734,7 @@ var TableMetadata = class {
|
|
|
700
734
|
field.optional = ftype.optional === true;
|
|
701
735
|
field.designType = ftype.type.tags?.has("objectId") ? "objectId" : resolveDesignType(ftype);
|
|
702
736
|
}
|
|
703
|
-
field.name = this.
|
|
737
|
+
field.name = this.physicalPath(field.name);
|
|
704
738
|
}
|
|
705
739
|
}
|
|
706
740
|
/**
|
|
@@ -780,7 +814,7 @@ var TableMetadata = class {
|
|
|
780
814
|
* An array `$select` holds plain field names and computed entries —
|
|
781
815
|
* aggregates (`{ $fn, $field }`, {@link aggregates}) and calendar buckets
|
|
782
816
|
* (`{ $bucket, $field }`, {@link buckets}). Entries arrive normalized
|
|
783
|
-
* (`
|
|
817
|
+
* (`normalizeComputedSelect` rejects any other shape before translation).
|
|
784
818
|
*/
|
|
785
819
|
var UniquSelect = class UniquSelect {
|
|
786
820
|
static UNRESOLVED = Symbol("unresolved");
|
|
@@ -913,7 +947,7 @@ var FieldMappingStrategy = class {
|
|
|
913
947
|
* equals a field name).
|
|
914
948
|
*
|
|
915
949
|
* `buckets` are the query's calendar buckets as the core's normalizer
|
|
916
|
-
* resolved them (`
|
|
950
|
+
* resolved them (`normalizeComputedSelect` — `AtscriptDbReadable.aggregate`
|
|
917
951
|
* runs it before the guards); they reach adapters with `field` made
|
|
918
952
|
* physical and the source descriptor as `fd`.
|
|
919
953
|
*/
|
|
@@ -948,16 +982,28 @@ var FieldMappingStrategy = class {
|
|
|
948
982
|
/**
|
|
949
983
|
* `$select` with its field paths made physical: array-form names and
|
|
950
984
|
* computed `$field`s (`'*'` kept), or the keys of the object
|
|
951
|
-
* (inclusion / exclusion) form.
|
|
985
|
+
* (inclusion / exclusion) form. An aggregate's output alias is fixed
|
|
986
|
+
* (`$as`) from its LOGICAL field first, so a default alias never leaks a
|
|
987
|
+
* physical name (`sum(amount)` over `@db.column 'amount_cents'` stays
|
|
988
|
+
* `sum_amount`); a bucket's alias is already resolved.
|
|
952
989
|
*/
|
|
953
990
|
physicalSelect(select, meta) {
|
|
954
991
|
if (!this.renamesPaths(meta)) return select;
|
|
955
992
|
if (Array.isArray(select)) return select.map((item) => {
|
|
956
993
|
if (typeof item === "string") return this.physicalPath(item, meta);
|
|
957
|
-
if (isAggregateExpr(item) || isBucketExpr(item))
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
994
|
+
if (isAggregateExpr(item) || isBucketExpr(item)) {
|
|
995
|
+
if (item.$field === "*") return item;
|
|
996
|
+
const physical = this.physicalPath(item.$field, meta);
|
|
997
|
+
if (physical === item.$field) return item;
|
|
998
|
+
return isAggregateExpr(item) ? {
|
|
999
|
+
...item,
|
|
1000
|
+
$as: resolveAlias(item),
|
|
1001
|
+
$field: physical
|
|
1002
|
+
} : {
|
|
1003
|
+
...item,
|
|
1004
|
+
$field: physical
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
961
1007
|
return item;
|
|
962
1008
|
});
|
|
963
1009
|
const translated = {};
|
|
@@ -987,11 +1033,24 @@ var FieldMappingStrategy = class {
|
|
|
987
1033
|
else if (key === "$not") result[key] = this.translateFilter(value, meta);
|
|
988
1034
|
else if (key.startsWith("$")) result[key] = value;
|
|
989
1035
|
else {
|
|
990
|
-
const
|
|
991
|
-
result[
|
|
1036
|
+
const physical = this.physicalPath(key, meta);
|
|
1037
|
+
result[physical] = this.formatFilterValue(physical, value, meta);
|
|
992
1038
|
}
|
|
993
1039
|
return result;
|
|
994
1040
|
}
|
|
1041
|
+
/** `$inc` / `$mul` field-op keys to physical names ({@link physicalPath}). */
|
|
1042
|
+
translateOpsKeys(ops, meta) {
|
|
1043
|
+
if (!this.renamesPaths(meta)) return ops;
|
|
1044
|
+
const physical = (rec) => {
|
|
1045
|
+
const out = {};
|
|
1046
|
+
for (const key in rec) out[this.physicalPath(key, meta)] = rec[key];
|
|
1047
|
+
return out;
|
|
1048
|
+
};
|
|
1049
|
+
return {
|
|
1050
|
+
inc: ops.inc && physical(ops.inc),
|
|
1051
|
+
mul: ops.mul && physical(ops.mul)
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
995
1054
|
/**
|
|
996
1055
|
* Reverse-maps `@db.column` renames on a row read from storage.
|
|
997
1056
|
* Renames physical keys back to logical names in-place.
|
|
@@ -1151,10 +1210,11 @@ var DocumentFieldMapper = class extends FieldMappingStrategy {
|
|
|
1151
1210
|
}
|
|
1152
1211
|
return this.formatWriteValues(data, meta);
|
|
1153
1212
|
}
|
|
1213
|
+
/** Patch keys (top-level or decomposed dotted) to document paths. */
|
|
1154
1214
|
translatePatchKeys(update, meta) {
|
|
1155
|
-
if (meta
|
|
1215
|
+
if (this.renamesPaths(meta)) {
|
|
1156
1216
|
const result = {};
|
|
1157
|
-
for (const key of Object.keys(update)) result[
|
|
1217
|
+
for (const key of Object.keys(update)) result[this.physicalPath(key, meta)] = update[key];
|
|
1158
1218
|
return this.formatWriteValues(result, meta);
|
|
1159
1219
|
}
|
|
1160
1220
|
return this.formatWriteValues(update, meta);
|
|
@@ -1382,7 +1442,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1382
1442
|
* - `$exists` with a non-boolean operand → `INVALID_QUERY`
|
|
1383
1443
|
* - malformed `$select` computed entries / `$groupBy` entries, calendar
|
|
1384
1444
|
* buckets outside grouped queries or with a bad unit / zone / alias →
|
|
1385
|
-
* `INVALID_QUERY` (the shared normalizer, `
|
|
1445
|
+
* `INVALID_QUERY` (the shared normalizer, `normalizeComputedSelect`)
|
|
1386
1446
|
* - a calendar bucket over a field that is not a bucket source
|
|
1387
1447
|
* (`bucketSourceVerdict`) → `INVALID_QUERY`, or `BUCKET_NOT_SUPPORTED`
|
|
1388
1448
|
* when the adapter has no calendar buckets at all; a unit the adapter's
|
|
@@ -1648,7 +1708,7 @@ function collectFilterKeys(filter, push, skip, refs) {
|
|
|
1648
1708
|
* `$sort` / `$having`; a bucket alias is also dropped from `groupBy`, which
|
|
1649
1709
|
* lists grouped fields only.
|
|
1650
1710
|
*
|
|
1651
|
-
* Entry shapes are not checked here — see `
|
|
1711
|
+
* Entry shapes are not checked here — see `normalizeComputedSelect`.
|
|
1652
1712
|
*/
|
|
1653
1713
|
function collectQueryPaths(query, aggregate) {
|
|
1654
1714
|
const refs = {
|
|
@@ -1788,7 +1848,7 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
|
1788
1848
|
* {@link guardPath}). Adapters may therefore assume every path they receive
|
|
1789
1849
|
* is physical.
|
|
1790
1850
|
*
|
|
1791
|
-
* Entry shapes are the caller's to normalize first (`
|
|
1851
|
+
* Entry shapes are the caller's to normalize first (`normalizeComputedSelect`
|
|
1792
1852
|
* — {@link guardQuery} / {@link guardAggregate} do).
|
|
1793
1853
|
*
|
|
1794
1854
|
* In aggregate mode (`aggregate = true`) `$select` computed entries have
|
|
@@ -1821,7 +1881,7 @@ function guardQuery(meta, adapter, query) {
|
|
|
1821
1881
|
if (!query) return;
|
|
1822
1882
|
guardFilter(meta, adapter, query.filter);
|
|
1823
1883
|
guardSort(meta, query.controls?.$sort);
|
|
1824
|
-
|
|
1884
|
+
normalizeComputedSelect(query.controls, meta, false);
|
|
1825
1885
|
guardPaths(meta, adapter, query);
|
|
1826
1886
|
}
|
|
1827
1887
|
/**
|
|
@@ -1844,16 +1904,17 @@ function checkHavingKeys(refs) {
|
|
|
1844
1904
|
}
|
|
1845
1905
|
/**
|
|
1846
1906
|
* Aggregate-path guard: $groupBy / $select / $having encryption refs + filter
|
|
1847
|
-
* + $sort, then the path guard, then the adapter's
|
|
1907
|
+
* + $sort, then the path guard, then the adapter's aggregate functions
|
|
1908
|
+
* (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
|
|
1848
1909
|
* (`BUCKET_NOT_SUPPORTED`), then the `$having` key rule
|
|
1849
1910
|
* ({@link checkHavingKeys} — after the path guard so an unknown key still
|
|
1850
1911
|
* reads `Unknown field`).
|
|
1851
1912
|
*
|
|
1852
1913
|
* `buckets` are the query's resolved calendar buckets when the caller already
|
|
1853
|
-
* ran `
|
|
1914
|
+
* ran `normalizeComputedSelect` (resolved here otherwise).
|
|
1854
1915
|
*/
|
|
1855
1916
|
function guardAggregate(meta, adapter, query, resolved) {
|
|
1856
|
-
const buckets = resolved ??
|
|
1917
|
+
const buckets = resolved ?? normalizeComputedSelect(query.controls, meta, true);
|
|
1857
1918
|
guardFilter(meta, adapter, query.filter);
|
|
1858
1919
|
const controls = query.controls;
|
|
1859
1920
|
if (meta.encryptedFields.size > 0) {
|
|
@@ -1866,11 +1927,26 @@ function guardAggregate(meta, adapter, query, resolved) {
|
|
|
1866
1927
|
guardSort(meta, controls.$sort);
|
|
1867
1928
|
}
|
|
1868
1929
|
const refs = guardPaths(meta, adapter, query, true);
|
|
1930
|
+
guardAggregateFns(adapter, controls.$select);
|
|
1869
1931
|
guardBucketUnits(adapter, buckets);
|
|
1870
1932
|
const having = refs ? checkHavingKeys(refs) : void 0;
|
|
1871
1933
|
if (having) throw new DbError("INVALID_QUERY", [having]);
|
|
1872
1934
|
}
|
|
1873
1935
|
/**
|
|
1936
|
+
* Rejects an aggregate whose (known — the normalizer checked the name)
|
|
1937
|
+
* function this adapter does not render (`aggregateFns()`) with
|
|
1938
|
+
* `AGG_FN_NOT_SUPPORTED`, before anything is translated — an adapter written
|
|
1939
|
+
* before a function existed never receives it.
|
|
1940
|
+
*/
|
|
1941
|
+
function guardAggregateFns(adapter, select) {
|
|
1942
|
+
if (!select) return;
|
|
1943
|
+
const fns = adapter.aggregateFns();
|
|
1944
|
+
for (const item of select) if (isAggregateExpr(item) && !fns.has(item.$fn)) throw new DbError("AGG_FN_NOT_SUPPORTED", [{
|
|
1945
|
+
path: "$select",
|
|
1946
|
+
message: `Aggregate function "${item.$fn}" is not supported by this adapter`
|
|
1947
|
+
}]);
|
|
1948
|
+
}
|
|
1949
|
+
/**
|
|
1874
1950
|
* Rejects a calendar bucket whose unit this adapter cannot group by
|
|
1875
1951
|
* (`calendarBucketUnits()`; an adapter with none was already answered per
|
|
1876
1952
|
* source by `bucketSourceVerdict`) with `BUCKET_NOT_SUPPORTED`,
|
|
@@ -2187,7 +2263,7 @@ var AtscriptDbReadable = class {
|
|
|
2187
2263
|
get syncMethod() {
|
|
2188
2264
|
return this._syncMethod;
|
|
2189
2265
|
}
|
|
2190
|
-
/** Logical → physical column name mapping from `@db.column
|
|
2266
|
+
/** Logical → physical column name mapping from `@db.column` (top-level only on document storage). */
|
|
2191
2267
|
get columnMap() {
|
|
2192
2268
|
this._ensureBuilt();
|
|
2193
2269
|
return this._meta.columnMap;
|
|
@@ -2377,13 +2453,15 @@ var AtscriptDbReadable = class {
|
|
|
2377
2453
|
*
|
|
2378
2454
|
* Validates:
|
|
2379
2455
|
* - `$select` computed entries and calendar buckets (the shared normalizer,
|
|
2380
|
-
* `
|
|
2456
|
+
* `normalizeComputedSelect`: shapes, unit, zone, alias, grouping)
|
|
2381
2457
|
* - Plain fields in $select are a subset of $groupBy
|
|
2382
2458
|
* - When dimensions/measures are defined (strict mode): $groupBy fields
|
|
2383
|
-
* must be dimensions, aggregate $field values must be measures (or '*'
|
|
2459
|
+
* must be dimensions, aggregate $field values must be measures (or '*';
|
|
2460
|
+
* a `countDistinct` field may also be a dimension)
|
|
2384
2461
|
* - the path guard (a bucket source must pass `bucketSourceVerdict` —
|
|
2385
2462
|
* timestamp type, no JSON ancestor, a dimension in strict mode, an
|
|
2386
|
-
* adapter with calendar buckets)
|
|
2463
|
+
* adapter with calendar buckets), the adapter's aggregate functions
|
|
2464
|
+
* (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
|
|
2387
2465
|
* (`BUCKET_NOT_SUPPORTED`)
|
|
2388
2466
|
*
|
|
2389
2467
|
* Translates field names, delegates to adapter.aggregate(),
|
|
@@ -2392,7 +2470,7 @@ var AtscriptDbReadable = class {
|
|
|
2392
2470
|
async aggregate(query) {
|
|
2393
2471
|
this._ensureBuilt();
|
|
2394
2472
|
const { $groupBy, $select } = query.controls;
|
|
2395
|
-
const buckets =
|
|
2473
|
+
const buckets = normalizeComputedSelect(query.controls, this._meta, true);
|
|
2396
2474
|
if ($select) {
|
|
2397
2475
|
const groupBySet = new Set($groupBy);
|
|
2398
2476
|
for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new DbError("INVALID_QUERY", [{
|
|
@@ -2409,8 +2487,16 @@ var AtscriptDbReadable = class {
|
|
|
2409
2487
|
path: "$groupBy",
|
|
2410
2488
|
message: `Field "${field}" is not a dimension`
|
|
2411
2489
|
}]);
|
|
2412
|
-
if ($select) {
|
|
2413
|
-
|
|
2490
|
+
if ($select) for (const item of $select) {
|
|
2491
|
+
if (!isAggregateExpr(item) || item.$field === "*" || measSet.has(item.$field)) continue;
|
|
2492
|
+
if (item.$fn === "countDistinct") {
|
|
2493
|
+
if (dimSet.has(item.$field)) continue;
|
|
2494
|
+
throw new DbError("INVALID_QUERY", [{
|
|
2495
|
+
path: "$select",
|
|
2496
|
+
message: `Aggregate field "${item.$field}" is not a dimension or measure`
|
|
2497
|
+
}]);
|
|
2498
|
+
}
|
|
2499
|
+
throw new DbError("INVALID_QUERY", [{
|
|
2414
2500
|
path: "$select",
|
|
2415
2501
|
message: `Aggregate field "${item.$field}" is not a measure`
|
|
2416
2502
|
}]);
|
|
@@ -2449,6 +2535,10 @@ var AtscriptDbReadable = class {
|
|
|
2449
2535
|
calendarBucketUnits() {
|
|
2450
2536
|
return this.adapter.calendarBucketUnits();
|
|
2451
2537
|
}
|
|
2538
|
+
/** Aggregate functions the adapter renders (proxies adapter capability). @since 0.1.136 */
|
|
2539
|
+
aggregateFns() {
|
|
2540
|
+
return this.adapter.aggregateFns();
|
|
2541
|
+
}
|
|
2452
2542
|
/** Whether the adapter can sort by a given field (proxies adapter capability). */
|
|
2453
2543
|
canSortField(fd) {
|
|
2454
2544
|
return this.adapter.canSortField(fd);
|
|
@@ -2739,7 +2829,7 @@ var AtscriptDbReadable = class {
|
|
|
2739
2829
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
2740
2830
|
*/
|
|
2741
2831
|
async loadRelations(rows, withRelations) {
|
|
2742
|
-
const { loadRelationsImpl } = await import("./relation-loader-
|
|
2832
|
+
const { loadRelationsImpl } = await import("./relation-loader-CTFaZpVa.mjs").then((n) => n.n);
|
|
2743
2833
|
return loadRelationsImpl(rows, withRelations, this);
|
|
2744
2834
|
}
|
|
2745
2835
|
/**
|
|
@@ -2868,6 +2958,15 @@ var BaseDbAdapter = class {
|
|
|
2868
2958
|
if (logger) this.logger = logger;
|
|
2869
2959
|
}
|
|
2870
2960
|
/**
|
|
2961
|
+
* Called by {@link DbSpace} right after its factory builds this adapter —
|
|
2962
|
+
* the administrative one included — before {@link registerReadable}. No-op
|
|
2963
|
+
* by default: override it to share state across a space's adapters when the
|
|
2964
|
+
* adapter has no driver to share it through (the memory adapter keeps one
|
|
2965
|
+
* store per space).
|
|
2966
|
+
* @since 0.1.137
|
|
2967
|
+
*/
|
|
2968
|
+
registerSpace(_space) {}
|
|
2969
|
+
/**
|
|
2871
2970
|
* Enables or disables verbose (debug-level) logging for this adapter.
|
|
2872
2971
|
* When disabled, no log strings are constructed — zero overhead.
|
|
2873
2972
|
*/
|
|
@@ -3038,6 +3137,31 @@ var BaseDbAdapter = class {
|
|
|
3038
3137
|
return EMPTY_BUCKET_UNITS;
|
|
3039
3138
|
}
|
|
3040
3139
|
/**
|
|
3140
|
+
* Aggregate functions (`{ $fn, $field }` in an aggregate `$select`) this
|
|
3141
|
+
* adapter renders. The default is `sum`, `count`, `avg`, `min` and `max`;
|
|
3142
|
+
* an adapter that also implements `countDistinct` (distinct non-null
|
|
3143
|
+
* values) returns `ALL_AGGREGATE_FNS`. The core rejects a known function
|
|
3144
|
+
* missing from this set with `AGG_FN_NOT_SUPPORTED` before dispatch, so
|
|
3145
|
+
* `aggregate()` only ever receives functions listed here; moost-db's
|
|
3146
|
+
* `/meta` advertises the set as `aggregateFns`.
|
|
3147
|
+
*
|
|
3148
|
+
* @since 0.1.136
|
|
3149
|
+
*/
|
|
3150
|
+
aggregateFns() {
|
|
3151
|
+
return BASE_AGGREGATE_FNS;
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Revision of how this adapter renders a managed view (its SQL / pipeline)
|
|
3155
|
+
* from an unchanged view definition. Stored in each managed view's sync
|
|
3156
|
+
* snapshot when defined, so bumping it recreates every managed view of the
|
|
3157
|
+
* adapter once on the next sync — return a new value whenever a rendering
|
|
3158
|
+
* fix changes what an existing view returns. `undefined` (the default)
|
|
3159
|
+
* leaves the snapshot, and its hash, as before. External views ignore it.
|
|
3160
|
+
*
|
|
3161
|
+
* @since 0.1.137
|
|
3162
|
+
*/
|
|
3163
|
+
viewRenderRevision() {}
|
|
3164
|
+
/**
|
|
3041
3165
|
* Whether this adapter enforces foreign key constraints natively.
|
|
3042
3166
|
* When `true`, the generic layer skips application-level cascade/setNull
|
|
3043
3167
|
* on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
|
|
@@ -3326,6 +3450,26 @@ var BaseDbAdapter = class {
|
|
|
3326
3450
|
*/
|
|
3327
3451
|
supportsColumnModify;
|
|
3328
3452
|
/**
|
|
3453
|
+
* Drops a table by name (without needing a registered readable).
|
|
3454
|
+
* Used by schema sync to remove tables no longer in the schema. A missing
|
|
3455
|
+
* table is not an error (`DROP TABLE IF EXISTS`). The default throws —
|
|
3456
|
+
* schema sync then reports the removed table as an `error` entry and keeps
|
|
3457
|
+
* it tracked. Since 0.1.137; before, the method was optional and a drop the
|
|
3458
|
+
* adapter lacked was skipped silently while sync reported it done.
|
|
3459
|
+
*/
|
|
3460
|
+
async dropTableByName(tableName) {
|
|
3461
|
+
throw new Error(`Cannot drop table "${tableName}": dropTableByName is not supported by this adapter`);
|
|
3462
|
+
}
|
|
3463
|
+
/**
|
|
3464
|
+
* Drops a view by name (without needing a registered readable).
|
|
3465
|
+
* Used by schema sync to remove views no longer in the schema, and to drop
|
|
3466
|
+
* a managed view before recreating it. A missing view is not an error. The
|
|
3467
|
+
* default throws, with the same history as {@link dropTableByName}.
|
|
3468
|
+
*/
|
|
3469
|
+
async dropViewByName(viewName) {
|
|
3470
|
+
throw new Error(`Cannot drop view "${viewName}": dropViewByName is not supported by this adapter`);
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3329
3473
|
* Drops several tables that reference each other (a foreign-key cycle) as
|
|
3330
3474
|
* one operation. Schema sync only calls this for cycles whose members are
|
|
3331
3475
|
* ALL being removed. Default: {@link dropTableByName} in the given order —
|
|
@@ -3335,7 +3479,7 @@ var BaseDbAdapter = class {
|
|
|
3335
3479
|
* @since 0.1.128
|
|
3336
3480
|
*/
|
|
3337
3481
|
async dropTablesByName(tableNames) {
|
|
3338
|
-
for (const name of tableNames) await this.dropTableByName
|
|
3482
|
+
for (const name of tableNames) await this.dropTableByName(name);
|
|
3339
3483
|
}
|
|
3340
3484
|
/**
|
|
3341
3485
|
* Whether the table has at least one row. Schema sync uses it in the
|
|
@@ -3447,7 +3591,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
3447
3591
|
const physicalToLogical = /* @__PURE__ */ new Map();
|
|
3448
3592
|
const physicalFields = [];
|
|
3449
3593
|
for (const logical of neededLogical) {
|
|
3450
|
-
const physical = meta.
|
|
3594
|
+
const physical = meta.physicalPath(logical);
|
|
3451
3595
|
physicalFields.push(physical);
|
|
3452
3596
|
physicalToLogical.set(physical, logical);
|
|
3453
3597
|
}
|
|
@@ -3915,19 +4059,6 @@ var RemoveGuardContext = class {
|
|
|
3915
4059
|
return this._pending;
|
|
3916
4060
|
}
|
|
3917
4061
|
};
|
|
3918
|
-
/** Translates a single ops record from logical to physical column names. */
|
|
3919
|
-
function _translateOpsRecord(rec, meta) {
|
|
3920
|
-
const out = {};
|
|
3921
|
-
for (const key in rec) out[meta.leafByLogical.get(key)?.physicalName ?? key] = rec[key];
|
|
3922
|
-
return out;
|
|
3923
|
-
}
|
|
3924
|
-
/** Translates ops keys from logical field names to physical column names. */
|
|
3925
|
-
function _translateOpsKeys(ops, meta) {
|
|
3926
|
-
return {
|
|
3927
|
-
inc: ops.inc ? _translateOpsRecord(ops.inc, meta) : void 0,
|
|
3928
|
-
mul: ops.mul ? _translateOpsRecord(ops.mul, meta) : void 0
|
|
3929
|
-
};
|
|
3930
|
-
}
|
|
3931
4062
|
/** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
|
|
3932
4063
|
const TOUCH_MANY_CHUNK = 500;
|
|
3933
4064
|
/** `touchMany` input rejection — always `INVALID_QUERY`, path names the key. */
|
|
@@ -4188,13 +4319,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4188
4319
|
let result;
|
|
4189
4320
|
if (this.adapter.supportsNativePatch()) {
|
|
4190
4321
|
const ops = separateFieldOps(data);
|
|
4191
|
-
const translatedOps = ops ?
|
|
4322
|
+
const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
|
|
4192
4323
|
const translatedData = this._fieldMapper.translatePatchKeys(data, this._meta);
|
|
4193
4324
|
result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion);
|
|
4194
4325
|
} else {
|
|
4195
4326
|
const update = decomposePatch(data, this);
|
|
4196
4327
|
const ops = separateFieldOps(update);
|
|
4197
|
-
const translatedOps = ops ?
|
|
4328
|
+
const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
|
|
4198
4329
|
const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
|
|
4199
4330
|
if (getArrayOpsFields(translatedUpdate).size > 0) {
|
|
4200
4331
|
const resolved = resolveArrayOps(translatedUpdate, await this.adapter.findOne({
|
|
@@ -4333,7 +4464,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4333
4464
|
await this._encryptItems([dataCopy], "patch");
|
|
4334
4465
|
const update = decomposePatch(dataCopy, this);
|
|
4335
4466
|
const ops = separateFieldOps(update);
|
|
4336
|
-
const translatedOps = ops ?
|
|
4467
|
+
const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
|
|
4337
4468
|
const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
|
|
4338
4469
|
const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
|
|
4339
4470
|
if (translatedOps === void 0 && isEmptyObject(translatedUpdate)) return {
|
|
@@ -4607,7 +4738,154 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4607
4738
|
}
|
|
4608
4739
|
};
|
|
4609
4740
|
//#endregion
|
|
4741
|
+
//#region src/table/view-source.ts
|
|
4742
|
+
const indexCache = /* @__PURE__ */ new WeakMap();
|
|
4743
|
+
function sourceIndex(type) {
|
|
4744
|
+
let idx = indexCache.get(type);
|
|
4745
|
+
if (idx) return idx;
|
|
4746
|
+
idx = {
|
|
4747
|
+
flatMap: /* @__PURE__ */ new Map(),
|
|
4748
|
+
columnMap: /* @__PURE__ */ new Map(),
|
|
4749
|
+
documentColumnMap: /* @__PURE__ */ new Map(),
|
|
4750
|
+
unstored: /* @__PURE__ */ new Set(),
|
|
4751
|
+
jsonRoots: /* @__PURE__ */ new Set(),
|
|
4752
|
+
encrypted: /* @__PURE__ */ new Set(),
|
|
4753
|
+
optional: /* @__PURE__ */ new Set()
|
|
4754
|
+
};
|
|
4755
|
+
if (type.type.kind === "object") {
|
|
4756
|
+
const navFields = /* @__PURE__ */ new Set();
|
|
4757
|
+
const collected = [];
|
|
4758
|
+
idx.flatMap = flattenAnnotatedType(type, {
|
|
4759
|
+
excludePhantomTypes: true,
|
|
4760
|
+
onField: (path, fieldType, metadata) => {
|
|
4761
|
+
if (isNavRelation(metadata)) navFields.add(path);
|
|
4762
|
+
collected.push([
|
|
4763
|
+
path,
|
|
4764
|
+
fieldType,
|
|
4765
|
+
metadata
|
|
4766
|
+
]);
|
|
4767
|
+
}
|
|
4768
|
+
});
|
|
4769
|
+
const storage = [];
|
|
4770
|
+
for (const [path, fieldType, metadata] of collected) {
|
|
4771
|
+
if (!path || findAncestorInSet(path, navFields) !== void 0) continue;
|
|
4772
|
+
const column = metadata.get("db.column");
|
|
4773
|
+
if (column) {
|
|
4774
|
+
idx.columnMap.set(path, column);
|
|
4775
|
+
if (columnOverrideApplies(path, true)) idx.documentColumnMap.set(path, column);
|
|
4776
|
+
}
|
|
4777
|
+
if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
|
|
4778
|
+
if (metadata.has("db.encrypted")) storage.push([path, false]);
|
|
4779
|
+
else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
|
|
4780
|
+
}
|
|
4781
|
+
storage.sort(([a], [b]) => a.split(".").length - b.split(".").length);
|
|
4782
|
+
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);
|
|
4783
|
+
for (const [path, node] of idx.flatMap) if (path && node.optional) idx.optional.add(path);
|
|
4784
|
+
}
|
|
4785
|
+
indexCache.set(type, idx);
|
|
4786
|
+
return idx;
|
|
4787
|
+
}
|
|
4788
|
+
/** `path` itself if it is in `set`, else its nearest ancestor in `set`. */
|
|
4789
|
+
function selfOrAncestor(path, set) {
|
|
4790
|
+
return set.has(path) ? path : findAncestorInSet(path, set);
|
|
4791
|
+
}
|
|
4792
|
+
/**
|
|
4793
|
+
* Resolves a LOGICAL path of a source table (a view field's chain ref, an
|
|
4794
|
+
* aggregate's field, a predicate operand) to where it is physically stored.
|
|
4795
|
+
* Internal — `AtscriptDbView.resolveRefSource` is the public entry.
|
|
4796
|
+
*
|
|
4797
|
+
* Relational rules (`TableMetadata`'s): the outermost `@db.json` or array
|
|
4798
|
+
* node with segments remaining is the column and the rest becomes
|
|
4799
|
+
* {@link TViewSource.jsonPath}; a flattened leaf is its parent segments
|
|
4800
|
+
* joined with `__` plus its `@db.column` (or segment); a top-level field is
|
|
4801
|
+
* its `@db.column` or name. Nested-object adapters use the document path
|
|
4802
|
+
* (only a top-level key is renamed) and never a JSON path.
|
|
4803
|
+
*
|
|
4804
|
+
* A path the type does not declare resolves to itself — the database
|
|
4805
|
+
* reports the unknown column, as before.
|
|
4806
|
+
* @throws for a path without storage (`@db.ignore`, a navigation relation,
|
|
4807
|
+
* or inside one), and — relational — for a path inside an `@db.encrypted` field.
|
|
4808
|
+
*/
|
|
4809
|
+
function resolveViewSource(sourceType, logicalPath, nestedObjects) {
|
|
4810
|
+
const idx = sourceIndex(sourceType);
|
|
4811
|
+
const node = idx.flatMap.get(logicalPath);
|
|
4812
|
+
const jsonRoot = selfOrAncestor(logicalPath, idx.jsonRoots);
|
|
4813
|
+
const optional = selfOrAncestor(logicalPath, idx.optional) !== void 0 || jsonRoot !== void 0 && jsonRoot !== logicalPath;
|
|
4814
|
+
if (!node) return {
|
|
4815
|
+
column: logicalPath,
|
|
4816
|
+
designType: "unknown",
|
|
4817
|
+
optional
|
|
4818
|
+
};
|
|
4819
|
+
const unstored = selfOrAncestor(logicalPath, idx.unstored);
|
|
4820
|
+
if (unstored !== void 0) throw new Error(`"${logicalPath}" has no column — "${unstored}" is @db.ignore or a navigation relation`);
|
|
4821
|
+
const designType = resolveDesignType(node);
|
|
4822
|
+
if (nestedObjects) return {
|
|
4823
|
+
column: documentPath(idx.documentColumnMap, logicalPath),
|
|
4824
|
+
designType,
|
|
4825
|
+
optional
|
|
4826
|
+
};
|
|
4827
|
+
const encrypted = findAncestorInSet(logicalPath, idx.encrypted);
|
|
4828
|
+
if (encrypted !== void 0) throw new Error(`"${logicalPath}" is inside the @db.encrypted field "${encrypted}" — reference "${encrypted}" itself`);
|
|
4829
|
+
if (jsonRoot !== void 0) {
|
|
4830
|
+
const column = relationalColumnName(jsonRoot, idx.columnMap.get(jsonRoot), jsonRoot.includes("."));
|
|
4831
|
+
return jsonRoot === logicalPath ? {
|
|
4832
|
+
column,
|
|
4833
|
+
designType,
|
|
4834
|
+
optional
|
|
4835
|
+
} : {
|
|
4836
|
+
column,
|
|
4837
|
+
jsonPath: logicalPath.slice(jsonRoot.length + 1).split("."),
|
|
4838
|
+
designType,
|
|
4839
|
+
optional
|
|
4840
|
+
};
|
|
4841
|
+
}
|
|
4842
|
+
if (designType === "object" && !idx.encrypted.has(logicalPath)) return {
|
|
4843
|
+
column: logicalPath.replace(/\./g, "__"),
|
|
4844
|
+
designType,
|
|
4845
|
+
flattened: true,
|
|
4846
|
+
optional
|
|
4847
|
+
};
|
|
4848
|
+
return {
|
|
4849
|
+
column: relationalColumnName(logicalPath, idx.columnMap.get(logicalPath), logicalPath.includes(".")),
|
|
4850
|
+
designType,
|
|
4851
|
+
optional
|
|
4852
|
+
};
|
|
4853
|
+
}
|
|
4854
|
+
//#endregion
|
|
4610
4855
|
//#region src/table/db-view.ts
|
|
4856
|
+
/** The `@db.agg.*` annotations, one per supported aggregate function. */
|
|
4857
|
+
const AGG_KEYS = SUPPORTED_AGGREGATE_FNS.map((fn) => `db.agg.${fn}`);
|
|
4858
|
+
/**
|
|
4859
|
+
* Reads a view field's `@db.agg.*` annotation. The compiled value is
|
|
4860
|
+
* `{ field?, condition? }`; models compiled by older versions carry `true`
|
|
4861
|
+
* (bare `@db.agg.count`) or a string (the field), which are read the same way.
|
|
4862
|
+
* A missing field or `true` is `'*'` (COUNT(*)).
|
|
4863
|
+
*/
|
|
4864
|
+
function readViewAgg(metadata) {
|
|
4865
|
+
for (const key of AGG_KEYS) {
|
|
4866
|
+
const val = metadata?.get(key);
|
|
4867
|
+
if (val === void 0) continue;
|
|
4868
|
+
const aggFn = key.slice(7);
|
|
4869
|
+
if (typeof val === "string") return {
|
|
4870
|
+
aggFn,
|
|
4871
|
+
aggField: val
|
|
4872
|
+
};
|
|
4873
|
+
if (val === null || typeof val !== "object") return {
|
|
4874
|
+
aggFn,
|
|
4875
|
+
aggField: "*"
|
|
4876
|
+
};
|
|
4877
|
+
return {
|
|
4878
|
+
aggFn,
|
|
4879
|
+
aggField: val.field ?? "*",
|
|
4880
|
+
...val.condition ? { aggFilter: val.condition } : {}
|
|
4881
|
+
};
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
const JSON_LEAF_TYPES = new Set([
|
|
4885
|
+
"string",
|
|
4886
|
+
"number",
|
|
4887
|
+
"boolean"
|
|
4888
|
+
]);
|
|
4611
4889
|
/**
|
|
4612
4890
|
* Database view abstraction driven by Atscript `@db.view.*` annotations.
|
|
4613
4891
|
*
|
|
@@ -4623,6 +4901,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
4623
4901
|
*/
|
|
4624
4902
|
var AtscriptDbView = class extends AtscriptDbReadable {
|
|
4625
4903
|
_viewPlan;
|
|
4904
|
+
_columnMappings;
|
|
4626
4905
|
get isView() {
|
|
4627
4906
|
return true;
|
|
4628
4907
|
}
|
|
@@ -4648,19 +4927,18 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
4648
4927
|
const metadata = this._type.metadata;
|
|
4649
4928
|
const forRef = metadata.get("db.view.for");
|
|
4650
4929
|
const entryType = typeof forRef === "function" ? forRef : forRef.type;
|
|
4651
|
-
const
|
|
4652
|
-
const entryTable = entryTypeResolved?.metadata?.get("db.table") || entryTypeResolved?.id || "";
|
|
4930
|
+
const entryTable = tableNameOf(entryType());
|
|
4653
4931
|
const rawJoins = metadata.get("db.view.joins");
|
|
4654
4932
|
const joins = [];
|
|
4655
4933
|
if (rawJoins) for (const join of rawJoins) {
|
|
4656
4934
|
const targetRef = join.target;
|
|
4657
4935
|
const targetType = typeof targetRef === "function" ? targetRef : targetRef.type;
|
|
4658
4936
|
const targetTypeResolved = targetType();
|
|
4659
|
-
const targetTable = targetTypeResolved?.metadata?.get("db.table") || targetTypeResolved?.id || "";
|
|
4660
4937
|
joins.push({
|
|
4661
4938
|
targetType,
|
|
4662
|
-
targetTable,
|
|
4663
|
-
condition: join.condition
|
|
4939
|
+
targetTable: tableNameOf(targetTypeResolved),
|
|
4940
|
+
condition: join.condition,
|
|
4941
|
+
kind: join.kind === "left" ? "left" : "inner"
|
|
4664
4942
|
});
|
|
4665
4943
|
}
|
|
4666
4944
|
const filter = metadata.get("db.view.filter");
|
|
@@ -4676,73 +4954,143 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
4676
4954
|
};
|
|
4677
4955
|
return this._viewPlan;
|
|
4678
4956
|
}
|
|
4957
|
+
/** Whether the adapter stores nested objects natively (document paths, no JSON columns). */
|
|
4958
|
+
get _nested() {
|
|
4959
|
+
return this.adapter.supportsNestedObjects();
|
|
4960
|
+
}
|
|
4679
4961
|
/**
|
|
4680
|
-
* Resolves a query field ref
|
|
4962
|
+
* Resolves a view query field ref (join condition, `@db.view.filter`,
|
|
4963
|
+
* conditional-aggregate predicate) to its table name and PHYSICAL source on
|
|
4964
|
+
* this view's adapter — the column (or document path) with `TableMetadata`'s
|
|
4965
|
+
* layout rules, the path inside a JSON column, and whether the value may be
|
|
4966
|
+
* absent. An unqualified ref resolves against the entry table.
|
|
4967
|
+
* @throws for a ref without storage (`@db.ignore`, navigation relation) or
|
|
4968
|
+
* inside an `@db.encrypted` field (relational adapters).
|
|
4969
|
+
* @since 0.1.136
|
|
4970
|
+
*/
|
|
4971
|
+
resolveRefSource(ref) {
|
|
4972
|
+
const type = ref.type ? ref.type() : this.viewPlan.entryType();
|
|
4973
|
+
return {
|
|
4974
|
+
table: tableNameOf(type),
|
|
4975
|
+
source: resolveViewSource(type, ref.field, this._nested)
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
/**
|
|
4979
|
+
* Resolves a query field ref (join condition, `@db.view.filter`) to a
|
|
4980
|
+
* quoted `table.column` SQL fragment — the PHYSICAL column (flattened
|
|
4981
|
+
* `__` name, `@db.column` rename). An unqualified ref
|
|
4982
|
+
* resolves against the entry table.
|
|
4681
4983
|
*
|
|
4682
4984
|
* @param ref - The field reference from the query tree.
|
|
4683
4985
|
* @param qi - Identifier quoting function (e.g. backtick for MySQL, double-quote for SQLite).
|
|
4684
4986
|
* Defaults to double-quote wrapping for backwards compatibility.
|
|
4987
|
+
* @throws when the ref reads inside a JSON column (not supported in view conditions).
|
|
4685
4988
|
*/
|
|
4686
4989
|
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)}`;
|
|
4990
|
+
const { table, source } = this.resolveRefSource(ref);
|
|
4991
|
+
if (source.jsonPath) throw new Error(`View "${this.tableName}": "${ref.field}" reads inside a JSON column — JSON paths are not supported in view conditions`);
|
|
4992
|
+
return `${qi(table)}.${qi(source.column)}`;
|
|
4693
4993
|
}
|
|
4694
4994
|
/**
|
|
4695
|
-
* Maps each view
|
|
4696
|
-
*
|
|
4995
|
+
* Maps each view column to its source table and PHYSICAL source column.
|
|
4996
|
+
*
|
|
4997
|
+
* View fields resolve through their chain ref; fields without a ref read
|
|
4998
|
+
* the entry table under the same name; aggregates read their `@db.agg.*`
|
|
4999
|
+
* field from the entry table (or their ref). Source names are
|
|
5000
|
+
* physical (flattened `__` names, `@db.column`, document paths), `viewColumn`
|
|
5001
|
+
* is the view's own physical name, an object field whose source is a
|
|
5002
|
+
* flattened object expands to one mapping per leaf, and a primitive leaf
|
|
5003
|
+
* inside a JSON column carries `json` (rendered by adapters that support
|
|
5004
|
+
* JSON extraction).
|
|
5005
|
+
*
|
|
5006
|
+
* Computed once per view (the plan and the type are immutable).
|
|
5007
|
+
*
|
|
5008
|
+
* @throws for an object field over a JSON column without `@db.json` on the
|
|
5009
|
+
* view field, a JSON leaf that is not a string / number / boolean, or an
|
|
5010
|
+
* aggregate other than `count` over `'*'`.
|
|
4697
5011
|
*/
|
|
4698
5012
|
getViewColumnMappings() {
|
|
5013
|
+
this._columnMappings ??= this._buildColumnMappings();
|
|
5014
|
+
return this._columnMappings;
|
|
5015
|
+
}
|
|
5016
|
+
_buildColumnMappings() {
|
|
4699
5017
|
const plan = this.viewPlan;
|
|
4700
5018
|
const mappings = [];
|
|
4701
5019
|
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
5020
|
const ignored = this.ignoredFields;
|
|
5021
|
+
const meta = this.getMetadata();
|
|
5022
|
+
const nested = this._nested;
|
|
5023
|
+
const viewName = (path) => nested ? meta.physicalPath(path) : meta.pathToPhysical.get(path);
|
|
5024
|
+
const fail = (field, message) => {
|
|
5025
|
+
throw new Error(`View "${this.tableName}" field "${field}": ${message}`);
|
|
5026
|
+
};
|
|
5027
|
+
const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.targetTable));
|
|
4710
5028
|
for (const [fieldName, fieldType] of this._type.type.props.entries()) {
|
|
4711
5029
|
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
|
-
}
|
|
5030
|
+
const agg = readViewAgg(fieldType.metadata);
|
|
5031
|
+
const aggField = agg?.aggField;
|
|
5032
|
+
if (aggField === "*" && agg?.aggFn !== "count") fail(fieldName, `aggregate "${agg?.aggFn}" needs a field — only count accepts *`);
|
|
5033
|
+
let sourceType;
|
|
5034
|
+
let sourcePath;
|
|
4722
5035
|
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
|
-
});
|
|
5036
|
+
sourceType = fieldType.ref.type();
|
|
5037
|
+
sourcePath = fieldType.ref.field || fieldName;
|
|
4733
5038
|
} else {
|
|
4734
|
-
|
|
5039
|
+
sourceType = plan.entryType();
|
|
5040
|
+
sourcePath = aggField && aggField !== "*" ? aggField : fieldName;
|
|
5041
|
+
}
|
|
5042
|
+
const sourceTable = tableNameOf(sourceType);
|
|
5043
|
+
const joinNullable = leftJoined.has(sourceTable);
|
|
5044
|
+
if (aggField === "*" && !fieldType.ref) {
|
|
4735
5045
|
mappings.push({
|
|
4736
|
-
viewColumn: fieldName,
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
5046
|
+
viewColumn: viewName(fieldName) ?? fieldName,
|
|
5047
|
+
viewPath: fieldName,
|
|
5048
|
+
sourceTable,
|
|
5049
|
+
sourceColumn: "*",
|
|
5050
|
+
...agg
|
|
4741
5051
|
});
|
|
5052
|
+
continue;
|
|
5053
|
+
}
|
|
5054
|
+
const source = resolveViewSource(sourceType, sourcePath, nested);
|
|
5055
|
+
const ownColumn = viewName(fieldName);
|
|
5056
|
+
if (ownColumn === void 0 && !nested) {
|
|
5057
|
+
if (!source.flattened) fail(fieldName, "source is a JSON column — add @db.json to the view field");
|
|
5058
|
+
const prefix = `${fieldName}.`;
|
|
5059
|
+
for (const [viewPath, viewColumn] of meta.pathToPhysical) {
|
|
5060
|
+
if (!viewPath.startsWith(prefix) || ignored.has(viewPath)) continue;
|
|
5061
|
+
const leafPath = `${sourcePath}.${viewPath.slice(prefix.length)}`;
|
|
5062
|
+
const leaf = resolveViewSource(sourceType, leafPath, nested);
|
|
5063
|
+
mappings.push(this._leafMapping(viewPath, viewColumn, sourceTable, leaf, joinNullable));
|
|
5064
|
+
}
|
|
5065
|
+
continue;
|
|
4742
5066
|
}
|
|
5067
|
+
if (source.flattened) fail(fieldName, "source is a flattened object — remove @db.json from the view field");
|
|
5068
|
+
const mapping = this._leafMapping(fieldName, ownColumn ?? fieldName, sourceTable, source, joinNullable);
|
|
5069
|
+
mappings.push(agg ? {
|
|
5070
|
+
...mapping,
|
|
5071
|
+
...agg
|
|
5072
|
+
} : mapping);
|
|
4743
5073
|
}
|
|
4744
5074
|
return mappings;
|
|
4745
5075
|
}
|
|
5076
|
+
/** One view column over one physical source (a column or a JSON leaf). */
|
|
5077
|
+
_leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable) {
|
|
5078
|
+
const mapping = {
|
|
5079
|
+
viewColumn,
|
|
5080
|
+
viewPath,
|
|
5081
|
+
sourceTable,
|
|
5082
|
+
sourceColumn: source.column
|
|
5083
|
+
};
|
|
5084
|
+
if (joinNullable || source.optional || source.designType === "unknown") mapping.nullable = true;
|
|
5085
|
+
if (source.jsonPath) {
|
|
5086
|
+
if (!JSON_LEAF_TYPES.has(source.designType)) throw new Error(`View "${this.tableName}" field "${viewPath}": JSON extraction supports string, number and boolean leaves only`);
|
|
5087
|
+
mapping.json = {
|
|
5088
|
+
path: source.jsonPath,
|
|
5089
|
+
type: source.designType
|
|
5090
|
+
};
|
|
5091
|
+
}
|
|
5092
|
+
return mapping;
|
|
5093
|
+
}
|
|
4746
5094
|
};
|
|
4747
5095
|
/**
|
|
4748
5096
|
* Structural type guard for views: `true` when the readable reports
|
|
@@ -4757,4 +5105,326 @@ function isAtscriptDbView(readable) {
|
|
|
4757
5105
|
return readable.isView;
|
|
4758
5106
|
}
|
|
4759
5107
|
//#endregion
|
|
4760
|
-
|
|
5108
|
+
//#region src/schema/schema-hash.ts
|
|
5109
|
+
/** Extracts sorted field snapshots from a readable's field descriptors. */
|
|
5110
|
+
function extractFieldSnapshots(fields, typeMapper) {
|
|
5111
|
+
return fields.filter((f) => !f.ignored).map((f) => {
|
|
5112
|
+
const snap = {
|
|
5113
|
+
physicalName: f.physicalName,
|
|
5114
|
+
designType: f.designType,
|
|
5115
|
+
optional: f.optional,
|
|
5116
|
+
isPrimaryKey: f.isPrimaryKey,
|
|
5117
|
+
storage: f.storage
|
|
5118
|
+
};
|
|
5119
|
+
if (f.defaultValue) snap.defaultValue = f.defaultValue;
|
|
5120
|
+
if (typeMapper) snap.mappedType = typeMapper(f);
|
|
5121
|
+
if (f.encrypted) snap.encrypted = true;
|
|
5122
|
+
return snap;
|
|
5123
|
+
}).toSorted((a, b) => a.physicalName.localeCompare(b.physicalName));
|
|
5124
|
+
}
|
|
5125
|
+
/**
|
|
5126
|
+
* Extracts a canonical, serializable snapshot from a readable's metadata.
|
|
5127
|
+
* Sorted deterministically so the hash is stable across runs.
|
|
5128
|
+
*
|
|
5129
|
+
* @param readable - The table/view readable.
|
|
5130
|
+
* @param typeMapper - Optional adapter-specific type mapper. When provided,
|
|
5131
|
+
* each field's mapped type (e.g., "VARCHAR(255)") is stored in the snapshot
|
|
5132
|
+
* for precise type change detection.
|
|
5133
|
+
*/
|
|
5134
|
+
function computeTableSnapshot(readable, typeMapper, tableOptions) {
|
|
5135
|
+
const fields = extractFieldSnapshots(readable.fieldDescriptors, typeMapper);
|
|
5136
|
+
const indexes = [...readable.indexes.values()].map((idx) => ({
|
|
5137
|
+
key: idx.key,
|
|
5138
|
+
type: idx.type,
|
|
5139
|
+
fields: idx.fields.map((f) => ({
|
|
5140
|
+
name: f.name,
|
|
5141
|
+
sort: f.sort
|
|
5142
|
+
}))
|
|
5143
|
+
})).toSorted((a, b) => a.key.localeCompare(b.key));
|
|
5144
|
+
const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
|
|
5145
|
+
fields: [...fk.fields].toSorted(),
|
|
5146
|
+
targetTable: fk.targetTable,
|
|
5147
|
+
targetFields: [...fk.targetFields].toSorted(),
|
|
5148
|
+
onDelete: fk.onDelete,
|
|
5149
|
+
onUpdate: fk.onUpdate
|
|
5150
|
+
})).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
|
|
5151
|
+
const snapshot = {
|
|
5152
|
+
tableName: readable.tableName,
|
|
5153
|
+
fields,
|
|
5154
|
+
indexes,
|
|
5155
|
+
foreignKeys
|
|
5156
|
+
};
|
|
5157
|
+
if (tableOptions?.length) snapshot.tableOptions = [...tableOptions].toSorted((a, b) => a.key.localeCompare(b.key));
|
|
5158
|
+
return snapshot;
|
|
5159
|
+
}
|
|
5160
|
+
/**
|
|
5161
|
+
* Extracts a canonical, serializable snapshot from a view's metadata.
|
|
5162
|
+
* Captures view plan (entry table, joins, filter, materialization) for
|
|
5163
|
+
* detecting view definition changes.
|
|
5164
|
+
*/
|
|
5165
|
+
function computeViewSnapshot(view) {
|
|
5166
|
+
const fields = extractFieldSnapshots(view.fieldDescriptors);
|
|
5167
|
+
if (view.isExternal) return {
|
|
5168
|
+
tableName: view.tableName,
|
|
5169
|
+
viewType: "E",
|
|
5170
|
+
fields
|
|
5171
|
+
};
|
|
5172
|
+
const plan = view.viewPlan;
|
|
5173
|
+
const qualify = (ref) => view.resolveFieldRef(ref, (n) => n);
|
|
5174
|
+
const canonical = (node) => JSON.stringify(canonicalizeQueryNode(node, qualify));
|
|
5175
|
+
const columns = view.getViewColumnMappings().map((m) => {
|
|
5176
|
+
const col = {
|
|
5177
|
+
column: m.viewColumn,
|
|
5178
|
+
sourceTable: m.sourceTable,
|
|
5179
|
+
sourceColumn: m.sourceColumn
|
|
5180
|
+
};
|
|
5181
|
+
if (m.json) {
|
|
5182
|
+
col.jsonPath = JSON.stringify(m.json.path);
|
|
5183
|
+
col.jsonType = m.json.type;
|
|
5184
|
+
}
|
|
5185
|
+
if (m.aggFn) col.aggFn = m.aggFn;
|
|
5186
|
+
if (m.aggField) col.aggField = m.aggField;
|
|
5187
|
+
if (m.aggFilter) col.aggFilter = canonical(m.aggFilter);
|
|
5188
|
+
return col;
|
|
5189
|
+
}).toSorted((a, b) => a.column < b.column ? -1 : a.column > b.column ? 1 : 0);
|
|
5190
|
+
const result = {
|
|
5191
|
+
tableName: view.tableName,
|
|
5192
|
+
viewType: plan.materialized ? "M" : "V",
|
|
5193
|
+
entryTable: plan.entryTable,
|
|
5194
|
+
joinTables: plan.joins.map((j) => {
|
|
5195
|
+
const join = {
|
|
5196
|
+
targetTable: j.targetTable,
|
|
5197
|
+
condition: canonical(j.condition)
|
|
5198
|
+
};
|
|
5199
|
+
if (j.kind === "left") join.kind = "left";
|
|
5200
|
+
return join;
|
|
5201
|
+
}),
|
|
5202
|
+
columns
|
|
5203
|
+
};
|
|
5204
|
+
if (plan.filter) result.filterHash = fnv1a(canonical(plan.filter));
|
|
5205
|
+
if (plan.having) result.havingHash = fnv1a(canonical(plan.having));
|
|
5206
|
+
if (plan.materialized) result.materialized = true;
|
|
5207
|
+
const renderRevision = view.dbAdapter.viewRenderRevision();
|
|
5208
|
+
if (renderRevision !== void 0) result.renderRevision = renderRevision;
|
|
5209
|
+
return {
|
|
5210
|
+
...result,
|
|
5211
|
+
fields
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
5214
|
+
/**
|
|
5215
|
+
* Converts a view predicate (join condition, `@db.view.filter`,
|
|
5216
|
+
* `@db.view.having`) into a serializable structure whose JSON is a stable
|
|
5217
|
+
* function of its MEANING: field refs become `qualify(ref)` — the view's
|
|
5218
|
+
* `resolveFieldRef(ref, (n) => n)`, i.e. `"<table>.<field>"`, so a predicate
|
|
5219
|
+
* retargeted to another table with the same field name changes — operators
|
|
5220
|
+
* and literal values are kept as-is, `$and`/`$or` keep declaration order, and
|
|
5221
|
+
* no function references survive. Two identical models produce byte-identical
|
|
5222
|
+
* JSON.
|
|
5223
|
+
* @since 0.1.128
|
|
5224
|
+
*/
|
|
5225
|
+
function canonicalizeQueryNode(node, qualify) {
|
|
5226
|
+
if ("$and" in node) return { and: node.$and.map((n) => canonicalizeQueryNode(n, qualify)) };
|
|
5227
|
+
if ("$or" in node) return { or: node.$or.map((n) => canonicalizeQueryNode(n, qualify)) };
|
|
5228
|
+
if ("$not" in node) return { not: canonicalizeQueryNode(node.$not, qualify) };
|
|
5229
|
+
const comp = node;
|
|
5230
|
+
const out = {
|
|
5231
|
+
l: qualify(comp.left),
|
|
5232
|
+
op: comp.op
|
|
5233
|
+
};
|
|
5234
|
+
if (comp.right !== void 0) out.r = comp.right !== null && typeof comp.right === "object" && "field" in comp.right ? { f: qualify(comp.right) } : comp.right;
|
|
5235
|
+
return out;
|
|
5236
|
+
}
|
|
5237
|
+
/**
|
|
5238
|
+
* Computes a deterministic hash string from multiple table snapshots.
|
|
5239
|
+
* Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
|
|
5240
|
+
*/
|
|
5241
|
+
function computeSchemaHash(snapshots) {
|
|
5242
|
+
const sorted = [...snapshots].toSorted((a, b) => a.tableName.localeCompare(b.tableName));
|
|
5243
|
+
return fnv1a(JSON.stringify(sorted));
|
|
5244
|
+
}
|
|
5245
|
+
/**
|
|
5246
|
+
* Computes a hash for a single table/view snapshot.
|
|
5247
|
+
* Used for per-table change detection via stored snapshots.
|
|
5248
|
+
*/
|
|
5249
|
+
function computeTableHash(snapshot) {
|
|
5250
|
+
return fnv1a(JSON.stringify(snapshot));
|
|
5251
|
+
}
|
|
5252
|
+
/**
|
|
5253
|
+
* Converts stored snapshot fields to `TExistingColumn[]` format
|
|
5254
|
+
* for use with `computeColumnDiff`. Used by adapters that lack
|
|
5255
|
+
* native column introspection (e.g., MongoDB).
|
|
5256
|
+
*
|
|
5257
|
+
* The `type` field uses `mappedType` when available (adapter-specific),
|
|
5258
|
+
* falling back to `designType`.
|
|
5259
|
+
*/
|
|
5260
|
+
function snapshotToExistingColumns(snapshot) {
|
|
5261
|
+
return snapshot.fields.map((f) => ({
|
|
5262
|
+
name: f.physicalName,
|
|
5263
|
+
type: f.mappedType ?? f.designType,
|
|
5264
|
+
notnull: !f.optional,
|
|
5265
|
+
pk: f.isPrimaryKey,
|
|
5266
|
+
dflt_value: serializeDefaultValue(f.defaultValue)
|
|
5267
|
+
}));
|
|
5268
|
+
}
|
|
5269
|
+
/**
|
|
5270
|
+
* Extracts table options from a stored snapshot for diff comparison.
|
|
5271
|
+
* Used as fallback when an adapter lacks native table option introspection.
|
|
5272
|
+
*/
|
|
5273
|
+
function snapshotToExistingTableOptions(snapshot) {
|
|
5274
|
+
return snapshot.tableOptions ?? [];
|
|
5275
|
+
}
|
|
5276
|
+
/** Serializes a TDbDefaultValue to a comparable string. */
|
|
5277
|
+
function serializeDefaultValue(dv) {
|
|
5278
|
+
if (!dv) return;
|
|
5279
|
+
if (dv.kind === "value") return dv.value;
|
|
5280
|
+
return `fn:${dv.fn}`;
|
|
5281
|
+
}
|
|
5282
|
+
/** FNV-1a 32-bit hash → hex string */
|
|
5283
|
+
function fnv1a(str) {
|
|
5284
|
+
let hash = 2166136261;
|
|
5285
|
+
for (let i = 0; i < str.length; i++) {
|
|
5286
|
+
hash ^= str.codePointAt(i);
|
|
5287
|
+
hash = Math.imul(hash, 16777619);
|
|
5288
|
+
}
|
|
5289
|
+
return Math.trunc(hash).toString(16).padStart(8, "0");
|
|
5290
|
+
}
|
|
5291
|
+
//#endregion
|
|
5292
|
+
//#region src/schema/fk-diff.ts
|
|
5293
|
+
/** Canonical key for an FK: sorted local field names, comma-joined. */
|
|
5294
|
+
function fkKey(fields) {
|
|
5295
|
+
return [...fields].toSorted().join(",");
|
|
5296
|
+
}
|
|
5297
|
+
/**
|
|
5298
|
+
* Compares desired FK constraints against stored snapshot to detect
|
|
5299
|
+
* additions, removals, and property changes (target table, target fields,
|
|
5300
|
+
* onDelete, onUpdate).
|
|
5301
|
+
*/
|
|
5302
|
+
function computeForeignKeyDiff(desired, existingSnapshot) {
|
|
5303
|
+
const added = [];
|
|
5304
|
+
const removed = [];
|
|
5305
|
+
const changed = [];
|
|
5306
|
+
const existingByKey = /* @__PURE__ */ new Map();
|
|
5307
|
+
for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
|
|
5308
|
+
const desiredKeys = /* @__PURE__ */ new Set();
|
|
5309
|
+
for (const fk of desired.values()) {
|
|
5310
|
+
const key = fkKey(fk.fields);
|
|
5311
|
+
desiredKeys.add(key);
|
|
5312
|
+
const existing = existingByKey.get(key);
|
|
5313
|
+
if (!existing) added.push(fk);
|
|
5314
|
+
else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
5315
|
+
desired: fk,
|
|
5316
|
+
existing
|
|
5317
|
+
});
|
|
5318
|
+
}
|
|
5319
|
+
for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
|
|
5320
|
+
return {
|
|
5321
|
+
added,
|
|
5322
|
+
removed,
|
|
5323
|
+
changed
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
/** Whether the FK diff contains any changes. */
|
|
5327
|
+
function hasForeignKeyChanges(diff) {
|
|
5328
|
+
return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
|
5329
|
+
}
|
|
5330
|
+
function fkPropertiesDiffer(desired, existing) {
|
|
5331
|
+
if (desired.targetTable !== existing.targetTable) return true;
|
|
5332
|
+
if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
|
|
5333
|
+
if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
|
|
5334
|
+
if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
|
|
5335
|
+
return false;
|
|
5336
|
+
}
|
|
5337
|
+
//#endregion
|
|
5338
|
+
//#region src/schema/column-diff.ts
|
|
5339
|
+
/**
|
|
5340
|
+
* Whether a live column's type differs from the type the adapter's
|
|
5341
|
+
* `typeMapper` gives the field — the one rule schema sync diffs column types
|
|
5342
|
+
* by (case-insensitive). Exported for adapters that must agree with it (the
|
|
5343
|
+
* PostgreSQL recreate converts exactly the columns this reports changed).
|
|
5344
|
+
* @since 0.1.137
|
|
5345
|
+
*/
|
|
5346
|
+
function isColumnTypeChanged(existingType, expectedType) {
|
|
5347
|
+
return existingType.toUpperCase() !== expectedType.toUpperCase();
|
|
5348
|
+
}
|
|
5349
|
+
/**
|
|
5350
|
+
* Computes the difference between desired schema fields and existing database columns.
|
|
5351
|
+
*
|
|
5352
|
+
* @param desired - Field descriptors from the Atscript type (after flattening).
|
|
5353
|
+
* @param existing - Columns currently in the database (from introspection).
|
|
5354
|
+
* @param typeMapper - Optional function to map field metadata to DB-native type strings.
|
|
5355
|
+
* Receives the full field meta (design type, annotations, PK status, etc.)
|
|
5356
|
+
* so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
|
|
5357
|
+
* Required for type change detection.
|
|
5358
|
+
*/
|
|
5359
|
+
function computeColumnDiff(desired, existing, typeMapper) {
|
|
5360
|
+
const existingByName = new Map(existing.map((c) => [c.name, c]));
|
|
5361
|
+
const desiredByName = /* @__PURE__ */ new Map();
|
|
5362
|
+
const renamedOldNames = /* @__PURE__ */ new Set();
|
|
5363
|
+
const added = [];
|
|
5364
|
+
const renamed = [];
|
|
5365
|
+
const typeChanged = [];
|
|
5366
|
+
const nullableChanged = [];
|
|
5367
|
+
const defaultChanged = [];
|
|
5368
|
+
const conflicts = [];
|
|
5369
|
+
for (const field of desired) {
|
|
5370
|
+
if (field.ignored) continue;
|
|
5371
|
+
desiredByName.set(field.physicalName, field);
|
|
5372
|
+
const existingCol = existingByName.get(field.physicalName);
|
|
5373
|
+
if (existingCol) if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
5374
|
+
conflicts.push({
|
|
5375
|
+
field,
|
|
5376
|
+
oldName: field.renamedFrom,
|
|
5377
|
+
conflictsWith: field.physicalName
|
|
5378
|
+
});
|
|
5379
|
+
renamedOldNames.add(field.renamedFrom);
|
|
5380
|
+
} else {
|
|
5381
|
+
if (typeMapper) {
|
|
5382
|
+
if (isColumnTypeChanged(existingCol.type, typeMapper(field))) typeChanged.push({
|
|
5383
|
+
field,
|
|
5384
|
+
existingType: existingCol.type
|
|
5385
|
+
});
|
|
5386
|
+
}
|
|
5387
|
+
if (!field.isPrimaryKey && !existingCol.pk) {
|
|
5388
|
+
const desiredNotNull = !field.optional;
|
|
5389
|
+
if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
|
|
5390
|
+
field,
|
|
5391
|
+
wasNullable: !existingCol.notnull
|
|
5392
|
+
});
|
|
5393
|
+
}
|
|
5394
|
+
const desiredDefault = serializeDefaultValue(field.defaultValue);
|
|
5395
|
+
if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
|
|
5396
|
+
field,
|
|
5397
|
+
oldDefault: existingCol.dflt_value,
|
|
5398
|
+
newDefault: desiredDefault
|
|
5399
|
+
});
|
|
5400
|
+
}
|
|
5401
|
+
else if (field.renamedFrom && existingByName.has(field.renamedFrom)) {
|
|
5402
|
+
renamed.push({
|
|
5403
|
+
field,
|
|
5404
|
+
oldName: field.renamedFrom
|
|
5405
|
+
});
|
|
5406
|
+
renamedOldNames.add(field.renamedFrom);
|
|
5407
|
+
} else added.push(field);
|
|
5408
|
+
}
|
|
5409
|
+
const diff = {
|
|
5410
|
+
added,
|
|
5411
|
+
removed: existing.filter((c) => !desiredByName.has(c.name) && !renamedOldNames.has(c.name)),
|
|
5412
|
+
renamed,
|
|
5413
|
+
typeChanged,
|
|
5414
|
+
nullableChanged,
|
|
5415
|
+
defaultChanged,
|
|
5416
|
+
conflicts
|
|
5417
|
+
};
|
|
5418
|
+
if (existing.length > 0) {
|
|
5419
|
+
const newNameByOld = new Map(renamed.map((r) => [r.oldName, r.field.physicalName]));
|
|
5420
|
+
const from = existing.filter((c) => c.pk).map((c) => newNameByOld.get(c.name) ?? c.name);
|
|
5421
|
+
const to = desired.filter((f) => !f.ignored && f.isPrimaryKey).map((f) => f.physicalName);
|
|
5422
|
+
if (fkKey(from) !== fkKey(to)) diff.primaryKeyChanged = {
|
|
5423
|
+
from,
|
|
5424
|
+
to
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
5427
|
+
return diff;
|
|
5428
|
+
}
|
|
5429
|
+
//#endregion
|
|
5430
|
+
export { jsonValueAncestor as $, bucketSourceVerdict as A, narrowerFilterOps as B, NativeIntegrity as C, ENCRYPTED_REASON as D, ADAPTER_FILTER_REASON as E, guardAggregate as F, FieldMappingStrategy as G, unsupportedOperatorMessage as H, guardFilter as I, findAncestorInSet as J, UniquSelect as K, guardPath as L, checkHavingKeys as M, classifyQueryPath as N, acceptedOperatorsHint as O, collectQueryPaths as P, isJsonValueField as Q, guardPaths as R, IntegrityStrategy as S, resolveDesignType as T, RelationalFieldMapper as U, sortFieldNames as V, DocumentFieldMapper as W, isGeoPointType as X, isGeoIndexableType as Y, isBucketableField as Z, decomposePatch as _, hasForeignKeyChanges as a, BaseDbAdapter as b, computeTableHash as c, snapshotToExistingColumns as d, normalizeComputedSelect as et, snapshotToExistingTableOptions as f, assertNoVersionWrites as g, AtscriptDbTable as h, fkKey as i, canFilterLeaf as j, assertGeoPoint as k, computeTableSnapshot as l, isAtscriptDbView as m, isColumnTypeChanged as n, NoopLogger as nt, canonicalizeQueryNode as o, AtscriptDbView as p, TableMetadata as q, computeForeignKeyDiff as r, computeSchemaHash as s, computeColumnDiff as t, resolveCalendarBuckets as tt, computeViewSnapshot as u, ApplicationIntegrity as v, AtscriptDbReadable as w, createFailureCollector as x, ALL_BUCKET_UNITS as y, guardQuery as z };
|