@atscript/db 0.1.146 → 0.1.147
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.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-CfsveE1w.mjs → aggregate-fns-C0BymEqa.mjs} +3 -1
- package/dist/{aggregate-fns-CGBv3E8S.cjs → aggregate-fns-CtsRbZn9.cjs} +8 -0
- package/dist/{buckets-DYFu0eZ8.d.cts → buckets-CjdPipCC.d.cts} +522 -17
- package/dist/{buckets-DRycmhOW.d.mts → buckets-DYopfd2Y.d.mts} +522 -17
- package/dist/{column-diff-e2oHc71_.mjs → column-diff-CO06iQvI.mjs} +862 -115
- package/dist/{column-diff-D_Kyuh0S.cjs → column-diff-Dt7EDJsA.cjs} +872 -113
- package/dist/{column-diff-Q9UmWn5x.d.mts → fk-diff-BtOmL4OW.d.mts} +48 -2
- package/dist/{column-diff-w-Mym_3w.d.cts → fk-diff-DLqwVkKG.d.cts} +48 -2
- package/dist/index.cjs +28 -33
- package/dist/index.d.cts +40 -15
- package/dist/index.d.mts +40 -15
- package/dist/index.mjs +5 -33
- package/dist/{nested-writer-CnOOAehr.mjs → nested-writer-D35UsDBT.mjs} +542 -2
- package/dist/{nested-writer-xfQwxplL.cjs → nested-writer-SqtYJHF0.cjs} +666 -0
- package/dist/plugin.cjs +266 -5
- package/dist/plugin.mjs +267 -6
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-B-0NRKat.d.mts → relation-helpers-C2bLtE-t.d.mts} +1 -1
- package/dist/{relation-helpers-BOMm_HUI.d.cts → relation-helpers-DH3LvwLV.d.cts} +1 -1
- package/dist/relation-loader-BFhVuJ-B.cjs +369 -0
- package/dist/relation-loader-C0qE8xgR.mjs +370 -0
- package/dist/shared.cjs +1 -1
- package/dist/shared.d.cts +4 -3
- package/dist/shared.d.mts +4 -3
- package/dist/shared.mjs +1 -1
- package/dist/sync.cjs +10 -6
- package/dist/sync.d.cts +2 -25
- package/dist/sync.d.mts +2 -25
- package/dist/sync.mjs +10 -6
- package/dist/{validation-utils-DOsB4e6G.cjs → validation-utils-Da2GjobR.cjs} +35 -24
- package/dist/{validation-utils-CMR4fe2M.mjs → validation-utils-Dq0uZ7ef.mjs} +35 -24
- package/dist/{validator-Bw6ks9Hy.d.mts → validator-CMnTI6r7.d.cts} +9 -6
- package/dist/{validator-Bw6ks9Hy.d.cts → validator-CMnTI6r7.d.mts} +9 -6
- package/dist/validator.d.cts +1 -1
- package/dist/validator.d.mts +1 -1
- package/package.json +8 -8
- package/dist/relation-loader-CBPY6kM7.cjs +0 -461
- package/dist/relation-loader-D9XuXaMv.mjs +0 -462
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const require_db_error = require("./db-error-DTkkeu5b.cjs");
|
|
2
|
-
const require_aggregate_fns = require("./aggregate-fns-
|
|
3
|
-
const require_nested_writer = require("./nested-writer-
|
|
2
|
+
const require_aggregate_fns = require("./aggregate-fns-CtsRbZn9.cjs");
|
|
3
|
+
const require_nested_writer = require("./nested-writer-SqtYJHF0.cjs");
|
|
4
4
|
const require_derived_rules = require("./derived-rules-YstgIxG-.cjs");
|
|
5
5
|
const require_object = require("./object-Djg28csK.cjs");
|
|
6
6
|
require("./agg.cjs");
|
|
@@ -471,6 +471,14 @@ var TableMetadata = class {
|
|
|
471
471
|
jsonValueParents = /* @__PURE__ */ new Set();
|
|
472
472
|
/** Every field descriptor's `physicalName` — reserved names a bucket alias may not take. */
|
|
473
473
|
physicalNames = /* @__PURE__ */ new Set();
|
|
474
|
+
/**
|
|
475
|
+
* Resolves / guards relational filter predicates (`{ nav: { $some: … } }`)
|
|
476
|
+
* against the related tables — installed by the owning readable when the
|
|
477
|
+
* table has navigation fields and a table resolver (a `DbSpace`). The field
|
|
478
|
+
* mappers and the path guard reach the related tables through it.
|
|
479
|
+
* @since 0.1.147
|
|
480
|
+
*/
|
|
481
|
+
relationFilters;
|
|
474
482
|
_built = false;
|
|
475
483
|
_identifications;
|
|
476
484
|
_alwaysAddressable;
|
|
@@ -552,7 +560,7 @@ var TableMetadata = class {
|
|
|
552
560
|
if (!this.nestedObjects) this._classifyFields();
|
|
553
561
|
const overrides = adapter.getMetadataOverrides?.(this);
|
|
554
562
|
if (overrides) this._applyOverrides(overrides);
|
|
555
|
-
this._buildFieldDescriptors(adapter);
|
|
563
|
+
this._buildFieldDescriptors(adapter, type);
|
|
556
564
|
this._buildGuardIndexes();
|
|
557
565
|
if (!this.nestedObjects) this._buildLeafIndexes();
|
|
558
566
|
this._buildIdentifications();
|
|
@@ -615,12 +623,15 @@ var TableMetadata = class {
|
|
|
615
623
|
const isArr = fieldType.type.kind === "array";
|
|
616
624
|
const elementType = isArr ? fieldType.type.of : fieldType;
|
|
617
625
|
const resolveTarget = () => elementType?.ref?.type() ?? elementType;
|
|
626
|
+
const relFilter = metadata.get("db.rel.filter");
|
|
618
627
|
this.relations.set(fieldName, {
|
|
619
628
|
direction,
|
|
620
629
|
alias,
|
|
621
630
|
targetType: resolveTarget,
|
|
622
631
|
isArray: isArr,
|
|
623
|
-
...direction === "via" ? { viaType: raw } : {}
|
|
632
|
+
...direction === "via" ? { viaType: raw } : {},
|
|
633
|
+
...metadata.has("db.rel.filterable") ? { filterable: true } : {},
|
|
634
|
+
...relFilter ? { filter: relFilter } : {}
|
|
624
635
|
});
|
|
625
636
|
}
|
|
626
637
|
if (metadata.has("db.rel.FK")) {
|
|
@@ -878,7 +889,7 @@ var TableMetadata = class {
|
|
|
878
889
|
* Called once during build() — everything it needs
|
|
879
890
|
* (flatMap, indexes, columnMap, etc.) is already populated.
|
|
880
891
|
*/
|
|
881
|
-
_buildFieldDescriptors(adapter) {
|
|
892
|
+
_buildFieldDescriptors(adapter, rootType) {
|
|
882
893
|
const descriptors = [];
|
|
883
894
|
const skipFlattening = this.nestedObjects;
|
|
884
895
|
const indexedFields = new Set([...this.primaryKeys, ...this.uniqueProps]);
|
|
@@ -929,10 +940,21 @@ var TableMetadata = class {
|
|
|
929
940
|
unitRefField,
|
|
930
941
|
encrypted: isEncrypted || underEncrypted || void 0,
|
|
931
942
|
isGeoPoint: isGeoPointType(type) || void 0,
|
|
932
|
-
derived: this.derivedFields.get(path)
|
|
943
|
+
derived: this.derivedFields.get(path),
|
|
944
|
+
computed: computedMeta(rootType, path)
|
|
933
945
|
});
|
|
934
946
|
}
|
|
935
|
-
|
|
947
|
+
const flatCache = /* @__PURE__ */ new Map();
|
|
948
|
+
const flatOf = (type) => {
|
|
949
|
+
let flat = flatCache.get(type);
|
|
950
|
+
if (!flat) {
|
|
951
|
+
flat = (0, _atscript_typescript_utils.flattenAnnotatedType)(type);
|
|
952
|
+
flatCache.set(type, flat);
|
|
953
|
+
}
|
|
954
|
+
return flat;
|
|
955
|
+
};
|
|
956
|
+
this._resolveFkTargetFields(descriptors, flatOf);
|
|
957
|
+
this._resolveFkPhysicalFields(flatOf);
|
|
936
958
|
Object.freeze(descriptors);
|
|
937
959
|
this.fieldDescriptors = descriptors;
|
|
938
960
|
this.columnDescriptors = Object.freeze(descriptors.filter((fd) => !fd.ignored && !(skipFlattening && fd.derived)));
|
|
@@ -956,9 +978,37 @@ var TableMetadata = class {
|
|
|
956
978
|
}
|
|
957
979
|
}
|
|
958
980
|
/**
|
|
981
|
+
* Fills `physicalFields` / `physicalTargetFields` on every FK: the local
|
|
982
|
+
* side from this table's path maps, the target side from the referenced
|
|
983
|
+
* type's own `@db.column` renames (same storage rules as this table — a
|
|
984
|
+
* dotted target path is a flattened column on relational storage, a
|
|
985
|
+
* renamed top-level key on document storage).
|
|
986
|
+
*/
|
|
987
|
+
_resolveFkPhysicalFields(flatOf) {
|
|
988
|
+
const targetPhysical = (fk, field) => {
|
|
989
|
+
const targetType = fk.targetTypeRef?.();
|
|
990
|
+
if (!targetType) return field;
|
|
991
|
+
const flat = flatOf(targetType);
|
|
992
|
+
const columnOf = (path) => flat.get(path)?.metadata?.get("db.column");
|
|
993
|
+
if (this.nestedObjects) {
|
|
994
|
+
const dot = field.indexOf(".");
|
|
995
|
+
const top = dot === -1 ? field : field.slice(0, dot);
|
|
996
|
+
const renamed = columnOf(top);
|
|
997
|
+
return renamed === void 0 ? field : renamed + field.slice(top.length);
|
|
998
|
+
}
|
|
999
|
+
return relationalColumnName(field, columnOf(field), field.includes("."));
|
|
1000
|
+
};
|
|
1001
|
+
for (const fk of this.foreignKeys.values()) {
|
|
1002
|
+
fk.physicalFields = fk.fields.map((f) => this.physicalPath(f));
|
|
1003
|
+
fk.physicalTargetFields = fk.targetFields.map((f) => targetPhysical(fk, f));
|
|
1004
|
+
const targetSchema = fk.targetTypeRef?.()?.metadata?.get("db.schema");
|
|
1005
|
+
if (targetSchema) fk.targetSchema = targetSchema;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
959
1009
|
* Resolves `fkTargetField` for FK fields in field descriptors.
|
|
960
1010
|
*/
|
|
961
|
-
_resolveFkTargetFields(descriptors) {
|
|
1011
|
+
_resolveFkTargetFields(descriptors, flatOf) {
|
|
962
1012
|
if (this.foreignKeys.size === 0) return;
|
|
963
1013
|
const fkFieldToTarget = /* @__PURE__ */ new Map();
|
|
964
1014
|
for (const fk of this.foreignKeys.values()) {
|
|
@@ -969,18 +1019,12 @@ var TableMetadata = class {
|
|
|
969
1019
|
});
|
|
970
1020
|
}
|
|
971
1021
|
if (fkFieldToTarget.size === 0) return;
|
|
972
|
-
const flatCache = /* @__PURE__ */ new Map();
|
|
973
1022
|
for (const descriptor of descriptors) {
|
|
974
1023
|
const target = fkFieldToTarget.get(descriptor.path);
|
|
975
1024
|
if (!target) continue;
|
|
976
1025
|
const targetType = target.targetTypeRef();
|
|
977
1026
|
if (!targetType) continue;
|
|
978
|
-
|
|
979
|
-
if (!targetFlatMap) {
|
|
980
|
-
targetFlatMap = (0, _atscript_typescript_utils.flattenAnnotatedType)(targetType);
|
|
981
|
-
flatCache.set(targetType, targetFlatMap);
|
|
982
|
-
}
|
|
983
|
-
const targetFieldType = targetFlatMap.get(target.targetField);
|
|
1027
|
+
const targetFieldType = flatOf(targetType).get(target.targetField);
|
|
984
1028
|
if (!targetFieldType) continue;
|
|
985
1029
|
const targetMetadata = targetFieldType.metadata;
|
|
986
1030
|
if (targetMetadata?.has("db.encrypted")) throw new Error(`FK field "${descriptor.path}" references encrypted field "${target.targetField}" — joins are impossible over ciphertext`);
|
|
@@ -1072,6 +1116,15 @@ var TableMetadata = class {
|
|
|
1072
1116
|
this.preferredId = selected ? [...selected.fields] : [...this.primaryKeys];
|
|
1073
1117
|
}
|
|
1074
1118
|
};
|
|
1119
|
+
/** `TDbFieldMeta.computed` of a top-level `@db.compute` view field (since 0.1.147). */
|
|
1120
|
+
function computedMeta(rootType, path) {
|
|
1121
|
+
if (path.includes(".")) return void 0;
|
|
1122
|
+
const computed = require_nested_writer.computedOperands(rootType, path);
|
|
1123
|
+
return computed ? {
|
|
1124
|
+
operands: Object.freeze(computed.operands),
|
|
1125
|
+
via: Object.freeze(computed.via)
|
|
1126
|
+
} : void 0;
|
|
1127
|
+
}
|
|
1075
1128
|
//#endregion
|
|
1076
1129
|
//#region src/query/uniqu-select.ts
|
|
1077
1130
|
/**
|
|
@@ -1372,19 +1425,43 @@ var FieldMappingStrategy = class {
|
|
|
1372
1425
|
return translated;
|
|
1373
1426
|
}
|
|
1374
1427
|
/**
|
|
1375
|
-
*
|
|
1376
|
-
* (
|
|
1377
|
-
*
|
|
1428
|
+
* Translates a logical filter for the adapter: relational predicates are
|
|
1429
|
+
* resolved first (`resolveRelationFilterTree`), then every key and value
|
|
1430
|
+
* goes through {@link translateResolvedFilter}. `depth` is the predicate
|
|
1431
|
+
* level of `filter` itself (0 for a query's own filter; the related tables
|
|
1432
|
+
* translate predicate operands at deeper levels).
|
|
1433
|
+
*/
|
|
1434
|
+
translateFilter(filter, meta, depth = 0) {
|
|
1435
|
+
const has = require_nested_writer.containsRelationFilter(filter);
|
|
1436
|
+
const resolved = has ? require_nested_writer.resolveRelationFilterTree(filter, meta, depth) : filter;
|
|
1437
|
+
return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* `out` — the translation of the caller's `filter` — with its pre-scan
|
|
1441
|
+
* result (`has`) cached for the adapter's repeated `containsRelationFilter`
|
|
1442
|
+
* checks; only when the core built it (never the caller's own object).
|
|
1443
|
+
*/
|
|
1444
|
+
noteTranslated(filter, out, has) {
|
|
1445
|
+
if (out !== filter) require_nested_writer.noteRelationFilter(out, has);
|
|
1446
|
+
return out;
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Recursively walks a filter expression (predicates already resolved),
|
|
1450
|
+
* applying `@db.column` key renames (document paths —
|
|
1451
|
+
* {@link TableMetadata.documentPath}) and adapter-specific value formatting
|
|
1452
|
+
* via `formatFilterValue`. A resolved predicate passes through under its
|
|
1453
|
+
* navigation-field key.
|
|
1378
1454
|
*
|
|
1379
1455
|
* The relational mapper overrides this to use `leafByLogical` for deeper
|
|
1380
1456
|
* key resolution (flattened nested paths).
|
|
1381
1457
|
*/
|
|
1382
|
-
|
|
1458
|
+
translateResolvedFilter(filter, meta) {
|
|
1383
1459
|
if (!filter || typeof filter !== "object") return filter;
|
|
1384
1460
|
if (!meta.toStorageFormatters && meta.columnMap.size === 0 && meta.derivedFields.size === 0) return filter;
|
|
1385
1461
|
const result = {};
|
|
1386
|
-
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.
|
|
1387
|
-
else if (key === "$not") result[key] = this.
|
|
1462
|
+
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateResolvedFilter(f, meta));
|
|
1463
|
+
else if (key === "$not") result[key] = this.translateResolvedFilter(value, meta);
|
|
1464
|
+
else if (meta.navFields.has(key)) result[key] = value;
|
|
1388
1465
|
else if (key.startsWith("$")) result[key] = value;
|
|
1389
1466
|
else {
|
|
1390
1467
|
const physical = this.physicalPath(key, meta);
|
|
@@ -1686,10 +1763,13 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1686
1763
|
return result;
|
|
1687
1764
|
}
|
|
1688
1765
|
translateQuery(query, meta) {
|
|
1766
|
+
const logical = query.filter;
|
|
1767
|
+
const has = require_nested_writer.containsRelationFilter(logical);
|
|
1768
|
+
const filter = has ? require_nested_writer.resolveRelationFilterTree(logical, meta, 0) : logical;
|
|
1689
1769
|
if (!meta.requiresMappings) {
|
|
1690
1770
|
const controls = query.controls;
|
|
1691
1771
|
return {
|
|
1692
|
-
filter: meta.toStorageFormatters ? this.
|
|
1772
|
+
filter: meta.toStorageFormatters ? this.noteTranslated(logical, this.translateResolvedFilter(filter, meta), has) : filter,
|
|
1693
1773
|
controls: {
|
|
1694
1774
|
...controls,
|
|
1695
1775
|
$with: void 0,
|
|
@@ -1699,7 +1779,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1699
1779
|
};
|
|
1700
1780
|
}
|
|
1701
1781
|
return {
|
|
1702
|
-
filter: this.translateFilterWithRename(
|
|
1782
|
+
filter: this.noteTranslated(logical, this.translateFilterWithRename(filter, meta), has),
|
|
1703
1783
|
controls: query.controls ? this.translateControls(query.controls, meta) : {},
|
|
1704
1784
|
insights: query.insights
|
|
1705
1785
|
};
|
|
@@ -1709,10 +1789,10 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1709
1789
|
return meta.leafByLogical.get(logical)?.physicalName ?? logical;
|
|
1710
1790
|
}
|
|
1711
1791
|
/**
|
|
1712
|
-
* Overrides the base `
|
|
1792
|
+
* Overrides the base `translateResolvedFilter` to use `leafByLogical` for key resolution
|
|
1713
1793
|
* (handles flattened nested paths like `contact.email` → `contact__email`).
|
|
1714
1794
|
*/
|
|
1715
|
-
|
|
1795
|
+
translateResolvedFilter(filter, meta) {
|
|
1716
1796
|
if (!filter || typeof filter !== "object") return filter;
|
|
1717
1797
|
if (!meta.requiresMappings && !meta.toStorageFormatters) return filter;
|
|
1718
1798
|
return this.translateFilterWithRename(filter, meta);
|
|
@@ -1720,13 +1800,16 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1720
1800
|
/**
|
|
1721
1801
|
* Translates filter with key renaming from logical to physical names.
|
|
1722
1802
|
* Used by the relational query path where field paths must be mapped
|
|
1723
|
-
* to `__`-separated column names.
|
|
1803
|
+
* to `__`-separated column names. Relational predicates must already be
|
|
1804
|
+
* resolved (`translateFilter` / `translateQuery` do it); a resolved
|
|
1805
|
+
* predicate passes through under its navigation-field key.
|
|
1724
1806
|
*/
|
|
1725
1807
|
translateFilterWithRename(filter, meta) {
|
|
1726
1808
|
if (!filter || typeof filter !== "object") return filter;
|
|
1727
1809
|
const result = {};
|
|
1728
1810
|
for (const [key, value] of Object.entries(filter)) if (key === "$and" || key === "$or") result[key] = value.map((f) => this.translateFilterWithRename(f, meta));
|
|
1729
1811
|
else if (key === "$not") result[key] = this.translateFilterWithRename(value, meta);
|
|
1812
|
+
else if (meta.navFields.has(key)) result[key] = value;
|
|
1730
1813
|
else if (key.startsWith("$")) result[key] = value;
|
|
1731
1814
|
else {
|
|
1732
1815
|
const physical = meta.leafByLogical.get(key)?.physicalName ?? key;
|
|
@@ -1856,6 +1939,234 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1856
1939
|
}
|
|
1857
1940
|
};
|
|
1858
1941
|
//#endregion
|
|
1942
|
+
//#region src/query/filter-values.ts
|
|
1943
|
+
const OPAQUE = {
|
|
1944
|
+
kinds: new Set(["any"]),
|
|
1945
|
+
timestamp: false
|
|
1946
|
+
};
|
|
1947
|
+
const NUMBER = {
|
|
1948
|
+
kinds: new Set(["number"]),
|
|
1949
|
+
timestamp: false
|
|
1950
|
+
};
|
|
1951
|
+
const INTEGER = {
|
|
1952
|
+
kinds: new Set(["integer"]),
|
|
1953
|
+
timestamp: false
|
|
1954
|
+
};
|
|
1955
|
+
const STRING = {
|
|
1956
|
+
kinds: new Set(["string"]),
|
|
1957
|
+
timestamp: false
|
|
1958
|
+
};
|
|
1959
|
+
function collectKinds(type, kinds, out, depth = 0) {
|
|
1960
|
+
const def = type?.type;
|
|
1961
|
+
const metadata = type?.metadata;
|
|
1962
|
+
if (!def || depth > 8) {
|
|
1963
|
+
kinds.add("any");
|
|
1964
|
+
return;
|
|
1965
|
+
}
|
|
1966
|
+
switch (def.kind) {
|
|
1967
|
+
case "": switch (def.designType) {
|
|
1968
|
+
case "string":
|
|
1969
|
+
kinds.add("string");
|
|
1970
|
+
return;
|
|
1971
|
+
case "number": {
|
|
1972
|
+
const timestamp = def.tags?.has("timestamp") === true;
|
|
1973
|
+
if (timestamp) out.timestamp = true;
|
|
1974
|
+
const integer = timestamp || def.tags?.has("int") === true || metadata?.has?.("expect.int") === true;
|
|
1975
|
+
kinds.add(integer ? "integer" : "number");
|
|
1976
|
+
return;
|
|
1977
|
+
}
|
|
1978
|
+
case "decimal":
|
|
1979
|
+
kinds.add("decimal");
|
|
1980
|
+
return;
|
|
1981
|
+
case "boolean":
|
|
1982
|
+
kinds.add("boolean");
|
|
1983
|
+
return;
|
|
1984
|
+
case "null":
|
|
1985
|
+
case "undefined":
|
|
1986
|
+
case "never": return;
|
|
1987
|
+
default:
|
|
1988
|
+
kinds.add("any");
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
case "union":
|
|
1992
|
+
for (const item of def.items ?? []) collectKinds(item, kinds, out, depth + 1);
|
|
1993
|
+
return;
|
|
1994
|
+
case "array":
|
|
1995
|
+
collectKinds(def.of, kinds, out, depth + 1);
|
|
1996
|
+
return;
|
|
1997
|
+
default: kinds.add("any");
|
|
1998
|
+
}
|
|
1999
|
+
}
|
|
2000
|
+
const typeCache = /* @__PURE__ */ new WeakMap();
|
|
2001
|
+
/**
|
|
2002
|
+
* The value kinds a filter on `fd` accepts (cached per descriptor). A leaf
|
|
2003
|
+
* inside a JSON value (a `@db.json` object or an array, addressable on
|
|
2004
|
+
* nested-object adapters) is opaque: its contents are not schema-enforced.
|
|
2005
|
+
*/
|
|
2006
|
+
function valueTypeOf(meta, fd) {
|
|
2007
|
+
let vt = typeCache.get(fd);
|
|
2008
|
+
if (vt) return vt;
|
|
2009
|
+
if (jsonValueAncestor(fd.path, meta.jsonValueParents) !== void 0 || fd.encrypted || fd.isGeoPoint || fd.designType === "json" || fd.designType === "object" || !fd.type) vt = OPAQUE;
|
|
2010
|
+
else {
|
|
2011
|
+
const kinds = /* @__PURE__ */ new Set();
|
|
2012
|
+
const out = { timestamp: false };
|
|
2013
|
+
collectKinds(fd.type, kinds, out);
|
|
2014
|
+
const metadata = fd.type.metadata;
|
|
2015
|
+
if ((fd.defaultValue?.kind === "fn" && fd.defaultValue.fn !== "uuid" || metadata?.has?.("db.agg.count") === true || metadata?.has?.("db.agg.countDistinct") === true) && kinds.delete("number")) kinds.add("integer");
|
|
2016
|
+
vt = kinds.size === 0 ? OPAQUE : {
|
|
2017
|
+
kinds,
|
|
2018
|
+
timestamp: out.timestamp
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
typeCache.set(fd, vt);
|
|
2022
|
+
return vt;
|
|
2023
|
+
}
|
|
2024
|
+
/** A decimal literal (`5`, `-1.5`, `.5`, `1e3`), surrounding blanks allowed — no hex, no `Infinity`. */
|
|
2025
|
+
const NUMERIC_RE = /^\s*[+-]?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?\s*$/i;
|
|
2026
|
+
/** An integer literal (`5`, `-12`), surrounding blanks allowed. */
|
|
2027
|
+
const INTEGER_RE = /^\s*[+-]?\d+\s*$/;
|
|
2028
|
+
function acceptsScalar(kind, value) {
|
|
2029
|
+
switch (kind) {
|
|
2030
|
+
case "any": return true;
|
|
2031
|
+
case "string": return typeof value === "string" || typeof value === "number" || typeof value === "boolean" || typeof value === "bigint";
|
|
2032
|
+
case "number":
|
|
2033
|
+
case "decimal": return typeof value === "number" || typeof value === "bigint" || typeof value === "string" && NUMERIC_RE.test(value);
|
|
2034
|
+
case "integer": return typeof value === "number" && Number.isInteger(value) || typeof value === "bigint" || typeof value === "string" && INTEGER_RE.test(value);
|
|
2035
|
+
default: return typeof value === "boolean" || value === 0 || value === 1;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
/** The first element of `value` (itself when not an array) `vt` rejects, or `undefined`. */
|
|
2039
|
+
function rejectedValue(vt, value) {
|
|
2040
|
+
if (vt.kinds.has("any") || value === null || value === void 0) return void 0;
|
|
2041
|
+
if (Array.isArray(value)) {
|
|
2042
|
+
for (const item of value) {
|
|
2043
|
+
const bad = rejectedValue(vt, item);
|
|
2044
|
+
if (bad) return bad;
|
|
2045
|
+
}
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (typeof value === "object" && !require_object.isPlainObject(value)) return void 0;
|
|
2049
|
+
for (const kind of vt.kinds) if (acceptsScalar(kind, value)) return void 0;
|
|
2050
|
+
return { value };
|
|
2051
|
+
}
|
|
2052
|
+
const KIND_LABEL = {
|
|
2053
|
+
string: "a string",
|
|
2054
|
+
number: "a number",
|
|
2055
|
+
integer: "an integer",
|
|
2056
|
+
decimal: "a decimal (number or numeric string)",
|
|
2057
|
+
boolean: "a boolean"
|
|
2058
|
+
};
|
|
2059
|
+
function expectedOf(vt) {
|
|
2060
|
+
const labels = [];
|
|
2061
|
+
for (const kind of vt.kinds) {
|
|
2062
|
+
if (kind === "any") continue;
|
|
2063
|
+
labels.push(kind === "integer" && vt.timestamp ? "an integer (epoch milliseconds)" : KIND_LABEL[kind]);
|
|
2064
|
+
}
|
|
2065
|
+
return labels.join(" or ");
|
|
2066
|
+
}
|
|
2067
|
+
function describeValue(value) {
|
|
2068
|
+
if (typeof value === "string") return JSON.stringify(value.length > 40 ? `${value.slice(0, 40)}…` : value);
|
|
2069
|
+
if (require_object.isPlainObject(value)) return "an object";
|
|
2070
|
+
return String(value);
|
|
2071
|
+
}
|
|
2072
|
+
function valueError(path, op, message) {
|
|
2073
|
+
return new require_db_error.DbError("INVALID_QUERY", [{
|
|
2074
|
+
path,
|
|
2075
|
+
message: `Invalid filter value for "${path}"${op ? ` (${op})` : ""}: ${message}`
|
|
2076
|
+
}]);
|
|
2077
|
+
}
|
|
2078
|
+
function holdsStrings(vt) {
|
|
2079
|
+
return vt.kinds.has("string") || vt.kinds.has("any");
|
|
2080
|
+
}
|
|
2081
|
+
function checkRegex(path, vt, op, pattern) {
|
|
2082
|
+
if (!holdsStrings(vt)) throw valueError(path, op, `a pattern match needs a string field, "${path}" holds ${expectedOf(vt)}`);
|
|
2083
|
+
if (typeof pattern !== "string" && !(pattern instanceof RegExp)) throw valueError(path, op, `expected a regular expression, got ${describeValue(pattern)}`);
|
|
2084
|
+
}
|
|
2085
|
+
function checkValue(path, vt, op, value) {
|
|
2086
|
+
if (value instanceof RegExp) {
|
|
2087
|
+
checkRegex(path, vt, op ?? "RegExp", value);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
const bad = rejectedValue(vt, value);
|
|
2091
|
+
if (bad) throw valueError(path, op, `expected ${expectedOf(vt)}, got ${describeValue(bad.value)}`);
|
|
2092
|
+
}
|
|
2093
|
+
/** Operators whose operand is compared with the field's values. */
|
|
2094
|
+
const COMPARE_OPS = new Set([
|
|
2095
|
+
"$eq",
|
|
2096
|
+
"$ne",
|
|
2097
|
+
"$gt",
|
|
2098
|
+
"$gte",
|
|
2099
|
+
"$lt",
|
|
2100
|
+
"$lte",
|
|
2101
|
+
"$in",
|
|
2102
|
+
"$nin"
|
|
2103
|
+
]);
|
|
2104
|
+
/** Checks one filter entry's value (a bare value or an operator map) against `vt`. */
|
|
2105
|
+
function checkEntry(path, vt, value) {
|
|
2106
|
+
if (vt.kinds.has("any")) return;
|
|
2107
|
+
if (!require_object.isPlainObject(value)) {
|
|
2108
|
+
checkValue(path, vt, void 0, value);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
for (const [op, operand] of Object.entries(value)) if (op === "$regex") checkRegex(path, vt, op, operand);
|
|
2112
|
+
else if (COMPARE_OPS.has(op)) checkValue(path, vt, op, operand);
|
|
2113
|
+
}
|
|
2114
|
+
/**
|
|
2115
|
+
* Walks `filter` (through `$and` / `$or` / `$not`; relational predicates are
|
|
2116
|
+
* the related table's) and checks every entry whose key `typeOf` knows.
|
|
2117
|
+
*/
|
|
2118
|
+
function walkFilterValues(filter, typeOf) {
|
|
2119
|
+
if (!require_object.isPlainObject(filter)) return;
|
|
2120
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
2121
|
+
if (key === "$and" || key === "$or") {
|
|
2122
|
+
if (Array.isArray(value)) for (const child of value) walkFilterValues(child, typeOf);
|
|
2123
|
+
continue;
|
|
2124
|
+
}
|
|
2125
|
+
if (key === "$not") {
|
|
2126
|
+
walkFilterValues(value, typeOf);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
if (key.startsWith("$") || require_nested_writer.hasRelationOp(value)) continue;
|
|
2130
|
+
const vt = typeOf(key);
|
|
2131
|
+
if (vt) checkEntry(key, vt, value);
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
2134
|
+
/**
|
|
2135
|
+
* Rejects (`INVALID_QUERY`, `path` = the field) a filter value that cannot
|
|
2136
|
+
* denote its field's declared type — see the module notes for the accepted
|
|
2137
|
+
* forms. Unknown paths are skipped (the path guard owns them).
|
|
2138
|
+
*/
|
|
2139
|
+
function guardFilterValues(meta, filter) {
|
|
2140
|
+
if (!filter) return;
|
|
2141
|
+
walkFilterValues(filter, (key) => {
|
|
2142
|
+
const fd = meta.descriptorByPath.get(key);
|
|
2143
|
+
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* `$having` values: an aggregate alias is a number (`count`,
|
|
2148
|
+
* `countDistinct`, `sum`, `avg`) or its source field's type (`min` / `max`),
|
|
2149
|
+
* a calendar-bucket alias a string label, any other key a `$groupBy`
|
|
2150
|
+
* field's own type.
|
|
2151
|
+
*/
|
|
2152
|
+
function guardHavingValues(meta, controls) {
|
|
2153
|
+
if (!controls?.$having) return;
|
|
2154
|
+
const aliases = /* @__PURE__ */ new Map();
|
|
2155
|
+
if (Array.isArray(controls.$select)) {
|
|
2156
|
+
for (const item of controls.$select) if ((0, _uniqu_core.isAggregateExpr)(item)) {
|
|
2157
|
+
const fd = item.$fn === "min" || item.$fn === "max" ? meta.descriptorByPath.get(item.$field) : void 0;
|
|
2158
|
+
const counts = item.$fn === "count" || item.$fn === "countDistinct";
|
|
2159
|
+
aliases.set((0, _uniqu_core.resolveAlias)(item), fd ? valueTypeOf(meta, fd) : counts ? INTEGER : NUMBER);
|
|
2160
|
+
} else if ((0, _uniqu_core.isBucketExpr)(item)) aliases.set((0, _uniqu_core.resolveAlias)(item), STRING);
|
|
2161
|
+
}
|
|
2162
|
+
walkFilterValues(controls.$having, (key) => {
|
|
2163
|
+
const alias = aliases.get(key);
|
|
2164
|
+
if (alias) return alias;
|
|
2165
|
+
const fd = meta.descriptorByPath.get(key);
|
|
2166
|
+
return fd ? valueTypeOf(meta, fd) : void 0;
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
//#endregion
|
|
1859
2170
|
//#region src/query/query-guards.ts
|
|
1860
2171
|
/**
|
|
1861
2172
|
* Engine-agnostic query-time guards, applied in the core layer BEFORE filter
|
|
@@ -1882,6 +2193,9 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
1882
2193
|
* {@link canFilterLeaf}) needs → `INVALID_QUERY` (see {@link guardPaths}).
|
|
1883
2194
|
* Runs after the checks above so `ENC_*` codes keep firing first for
|
|
1884
2195
|
* encrypted subtrees.
|
|
2196
|
+
* - every filter / `$having` comparison value must be able to denote its
|
|
2197
|
+
* field's declared type (`?n='x'` on a number) → `INVALID_QUERY`, after
|
|
2198
|
+
* the path guard (see `guardFilterValues`, since 0.1.147).
|
|
1885
2199
|
*/
|
|
1886
2200
|
/** Validates a `[lng, lat]` tuple (GeoJSON coordinate order). */
|
|
1887
2201
|
function assertGeoPoint(point, path) {
|
|
@@ -1938,6 +2252,10 @@ function guardFilter(meta, adapter, filter, encCode = "ENC_FIELD_FILTER") {
|
|
|
1938
2252
|
continue;
|
|
1939
2253
|
}
|
|
1940
2254
|
if (key.startsWith("$")) continue;
|
|
2255
|
+
if (require_nested_writer.hasRelationOp(value)) {
|
|
2256
|
+
guardRelationOperands(key, value);
|
|
2257
|
+
continue;
|
|
2258
|
+
}
|
|
1941
2259
|
if (hasEncrypted && isEncryptedRef(meta, key)) throw encryptedRefError(encCode, key, "filter on");
|
|
1942
2260
|
if (!(0, _uniqu_core.isPrimitive)(value)) {
|
|
1943
2261
|
for (const [op, opValue] of Object.entries(value)) if (op === "$geoWithin") guardGeoWithin(meta, adapter, key, opValue);
|
|
@@ -1948,6 +2266,23 @@ function guardFilter(meta, adapter, filter, encCode = "ENC_FIELD_FILTER") {
|
|
|
1948
2266
|
}
|
|
1949
2267
|
}
|
|
1950
2268
|
}
|
|
2269
|
+
/**
|
|
2270
|
+
* Shape rules of a relational predicate's operator map: only `$some` /
|
|
2271
|
+
* `$none` (never mixed with comparison operators), each operand a filter
|
|
2272
|
+
* object.
|
|
2273
|
+
*/
|
|
2274
|
+
function guardRelationOperands(key, ops) {
|
|
2275
|
+
for (const [op, operand] of Object.entries(ops)) {
|
|
2276
|
+
if (!(0, _uniqu_core.isRelationOp)(op)) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
2277
|
+
path: key,
|
|
2278
|
+
message: `Cannot mix "$some" / "$none" with "${op}" on "${key}"`
|
|
2279
|
+
}]);
|
|
2280
|
+
if (!require_object.isPlainObject(operand)) throw new require_db_error.DbError("INVALID_QUERY", [{
|
|
2281
|
+
path: key,
|
|
2282
|
+
message: `"${op}" on "${key}" expects a filter object`
|
|
2283
|
+
}]);
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
1951
2286
|
/** Rejects `$sort` keys referencing encrypted fields. */
|
|
1952
2287
|
function guardSort(meta, sort) {
|
|
1953
2288
|
if (!sort || typeof sort !== "object" || meta.encryptedFields.size === 0) return;
|
|
@@ -2021,8 +2356,18 @@ function filterPredicateOf(value) {
|
|
|
2021
2356
|
const ops = value;
|
|
2022
2357
|
if ("$geoWithin" in ops) return "geo";
|
|
2023
2358
|
const keys = Object.keys(ops);
|
|
2359
|
+
if (keys.length > 0 && keys.every(_uniqu_core.isRelationOp)) return "relation";
|
|
2024
2360
|
return keys.length === 1 && keys[0] === "$exists" ? "exists" : "compare";
|
|
2025
2361
|
}
|
|
2362
|
+
/** The `{ op, filter }` list of a `relation` entry's operator map. */
|
|
2363
|
+
function relationOpsOf(value) {
|
|
2364
|
+
const out = [];
|
|
2365
|
+
for (const [op, filter] of Object.entries(value)) if ((0, _uniqu_core.isRelationOp)(op)) out.push({
|
|
2366
|
+
op,
|
|
2367
|
+
filter
|
|
2368
|
+
});
|
|
2369
|
+
return out;
|
|
2370
|
+
}
|
|
2026
2371
|
/**
|
|
2027
2372
|
* Whether a stored leaf physically supports a filter predicate of this class
|
|
2028
2373
|
* — the one rule the core path guard and moost-db's HTTP capability index
|
|
@@ -2039,6 +2384,7 @@ function filterPredicateOf(value) {
|
|
|
2039
2384
|
function canFilterLeaf(fd, predicate, adapter) {
|
|
2040
2385
|
if (fd.encrypted) return false;
|
|
2041
2386
|
switch (predicate) {
|
|
2387
|
+
case "relation": return false;
|
|
2042
2388
|
case "exists": return true;
|
|
2043
2389
|
case "geo": return fd.isGeoPoint === true && adapter.isGeoSearchable();
|
|
2044
2390
|
default: return adapter.canFilterField(fd);
|
|
@@ -2150,10 +2496,17 @@ function collectQueryPaths(query, aggregate) {
|
|
|
2150
2496
|
bucket: [],
|
|
2151
2497
|
aggregateMode: false
|
|
2152
2498
|
};
|
|
2153
|
-
collectFilterKeys(query.filter, (path, value) =>
|
|
2154
|
-
|
|
2155
|
-
predicate
|
|
2156
|
-
|
|
2499
|
+
collectFilterKeys(query.filter, (path, value) => {
|
|
2500
|
+
const predicate = filterPredicateOf(value);
|
|
2501
|
+
refs.filter.push(predicate === "relation" ? {
|
|
2502
|
+
path,
|
|
2503
|
+
predicate,
|
|
2504
|
+
relation: relationOpsOf(value)
|
|
2505
|
+
} : {
|
|
2506
|
+
path,
|
|
2507
|
+
predicate
|
|
2508
|
+
});
|
|
2509
|
+
}, void 0, refs);
|
|
2157
2510
|
const controls = query.controls ?? {};
|
|
2158
2511
|
const rawGroupBy = controls.$groupBy;
|
|
2159
2512
|
const groupBy = Array.isArray(rawGroupBy) ? rawGroupBy.filter((f) => typeof f === "string") : typeof rawGroupBy === "string" ? [rawGroupBy] : [];
|
|
@@ -2245,8 +2598,11 @@ function pathSourceOf(meta) {
|
|
|
2245
2598
|
function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
2246
2599
|
const verb = OP_VERB[op];
|
|
2247
2600
|
const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
|
|
2601
|
+
if (predicate === "relation" && kind !== "nav" && kind !== "unknown") throw pathError(path, `"$some" / "$none" are only valid on a navigation relation — "${path}" is not one`);
|
|
2248
2602
|
switch (kind) {
|
|
2249
|
-
case "nav":
|
|
2603
|
+
case "nav":
|
|
2604
|
+
if (op === "filter" && predicate === "relation" && parent === void 0) return;
|
|
2605
|
+
throw pathError(path, navPathMessage(path, verb, op, parent));
|
|
2250
2606
|
case "leaf": {
|
|
2251
2607
|
if (op === "select") return;
|
|
2252
2608
|
const fd = meta.descriptorByPath.get(path);
|
|
@@ -2271,6 +2627,35 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
|
2271
2627
|
}
|
|
2272
2628
|
}
|
|
2273
2629
|
/**
|
|
2630
|
+
* The rejection of a navigation path in a non-predicate position. A filter
|
|
2631
|
+
* names the predicate that expresses it: `ticket.status` →
|
|
2632
|
+
* `{ ticket: { $some: { status: … } } }`.
|
|
2633
|
+
*/
|
|
2634
|
+
function navPathMessage(path, verb, op, parent) {
|
|
2635
|
+
const base = `Cannot ${verb} "${path}" — navigation path`;
|
|
2636
|
+
if (op !== "filter") return base;
|
|
2637
|
+
return `${base}; use { ${parent ?? path}: { $some: ${parent === void 0 ? "…" : `{ ${path.slice(parent.length + 1)}: … }`} } }`;
|
|
2638
|
+
}
|
|
2639
|
+
/**
|
|
2640
|
+
* A `relation` filter entry: the adapter must render predicates in this
|
|
2641
|
+
* mode, the table must be wired to its related tables (a `DbSpace`), and
|
|
2642
|
+
* each operand is guarded by the related table (depth / count caps, the
|
|
2643
|
+
* related table's own path rules) — see `TRelationFilterHost.guard`.
|
|
2644
|
+
*/
|
|
2645
|
+
function guardRelationRef(meta, adapter, ref, state) {
|
|
2646
|
+
const path = state.path ? `${state.path}.${ref.path}` : ref.path;
|
|
2647
|
+
const mode = state.write ? "write" : "read";
|
|
2648
|
+
if (!adapter.supportsRelationFilters(mode)) throw new require_db_error.DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
2649
|
+
path,
|
|
2650
|
+
message: `Relational predicates ($some / $none) are not supported by this adapter${state.write ? " in mutation filters" : ""}`
|
|
2651
|
+
}]);
|
|
2652
|
+
if (!meta.relationFilters) throw new require_db_error.DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
2653
|
+
path,
|
|
2654
|
+
message: `Relational predicate on "${path}" needs the table to come from a DbSpace (no table resolver)`
|
|
2655
|
+
}]);
|
|
2656
|
+
for (const { op, filter } of ref.relation ?? []) meta.relationFilters.guard(ref.path, op, filter, state);
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2274
2659
|
* Core backstop for every read / aggregate / mutation-filter entry point:
|
|
2275
2660
|
* each referenced path (see {@link collectQueryPaths}) must exist on THIS
|
|
2276
2661
|
* adapter with the physical capability the position needs (see
|
|
@@ -2285,20 +2670,29 @@ function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
|
2285
2670
|
* `bucket`), `$groupBy` fields are checked, and computed aliases (`$as` or
|
|
2286
2671
|
* the default) are exempt in `$sort` / `$having`.
|
|
2287
2672
|
*
|
|
2673
|
+
* Then every filter comparison value must be able to denote its field's
|
|
2674
|
+
* declared type (`guardFilterValues`, since 0.1.147) — after the paths, so
|
|
2675
|
+
* an unknown or unfilterable field keeps its own rejection.
|
|
2676
|
+
*
|
|
2288
2677
|
* Returns the collected refs so callers can run further structural rules
|
|
2289
2678
|
* (see {@link checkHavingKeys}) without walking the query again.
|
|
2290
2679
|
*/
|
|
2291
|
-
function guardPaths(meta, adapter, query, aggregate = false) {
|
|
2680
|
+
function guardPaths(meta, adapter, query, aggregate = false, state) {
|
|
2292
2681
|
if (!query) return;
|
|
2293
2682
|
const refs = collectQueryPaths(query, aggregate);
|
|
2294
2683
|
if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
|
|
2295
|
-
|
|
2684
|
+
let relState = state;
|
|
2685
|
+
for (const ref of refs.filter) {
|
|
2686
|
+
guardPath(meta, adapter, ref.path, "filter", ref.predicate);
|
|
2687
|
+
if (ref.predicate === "relation") guardRelationRef(meta, adapter, ref, relState ??= require_nested_writer.relGuardState());
|
|
2688
|
+
}
|
|
2296
2689
|
for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
|
|
2297
2690
|
for (const path of refs.select) guardPath(meta, adapter, path, "select");
|
|
2298
2691
|
for (const path of refs.aggregate) guardPath(meta, adapter, path, "aggregate");
|
|
2299
2692
|
for (const path of refs.bucket) guardPath(meta, adapter, path, "bucket");
|
|
2300
2693
|
for (const path of refs.groupBy) guardPath(meta, adapter, path, "groupBy");
|
|
2301
2694
|
for (const path of refs.having) guardPath(meta, adapter, path, "having");
|
|
2695
|
+
guardFilterValues(meta, query.filter);
|
|
2302
2696
|
return refs;
|
|
2303
2697
|
}
|
|
2304
2698
|
/**
|
|
@@ -2306,12 +2700,12 @@ function guardPaths(meta, adapter, query, aggregate = false) {
|
|
|
2306
2700
|
* normalizer (a calendar bucket is invalid outside a grouped query), then
|
|
2307
2701
|
* the path guard.
|
|
2308
2702
|
*/
|
|
2309
|
-
function guardQuery(meta, adapter, query) {
|
|
2703
|
+
function guardQuery(meta, adapter, query, state) {
|
|
2310
2704
|
if (!query) return;
|
|
2311
2705
|
guardFilter(meta, adapter, query.filter);
|
|
2312
2706
|
guardSort(meta, query.controls?.$sort);
|
|
2313
2707
|
normalizeComputedSelect(query.controls, meta, false);
|
|
2314
|
-
guardPaths(meta, adapter, query);
|
|
2708
|
+
guardPaths(meta, adapter, query, false, state);
|
|
2315
2709
|
}
|
|
2316
2710
|
/**
|
|
2317
2711
|
* `$having` is a post-aggregation filter, so a key is either a computed
|
|
@@ -2337,7 +2731,7 @@ function checkHavingKeys(refs) {
|
|
|
2337
2731
|
* (`AGG_FN_NOT_SUPPORTED`) and calendar-bucket units
|
|
2338
2732
|
* (`BUCKET_NOT_SUPPORTED`), then the `$having` key rule
|
|
2339
2733
|
* ({@link checkHavingKeys} — after the path guard so an unknown key still
|
|
2340
|
-
* reads `Unknown field`).
|
|
2734
|
+
* reads `Unknown field`), then the `$having` values (`guardHavingValues`).
|
|
2341
2735
|
*
|
|
2342
2736
|
* `buckets` are the query's resolved calendar buckets when the caller already
|
|
2343
2737
|
* ran `normalizeComputedSelect` (resolved here otherwise).
|
|
@@ -2360,6 +2754,7 @@ function guardAggregate(meta, adapter, query, resolved) {
|
|
|
2360
2754
|
guardBucketUnits(adapter, buckets);
|
|
2361
2755
|
const having = refs ? checkHavingKeys(refs) : void 0;
|
|
2362
2756
|
if (having) throw new require_db_error.DbError("INVALID_QUERY", [having]);
|
|
2757
|
+
guardHavingValues(meta, controls);
|
|
2363
2758
|
}
|
|
2364
2759
|
/**
|
|
2365
2760
|
* Rejects an aggregate whose (known — the normalizer checked the name)
|
|
@@ -2544,7 +2939,13 @@ var AtscriptDbReadable = class {
|
|
|
2544
2939
|
}
|
|
2545
2940
|
/** Ensures metadata is built. Called before any metadata access. */
|
|
2546
2941
|
_ensureBuilt() {
|
|
2547
|
-
if (!this._meta.isBuilt)
|
|
2942
|
+
if (!this._meta.isBuilt) {
|
|
2943
|
+
this._meta.build(this.type, this.adapter, this.logger);
|
|
2944
|
+
if (this._meta.navFields.size > 0 && this._tableResolver) {
|
|
2945
|
+
const resolver = this._tableResolver;
|
|
2946
|
+
this._meta.relationFilters = require_nested_writer.createRelationFilterHost(this, (type) => resolver(type));
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2548
2949
|
if (this._meta.encryptedFields.size > 0 && !this._encryption) throw new require_db_error.DbError("ENC_CONFIG_MISSING", [{
|
|
2549
2950
|
path: "",
|
|
2550
2951
|
message: `Table "${this.tableName}" declares @db.encrypted fields but the DbSpace has no encryption configuration — pass { encryption: { defaultKeyId, keys } } to the DbSpace options`
|
|
@@ -2571,6 +2972,56 @@ var AtscriptDbReadable = class {
|
|
|
2571
2972
|
_guardQuery(query) {
|
|
2572
2973
|
guardQuery(this._meta, this.adapter, query);
|
|
2573
2974
|
}
|
|
2975
|
+
/**
|
|
2976
|
+
* Guards a relational predicate operand against THIS table — the filter
|
|
2977
|
+
* guard and the path guard with the predicate's shared `state` (depth,
|
|
2978
|
+
* count, read/write mode). Called by the source table's relation host.
|
|
2979
|
+
*
|
|
2980
|
+
* @internal Core wiring for relational predicates; not consumer API.
|
|
2981
|
+
*/
|
|
2982
|
+
_guardRelationOperand(filter, state) {
|
|
2983
|
+
this._ensureBuilt();
|
|
2984
|
+
guardFilter(this._meta, this.adapter, filter);
|
|
2985
|
+
guardPaths(this._meta, this.adapter, { filter }, false, state);
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Translates a relational predicate operand for THIS table's adapter (its
|
|
2989
|
+
* own field mapper; nested predicates resolved at `depth + 1`).
|
|
2990
|
+
*
|
|
2991
|
+
* @internal Core wiring for relational predicates; not consumer API.
|
|
2992
|
+
*/
|
|
2993
|
+
_resolveRelationOperand(filter, depth) {
|
|
2994
|
+
this._ensureBuilt();
|
|
2995
|
+
return this._fieldMapper.translateFilter(filter, this._meta, depth);
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Translates a logical query (filter + controls) for THIS table's adapter
|
|
2999
|
+
* after the read guards — exactly what `findMany` hands the adapter.
|
|
3000
|
+
* For adapters that load `$with` relations natively and must address the
|
|
3001
|
+
* related table's physical names.
|
|
3002
|
+
*
|
|
3003
|
+
* @internal Adapter-facing surface; not part of the consumer API.
|
|
3004
|
+
* @since 0.1.147
|
|
3005
|
+
*/
|
|
3006
|
+
_translateForAdapter(query) {
|
|
3007
|
+
this._ensureBuilt();
|
|
3008
|
+
this._guardQuery(query);
|
|
3009
|
+
return this._fieldMapper.translateQuery(query, this._meta);
|
|
3010
|
+
}
|
|
3011
|
+
/**
|
|
3012
|
+
* Physical rows of THIS table → logical rows (field mapping, value
|
|
3013
|
+
* formatters, decryption) — what every read does before `$with` loading.
|
|
3014
|
+
* `controls` are the logical read controls the rows were read with.
|
|
3015
|
+
*
|
|
3016
|
+
* @internal Adapter-facing surface; not part of the consumer API.
|
|
3017
|
+
* @since 0.1.147
|
|
3018
|
+
*/
|
|
3019
|
+
async _rowsFromAdapter(rows, controls) {
|
|
3020
|
+
this._ensureBuilt();
|
|
3021
|
+
const out = this._fromRead(rows, controls);
|
|
3022
|
+
await this._decryptRows(out);
|
|
3023
|
+
return out;
|
|
3024
|
+
}
|
|
2574
3025
|
_encryptedPathsCache;
|
|
2575
3026
|
/** Pre-split `encryptedFields` paths — computed once, reused on every read/write. */
|
|
2576
3027
|
get _encryptedPaths() {
|
|
@@ -2825,6 +3276,15 @@ var AtscriptDbReadable = class {
|
|
|
2825
3276
|
this._ensureBuilt();
|
|
2826
3277
|
return this._meta.pathToPhysical;
|
|
2827
3278
|
}
|
|
3279
|
+
/**
|
|
3280
|
+
* Physical column (or document path) of a logical field path —
|
|
3281
|
+
* `@db.column` renames and flattening applied.
|
|
3282
|
+
* @since 0.1.147
|
|
3283
|
+
*/
|
|
3284
|
+
physicalPath(logical) {
|
|
3285
|
+
this._ensureBuilt();
|
|
3286
|
+
return this._meta.physicalPath(logical);
|
|
3287
|
+
}
|
|
2828
3288
|
/** Precomputed physical column name → logical dot-path map (inverse). */
|
|
2829
3289
|
get physicalToPath() {
|
|
2830
3290
|
this._ensureBuilt();
|
|
@@ -2879,10 +3339,14 @@ var AtscriptDbReadable = class {
|
|
|
2879
3339
|
widened
|
|
2880
3340
|
};
|
|
2881
3341
|
}
|
|
2882
|
-
/**
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
3342
|
+
/**
|
|
3343
|
+
* Reconstructs + decrypts a read's rows, keeps the ones `pick` selects (all
|
|
3344
|
+
* by default), loads their `$with` relations and strips widened keys.
|
|
3345
|
+
*/
|
|
3346
|
+
async _finishRead(results, read, pick) {
|
|
3347
|
+
const all = this._fromRead(results, read.controls);
|
|
3348
|
+
await this._decryptRows(all);
|
|
3349
|
+
const rows = pick ? pick(all) : all;
|
|
2886
3350
|
if (read.withRelations?.length) {
|
|
2887
3351
|
await this.loadRelations(rows, read.withRelations);
|
|
2888
3352
|
for (const key of read.widened) for (const row of rows) require_object.deletePath(row, key);
|
|
@@ -2991,11 +3455,12 @@ var AtscriptDbReadable = class {
|
|
|
2991
3455
|
* table. Defense-in-depth for query-path validation: `flattenAnnotatedType`
|
|
2992
3456
|
* still truncates real self-referential cycles, so paths like
|
|
2993
3457
|
* `parent.parent.name` on a self-ref schema would miss `flatMap.has` but
|
|
2994
|
-
* remain valid field references on the target
|
|
3458
|
+
* remain valid field references on the target — a path may cross the same
|
|
3459
|
+
* relation any number of times (callers cap the depth).
|
|
2995
3460
|
*
|
|
2996
|
-
*
|
|
3461
|
+
* Terminates on cyclic schemas: every hop consumes one path segment.
|
|
2997
3462
|
*/
|
|
2998
|
-
isValidFieldPath(path
|
|
3463
|
+
isValidFieldPath(path) {
|
|
2999
3464
|
if (this.flatMap.has(path)) return true;
|
|
3000
3465
|
const dotIdx = path.indexOf(".");
|
|
3001
3466
|
if (dotIdx === -1) return false;
|
|
@@ -3003,11 +3468,7 @@ var AtscriptDbReadable = class {
|
|
|
3003
3468
|
const tail = path.slice(dotIdx + 1);
|
|
3004
3469
|
const targetTable = this.relatedTable(head);
|
|
3005
3470
|
if (!targetTable || typeof targetTable.isValidFieldPath !== "function") return false;
|
|
3006
|
-
|
|
3007
|
-
const cycleKey = `${this.tableName}:${head}`;
|
|
3008
|
-
if (visited.has(cycleKey)) return false;
|
|
3009
|
-
visited.add(cycleKey);
|
|
3010
|
-
return targetTable.isValidFieldPath(tail, visited);
|
|
3471
|
+
return targetTable.isValidFieldPath(tail);
|
|
3011
3472
|
}
|
|
3012
3473
|
/**
|
|
3013
3474
|
* Creates a new validator with custom options.
|
|
@@ -3041,6 +3502,22 @@ var AtscriptDbReadable = class {
|
|
|
3041
3502
|
return await this._finishRead(await this.adapter.findMany(read.translated), read);
|
|
3042
3503
|
}
|
|
3043
3504
|
/**
|
|
3505
|
+
* `findMany` for the generic `$with` loader. With `partitionBy` (logical
|
|
3506
|
+
* fields), `$skip` / `$limit` apply per group of rows sharing those fields'
|
|
3507
|
+
* values (`BaseDbAdapter.findManyPerPartition`); `pick` chooses which of the
|
|
3508
|
+
* read rows to keep before their own `$with` relations load.
|
|
3509
|
+
*
|
|
3510
|
+
* @internal Relation-loader surface; not part of the consumer API.
|
|
3511
|
+
* @since 0.1.147
|
|
3512
|
+
*/
|
|
3513
|
+
async _findManyForRelation(query, opts) {
|
|
3514
|
+
this._ensureBuilt();
|
|
3515
|
+
this._guardQuery(query);
|
|
3516
|
+
const read = this._translateRead(query);
|
|
3517
|
+
const results = opts.partitionBy ? await this.adapter.findManyPerPartition(read.translated, opts.partitionBy.map((field) => this._meta.physicalPath(field))) : await this.adapter.findMany(read.translated);
|
|
3518
|
+
return this._finishRead(results, read, opts.pick);
|
|
3519
|
+
}
|
|
3520
|
+
/**
|
|
3044
3521
|
* Counts records matching the query.
|
|
3045
3522
|
*/
|
|
3046
3523
|
async count(query) {
|
|
@@ -3561,7 +4038,7 @@ var AtscriptDbReadable = class {
|
|
|
3561
4038
|
* Public entry point for relation loading. Used by adapters for nested $with delegation.
|
|
3562
4039
|
*/
|
|
3563
4040
|
async loadRelations(rows, withRelations) {
|
|
3564
|
-
const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-
|
|
4041
|
+
const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-BFhVuJ-B.cjs")).then((n) => n.relation_loader_exports);
|
|
3565
4042
|
return loadRelationsImpl(rows, withRelations, this);
|
|
3566
4043
|
}
|
|
3567
4044
|
/**
|
|
@@ -3627,8 +4104,14 @@ function createFailureCollector(what) {
|
|
|
3627
4104
|
//#region src/base-adapter.ts
|
|
3628
4105
|
const EMPTY_DEFAULT_FNS = /* @__PURE__ */ new Set();
|
|
3629
4106
|
const EMPTY_BUCKET_UNITS = /* @__PURE__ */ new Set();
|
|
3630
|
-
/**
|
|
4107
|
+
/**
|
|
4108
|
+
* Every calendar-bucket unit — what an adapter that renders them all returns
|
|
4109
|
+
* from `calendarBucketUnits()`. Includes `'hour'` since 0.1.147.
|
|
4110
|
+
*/
|
|
3631
4111
|
const ALL_BUCKET_UNITS = new Set(_uniqu_core.BUCKET_UNITS);
|
|
4112
|
+
const NO_VIEW_CAPABILITIES = /* @__PURE__ */ new Set();
|
|
4113
|
+
/** Every view capability — what the bundled adapters return from `viewCapabilities()`. @since 0.1.147 */
|
|
4114
|
+
const ALL_VIEW_CAPABILITIES = new Set(["compute", "firstJoin"]);
|
|
3632
4115
|
const txStorage = new node_async_hooks.AsyncLocalStorage();
|
|
3633
4116
|
/** The innermost open transaction of `owner` in the current async chain. */
|
|
3634
4117
|
function findTxContext(owner) {
|
|
@@ -3688,6 +4171,26 @@ var BaseDbAdapter = class {
|
|
|
3688
4171
|
registerReadable(readable, logger) {
|
|
3689
4172
|
this._table = readable;
|
|
3690
4173
|
if (logger) this.logger = logger;
|
|
4174
|
+
if (readable.isView) this._guardViewCapabilities(readable);
|
|
4175
|
+
}
|
|
4176
|
+
/**
|
|
4177
|
+
* Makes `ensureTable()` of a managed view fail closed (since 0.1.147): it
|
|
4178
|
+
* throws before the adapter renders a computed column / first-row join its
|
|
4179
|
+
* {@link viewCapabilities} does not list — schema sync refuses such a view
|
|
4180
|
+
* up front, a direct `ensureTable()` call must not render it as a plain
|
|
4181
|
+
* (row-multiplying) join either. Wraps the subclass's own implementation, so
|
|
4182
|
+
* third-party adapters get the guard without code changes.
|
|
4183
|
+
*/
|
|
4184
|
+
_guardViewCapabilities(readable) {
|
|
4185
|
+
if (Object.prototype.hasOwnProperty.call(this, "ensureTable")) return;
|
|
4186
|
+
const view = readable;
|
|
4187
|
+
if (typeof view.viewCapabilityProblems !== "function") return;
|
|
4188
|
+
const ensureTable = this.ensureTable.bind(this);
|
|
4189
|
+
this.ensureTable = (opts) => {
|
|
4190
|
+
const problems = view.viewCapabilityProblems();
|
|
4191
|
+
if (problems.length > 0) return Promise.reject(new Error(problems.join("; ")));
|
|
4192
|
+
return ensureTable(opts);
|
|
4193
|
+
};
|
|
3691
4194
|
}
|
|
3692
4195
|
/**
|
|
3693
4196
|
* Called by {@link DbSpace} right after its factory builds this adapter —
|
|
@@ -3872,8 +4375,10 @@ var BaseDbAdapter = class {
|
|
|
3872
4375
|
* be adopted adapter by adapter. An adapter that returns a unit must group
|
|
3873
4376
|
* by the bucket alias in `$groupBy` — see `controls.$select.buckets`
|
|
3874
4377
|
* (`TResolvedBucket`: physical `field`, source `fd`) — and return the
|
|
3875
|
-
* `YYYY-MM-DD` label of the bucket's first local day
|
|
3876
|
-
*
|
|
4378
|
+
* `YYYY-MM-DD` label of the bucket's first local day, or for `'hour'` the
|
|
4379
|
+
* local wall-clock hour `YYYY-MM-DDTHH:00` (null for a null or out-of-range
|
|
4380
|
+
* source, uniqu's `bucketLabel` semantics). Since 0.1.132; `'hour'` since
|
|
4381
|
+
* 0.1.147 — an adapter returning {@link ALL_BUCKET_UNITS} must render it.
|
|
3877
4382
|
*/
|
|
3878
4383
|
calendarBucketUnits() {
|
|
3879
4384
|
return EMPTY_BUCKET_UNITS;
|
|
@@ -3904,6 +4409,18 @@ var BaseDbAdapter = class {
|
|
|
3904
4409
|
*/
|
|
3905
4410
|
viewRenderRevision() {}
|
|
3906
4411
|
/**
|
|
4412
|
+
* The managed-view features this adapter renders: `compute` — computed
|
|
4413
|
+
* columns (`@db.compute`); `firstJoin` — first-row joins (the ordered 4th
|
|
4414
|
+
* argument of `@db.view.joins`). Schema sync refuses a view using a feature
|
|
4415
|
+
* not listed. The default is EMPTY (fail-closed): a third-party adapter
|
|
4416
|
+
* opts in once it renders them.
|
|
4417
|
+
*
|
|
4418
|
+
* @since 0.1.147
|
|
4419
|
+
*/
|
|
4420
|
+
viewCapabilities() {
|
|
4421
|
+
return NO_VIEW_CAPABILITIES;
|
|
4422
|
+
}
|
|
4423
|
+
/**
|
|
3907
4424
|
* Whether this adapter enforces foreign key constraints natively.
|
|
3908
4425
|
* When `true`, the generic layer skips application-level cascade/setNull
|
|
3909
4426
|
* on delete — the DB engine handles it (e.g. SQLite `ON DELETE CASCADE`).
|
|
@@ -3961,6 +4478,39 @@ var BaseDbAdapter = class {
|
|
|
3961
4478
|
return false;
|
|
3962
4479
|
}
|
|
3963
4480
|
/**
|
|
4481
|
+
* Whether this adapter renders relational filter predicates
|
|
4482
|
+
* (`{ nav: { $some | $none: … } }`) in `mode` — `read` for find / count /
|
|
4483
|
+
* search / aggregate filters, `write` for mutation filters
|
|
4484
|
+
* (`updateMany`, `deleteMany`, …). Default `false`: the core rejects such
|
|
4485
|
+
* filters with `REL_FILTER_NOT_SUPPORTED` before they reach the adapter.
|
|
4486
|
+
*
|
|
4487
|
+
* An adapter returning `true` receives each predicate already resolved by
|
|
4488
|
+
* the core: the filter visitor's `relation(field, op, operand)` callback
|
|
4489
|
+
* gets a `ResolvedRelationFilter` operand (`kind`, physical
|
|
4490
|
+
* correlation `pairs`, `target` / `junction` tables with their adapters,
|
|
4491
|
+
* and the inner `filter` already translated to the target's physical
|
|
4492
|
+
* names, nested predicates resolved too). Keep `relation` on every
|
|
4493
|
+
* `walkFilter` visitor that may meet such a filter.
|
|
4494
|
+
*
|
|
4495
|
+
* @since 0.1.147
|
|
4496
|
+
*/
|
|
4497
|
+
supportsRelationFilters(_mode) {
|
|
4498
|
+
return false;
|
|
4499
|
+
}
|
|
4500
|
+
/**
|
|
4501
|
+
* Whether `other` serves a table of the SAME store as this adapter, so one
|
|
4502
|
+
* statement / pipeline can correlate both (a relational predicate renders
|
|
4503
|
+
* the related table inside this table's query). Default: same adapter class
|
|
4504
|
+
* and same {@link _transactionOwner} (the driver / pool / client the
|
|
4505
|
+
* adapter was built with). Override when the owner is shared across
|
|
4506
|
+
* separate databases (e.g. one Mongo client over several databases).
|
|
4507
|
+
*
|
|
4508
|
+
* @since 0.1.147
|
|
4509
|
+
*/
|
|
4510
|
+
sharesStoreWith(other) {
|
|
4511
|
+
return other.constructor === this.constructor && other._transactionOwner() === this._transactionOwner();
|
|
4512
|
+
}
|
|
4513
|
+
/**
|
|
3964
4514
|
* Loads relations onto result rows using adapter-native operations.
|
|
3965
4515
|
* Only called when {@link supportsNativeRelations} returns `true`.
|
|
3966
4516
|
*
|
|
@@ -4197,6 +4747,31 @@ var BaseDbAdapter = class {
|
|
|
4197
4747
|
};
|
|
4198
4748
|
}
|
|
4199
4749
|
/**
|
|
4750
|
+
* Reads like {@link findMany}, except that `$skip` / `$limit` apply to each
|
|
4751
|
+
* partition — the rows sharing the values of the `partitionBy` columns
|
|
4752
|
+
* (physical names) — instead of to the whole result. The generic `$with`
|
|
4753
|
+
* loader reads the related rows of many parent rows at once this way, so a
|
|
4754
|
+
* relation's `$skip` / `$limit` page each parent row's related rows.
|
|
4755
|
+
* `$sort` orders the rows within a partition; how partitions interleave is
|
|
4756
|
+
* unspecified.
|
|
4757
|
+
*
|
|
4758
|
+
* Default: one {@link findMany} without `$skip` / `$limit`, paged per
|
|
4759
|
+
* partition in memory. The SQL adapters override it with a `ROW_NUMBER()`
|
|
4760
|
+
* window, so only the kept rows are read.
|
|
4761
|
+
*
|
|
4762
|
+
* @since 0.1.147
|
|
4763
|
+
*/
|
|
4764
|
+
async findManyPerPartition(query, partitionBy) {
|
|
4765
|
+
const { $skip, $limit, ...controls } = query.controls;
|
|
4766
|
+
return require_nested_writer.slicePerGroup(await this.findMany({
|
|
4767
|
+
...query,
|
|
4768
|
+
controls
|
|
4769
|
+
}), (row) => require_nested_writer.compositeKey(partitionBy, row, require_object.getPath), {
|
|
4770
|
+
skip: $skip ?? void 0,
|
|
4771
|
+
limit: $limit ?? void 0
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4774
|
+
/**
|
|
4200
4775
|
* Executes an aggregate query (GROUP BY + aggregate functions).
|
|
4201
4776
|
* Default throws — override in adapters that support aggregation.
|
|
4202
4777
|
*/
|
|
@@ -4267,6 +4842,8 @@ var BaseDbAdapter = class {
|
|
|
4267
4842
|
//#endregion
|
|
4268
4843
|
//#region src/strategies/application-integrity.ts
|
|
4269
4844
|
const MAX_CASCADE_DEPTH = 100;
|
|
4845
|
+
/** Rows per pinned-key delete batch (keeps `IN (…)` / `$or` lists bounded). */
|
|
4846
|
+
const PIN_BATCH = 1e3;
|
|
4270
4847
|
const cascadeStorage = new node_async_hooks.AsyncLocalStorage();
|
|
4271
4848
|
/**
|
|
4272
4849
|
* Integrity strategy for adapters without native FK support (e.g. MongoDB).
|
|
@@ -4335,6 +4912,12 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4335
4912
|
* - `restrict`: throws if any children exist
|
|
4336
4913
|
* - `cascade`: recursively deletes child records
|
|
4337
4914
|
* - `setNull`: sets FK fields to null
|
|
4915
|
+
*
|
|
4916
|
+
* When `filter` holds a relational predicate, returns the matched rows
|
|
4917
|
+
* PINNED by primary key (see {@link TCascadePin}): the caller deletes those
|
|
4918
|
+
* rows, never re-evaluating `filter` on data the cascade just changed (a
|
|
4919
|
+
* `$some` over a cascaded child relation would otherwise stop matching and
|
|
4920
|
+
* leave the parent behind). Any other filter → `undefined`.
|
|
4338
4921
|
*/
|
|
4339
4922
|
async cascadeBeforeDelete(filter, tableName, meta, cascadeResolver, translateFilter, adapter) {
|
|
4340
4923
|
const parentCtx = cascadeStorage.getStore();
|
|
@@ -4346,6 +4929,11 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4346
4929
|
}]);
|
|
4347
4930
|
const targets = cascadeResolver(tableName);
|
|
4348
4931
|
if (targets.length === 0) return;
|
|
4932
|
+
const pkPhysical = require_nested_writer.containsRelationFilter(filter) ? meta.primaryKeys.map((pk) => meta.physicalPath(pk)) : void 0;
|
|
4933
|
+
if (pkPhysical?.length === 0) throw new require_db_error.DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
4934
|
+
path: "",
|
|
4935
|
+
message: "Cannot delete by a relational predicate with application-level cascades: the table has no primary key"
|
|
4936
|
+
}]);
|
|
4349
4937
|
const neededLogical = /* @__PURE__ */ new Set();
|
|
4350
4938
|
for (const t of targets) for (const tf of t.fk.targetFields) neededLogical.add(tf);
|
|
4351
4939
|
for (const pk of meta.primaryKeys) neededLogical.add(pk);
|
|
@@ -4360,7 +4948,8 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4360
4948
|
filter: translateFilter(filter),
|
|
4361
4949
|
controls: { $select: new UniquSelect(physicalFields) }
|
|
4362
4950
|
});
|
|
4363
|
-
|
|
4951
|
+
const pin = pkPhysical ? pinByPrimaryKey(rawRecords, pkPhysical) : void 0;
|
|
4952
|
+
if (rawRecords.length === 0) return pin;
|
|
4364
4953
|
const allRecords = rawRecords.map((r) => {
|
|
4365
4954
|
const mapped = {};
|
|
4366
4955
|
for (const [key, val] of Object.entries(r)) mapped[physicalToLogical.get(key) ?? key] = val;
|
|
@@ -4376,7 +4965,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4376
4965
|
addedKeys.push(key);
|
|
4377
4966
|
records.push(record);
|
|
4378
4967
|
}
|
|
4379
|
-
if (records.length === 0) return;
|
|
4968
|
+
if (records.length === 0) return pin;
|
|
4380
4969
|
try {
|
|
4381
4970
|
await cascadeStorage.run({
|
|
4382
4971
|
visited,
|
|
@@ -4416,6 +5005,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4416
5005
|
} finally {
|
|
4417
5006
|
for (const key of addedKeys) visited.delete(key);
|
|
4418
5007
|
}
|
|
5008
|
+
return pin;
|
|
4419
5009
|
}
|
|
4420
5010
|
needsCascade(cascadeResolver) {
|
|
4421
5011
|
return !!cascadeResolver;
|
|
@@ -4456,6 +5046,23 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
|
|
|
4456
5046
|
return orFilters.length === 1 ? orFilters[0] : { $or: orFilters };
|
|
4457
5047
|
}
|
|
4458
5048
|
};
|
|
5049
|
+
/**
|
|
5050
|
+
* Adapter-ready filters addressing exactly `rows` by their (physical) primary
|
|
5051
|
+
* key, in batches of {@link PIN_BATCH}: `{ pk: { $in } }` for a single key,
|
|
5052
|
+
* `{ $or: [{ a, b }, …] }` for a composite one. Values are the raw stored
|
|
5053
|
+
* values the adapter returned, so no value formatting is re-applied.
|
|
5054
|
+
*/
|
|
5055
|
+
function pinByPrimaryKey(rows, pk) {
|
|
5056
|
+
const out = [];
|
|
5057
|
+
for (let i = 0; i < rows.length; i += PIN_BATCH) {
|
|
5058
|
+
const batch = rows.slice(i, i + PIN_BATCH);
|
|
5059
|
+
if (pk.length === 1) {
|
|
5060
|
+
const field = pk[0];
|
|
5061
|
+
out.push({ [field]: { $in: batch.map((r) => r[field]) } });
|
|
5062
|
+
} else out.push({ $or: batch.map((r) => Object.fromEntries(pk.map((f) => [f, r[f]]))) });
|
|
5063
|
+
}
|
|
5064
|
+
return out;
|
|
5065
|
+
}
|
|
4459
5066
|
//#endregion
|
|
4460
5067
|
//#region src/patch/array-ops-resolver.ts
|
|
4461
5068
|
/**
|
|
@@ -5331,7 +5938,10 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
5331
5938
|
const filter = this._andScope(pinned, opts?.scope);
|
|
5332
5939
|
const translated = this._fieldMapper.translateFilter(filter, this._meta);
|
|
5333
5940
|
if (guard) await guard(new RemoveGuardContext(id, filter, this));
|
|
5334
|
-
if (needsCascade)
|
|
5941
|
+
if (needsCascade) {
|
|
5942
|
+
const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
|
|
5943
|
+
if (pin) return pin.length > 0 ? this.adapter.deleteOne(pin[0]) : { deletedCount: 0 };
|
|
5944
|
+
}
|
|
5335
5945
|
return this.adapter.deleteOne(translated);
|
|
5336
5946
|
};
|
|
5337
5947
|
return require_nested_writer.remapDeleteFkViolation(this.tableName, () => guard || needsCascade || candidates.length > 1 ? this.adapter.withTransaction(run) : run());
|
|
@@ -5375,7 +5985,12 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
5375
5985
|
this._ensureBuilt();
|
|
5376
5986
|
this._guardMutationFilter(filter);
|
|
5377
5987
|
if (this._integrity.needsCascade(this._cascadeResolver)) return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
|
|
5378
|
-
await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
|
|
5988
|
+
const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
|
|
5989
|
+
if (pin) {
|
|
5990
|
+
let deletedCount = 0;
|
|
5991
|
+
for (const batch of pin) deletedCount += (await this.adapter.deleteMany(batch)).deletedCount;
|
|
5992
|
+
return { deletedCount };
|
|
5993
|
+
}
|
|
5379
5994
|
return this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta));
|
|
5380
5995
|
}));
|
|
5381
5996
|
return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta)));
|
|
@@ -5397,7 +6012,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
5397
6012
|
/** Engine-agnostic guard for user-supplied mutation filters (updateMany/deleteMany/…). */
|
|
5398
6013
|
_guardMutationFilter(filter) {
|
|
5399
6014
|
guardFilter(this._meta, this.adapter, filter);
|
|
5400
|
-
guardPaths(this._meta, this.adapter, { filter });
|
|
6015
|
+
guardPaths(this._meta, this.adapter, { filter }, false, require_nested_writer.relGuardState(true));
|
|
5401
6016
|
}
|
|
5402
6017
|
/**
|
|
5403
6018
|
* Encrypts `@db.encrypted` field values in place on (already validated)
|
|
@@ -5794,8 +6409,16 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
5794
6409
|
};
|
|
5795
6410
|
//#endregion
|
|
5796
6411
|
//#region src/table/db-view.ts
|
|
5797
|
-
/**
|
|
5798
|
-
|
|
6412
|
+
/**
|
|
6413
|
+
* The single `@meta.id` field of a first-row join target (its source type,
|
|
6414
|
+
* past `@db.alias`) — the anchor of the join's correlated subquery.
|
|
6415
|
+
* @throws when the target declares no or several primary-key fields.
|
|
6416
|
+
*/
|
|
6417
|
+
function firstJoinKey(view, type, scope) {
|
|
6418
|
+
const ids = type.type.kind === "object" ? [...type.type.props.entries()].filter(([, p]) => p.metadata.has("meta.id")) : [];
|
|
6419
|
+
if (ids.length !== 1) throw new Error(`View "${view}": the first-row join on "${scope}" needs a target with exactly one @meta.id field (found ${ids.length})`);
|
|
6420
|
+
return ids[0][0];
|
|
6421
|
+
}
|
|
5799
6422
|
/**
|
|
5800
6423
|
* Reads a view field's `@db.agg.*` annotation. The compiled value is
|
|
5801
6424
|
* `{ field?, condition? }`; models compiled by older versions carry `true`
|
|
@@ -5803,7 +6426,7 @@ const AGG_KEYS = require_aggregate_fns.SUPPORTED_AGGREGATE_FNS.map((fn) => `db.a
|
|
|
5803
6426
|
* A missing field or `true` is `'*'` (COUNT(*)).
|
|
5804
6427
|
*/
|
|
5805
6428
|
function readViewAgg(metadata) {
|
|
5806
|
-
for (const key of
|
|
6429
|
+
for (const key of require_aggregate_fns.AGG_ANNOTATIONS) {
|
|
5807
6430
|
const val = metadata?.get(key);
|
|
5808
6431
|
if (val === void 0) continue;
|
|
5809
6432
|
const aggFn = key.slice(7);
|
|
@@ -5881,7 +6504,9 @@ function inheritViewFieldSeals(viewType) {
|
|
|
5881
6504
|
try {
|
|
5882
6505
|
const forRef = viewType.metadata.get("db.view.for");
|
|
5883
6506
|
const entryType = forRef && refType(forRef);
|
|
5884
|
-
|
|
6507
|
+
const props = viewType.type.props;
|
|
6508
|
+
for (const [fieldName, fieldType] of props.entries()) {
|
|
6509
|
+
if (require_nested_writer.computeOf(fieldType) !== void 0) continue;
|
|
5885
6510
|
const { agg, sourceType, sourcePath } = viewFieldSource(fieldName, fieldType, entryType);
|
|
5886
6511
|
if (agg?.aggField === "*" || !sourceType) continue;
|
|
5887
6512
|
const source = viewSourceOf(sourceType).type;
|
|
@@ -5892,6 +6517,14 @@ function inheritViewFieldSeals(viewType) {
|
|
|
5892
6517
|
if (agg) throw new Error(`View "${require_nested_writer.tableNameOf(viewType)}" field "${fieldName}": @db.agg.${agg.aggFn} over the @db.encrypted field "${sourcePath}" — ciphertext cannot be aggregated`);
|
|
5893
6518
|
fieldType.metadata.set("db.encrypted", true);
|
|
5894
6519
|
}
|
|
6520
|
+
for (const [fieldName, fieldType] of props.entries()) {
|
|
6521
|
+
const computed = require_nested_writer.computedOperands(viewType, fieldName);
|
|
6522
|
+
if (!computed) continue;
|
|
6523
|
+
const { operands, via } = computed;
|
|
6524
|
+
const encrypted = operands.find((p) => props.get(p)?.metadata.has("db.encrypted"));
|
|
6525
|
+
if (encrypted) throw new Error(`View "${require_nested_writer.tableNameOf(viewType)}": @db.compute over the @db.encrypted field "${encrypted}" — ciphertext cannot be computed`);
|
|
6526
|
+
if ([...operands, ...via].some((p) => props.get(p)?.metadata.has("db.writeOnly"))) fieldType.metadata.set("db.writeOnly", true);
|
|
6527
|
+
}
|
|
5895
6528
|
} catch (error) {
|
|
5896
6529
|
sealedViews.delete(viewType);
|
|
5897
6530
|
throw error;
|
|
@@ -5954,13 +6587,39 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
5954
6587
|
if (rawJoins) for (const join of rawJoins) {
|
|
5955
6588
|
const targetType = refType(join.target);
|
|
5956
6589
|
const target = viewSourceOf(targetType());
|
|
5957
|
-
|
|
6590
|
+
const viewJoin = {
|
|
5958
6591
|
targetType,
|
|
5959
6592
|
targetTable: target.table,
|
|
5960
6593
|
scope: target.name,
|
|
5961
6594
|
condition: join.condition,
|
|
5962
6595
|
kind: join.kind === "left" ? "left" : "inner"
|
|
5963
|
-
}
|
|
6596
|
+
};
|
|
6597
|
+
if (join.order?.length) {
|
|
6598
|
+
const key = firstJoinKey(this.tableName, target.type, target.name);
|
|
6599
|
+
const order = join.order.map((item) => {
|
|
6600
|
+
const seals = sourceFieldSeals(target.type, item.ref.field);
|
|
6601
|
+
if (seals.encrypted || seals.writeOnly) throw new Error(`View "${this.tableName}": the first-row join on "${target.name}" cannot order by the ${seals.encrypted ? "@db.encrypted" : "@db.writeOnly"} field "${item.ref.field}"`);
|
|
6602
|
+
return {
|
|
6603
|
+
ref: {
|
|
6604
|
+
type: targetType,
|
|
6605
|
+
field: item.ref.field
|
|
6606
|
+
},
|
|
6607
|
+
desc: item.desc === true
|
|
6608
|
+
};
|
|
6609
|
+
});
|
|
6610
|
+
if (!order.some((item) => item.ref.field === key)) order.push({
|
|
6611
|
+
ref: {
|
|
6612
|
+
type: targetType,
|
|
6613
|
+
field: key
|
|
6614
|
+
},
|
|
6615
|
+
desc: false
|
|
6616
|
+
});
|
|
6617
|
+
viewJoin.first = {
|
|
6618
|
+
order,
|
|
6619
|
+
key
|
|
6620
|
+
};
|
|
6621
|
+
}
|
|
6622
|
+
joins.push(viewJoin);
|
|
5964
6623
|
}
|
|
5965
6624
|
const filter = metadata.get("db.view.filter");
|
|
5966
6625
|
const having = metadata.get("db.view.having");
|
|
@@ -6037,6 +6696,26 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
6037
6696
|
this._columnMappings ??= this._buildColumnMappings();
|
|
6038
6697
|
return this._columnMappings;
|
|
6039
6698
|
}
|
|
6699
|
+
/**
|
|
6700
|
+
* Why the bound adapter cannot render this managed view: one message per
|
|
6701
|
+
* computed column / first-row join whose feature its `viewCapabilities()`
|
|
6702
|
+
* does not list. Empty for an external view or when every feature is
|
|
6703
|
+
* rendered. Schema sync refuses such a view; the adapter's `ensureTable()`
|
|
6704
|
+
* throws for it (fail-closed for adapters that predate the features).
|
|
6705
|
+
* @since 0.1.147
|
|
6706
|
+
*/
|
|
6707
|
+
viewCapabilityProblems() {
|
|
6708
|
+
if (this.isExternal) return [];
|
|
6709
|
+
const caps = this.dbAdapter.viewCapabilities();
|
|
6710
|
+
const problems = [];
|
|
6711
|
+
if (!caps.has("compute")) {
|
|
6712
|
+
for (const col of this.getViewColumnMappings()) if (col.expr !== void 0) problems.push(`View "${this.tableName}" field "${col.viewPath}": computed columns (@db.compute) are not supported by this adapter (viewCapabilities())`);
|
|
6713
|
+
}
|
|
6714
|
+
if (!caps.has("firstJoin")) {
|
|
6715
|
+
for (const join of this.viewPlan.joins) if (join.first) problems.push(`View "${this.tableName}" join "${join.scope}": first-row joins are not supported by this adapter (viewCapabilities())`);
|
|
6716
|
+
}
|
|
6717
|
+
return problems;
|
|
6718
|
+
}
|
|
6040
6719
|
_buildColumnMappings() {
|
|
6041
6720
|
const plan = this.viewPlan;
|
|
6042
6721
|
const mappings = [];
|
|
@@ -6051,6 +6730,17 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
6051
6730
|
const leftJoined = new Set(plan.joins.filter((j) => j.kind === "left").map((j) => j.scope));
|
|
6052
6731
|
for (const [fieldName, fieldType] of this._type.type.props.entries()) {
|
|
6053
6732
|
if (ignored.has(fieldName)) continue;
|
|
6733
|
+
const expr = require_nested_writer.computeOf(fieldType);
|
|
6734
|
+
if (expr !== void 0) {
|
|
6735
|
+
mappings.push({
|
|
6736
|
+
viewColumn: viewName(fieldName) ?? fieldName,
|
|
6737
|
+
viewPath: fieldName,
|
|
6738
|
+
sourceTable: plan.entryTable,
|
|
6739
|
+
sourceColumn: "",
|
|
6740
|
+
expr
|
|
6741
|
+
});
|
|
6742
|
+
continue;
|
|
6743
|
+
}
|
|
6054
6744
|
const { agg, chained, sourceType, sourcePath } = viewFieldSource(fieldName, fieldType, plan.entryType);
|
|
6055
6745
|
const aggField = agg?.aggField;
|
|
6056
6746
|
if (aggField === "*" && agg?.aggFn !== "count") fail(fieldName, `aggregate "${agg?.aggFn}" needs a field — only count accepts *`);
|
|
@@ -6087,8 +6777,37 @@ var AtscriptDbView = class extends AtscriptDbReadable {
|
|
|
6087
6777
|
...agg
|
|
6088
6778
|
} : mapping);
|
|
6089
6779
|
}
|
|
6780
|
+
this._checkComputed(mappings, fail);
|
|
6090
6781
|
return mappings;
|
|
6091
6782
|
}
|
|
6783
|
+
/**
|
|
6784
|
+
* The runtime twin of the compile-time `@db.compute` rules the renderers
|
|
6785
|
+
* rely on: every leaf names a (non-ignored) view column, computed columns
|
|
6786
|
+
* form no cycle; sets each computed mapping's `nullable`.
|
|
6787
|
+
*/
|
|
6788
|
+
_checkComputed(mappings, fail) {
|
|
6789
|
+
const computed = mappings.filter((m) => m.expr !== void 0);
|
|
6790
|
+
if (computed.length === 0) return;
|
|
6791
|
+
const byPath = new Map(mappings.map((m) => [m.viewPath, m]));
|
|
6792
|
+
const props = this._type.type.kind === "object" ? this._type.type.props : void 0;
|
|
6793
|
+
const state = /* @__PURE__ */ new Map();
|
|
6794
|
+
const visit = (m, chain) => {
|
|
6795
|
+
if (state.get(m.viewPath) === "done") return;
|
|
6796
|
+
if (state.get(m.viewPath) === "visiting") fail(m.viewPath, `@db.compute depends on itself: ${[...chain, m.viewPath].join(" → ")}`);
|
|
6797
|
+
state.set(m.viewPath, "visiting");
|
|
6798
|
+
require_nested_writer.walkViewExpr(m.expr, (path) => {
|
|
6799
|
+
const leaf = byPath.get(path);
|
|
6800
|
+
if (!leaf) fail(m.viewPath, `@db.compute references "${path}", which is not a column of the view`);
|
|
6801
|
+
if (leaf.expr !== void 0) visit(leaf, [...chain, m.viewPath]);
|
|
6802
|
+
});
|
|
6803
|
+
state.set(m.viewPath, "done");
|
|
6804
|
+
if (require_nested_writer.viewExprNullable(m.expr, (path) => {
|
|
6805
|
+
const leaf = byPath.get(path);
|
|
6806
|
+
return leaf.expr === void 0 ? props?.get(path)?.optional === true : leaf.nullable === true;
|
|
6807
|
+
})) m.nullable = true;
|
|
6808
|
+
};
|
|
6809
|
+
for (const m of computed) visit(m, []);
|
|
6810
|
+
}
|
|
6092
6811
|
/** One view column over one physical source (a column or a JSON leaf). */
|
|
6093
6812
|
_leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable) {
|
|
6094
6813
|
const mapping = {
|
|
@@ -6121,6 +6840,62 @@ function isAtscriptDbView(readable) {
|
|
|
6121
6840
|
return readable.isView;
|
|
6122
6841
|
}
|
|
6123
6842
|
//#endregion
|
|
6843
|
+
//#region src/schema/fk-diff.ts
|
|
6844
|
+
/** Canonical key for an FK: sorted local field names, comma-joined. */
|
|
6845
|
+
function fkKey(fields) {
|
|
6846
|
+
return [...fields].toSorted().join(",");
|
|
6847
|
+
}
|
|
6848
|
+
/**
|
|
6849
|
+
* Physical local / target column names of a desired FK (`@db.column` renames
|
|
6850
|
+
* applied) — what DDL, constraint sync, the FK diff and the snapshot compare.
|
|
6851
|
+
*/
|
|
6852
|
+
function fkColumns(fk) {
|
|
6853
|
+
return {
|
|
6854
|
+
fields: fk.physicalFields ?? fk.fields,
|
|
6855
|
+
targetFields: fk.physicalTargetFields ?? fk.targetFields
|
|
6856
|
+
};
|
|
6857
|
+
}
|
|
6858
|
+
/**
|
|
6859
|
+
* Compares desired FK constraints against stored snapshot to detect
|
|
6860
|
+
* additions, removals, and property changes (target table, target fields,
|
|
6861
|
+
* onDelete, onUpdate).
|
|
6862
|
+
*/
|
|
6863
|
+
function computeForeignKeyDiff(desired, existingSnapshot) {
|
|
6864
|
+
const added = [];
|
|
6865
|
+
const removed = [];
|
|
6866
|
+
const changed = [];
|
|
6867
|
+
const existingByKey = /* @__PURE__ */ new Map();
|
|
6868
|
+
for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
|
|
6869
|
+
const desiredKeys = /* @__PURE__ */ new Set();
|
|
6870
|
+
for (const fk of desired.values()) {
|
|
6871
|
+
const key = fkKey(fkColumns(fk).fields);
|
|
6872
|
+
desiredKeys.add(key);
|
|
6873
|
+
const existing = existingByKey.get(key);
|
|
6874
|
+
if (!existing) added.push(fk);
|
|
6875
|
+
else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
6876
|
+
desired: fk,
|
|
6877
|
+
existing
|
|
6878
|
+
});
|
|
6879
|
+
}
|
|
6880
|
+
for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
|
|
6881
|
+
return {
|
|
6882
|
+
added,
|
|
6883
|
+
removed,
|
|
6884
|
+
changed
|
|
6885
|
+
};
|
|
6886
|
+
}
|
|
6887
|
+
/** Whether the FK diff contains any changes. */
|
|
6888
|
+
function hasForeignKeyChanges(diff) {
|
|
6889
|
+
return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
|
6890
|
+
}
|
|
6891
|
+
function fkPropertiesDiffer(desired, existing) {
|
|
6892
|
+
if (desired.targetTable !== existing.targetTable) return true;
|
|
6893
|
+
if (fkKey(fkColumns(desired).targetFields) !== fkKey(existing.targetFields)) return true;
|
|
6894
|
+
if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
|
|
6895
|
+
if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
|
|
6896
|
+
return false;
|
|
6897
|
+
}
|
|
6898
|
+
//#endregion
|
|
6124
6899
|
//#region src/schema/schema-hash.ts
|
|
6125
6900
|
/**
|
|
6126
6901
|
* The physical sources a stored view snapshot reads: its entry table and the
|
|
@@ -6187,9 +6962,9 @@ function computeTableSnapshot(readable, typeMapper, tableOptions) {
|
|
|
6187
6962
|
}))
|
|
6188
6963
|
})).toSorted((a, b) => a.key.localeCompare(b.key));
|
|
6189
6964
|
const foreignKeys = [...readable.foreignKeys.values()].map((fk) => ({
|
|
6190
|
-
fields: [...fk.fields].toSorted(),
|
|
6965
|
+
fields: [...fkColumns(fk).fields].toSorted(),
|
|
6191
6966
|
targetTable: fk.targetTable,
|
|
6192
|
-
targetFields: [...fk.targetFields].toSorted(),
|
|
6967
|
+
targetFields: [...fkColumns(fk).targetFields].toSorted(),
|
|
6193
6968
|
onDelete: fk.onDelete,
|
|
6194
6969
|
onUpdate: fk.onUpdate
|
|
6195
6970
|
})).toSorted((a, b) => a.fields.join(",").localeCompare(b.fields.join(",")));
|
|
@@ -6217,7 +6992,9 @@ function computeViewSnapshot(view) {
|
|
|
6217
6992
|
const plan = view.viewPlan;
|
|
6218
6993
|
const qualify = (ref) => view.resolveFieldRef(ref, (n) => n);
|
|
6219
6994
|
const canonical = (node) => JSON.stringify(canonicalizeQueryNode(node, qualify));
|
|
6220
|
-
const
|
|
6995
|
+
const mappings = view.getViewColumnMappings();
|
|
6996
|
+
const columnOf = new Map(mappings.map((m) => [m.viewPath, m.viewColumn]));
|
|
6997
|
+
const columns = mappings.map((m) => {
|
|
6221
6998
|
const col = {
|
|
6222
6999
|
column: m.viewColumn,
|
|
6223
7000
|
sourceTable: m.sourceTable,
|
|
@@ -6230,6 +7007,7 @@ function computeViewSnapshot(view) {
|
|
|
6230
7007
|
if (m.aggFn) col.aggFn = m.aggFn;
|
|
6231
7008
|
if (m.aggField) col.aggField = m.aggField;
|
|
6232
7009
|
if (m.aggFilter) col.aggFilter = canonical(m.aggFilter);
|
|
7010
|
+
if (m.expr !== void 0) col.expr = JSON.stringify(canonicalizeViewExpr(m.expr, (path) => columnOf.get(path) ?? path));
|
|
6233
7011
|
return col;
|
|
6234
7012
|
}).toSorted((a, b) => a.column < b.column ? -1 : a.column > b.column ? 1 : 0);
|
|
6235
7013
|
const result = {
|
|
@@ -6243,6 +7021,7 @@ function computeViewSnapshot(view) {
|
|
|
6243
7021
|
condition: canonical(j.condition)
|
|
6244
7022
|
};
|
|
6245
7023
|
if (j.kind === "left") join.kind = "left";
|
|
7024
|
+
if (j.first) join.order = JSON.stringify(j.first.order.map((item) => [qualify(item.ref), item.desc ? -1 : 1]));
|
|
6246
7025
|
return join;
|
|
6247
7026
|
}),
|
|
6248
7027
|
columns
|
|
@@ -6281,6 +7060,20 @@ function canonicalizeQueryNode(node, qualify) {
|
|
|
6281
7060
|
return out;
|
|
6282
7061
|
}
|
|
6283
7062
|
/**
|
|
7063
|
+
* Converts a `@db.compute` expression into a serializable structure whose
|
|
7064
|
+
* JSON is a stable function of its meaning: leaves become the view column
|
|
7065
|
+
* they read (`column(path)`), literals `{ n }`, operations `{ op, a }`.
|
|
7066
|
+
* @since 0.1.147
|
|
7067
|
+
*/
|
|
7068
|
+
function canonicalizeViewExpr(expr, column) {
|
|
7069
|
+
if (typeof expr === "number") return { n: expr };
|
|
7070
|
+
if ("field" in expr) return { c: column(expr.field) };
|
|
7071
|
+
return {
|
|
7072
|
+
op: expr.op,
|
|
7073
|
+
a: expr.args.map((arg) => canonicalizeViewExpr(arg, column))
|
|
7074
|
+
};
|
|
7075
|
+
}
|
|
7076
|
+
/**
|
|
6284
7077
|
* Computes a deterministic hash string from multiple table snapshots.
|
|
6285
7078
|
* Uses FNV-1a for speed — not cryptographic, just needs stability + collision resistance.
|
|
6286
7079
|
*/
|
|
@@ -6344,52 +7137,6 @@ function fnv1a(str) {
|
|
|
6344
7137
|
return Math.trunc(hash).toString(16).padStart(8, "0");
|
|
6345
7138
|
}
|
|
6346
7139
|
//#endregion
|
|
6347
|
-
//#region src/schema/fk-diff.ts
|
|
6348
|
-
/** Canonical key for an FK: sorted local field names, comma-joined. */
|
|
6349
|
-
function fkKey(fields) {
|
|
6350
|
-
return [...fields].toSorted().join(",");
|
|
6351
|
-
}
|
|
6352
|
-
/**
|
|
6353
|
-
* Compares desired FK constraints against stored snapshot to detect
|
|
6354
|
-
* additions, removals, and property changes (target table, target fields,
|
|
6355
|
-
* onDelete, onUpdate).
|
|
6356
|
-
*/
|
|
6357
|
-
function computeForeignKeyDiff(desired, existingSnapshot) {
|
|
6358
|
-
const added = [];
|
|
6359
|
-
const removed = [];
|
|
6360
|
-
const changed = [];
|
|
6361
|
-
const existingByKey = /* @__PURE__ */ new Map();
|
|
6362
|
-
for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
|
|
6363
|
-
const desiredKeys = /* @__PURE__ */ new Set();
|
|
6364
|
-
for (const fk of desired.values()) {
|
|
6365
|
-
const key = fkKey(fk.fields);
|
|
6366
|
-
desiredKeys.add(key);
|
|
6367
|
-
const existing = existingByKey.get(key);
|
|
6368
|
-
if (!existing) added.push(fk);
|
|
6369
|
-
else if (fkPropertiesDiffer(fk, existing)) changed.push({
|
|
6370
|
-
desired: fk,
|
|
6371
|
-
existing
|
|
6372
|
-
});
|
|
6373
|
-
}
|
|
6374
|
-
for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
|
|
6375
|
-
return {
|
|
6376
|
-
added,
|
|
6377
|
-
removed,
|
|
6378
|
-
changed
|
|
6379
|
-
};
|
|
6380
|
-
}
|
|
6381
|
-
/** Whether the FK diff contains any changes. */
|
|
6382
|
-
function hasForeignKeyChanges(diff) {
|
|
6383
|
-
return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
|
|
6384
|
-
}
|
|
6385
|
-
function fkPropertiesDiffer(desired, existing) {
|
|
6386
|
-
if (desired.targetTable !== existing.targetTable) return true;
|
|
6387
|
-
if (fkKey(desired.targetFields) !== fkKey(existing.targetFields)) return true;
|
|
6388
|
-
if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
|
|
6389
|
-
if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
|
|
6390
|
-
return false;
|
|
6391
|
-
}
|
|
6392
|
-
//#endregion
|
|
6393
7140
|
//#region src/schema/column-diff.ts
|
|
6394
7141
|
/**
|
|
6395
7142
|
* Why a derived column (desired, live, or both) must be dropped and re-added,
|
|
@@ -6530,6 +7277,12 @@ Object.defineProperty(exports, "ALL_BUCKET_UNITS", {
|
|
|
6530
7277
|
return ALL_BUCKET_UNITS;
|
|
6531
7278
|
}
|
|
6532
7279
|
});
|
|
7280
|
+
Object.defineProperty(exports, "ALL_VIEW_CAPABILITIES", {
|
|
7281
|
+
enumerable: true,
|
|
7282
|
+
get: function() {
|
|
7283
|
+
return ALL_VIEW_CAPABILITIES;
|
|
7284
|
+
}
|
|
7285
|
+
});
|
|
6533
7286
|
Object.defineProperty(exports, "ApplicationIntegrity", {
|
|
6534
7287
|
enumerable: true,
|
|
6535
7288
|
get: function() {
|
|
@@ -6722,6 +7475,12 @@ Object.defineProperty(exports, "decomposePatch", {
|
|
|
6722
7475
|
return decomposePatch;
|
|
6723
7476
|
}
|
|
6724
7477
|
});
|
|
7478
|
+
Object.defineProperty(exports, "fkColumns", {
|
|
7479
|
+
enumerable: true,
|
|
7480
|
+
get: function() {
|
|
7481
|
+
return fkColumns;
|
|
7482
|
+
}
|
|
7483
|
+
});
|
|
6725
7484
|
Object.defineProperty(exports, "fkKey", {
|
|
6726
7485
|
enumerable: true,
|
|
6727
7486
|
get: function() {
|