@atscript/db 0.1.154 → 0.1.156
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.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/{buckets-CBKL_ZWE.d.mts → buckets-8mG67Mtw.d.mts} +168 -16
- package/dist/{buckets-jy8BfswO.d.cts → buckets-B4T_alUe.d.cts} +168 -16
- package/dist/{column-diff-DAIAq6XR.mjs → column-diff-DR1glykD.mjs} +419 -75
- package/dist/{column-diff-qnzx1o3c.cjs → column-diff-DpeUbhEI.cjs} +430 -74
- package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-5gX42hBt.d.mts} +2 -1
- package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-BWdQ7kRZ.d.cts} +2 -1
- package/dist/index.cjs +3 -3
- package/dist/index.d.cts +15 -9
- package/dist/index.d.mts +15 -9
- package/dist/index.mjs +3 -3
- package/dist/plugin.cjs +21 -4
- package/dist/plugin.mjs +21 -4
- package/dist/rel.d.cts +2 -2
- package/dist/rel.d.mts +2 -2
- package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-17_2f5hX.d.mts} +1 -1
- package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-BLBrLQYi.d.cts} +1 -1
- package/dist/{search-fields-DBIZKwyb.mjs → search-fields-CE8cmj9X.mjs} +1 -0
- package/dist/{search-fields-DwrsJUC1.cjs → search-fields-CT_UZ9fg.cjs} +1 -0
- package/dist/shared.cjs +2 -1
- package/dist/shared.d.cts +10 -3
- package/dist/shared.d.mts +10 -3
- package/dist/shared.mjs +2 -2
- package/dist/sync.cjs +317 -35
- package/dist/sync.d.cts +34 -5
- package/dist/sync.d.mts +34 -5
- package/dist/sync.mjs +317 -35
- package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-BGjqc2jU.mjs} +24 -6
- package/dist/{validation-utils-debIYpq0.cjs → validation-utils-CtLNlBtB.cjs} +29 -5
- package/dist/{validator-BACewoRj.mjs → validator-B3fPJjS1.mjs} +178 -10
- package/dist/{validator-BxWwh3wf.cjs → validator-B4YDLUkj.cjs} +201 -9
- package/dist/validator.cjs +1 -1
- package/dist/validator.mjs +1 -1
- package/package.json +6 -6
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { a as aggregateExpressionsNotSupported, n as CasMismatchError, r as DbError, u as spaceClosedError } from "./db-error-eSsQX7rt.mjs";
|
|
2
2
|
import { a as SOURCE_VALUE_FNS, c as isFirstLast, o as SUPPORTED_AGGREGATE_FNS, r as BASE_AGGREGATE_FNS, t as AGG_ANNOTATIONS } from "./aggregate-fns-B_sgl4mB.mjs";
|
|
3
3
|
import { A as hasRelationOp, D as containsRelationFilter, F as resolveRelationFilterTree, H as tableNameOf, I as compositeKey, J as viewExprNullable, K as isFieldRef, L as findFKEntryForRelation, M as noteRelationFilter, N as relGuardState, O as createRelationFilterHost, R as findFKForRelation, S as uniqueKeyTuple, U as computeOf, V as slicePerGroup, W as computedOperands, Y as walkViewExpr, _ as remapDeleteFkViolation, a as batchPatchNestedFrom, b as rowMatchesKey, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as enrichFkViolation, h as validateBatch, i as batchInsertNestedVia, l as batchReplaceNestedTo, m as preValidateNestedFrom, n as batchInsertNestedFrom, p as planPatchNestedTo, r as batchInsertNestedTo, s as batchPatchNestedVia, t as applyPatchNestedTo, u as batchReplaceNestedVia, v as findRowsByKeys, x as sameKey, y as pkTupleKey, z as findRemoteFK } from "./nested-writer-BOj_c_lI.mjs";
|
|
4
|
-
import { a as
|
|
4
|
+
import { a as forceNavNonOptional, c as getKeyProps, d as soleUnionMember, f as unionValueMembers, i as dbPlugin, l as columnUnionBase, n as buildPatchPartial, t as buildDbValidator, u as pathPresence } from "./validator-B3fPJjS1.mjs";
|
|
5
|
+
import { a as DERIVED_INCOMPATIBLE, n as searchMemberKind, r as numericTypeProblem, s as isJsonLeafType } from "./search-fields-CE8cmj9X.mjs";
|
|
5
6
|
import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, t as deletePath } from "./object-TkiJQ-Dp.mjs";
|
|
6
7
|
import { resolveAlias as resolveAlias$1 } from "./agg.mjs";
|
|
7
8
|
import { t as NOT_DIMENSION_REASON } from "./aggregate-rules-jdPrxqWa.mjs";
|
|
8
9
|
import { isDbFieldOp, separateCas, separateFieldOps } from "./ops.mjs";
|
|
9
|
-
import { a as forceNavNonOptional, c as getKeyProps, i as dbPlugin, n as buildPatchPartial, t as buildDbValidator } from "./validator-BACewoRj.mjs";
|
|
10
10
|
import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
|
|
11
11
|
import { ARITH_MAX_NODES, BUCKET_UNITS, isAggregateExpr, isAggregateOfExpr, isBucketExpr, isNullsPlacement, isPrimitive, isRelationOp, isSelectArithExpr, resolveAlias, resolveBuckets } from "@uniqu/core";
|
|
12
12
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
@@ -215,7 +215,7 @@ function sourceIndex(type) {
|
|
|
215
215
|
if (metadata.has("db.ignore") || navFields.has(path)) idx.unstored.add(path);
|
|
216
216
|
if (metadata.has("db.column.derived") && fieldType.ref?.field) idx.derived.set(path, fieldType.ref.field);
|
|
217
217
|
if (metadata.has("db.encrypted")) storage.push([path, false]);
|
|
218
|
-
else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array") storage.push([path, true]);
|
|
218
|
+
else if (metadata.has("db.json") || resolveDesignType(fieldType) === "array" || isStructuredMixedUnion(fieldType)) storage.push([path, true]);
|
|
219
219
|
}
|
|
220
220
|
storage.sort(([a], [b]) => a.split(".").length - b.split(".").length);
|
|
221
221
|
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);
|
|
@@ -264,8 +264,8 @@ function sourceFieldMetadata(sourceType, logicalPath) {
|
|
|
264
264
|
* aggregate's field, a predicate operand) to where it is physically stored.
|
|
265
265
|
* Internal — `AtscriptDbView.resolveRefSource` is the public entry.
|
|
266
266
|
*
|
|
267
|
-
* Relational rules (`TableMetadata`'s): the outermost `@db.json
|
|
268
|
-
* node with segments remaining is the column and the rest becomes
|
|
267
|
+
* Relational rules (`TableMetadata`'s): the outermost `@db.json`, array or
|
|
268
|
+
* structured mixed union (`isStructuredMixedUnion`) node with segments remaining is the column and the rest becomes
|
|
269
269
|
* {@link TViewSource.jsonPath}; a flattened leaf is its parent segments
|
|
270
270
|
* joined with `__` plus its `@db.column` (or segment); a top-level field is
|
|
271
271
|
* its `@db.column` or name. Nested-object adapters use the document path
|
|
@@ -297,7 +297,8 @@ function resolveViewSource(sourceType, logicalPath, nestedObjects) {
|
|
|
297
297
|
};
|
|
298
298
|
}
|
|
299
299
|
const jsonRoot = selfOrAncestor(logicalPath, idx.jsonRoots);
|
|
300
|
-
const
|
|
300
|
+
const presence = pathPresence(sourceType, logicalPath);
|
|
301
|
+
const optional = (presence === void 0 ? selfOrAncestor(logicalPath, idx.optional) !== void 0 : presence !== "required") || jsonRoot !== void 0 && jsonRoot !== logicalPath;
|
|
301
302
|
if (!node) return {
|
|
302
303
|
column: logicalPath,
|
|
303
304
|
designType: "unknown",
|
|
@@ -385,6 +386,38 @@ function relationalColumnName(path, override, flattened) {
|
|
|
385
386
|
function isNavRelation(metadata) {
|
|
386
387
|
return metadata.has("db.rel.to") || metadata.has("db.rel.from") || metadata.has("db.rel.via");
|
|
387
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* A union whose value members disagree on their design type and include a
|
|
391
|
+
* structure — `Addr | string`, or the leaf `a` of `{ a: Addr } | { a: string }`.
|
|
392
|
+
* It has no column layout of its own: relational storage keeps it as one
|
|
393
|
+
* JSON column, like `@db.json` (since 0.1.155).
|
|
394
|
+
*/
|
|
395
|
+
function isStructuredMixedUnion(type) {
|
|
396
|
+
if (type.type.kind !== "union" || resolveDesignType(type) !== "union") return false;
|
|
397
|
+
return unionValueMembers(type).members.some((member) => {
|
|
398
|
+
const kind = member.type.kind;
|
|
399
|
+
return kind === "object" || kind === "array" || kind === "tuple" || kind === "intersection";
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* The type a column of `type` is described by: the value member of a
|
|
404
|
+
* nullable union (`number.int | null` → `number.int`, so type mappers see
|
|
405
|
+
* its tags) carrying the field's own annotations and optionality, else
|
|
406
|
+
* `type` itself.
|
|
407
|
+
*/
|
|
408
|
+
function columnType(type) {
|
|
409
|
+
const member = soleUnionMember(type);
|
|
410
|
+
if (!member) return type;
|
|
411
|
+
return {
|
|
412
|
+
__is_atscript_annotated_type: true,
|
|
413
|
+
type: member.type,
|
|
414
|
+
metadata: type.metadata,
|
|
415
|
+
optional: type.optional,
|
|
416
|
+
id: member.id,
|
|
417
|
+
ref: member.ref,
|
|
418
|
+
validator: (...args) => type.validator(...args)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
388
421
|
/** Returns true if the annotated type IS the `db.geoPoint` primitive (tag-based). */
|
|
389
422
|
function isGeoPointType(fieldType) {
|
|
390
423
|
return fieldType.type.tags?.has("geoPoint") === true;
|
|
@@ -470,6 +503,12 @@ var TableMetadata = class {
|
|
|
470
503
|
/** Adapter-contributed multi-field unique keys (logical paths) — see `TMetadataOverrides.addUniqueKeys`. */
|
|
471
504
|
uniqueKeys = [];
|
|
472
505
|
defaults = /* @__PURE__ */ new Map();
|
|
506
|
+
/**
|
|
507
|
+
* Logical paths annotated with `@db.onUpdate.now` (`number.timestamp.updated`
|
|
508
|
+
* applies it): set to the current time by every update and replace.
|
|
509
|
+
* @since 0.1.156
|
|
510
|
+
*/
|
|
511
|
+
onUpdateNow = /* @__PURE__ */ new Set();
|
|
473
512
|
/** Logical path → `@db.column` override (top-level keys only on document storage). */
|
|
474
513
|
columnMap = /* @__PURE__ */ new Map();
|
|
475
514
|
dimensions = [];
|
|
@@ -554,6 +593,9 @@ var TableMetadata = class {
|
|
|
554
593
|
_alwaysAddressable;
|
|
555
594
|
_collateMap = /* @__PURE__ */ new Map();
|
|
556
595
|
_columnFromMap = /* @__PURE__ */ new Map();
|
|
596
|
+
/** The annotated type `build()` ran on — the root of {@link presence}. */
|
|
597
|
+
_rootType;
|
|
598
|
+
_presence = /* @__PURE__ */ new Map();
|
|
557
599
|
constructor(nestedObjects) {
|
|
558
600
|
this.nestedObjects = nestedObjects;
|
|
559
601
|
}
|
|
@@ -561,6 +603,72 @@ var TableMetadata = class {
|
|
|
561
603
|
return this._built;
|
|
562
604
|
}
|
|
563
605
|
/**
|
|
606
|
+
* Whether the value of the LOGICAL field `path` is always there, may be
|
|
607
|
+
* NULL / missing, or is declared by only some members of a union of
|
|
608
|
+
* objects on the way ({@link TPathPresence}); `undefined` inside an array.
|
|
609
|
+
* @since 0.1.155
|
|
610
|
+
*/
|
|
611
|
+
presence(path) {
|
|
612
|
+
if (this._presence.has(path)) return this._presence.get(path);
|
|
613
|
+
const root = this._rootType;
|
|
614
|
+
const answer = root ? pathPresence(root, path) : void 0;
|
|
615
|
+
this._presence.set(path, answer);
|
|
616
|
+
return answer;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Whether the LOGICAL field `path` can hold NULL (or be missing): it or a
|
|
620
|
+
* parent object is optional or a `| null` union, or only some members of
|
|
621
|
+
* a union of objects declare it. Inside an array: the field or a parent
|
|
622
|
+
* is optional.
|
|
623
|
+
* @since 0.1.155
|
|
624
|
+
*/
|
|
625
|
+
isNullable(path) {
|
|
626
|
+
const presence = this.presence(path);
|
|
627
|
+
if (presence !== void 0) return presence !== "required";
|
|
628
|
+
for (let p = path;;) {
|
|
629
|
+
if (this.flatMap.get(p)?.optional === true) return true;
|
|
630
|
+
const dot = p.lastIndexOf(".");
|
|
631
|
+
if (dot === -1) return false;
|
|
632
|
+
p = p.slice(0, dot);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
_objectLeaves;
|
|
636
|
+
/**
|
|
637
|
+
* The stored fields under the object field `path` that a null test on the
|
|
638
|
+
* object reads (`rewriteObjectNullTests`, since 0.1.155): its leaf columns
|
|
639
|
+
* on relational storage, its scalar / array / JSON fields on document
|
|
640
|
+
* storage (not inside an array or a JSON value). `undefined` when `path`
|
|
641
|
+
* is no stored object (a leaf, a `@db.json` / encrypted / ignored field,
|
|
642
|
+
* a navigation relation, inside a JSON value, unknown).
|
|
643
|
+
*/
|
|
644
|
+
objectLeaves(path) {
|
|
645
|
+
this._objectLeaves ??= /* @__PURE__ */ new Map();
|
|
646
|
+
if (this._objectLeaves.has(path)) return this._objectLeaves.get(path);
|
|
647
|
+
let leaves;
|
|
648
|
+
const fd = this.descriptorByPath.get(path);
|
|
649
|
+
if (findAncestorInSet(path, this.jsonValueParents) === void 0 && (this.nestedObjects ? fd !== void 0 && fd.designType === "object" && fd.storage !== "json" : this.flattenedParents.has(path) && !this.ignoredFields.has(path))) {
|
|
650
|
+
const prefix = `${path}.`;
|
|
651
|
+
leaves = [];
|
|
652
|
+
for (const [leaf, leafFd] of this.descriptorByPath) {
|
|
653
|
+
if (!leaf.startsWith(prefix) || leafFd.designType === "object") continue;
|
|
654
|
+
if (findAncestorInSet(leaf, this.jsonValueParents) !== void 0) continue;
|
|
655
|
+
if (this.presence(leaf) === void 0) continue;
|
|
656
|
+
leaves.push(leaf);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
this._objectLeaves.set(path, leaves);
|
|
660
|
+
return leaves;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Whether the column of `path` is nullable: {@link isNullable}, except
|
|
664
|
+
* inside an array (document storage only), where the field's own
|
|
665
|
+
* optionality counts.
|
|
666
|
+
*/
|
|
667
|
+
_columnNullable(path, type) {
|
|
668
|
+
const presence = this.presence(path);
|
|
669
|
+
return presence === void 0 ? type.optional === true : presence !== "required";
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
564
672
|
* {@link documentPath} over this table's `columnMap`. A `@db.column.derived`
|
|
565
673
|
* field has no stored path of its own: it maps to its source leaf.
|
|
566
674
|
*/
|
|
@@ -604,6 +712,7 @@ var TableMetadata = class {
|
|
|
604
712
|
build(type, adapter, logger) {
|
|
605
713
|
if (this._built) return;
|
|
606
714
|
adapter.onBeforeFlatten?.(type);
|
|
715
|
+
this._rootType = type;
|
|
607
716
|
const collected = [];
|
|
608
717
|
this.flatMap = flattenAnnotatedType(type, {
|
|
609
718
|
topLevelArrayTag: adapter.getTopLevelArrayTag?.() ?? "db.__topLevelArray",
|
|
@@ -662,10 +771,13 @@ var TableMetadata = class {
|
|
|
662
771
|
* into the synthetic flat entry of the prop's path, so a member's
|
|
663
772
|
* annotations would reach the column (DDL type, size, default). Since
|
|
664
773
|
* atscript 0.1.103 a built-in primitive member carries its built-in
|
|
665
|
-
* annotations (`
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
774
|
+
* annotations (`string.char` → `@expect.maxLength 1`). Rebuild such an
|
|
775
|
+
* entry from the prop's own annotations plus the non-`db.*` annotations of
|
|
776
|
+
* named members (aliases, interfaces), so the column is described as before
|
|
777
|
+
* 0.1.103. Exception: the one non-`null` member of `T | null` is the
|
|
778
|
+
* column's type, so its `db.*` annotations apply too — `createdAt:
|
|
779
|
+
* number.timestamp.created | null` gets `@db.default.now` like
|
|
780
|
+
* `createdAt: number.timestamp.created`.
|
|
669
781
|
*/
|
|
670
782
|
_dropMemberMetadata(collected) {
|
|
671
783
|
const byPath = /* @__PURE__ */ new Map();
|
|
@@ -678,12 +790,16 @@ var TableMetadata = class {
|
|
|
678
790
|
if (!entries.some((e) => e.member)) continue;
|
|
679
791
|
const flat = this.flatMap.get(path);
|
|
680
792
|
if (!flat?.__flat_union) continue;
|
|
793
|
+
const prop = entries.find((e) => !e.member);
|
|
794
|
+
const base = prop && columnUnionBase(prop.type);
|
|
681
795
|
const target = flat.metadata;
|
|
682
796
|
target.clear();
|
|
683
797
|
for (const entry of entries) {
|
|
684
|
-
|
|
685
|
-
|
|
798
|
+
const inline = entry.member && isInlinePrimitive(entry.type);
|
|
799
|
+
const isBase = entry.member && entry.type === base;
|
|
800
|
+
for (const [key, value] of entry.metadata) if (!entry.member || (key.startsWith("db.") ? isBase : !inline)) target.set(key, value);
|
|
686
801
|
}
|
|
802
|
+
if (base && prop) prop.metadata = target;
|
|
687
803
|
}
|
|
688
804
|
}
|
|
689
805
|
/**
|
|
@@ -706,7 +822,7 @@ var TableMetadata = class {
|
|
|
706
822
|
* Scans `@db.*` and `@meta.id` annotations on a field during flattening.
|
|
707
823
|
*/
|
|
708
824
|
_scanGenericAnnotations(fieldName, fieldType, metadata, logger) {
|
|
709
|
-
if (metadata.has("meta.id")) {
|
|
825
|
+
if (metadata.has("meta.id") && !fieldName.includes(".")) {
|
|
710
826
|
this.primaryKeys.push(fieldName);
|
|
711
827
|
this.originalMetaIdFields.push(fieldName);
|
|
712
828
|
}
|
|
@@ -717,6 +833,7 @@ var TableMetadata = class {
|
|
|
717
833
|
if (columnFrom && renamable) this._columnFromMap.set(fieldName, columnFrom);
|
|
718
834
|
const resolvedDefault = resolveDefaultFromMetadata(metadata);
|
|
719
835
|
if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
|
|
836
|
+
if (metadata.has("db.onUpdate.now")) this.onUpdateNow.add(fieldName);
|
|
720
837
|
if (metadata.has("db.ignore")) this.ignoredFields.add(fieldName);
|
|
721
838
|
if (isNavRelation(metadata)) {
|
|
722
839
|
this.navFields.add(fieldName);
|
|
@@ -886,6 +1003,7 @@ var TableMetadata = class {
|
|
|
886
1003
|
if (metadata.has("db.search.vector") || metadata.has("db.search.filter")) reject("@db.search.*", "search over ciphertext is impossible");
|
|
887
1004
|
if (metadata.has("db.mongo.search.text") || metadata.has("db.mongo.search.autocomplete")) reject("@db.mongo.search.*", "Atlas Search over ciphertext is impossible");
|
|
888
1005
|
if (metadata.has("db.column.version")) reject("@db.column.version", "the OCC filter needs cleartext equality");
|
|
1006
|
+
if (metadata.has("db.onUpdate.now")) reject("@db.onUpdate.now", "the update time is set after the encryption transform");
|
|
889
1007
|
if (metadata.has("db.default.increment") || metadata.has("db.default.now")) reject("@db.default.increment / @db.default.now", "engine-side defaults bypass the encryption transform");
|
|
890
1008
|
if (metadata.get("db.patch.strategy") === "merge") reject("@db.patch.strategy \"merge\"", "ciphertext is opaque — partial merges would silently drop omitted keys");
|
|
891
1009
|
}
|
|
@@ -969,7 +1087,7 @@ var TableMetadata = class {
|
|
|
969
1087
|
const isJson = this.jsonFields.has(path);
|
|
970
1088
|
const isArray = designType === "array";
|
|
971
1089
|
const isObject = designType === "object";
|
|
972
|
-
if (isArray) this.jsonFields.add(path);
|
|
1090
|
+
if (isArray || isStructuredMixedUnion(type)) this.jsonFields.add(path);
|
|
973
1091
|
else if (isObject && isJson) {} else if (isObject && !isJson) this.flattenedParents.add(path);
|
|
974
1092
|
}
|
|
975
1093
|
for (const ignoredField of this.ignoredFields) if (this.flattenedParents.has(ignoredField)) {
|
|
@@ -1071,6 +1189,7 @@ var TableMetadata = class {
|
|
|
1071
1189
|
const isJson = this.jsonFields.has(path);
|
|
1072
1190
|
const isFlattened = !skipFlattening && findAncestorInSet(path, this.flattenedParents) !== void 0;
|
|
1073
1191
|
const designType = isEncrypted ? "string" : isJson ? "json" : resolveDesignType(type);
|
|
1192
|
+
const colType = columnType(type);
|
|
1074
1193
|
let storage;
|
|
1075
1194
|
if (skipFlattening) storage = "column";
|
|
1076
1195
|
else if (isEncrypted) storage = isFlattened ? "flattened" : "column";
|
|
@@ -1089,10 +1208,10 @@ var TableMetadata = class {
|
|
|
1089
1208
|
if (quantityRef) this.quantityRefByField.set(path, quantityRef);
|
|
1090
1209
|
descriptors.push({
|
|
1091
1210
|
path,
|
|
1092
|
-
type,
|
|
1211
|
+
type: colType,
|
|
1093
1212
|
physicalName,
|
|
1094
1213
|
designType,
|
|
1095
|
-
optional: type.optional === true,
|
|
1214
|
+
optional: skipFlattening ? type.optional === true : this._columnNullable(path, type),
|
|
1096
1215
|
isPrimaryKey: this.primaryKeys.includes(path),
|
|
1097
1216
|
ignored: this.ignoredFields.has(path),
|
|
1098
1217
|
defaultValue: this.defaults.get(path),
|
|
@@ -1106,7 +1225,7 @@ var TableMetadata = class {
|
|
|
1106
1225
|
unitCode,
|
|
1107
1226
|
unitRefField,
|
|
1108
1227
|
encrypted: isEncrypted || underEncrypted || void 0,
|
|
1109
|
-
isGeoPoint: isGeoPointType(
|
|
1228
|
+
isGeoPoint: isGeoPointType(colType) || void 0,
|
|
1110
1229
|
derived: this.derivedFields.get(path),
|
|
1111
1230
|
computed: computedMeta(rootType, path)
|
|
1112
1231
|
});
|
|
@@ -1234,7 +1353,7 @@ var TableMetadata = class {
|
|
|
1234
1353
|
for (const index of this.indexes.values()) for (const field of index.fields) {
|
|
1235
1354
|
const ftype = this.flatMap.get(field.name);
|
|
1236
1355
|
if (ftype) {
|
|
1237
|
-
field.optional =
|
|
1356
|
+
field.optional = this._columnNullable(field.name, ftype);
|
|
1238
1357
|
field.designType = ftype.type.tags?.has("objectId") ? "objectId" : resolveDesignType(ftype);
|
|
1239
1358
|
}
|
|
1240
1359
|
field.name = this.physicalPath(field.name);
|
|
@@ -1647,6 +1766,78 @@ function rewriteIntegerRegex(filter, meta) {
|
|
|
1647
1766
|
return out ?? filter;
|
|
1648
1767
|
}
|
|
1649
1768
|
//#endregion
|
|
1769
|
+
//#region src/query/object-null.ts
|
|
1770
|
+
/**
|
|
1771
|
+
* Whether a filter entry's value only asks if the value is there: `null`,
|
|
1772
|
+
* `{ $eq: null }`, `{ $ne: null }`, `{ $exists: true | false }` (and
|
|
1773
|
+
* combinations of them).
|
|
1774
|
+
*/
|
|
1775
|
+
function isNullTest(value) {
|
|
1776
|
+
if (value === null) return true;
|
|
1777
|
+
if (!isPlainObject(value)) return false;
|
|
1778
|
+
const ops = Object.entries(value);
|
|
1779
|
+
return ops.length > 0 && ops.every(([op, operand]) => op === "$exists" ? typeof operand === "boolean" : (op === "$eq" || op === "$ne") && operand === null);
|
|
1780
|
+
}
|
|
1781
|
+
/**
|
|
1782
|
+
* The leaf form of one null test on an object (see
|
|
1783
|
+
* {@link rewriteObjectNullTests}) as `$and` members: absent — `$exists: false`
|
|
1784
|
+
* on every leaf; present — one `$or` of `$exists: true` per leaf.
|
|
1785
|
+
*/
|
|
1786
|
+
function objectTest(value, leaves) {
|
|
1787
|
+
const ops = value === null ? { $eq: null } : value;
|
|
1788
|
+
const out = [];
|
|
1789
|
+
for (const [op, operand] of Object.entries(ops)) {
|
|
1790
|
+
const present = op === "$exists" ? operand : op === "$ne";
|
|
1791
|
+
const tests = leaves.map((leaf) => ({ [leaf]: { $exists: present } }));
|
|
1792
|
+
if (present) out.push({ $or: tests });
|
|
1793
|
+
else out.push(...tests);
|
|
1794
|
+
}
|
|
1795
|
+
return out;
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* A null test on a stored object (since 0.1.155) — `{ address: null }`,
|
|
1799
|
+
* `{ address: { $ne: null } }`, `{ address: { $exists: … } }` — on the
|
|
1800
|
+
* object's leaves: the object is null when none of its fields holds a value
|
|
1801
|
+
* (`$exists: false` on every leaf), present when one does. Relational
|
|
1802
|
+
* storage has no column for the object (an all-NULL row reads back as
|
|
1803
|
+
* `null`); document storage answers the same, so an object stored as `{}`
|
|
1804
|
+
* or with only null fields counts as null everywhere. LOGICAL filter;
|
|
1805
|
+
* copy-on-write (the same object comes back when nothing changed).
|
|
1806
|
+
*/
|
|
1807
|
+
function rewriteObjectNullTests(filter, meta) {
|
|
1808
|
+
if (!isPlainObject(filter)) return filter;
|
|
1809
|
+
let out;
|
|
1810
|
+
const extra = [];
|
|
1811
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
1812
|
+
let next = value;
|
|
1813
|
+
if (key === "$and" || key === "$or") {
|
|
1814
|
+
if (Array.isArray(value)) {
|
|
1815
|
+
const mapped = value.map((f) => rewriteObjectNullTests(f, meta));
|
|
1816
|
+
if (mapped.some((m, i) => m !== value[i])) next = mapped;
|
|
1817
|
+
}
|
|
1818
|
+
} else if (key === "$not") next = rewriteObjectNullTests(value, meta);
|
|
1819
|
+
else if (!key.startsWith("$") && isNullTest(value)) {
|
|
1820
|
+
const leaves = meta.objectLeaves(key);
|
|
1821
|
+
if (leaves && leaves.length > 0) {
|
|
1822
|
+
extra.push(...objectTest(value, leaves));
|
|
1823
|
+
out ??= { ...filter };
|
|
1824
|
+
delete out[key];
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
if (next !== value) {
|
|
1829
|
+
out ??= { ...filter };
|
|
1830
|
+
out[key] = next;
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
if (!out) return filter;
|
|
1834
|
+
if (extra.length > 0) {
|
|
1835
|
+
const and = out.$and;
|
|
1836
|
+
out.$and = [...Array.isArray(and) ? and : [], ...extra];
|
|
1837
|
+
}
|
|
1838
|
+
return out;
|
|
1839
|
+
}
|
|
1840
|
+
//#endregion
|
|
1650
1841
|
//#region src/query/aggregate-expr.ts
|
|
1651
1842
|
/**
|
|
1652
1843
|
* Output aliases of the computed `$select` entries of a grouped query —
|
|
@@ -2298,7 +2489,7 @@ var FieldMappingStrategy = class {
|
|
|
2298
2489
|
* translate predicate operands at deeper levels).
|
|
2299
2490
|
*/
|
|
2300
2491
|
translateFilter(filter, meta, depth = 0) {
|
|
2301
|
-
filter = rewriteIntegerRegex(filter, meta);
|
|
2492
|
+
filter = rewriteObjectNullTests(rewriteIntegerRegex(filter, meta), meta);
|
|
2302
2493
|
const has = containsRelationFilter(filter);
|
|
2303
2494
|
const resolved = has ? resolveRelationFilterTree(filter, meta, depth) : filter;
|
|
2304
2495
|
return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
|
|
@@ -2640,13 +2831,13 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2640
2831
|
current = created;
|
|
2641
2832
|
} else current = next;
|
|
2642
2833
|
}
|
|
2643
|
-
current[col.last] = value;
|
|
2834
|
+
if (!col.partial || value !== null && value !== void 0) current[col.last] = value;
|
|
2644
2835
|
}
|
|
2645
2836
|
for (const parent of plan.parents) collapseNullParent(result, parent);
|
|
2646
2837
|
return result;
|
|
2647
2838
|
}
|
|
2648
2839
|
translateQuery(query, meta) {
|
|
2649
|
-
const logical = rewriteIntegerRegex(query.filter, meta);
|
|
2840
|
+
const logical = rewriteObjectNullTests(rewriteIntegerRegex(query.filter, meta), meta);
|
|
2650
2841
|
const has = containsRelationFilter(logical);
|
|
2651
2842
|
const filter = has ? resolveRelationFilterTree(logical, meta, 0) : logical;
|
|
2652
2843
|
if (!meta.requiresMappings) {
|
|
@@ -2701,7 +2892,11 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2701
2892
|
return result;
|
|
2702
2893
|
}
|
|
2703
2894
|
prepareForWrite(payload, meta, adapter) {
|
|
2704
|
-
if (meta.requiresMappings && !meta.onlyColumnRenames)
|
|
2895
|
+
if (meta.requiresMappings && !meta.onlyColumnRenames) {
|
|
2896
|
+
const flat = this.flattenWritePayload(payload, meta, adapter);
|
|
2897
|
+
nullAbsentNested(payload, flat, meta);
|
|
2898
|
+
return this.formatWriteValues(flat, meta);
|
|
2899
|
+
}
|
|
2705
2900
|
const data = { ...payload };
|
|
2706
2901
|
this.prepareCommon(data, meta, adapter);
|
|
2707
2902
|
if (meta.onlyColumnRenames) {
|
|
@@ -2728,7 +2923,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2728
2923
|
const suffix = operatorMatch ? operatorMatch[2] : "";
|
|
2729
2924
|
const fd = meta.leafByLogical.get(basePath);
|
|
2730
2925
|
const finalKey = (fd?.physicalName ?? basePath) + suffix;
|
|
2731
|
-
if (fd?.storage === "json" &&
|
|
2926
|
+
if (fd?.storage === "json" && value !== null && value !== void 0 && !suffix) result[finalKey] = JSON.stringify(value);
|
|
2732
2927
|
else result[finalKey] = value;
|
|
2733
2928
|
}
|
|
2734
2929
|
return this.formatWriteValues(result, meta);
|
|
@@ -2808,6 +3003,49 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
|
|
|
2808
3003
|
return result;
|
|
2809
3004
|
}
|
|
2810
3005
|
};
|
|
3006
|
+
const nestedColumns = /* @__PURE__ */ new WeakMap();
|
|
3007
|
+
function nestedColumnsFor(meta) {
|
|
3008
|
+
let list = nestedColumns.get(meta);
|
|
3009
|
+
if (list === void 0) {
|
|
3010
|
+
list = [];
|
|
3011
|
+
for (const [physical, fd] of meta.leafByPhysical) {
|
|
3012
|
+
if (fd.derived || fd.isPrimaryKey || fd.storage === "column" || !fd.path.includes(".")) continue;
|
|
3013
|
+
const segs = fd.path.split(".");
|
|
3014
|
+
list.push({
|
|
3015
|
+
physical,
|
|
3016
|
+
parents: segs.slice(0, -1),
|
|
3017
|
+
partial: meta.presence(fd.path) === "partial"
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
nestedColumns.set(meta, list);
|
|
3021
|
+
}
|
|
3022
|
+
return list;
|
|
3023
|
+
}
|
|
3024
|
+
/**
|
|
3025
|
+
* Every write through `prepareForWrite` stores a whole row (insert, replace).
|
|
3026
|
+
* A column the flattened row leaves out would take its column `DEFAULT` —
|
|
3027
|
+
* a `now` default below an absent optional object would make the object
|
|
3028
|
+
* reappear on read, and one of another union member's leaves would add a
|
|
3029
|
+
* field to the stored member. So a column below an absent / `null` parent
|
|
3030
|
+
* object, or one only some union members declare that the written member
|
|
3031
|
+
* leaves out, is written as `NULL` (since 0.1.155), as the document adapters
|
|
3032
|
+
* store such a row.
|
|
3033
|
+
*/
|
|
3034
|
+
function nullAbsentNested(payload, flat, meta) {
|
|
3035
|
+
for (const col of nestedColumnsFor(meta)) {
|
|
3036
|
+
if (col.physical in flat) continue;
|
|
3037
|
+
let absent = false;
|
|
3038
|
+
let current = payload;
|
|
3039
|
+
for (const seg of col.parents) {
|
|
3040
|
+
current = current[seg];
|
|
3041
|
+
if (current === null || current === void 0 || typeof current !== "object") {
|
|
3042
|
+
absent = true;
|
|
3043
|
+
break;
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
3046
|
+
if (absent || col.partial) flat[col.physical] = null;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
2811
3049
|
/**
|
|
2812
3050
|
* Per-table read plan of {@link RelationalFieldMapper.reconstructFromRead}:
|
|
2813
3051
|
* the per-column decisions (coercion, JSON, nesting — the dot-path pre-split)
|
|
@@ -2827,12 +3065,14 @@ function compileReadPlan(meta) {
|
|
|
2827
3065
|
const fromFmts = meta.fromStorageFormatters;
|
|
2828
3066
|
const columns = /* @__PURE__ */ new Map();
|
|
2829
3067
|
for (const [physical, fd] of meta.leafByPhysical) {
|
|
2830
|
-
const
|
|
3068
|
+
const nested = (fd.storage === "json" || fd.storage === "flattened") && fd.path.includes(".");
|
|
3069
|
+
const segs = nested ? fd.path.split(".") : void 0;
|
|
2831
3070
|
columns.set(physical, {
|
|
2832
3071
|
path: fd.path,
|
|
2833
3072
|
parents: segs?.slice(0, -1),
|
|
2834
3073
|
last: segs ? segs[segs.length - 1] : fd.path,
|
|
2835
3074
|
json: fd.storage === "json",
|
|
3075
|
+
partial: nested && meta.presence(fd.path) === "partial",
|
|
2836
3076
|
coerce: fd.designType === "boolean" ? toBool : fd.designType === "decimal" ? toDecimalString : void 0,
|
|
2837
3077
|
fromFmt: fromFmts?.get(physical)
|
|
2838
3078
|
});
|
|
@@ -2843,15 +3083,16 @@ function compileReadPlan(meta) {
|
|
|
2843
3083
|
parents.push({
|
|
2844
3084
|
ancestors: segs.slice(0, -1),
|
|
2845
3085
|
last: segs[segs.length - 1],
|
|
2846
|
-
|
|
3086
|
+
presence: meta.presence(parentPath) ?? "required"
|
|
2847
3087
|
});
|
|
2848
3088
|
}
|
|
3089
|
+
parents.sort((a, b) => b.ancestors.length - a.ancestors.length);
|
|
2849
3090
|
return {
|
|
2850
3091
|
columns,
|
|
2851
3092
|
parents
|
|
2852
3093
|
};
|
|
2853
3094
|
}
|
|
2854
|
-
/** If every child of a flattened parent is null / undefined, collapse it (
|
|
3095
|
+
/** If every child of a flattened parent is null / undefined, collapse it (see {@link TReadParent}). */
|
|
2855
3096
|
function collapseNullParent(obj, parent) {
|
|
2856
3097
|
let current = obj;
|
|
2857
3098
|
const ancestors = parent.ancestors;
|
|
@@ -2866,7 +3107,8 @@ function collapseNullParent(obj, parent) {
|
|
|
2866
3107
|
const v = parentObj[k];
|
|
2867
3108
|
if (v !== null && v !== void 0) return;
|
|
2868
3109
|
}
|
|
2869
|
-
|
|
3110
|
+
if (parent.presence === "partial") delete current[parent.last];
|
|
3111
|
+
else current[parent.last] = parent.presence === "nullable" ? null : {};
|
|
2870
3112
|
}
|
|
2871
3113
|
/**
|
|
2872
3114
|
* Per-table write plan of the relational flatten: each logical path's
|
|
@@ -3308,6 +3550,10 @@ function collectQueryPaths(query, aggregate) {
|
|
|
3308
3550
|
path,
|
|
3309
3551
|
predicate,
|
|
3310
3552
|
relation: relationOpsOf(value)
|
|
3553
|
+
} : isNullTest(value) ? {
|
|
3554
|
+
path,
|
|
3555
|
+
predicate,
|
|
3556
|
+
nullTest: true
|
|
3311
3557
|
} : {
|
|
3312
3558
|
path,
|
|
3313
3559
|
predicate
|
|
@@ -3407,10 +3653,11 @@ function pathSourceOf(meta) {
|
|
|
3407
3653
|
* `FieldCapabilityIndex`, with `$with` hints and leaf lists) is what clients
|
|
3408
3654
|
* see and is authoritative — the HTTP gate always answers first.
|
|
3409
3655
|
*/
|
|
3410
|
-
function guardPath(meta, adapter, path, op, predicate = "compare") {
|
|
3656
|
+
function guardPath(meta, adapter, path, op, predicate = "compare", nullTest = false) {
|
|
3411
3657
|
const verb = OP_VERB[op];
|
|
3412
3658
|
const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
|
|
3413
3659
|
if (predicate === "relation" && kind !== "nav" && kind !== "unknown") throw pathError(path, `"$some" / "$none" are only valid on a navigation relation — "${path}" is not one`);
|
|
3660
|
+
if (op === "filter" && nullTest && meta.objectLeaves(path)?.length) return;
|
|
3414
3661
|
switch (kind) {
|
|
3415
3662
|
case "nav":
|
|
3416
3663
|
if (op === "filter" && predicate === "relation" && parent === void 0) return;
|
|
@@ -3495,7 +3742,7 @@ function guardPaths(meta, adapter, query, aggregate = false, state) {
|
|
|
3495
3742
|
if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
|
|
3496
3743
|
let relState = state;
|
|
3497
3744
|
for (const ref of refs.filter) {
|
|
3498
|
-
guardPath(meta, adapter, ref.path, "filter", ref.predicate);
|
|
3745
|
+
guardPath(meta, adapter, ref.path, "filter", ref.predicate, ref.nullTest);
|
|
3499
3746
|
if (ref.predicate === "relation") guardRelationRef(meta, adapter, ref, relState ??= relGuardState());
|
|
3500
3747
|
}
|
|
3501
3748
|
for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
|
|
@@ -3637,10 +3884,12 @@ var NullsSource = class {
|
|
|
3637
3884
|
}
|
|
3638
3885
|
/**
|
|
3639
3886
|
* Whether ordering by the LOGICAL field `path` can meet NULL (or a missing
|
|
3640
|
-
* value): the field
|
|
3641
|
-
*
|
|
3642
|
-
*
|
|
3643
|
-
*
|
|
3887
|
+
* value): the field can hold NULL (`TableMetadata.isNullable` — it or a
|
|
3888
|
+
* parent object is optional or a `| null` union, or only some members of a
|
|
3889
|
+
* union of objects declare it), the field is a derived column, it is not a
|
|
3890
|
+
* known field, or the source is a view. A `$nulls` entry on any other
|
|
3891
|
+
* field changes nothing and is dropped, so the plain `ORDER BY` keeps
|
|
3892
|
+
* using the index.
|
|
3644
3893
|
*/
|
|
3645
3894
|
nullable(path) {
|
|
3646
3895
|
let answer = this._nullable.get(path);
|
|
@@ -3654,12 +3903,7 @@ var NullsSource = class {
|
|
|
3654
3903
|
const { meta } = this._init;
|
|
3655
3904
|
const fd = meta.descriptorByPath.get(path);
|
|
3656
3905
|
if (!fd || fd.derived) return true;
|
|
3657
|
-
|
|
3658
|
-
if (meta.flatMap.get(p)?.optional === true) return true;
|
|
3659
|
-
const dot = p.lastIndexOf(".");
|
|
3660
|
-
if (dot === -1) return false;
|
|
3661
|
-
p = p.slice(0, dot);
|
|
3662
|
-
}
|
|
3906
|
+
return meta.isNullable(path);
|
|
3663
3907
|
}
|
|
3664
3908
|
/**
|
|
3665
3909
|
* The effective `$nulls` of the `$sort` keys of one read: the request's
|
|
@@ -3774,7 +4018,9 @@ function geoIndexNotFoundMessage(tableName, indexName) {
|
|
|
3774
4018
|
* otherwise trips up every adapter author.
|
|
3775
4019
|
*
|
|
3776
4020
|
* For union types (e.g., from flattened `{...} | {...}` objects):
|
|
3777
|
-
* -
|
|
4021
|
+
* - `null` / `undefined` members are left out (since 0.1.155): `T | null`
|
|
4022
|
+
* resolves to the design type of `T`
|
|
4023
|
+
* - If all other members resolve to the same type → returns that type (strong type)
|
|
3778
4024
|
* - If members disagree → returns `'union'` (out of scope for type management)
|
|
3779
4025
|
*/
|
|
3780
4026
|
function resolveDesignType(fieldType) {
|
|
@@ -3782,9 +4028,9 @@ function resolveDesignType(fieldType) {
|
|
|
3782
4028
|
if (fieldType.type.kind === "object") return "object";
|
|
3783
4029
|
if (fieldType.type.kind === "array") return "array";
|
|
3784
4030
|
if (fieldType.type.kind === "union") {
|
|
3785
|
-
const
|
|
3786
|
-
if (
|
|
3787
|
-
const resolved =
|
|
4031
|
+
const { members } = unionValueMembers(fieldType);
|
|
4032
|
+
if (members.length > 0) {
|
|
4033
|
+
const resolved = members.map((item) => resolveDesignType(item));
|
|
3788
4034
|
if (resolved.every((type) => type === resolved[0])) return resolved[0];
|
|
3789
4035
|
}
|
|
3790
4036
|
return "union";
|
|
@@ -4440,21 +4686,13 @@ var AtscriptDbReadable = class {
|
|
|
4440
4686
|
_totalOrderKeys() {
|
|
4441
4687
|
if (this._totalOrderKeysCache) return this._totalOrderKeysCache;
|
|
4442
4688
|
const meta = this._meta;
|
|
4443
|
-
const nullable = (logical) => {
|
|
4444
|
-
for (let path = logical;;) {
|
|
4445
|
-
if (meta.flatMap.get(path)?.optional === true) return true;
|
|
4446
|
-
const dot = path.lastIndexOf(".");
|
|
4447
|
-
if (dot === -1) return false;
|
|
4448
|
-
path = path.slice(0, dot);
|
|
4449
|
-
}
|
|
4450
|
-
};
|
|
4451
4689
|
const sets = [];
|
|
4452
4690
|
if (meta.primaryKeys.length > 0) sets.push(meta.primaryKeys.map((f) => meta.physicalPath(f)));
|
|
4453
4691
|
for (const index of meta.indexes.values()) {
|
|
4454
4692
|
if (index.type !== "unique") continue;
|
|
4455
|
-
if (index.fields.every((f) => f.optional !== true && !
|
|
4693
|
+
if (index.fields.every((f) => f.optional !== true && !meta.isNullable(meta.physicalToPath.get(f.name) ?? f.name))) sets.push(index.fields.map((f) => f.name));
|
|
4456
4694
|
}
|
|
4457
|
-
for (const prop of meta.uniqueProps) if (!
|
|
4695
|
+
for (const prop of meta.uniqueProps) if (!meta.isNullable(prop)) sets.push([meta.physicalPath(prop)]);
|
|
4458
4696
|
this._totalOrderKeysCache = sets;
|
|
4459
4697
|
return sets;
|
|
4460
4698
|
}
|
|
@@ -6075,7 +6313,7 @@ var BaseDbAdapter = class {
|
|
|
6075
6313
|
}
|
|
6076
6314
|
/**
|
|
6077
6315
|
* Whether the table has at least one row. Schema sync uses it in the
|
|
6078
|
-
* pre-flight phase to
|
|
6316
|
+
* pre-flight phase to check a primary-key change on a populated table.
|
|
6079
6317
|
* Override with an EXISTS/LIMIT 1 probe — this default is `count() > 0`,
|
|
6080
6318
|
* a full scan on some engines, and it can only answer for the adapter's
|
|
6081
6319
|
* OWN table: for another `tableName` (or on an administrative adapter
|
|
@@ -6495,6 +6733,10 @@ function flattenPatchPayload(payload, prefix, update, meta, table) {
|
|
|
6495
6733
|
const key = prefix ? `${prefix}.${_key}` : _key;
|
|
6496
6734
|
if (primaryKeys.includes(key)) continue;
|
|
6497
6735
|
if (!(typeof value === "object" && value !== null && !Array.isArray(value))) {
|
|
6736
|
+
if (value === null && meta.flattenedParents.has(key)) {
|
|
6737
|
+
for (const lp of meta.optionalLeavesByLogicalParent.get(key) ?? []) update[lp] = null;
|
|
6738
|
+
continue;
|
|
6739
|
+
}
|
|
6498
6740
|
update[key] = value;
|
|
6499
6741
|
continue;
|
|
6500
6742
|
}
|
|
@@ -6575,10 +6817,11 @@ function assertNoVersionWrites(data, versionColumn) {
|
|
|
6575
6817
|
* Whether a logical (pre-decomposition) patch writes ONLY version-exempt
|
|
6576
6818
|
* fields (`@db.column.version.exempt`). `data` has nav fields, filter keys and
|
|
6577
6819
|
* derived fields removed and `$cas` separated. Mirrors `flattenPatchPayload`
|
|
6578
|
-
* (keep the two side by side): PK keys
|
|
6579
|
-
*
|
|
6580
|
-
* array ops, object); a non-exempt
|
|
6581
|
-
* supplied children are written);
|
|
6820
|
+
* (keep the two side by side): PK keys and `@db.onUpdate.now` fields are
|
|
6821
|
+
* skipped; an exempt path (or one under an exempt ancestor) is covered
|
|
6822
|
+
* whatever its value (scalar, field op, array ops, object); a non-exempt
|
|
6823
|
+
* merge-strategy object recurses (only the supplied children are written);
|
|
6824
|
+
* anything else is a non-exempt write.
|
|
6582
6825
|
* True iff at least one key is covered and none is not.
|
|
6583
6826
|
* @since 0.1.150
|
|
6584
6827
|
*/
|
|
@@ -6587,11 +6830,12 @@ function isVersionExemptPatch(data, table) {
|
|
|
6587
6830
|
if (meta.versionExemptPaths.size === 0) return false;
|
|
6588
6831
|
const primaryKeys = meta.primaryKeys;
|
|
6589
6832
|
const flatMap = meta.flatMap;
|
|
6833
|
+
const stamps = meta.onUpdateNow;
|
|
6590
6834
|
const walk = (obj, prefix) => {
|
|
6591
6835
|
let result = "none";
|
|
6592
6836
|
for (const [k, value] of Object.entries(obj)) {
|
|
6593
6837
|
const key = prefix ? `${prefix}.${k}` : k;
|
|
6594
|
-
if (primaryKeys.includes(key)) continue;
|
|
6838
|
+
if (primaryKeys.includes(key) || stamps.has(key)) continue;
|
|
6595
6839
|
if (meta.isVersionExemptPath(key)) {
|
|
6596
6840
|
result = "exempt";
|
|
6597
6841
|
continue;
|
|
@@ -6904,7 +7148,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6904
7148
|
const singleStatement = !ignore && this._isSingleStatementWrite(payloads, depth, guard, check);
|
|
6905
7149
|
return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
|
|
6906
7150
|
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
6907
|
-
|
|
7151
|
+
const now = Date.now();
|
|
7152
|
+
let items = payloads.map((p) => this._applyDefaults(clone(p), now));
|
|
6908
7153
|
if (ignore) this._linkToByKeyInIgnoreMode(items);
|
|
6909
7154
|
let originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
6910
7155
|
const validator = this.getValidator("insert");
|
|
@@ -6980,10 +7225,12 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
6980
7225
|
const versionColumn = this.versionColumn;
|
|
6981
7226
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
6982
7227
|
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
7228
|
+
const now = Date.now();
|
|
6983
7229
|
const items = payloads.map((p, i) => {
|
|
6984
7230
|
const c = clone(p);
|
|
6985
7231
|
expectedVersions[i] = separateCas(c, versionColumn);
|
|
6986
|
-
|
|
7232
|
+
this._stampOnUpdate(c, now, true);
|
|
7233
|
+
return this._applyDefaults(c, now);
|
|
6987
7234
|
});
|
|
6988
7235
|
const originals = canNest ? items.map((item) => ({ ...item })) : [];
|
|
6989
7236
|
const validator = this.getValidator("bulkReplace");
|
|
@@ -7067,9 +7314,11 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7067
7314
|
const versionColumn = this.versionColumn;
|
|
7068
7315
|
const expectedVersions = Array.from({ length: payloads.length });
|
|
7069
7316
|
const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
|
|
7317
|
+
const now = Date.now();
|
|
7070
7318
|
const cloned = payloads.map((p, i) => {
|
|
7071
7319
|
const c = clone(p);
|
|
7072
7320
|
expectedVersions[i] = separateCas(c, versionColumn);
|
|
7321
|
+
this._stampOnUpdate(c, now, false);
|
|
7073
7322
|
return c;
|
|
7074
7323
|
});
|
|
7075
7324
|
const validator = this.getValidator("bulkUpdate");
|
|
@@ -7116,6 +7365,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7116
7365
|
continue;
|
|
7117
7366
|
}
|
|
7118
7367
|
const updateOpts = expectedVersion === void 0 && versionColumn !== void 0 && isVersionExemptPatch(data, this) ? [KEEP_VERSION] : [];
|
|
7368
|
+
this._stampTopLevel(data, now);
|
|
7119
7369
|
let result;
|
|
7120
7370
|
if (this.adapter.supportsNativePatch()) {
|
|
7121
7371
|
const ops = separateFieldOps(data);
|
|
@@ -7275,6 +7525,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7275
7525
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
|
|
7276
7526
|
const dataCopy = _cloneWritePayload(data);
|
|
7277
7527
|
this._meta.stripDerived(dataCopy);
|
|
7528
|
+
const now = Date.now();
|
|
7529
|
+
this._stampOnUpdate(dataCopy, now, false);
|
|
7278
7530
|
const versionColumn = this.versionColumn;
|
|
7279
7531
|
if ("$cas" in dataCopy) throw new DbError("INVALID_QUERY", [{
|
|
7280
7532
|
path: "$cas",
|
|
@@ -7282,6 +7534,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7282
7534
|
}]);
|
|
7283
7535
|
if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
|
|
7284
7536
|
const updateOpts = versionColumn !== void 0 && isVersionExemptPatch(dataCopy, this) ? [KEEP_VERSION] : [];
|
|
7537
|
+
this._stampTopLevel(dataCopy, now);
|
|
7285
7538
|
await this._encryptItems([dataCopy], "patch");
|
|
7286
7539
|
const update = decomposePatch(dataCopy, this);
|
|
7287
7540
|
const ops = separateFieldOps(update);
|
|
@@ -7302,6 +7555,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7302
7555
|
this._guardMutationFilter(filter);
|
|
7303
7556
|
await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver);
|
|
7304
7557
|
const dataCopy = _cloneWritePayload(data);
|
|
7558
|
+
this._stampOnUpdate(dataCopy, Date.now(), true);
|
|
7305
7559
|
await this._encryptItems([dataCopy], "write");
|
|
7306
7560
|
return enrichFkViolation(this._meta, () => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite(dataCopy, this._meta, this.adapter)));
|
|
7307
7561
|
}
|
|
@@ -7672,27 +7926,79 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7672
7926
|
* full row). Function defaults (`now` / `uuid` / `increment` / custom) the
|
|
7673
7927
|
* adapter handles natively are NOT filled — the field stays absent so the
|
|
7674
7928
|
* engine's own default applies. The version column is never touched.
|
|
7675
|
-
|
|
7676
|
-
|
|
7929
|
+
* A nested field (`audit.createdAt`) is filled inside its object, only when
|
|
7930
|
+
* that object is present (an absent or `null` embedded object stays so), and
|
|
7931
|
+
* in every object of an array on the way; a field inside a JSON value or an
|
|
7932
|
+
* array has no column default, so the SDK fills its function default too.
|
|
7933
|
+
* Every `now` default the SDK fills gets the one time `now` (one per write
|
|
7934
|
+
* call, so `createdAt` / `updatedAt` of a new row are equal).
|
|
7935
|
+
*/
|
|
7936
|
+
_applyDefaults(data, now = Date.now()) {
|
|
7677
7937
|
const nativeFns = this.adapter.nativeDefaultFns();
|
|
7678
7938
|
const versionField = this._meta.versionField;
|
|
7679
7939
|
for (const [field, def] of this._meta.defaults.entries()) {
|
|
7680
7940
|
if (field === versionField) continue;
|
|
7681
|
-
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
break;
|
|
7687
|
-
case "uuid":
|
|
7688
|
-
data[field] = crypto.randomUUID();
|
|
7689
|
-
break;
|
|
7690
|
-
}
|
|
7941
|
+
let make;
|
|
7942
|
+
if (def.kind === "value") make = () => this._valueDefault(field, def.value);
|
|
7943
|
+
else if (def.kind === "fn" && (!nativeFns.has(def.fn) || findAncestorInSet(field, this._meta.jsonParents) !== void 0)) {
|
|
7944
|
+
if (def.fn === "now") make = () => now;
|
|
7945
|
+
else if (def.fn === "uuid") make = () => crypto.randomUUID();
|
|
7691
7946
|
}
|
|
7947
|
+
if (!make) continue;
|
|
7948
|
+
if (!field.includes(".")) {
|
|
7949
|
+
if (data[field] === void 0) data[field] = make();
|
|
7950
|
+
} else if (this._nestedDefaultFillable(field)) fillNested(data, field.split("."), 0, make);
|
|
7692
7951
|
}
|
|
7693
7952
|
return data;
|
|
7694
7953
|
}
|
|
7695
7954
|
/**
|
|
7955
|
+
* `@db.onUpdate.now` (since 0.1.156): sets each such field of a replace /
|
|
7956
|
+
* patch payload to `now`, overriding the caller's value. A nested field
|
|
7957
|
+
* (`audit.updatedAt`) is set inside its object only when the payload
|
|
7958
|
+
* carries that object — in every item of an array on the way and of the
|
|
7959
|
+
* items an array patch operator adds or updates — like
|
|
7960
|
+
* {@link _applyDefaults}. `topLevel: false` (a patch) instead drops the
|
|
7961
|
+
* caller's values of the table's own columns: {@link _stampTopLevel} sets
|
|
7962
|
+
* them once the patch is known to write something.
|
|
7963
|
+
*/
|
|
7964
|
+
_stampOnUpdate(data, now, topLevel) {
|
|
7965
|
+
for (const field of this._meta.onUpdateNow) if (!field.includes(".")) if (topLevel) data[field] = now;
|
|
7966
|
+
else delete data[field];
|
|
7967
|
+
else if (this._nestedDefaultFillable(field)) stampNested(data, field.split("."), 0, now);
|
|
7968
|
+
}
|
|
7969
|
+
/** Sets the top-level `@db.onUpdate.now` columns of a non-empty patch to `now`. */
|
|
7970
|
+
_stampTopLevel(data, now) {
|
|
7971
|
+
if (isEmptyObject(data)) return;
|
|
7972
|
+
for (const field of this._meta.onUpdateNow) if (!field.includes(".")) data[field] = now;
|
|
7973
|
+
}
|
|
7974
|
+
/** {@link _nestedDefaultFillable} per nested default path. */
|
|
7975
|
+
_fillableDefaults;
|
|
7976
|
+
/**
|
|
7977
|
+
* Whether every parent of the nested default `field` is one object type —
|
|
7978
|
+
* an object, `T | null` of one, or an array of one. Below a tuple or a
|
|
7979
|
+
* union of several types the default cannot be placed (the item it belongs
|
|
7980
|
+
* to is unknown), so it is not filled.
|
|
7981
|
+
*/
|
|
7982
|
+
_nestedDefaultFillable(field) {
|
|
7983
|
+
const cache = this._fillableDefaults ??= /* @__PURE__ */ new Map();
|
|
7984
|
+
let fillable = cache.get(field);
|
|
7985
|
+
if (fillable === void 0) {
|
|
7986
|
+
fillable = true;
|
|
7987
|
+
for (let dot = field.indexOf("."); dot !== -1; dot = field.indexOf(".", dot + 1)) {
|
|
7988
|
+
let type = this.flatMap.get(field.slice(0, dot));
|
|
7989
|
+
if (type?.type.kind === "union") type = soleUnionMember(type);
|
|
7990
|
+
if (type?.type.kind === "array") type = type.type.of;
|
|
7991
|
+
if (type?.type.kind === "union") type = soleUnionMember(type);
|
|
7992
|
+
if (type?.type.kind !== "object") {
|
|
7993
|
+
fillable = false;
|
|
7994
|
+
break;
|
|
7995
|
+
}
|
|
7996
|
+
}
|
|
7997
|
+
cache.set(field, fillable);
|
|
7998
|
+
}
|
|
7999
|
+
return fillable;
|
|
8000
|
+
}
|
|
8001
|
+
/**
|
|
7696
8002
|
* Parsed `@db.default 'literal'` values that are primitives — reused for
|
|
7697
8003
|
* every row (an object / array default is parsed per row: each row gets
|
|
7698
8004
|
* its own instance).
|
|
@@ -7857,6 +8163,42 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
|
|
|
7857
8163
|
return this.createValidator({ plugins: adapterPlugins });
|
|
7858
8164
|
}
|
|
7859
8165
|
};
|
|
8166
|
+
/**
|
|
8167
|
+
* Sets `segs` below `value` to `now` (see `_stampOnUpdate`), through
|
|
8168
|
+
* arrays and the item lists of array patch operators (`$insert`, `$upsert`,
|
|
8169
|
+
* `$replace`, `$update` — not `$remove`).
|
|
8170
|
+
*/
|
|
8171
|
+
function stampNested(value, segs, i, now) {
|
|
8172
|
+
if (Array.isArray(value)) {
|
|
8173
|
+
for (const item of value) stampNested(item, segs, i, now);
|
|
8174
|
+
return;
|
|
8175
|
+
}
|
|
8176
|
+
if (!isPlainObject(value)) return;
|
|
8177
|
+
const key = segs[i];
|
|
8178
|
+
if (i === segs.length - 1) {
|
|
8179
|
+
value[key] = now;
|
|
8180
|
+
return;
|
|
8181
|
+
}
|
|
8182
|
+
const next = value[key];
|
|
8183
|
+
if (isPlainObject(next) && Object.keys(next).some((k) => k.startsWith("$"))) {
|
|
8184
|
+
for (const [op, items] of Object.entries(next)) if (op !== "$remove") stampNested(items, segs, i + 1, now);
|
|
8185
|
+
} else stampNested(next, segs, i + 1, now);
|
|
8186
|
+
}
|
|
8187
|
+
/**
|
|
8188
|
+
* Sets `segs[i..]` to `make()` where it is `undefined`, inside present plain
|
|
8189
|
+
* objects only — fanned out over the objects of every array on the way.
|
|
8190
|
+
*/
|
|
8191
|
+
function fillNested(value, segs, i, make) {
|
|
8192
|
+
if (Array.isArray(value)) {
|
|
8193
|
+
for (const item of value) fillNested(item, segs, i, make);
|
|
8194
|
+
return;
|
|
8195
|
+
}
|
|
8196
|
+
if (!isPlainObject(value)) return;
|
|
8197
|
+
const key = segs[i];
|
|
8198
|
+
if (i === segs.length - 1) {
|
|
8199
|
+
if (value[key] === void 0) value[key] = make();
|
|
8200
|
+
} else fillNested(value[key], segs, i + 1, make);
|
|
8201
|
+
}
|
|
7860
8202
|
//#endregion
|
|
7861
8203
|
//#region src/table/view-read-plan.ts
|
|
7862
8204
|
/** Design types whose `=` compares one scalar to one scalar on every engine. */
|
|
@@ -8969,7 +9311,9 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
|
|
|
8969
9311
|
});
|
|
8970
9312
|
}
|
|
8971
9313
|
const desiredDefault = serializeDefaultValue(field.defaultValue);
|
|
8972
|
-
|
|
9314
|
+
const fn = field.defaultValue?.kind === "fn" ? field.defaultValue.fn : void 0;
|
|
9315
|
+
const missingNative = fn !== void 0 && fn !== "increment" && opts?.nativeDefaultFns?.has(fn) === true;
|
|
9316
|
+
if ((existingCol.dflt_value !== void 0 || missingNative) && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
|
|
8973
9317
|
field,
|
|
8974
9318
|
oldDefault: existingCol.dflt_value,
|
|
8975
9319
|
newDefault: desiredDefault
|
|
@@ -9006,4 +9350,4 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
|
|
|
9006
9350
|
return diff;
|
|
9007
9351
|
}
|
|
9008
9352
|
//#endregion
|
|
9009
|
-
export {
|
|
9353
|
+
export { RelationalFieldMapper as $, AtscriptDbReadable as A, canFilterLeaf as B, ApplicationIntegrity as C, createFailureCollector as D, BaseDbAdapter as E, ADAPTER_FILTER_REASON as F, guardAggregate as G, classifyQueryPath as H, ENCRYPTED_REASON as I, guardPaths as J, guardFilter as K, acceptedOperatorsHint as L, geoIndexNotFoundMessage as M, searchIndexNotFoundMessage as N, IntegrityStrategy as O, vectorIndexNotFoundMessage as P, unsupportedOperatorMessage as Q, assertGeoPoint as R, decomposePatch as S, ALL_VIEW_CAPABILITIES as T, collectQueryPaths as U, checkHavingKeys as V, groupSourceVerdict as W, narrowerFilterOps as X, guardQuery as Y, sortFieldNames as Z, isAtscriptDbView as _, normalizeComputedSelect as _t, computeTableHash as a, defaultFulltextIndex as at, isVersionExemptPatch as b, serializeDefaultValue as c, splitFulltextFields as ct, viewSnapshotSources as d, isGeoPointType as dt, DocumentFieldMapper as et, computeForeignKeyDiff as f, relationalColumnName as ft, AtscriptDbView as g, jsonValueAncestor as gt, hasForeignKeyChanges as h, isJsonValueField as ht, computeSchemaHash as i, INTEGER_REGEX_OP as it, resolveDesignType as j, NativeIntegrity as k, snapshotToExistingColumns as l, TableMetadata as lt, fkKey as m, isBucketableField as mt, isColumnTypeChanged as n, UniquSelect as nt, computeTableSnapshot as o, describeFulltext as ot, fkColumns as p, aliasTargetOf as pt, guardPath as q, canonicalizeQueryNode as r, numericOperandProblem as rt, computeViewSnapshot as s, searchTermInteger as st, computeColumnDiff as t, FieldMappingStrategy as tt, snapshotToExistingTableOptions as u, isGeoIndexableType as ut, isViewType as v, resolveCalendarBuckets as vt, ALL_BUCKET_UNITS as w, assertNoVersionWrites as x, AtscriptDbTable as y, NoopLogger as yt, bucketSourceVerdict as z };
|