@atscript/db 0.1.154 → 0.1.155

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.
Files changed (33) hide show
  1. package/dist/agg.d.cts +1 -1
  2. package/dist/agg.d.mts +1 -1
  3. package/dist/{buckets-jy8BfswO.d.cts → buckets-DcWm3SLt.d.cts} +120 -10
  4. package/dist/{buckets-CBKL_ZWE.d.mts → buckets-goCoky8v.d.mts} +120 -10
  5. package/dist/{column-diff-DAIAq6XR.mjs → column-diff-C68V_eYM.mjs} +346 -65
  6. package/dist/{column-diff-qnzx1o3c.cjs → column-diff-DMkGYAGM.cjs} +357 -64
  7. package/dist/{fk-diff-BIJ8l_hF.d.mts → fk-diff-BYLNWoF9.d.mts} +2 -1
  8. package/dist/{fk-diff-D1mrLMjU.d.cts → fk-diff-DblOPBLS.d.cts} +2 -1
  9. package/dist/index.cjs +2 -2
  10. package/dist/index.d.cts +10 -5
  11. package/dist/index.d.mts +10 -5
  12. package/dist/index.mjs +2 -2
  13. package/dist/plugin.cjs +3 -3
  14. package/dist/plugin.mjs +3 -3
  15. package/dist/rel.d.cts +2 -2
  16. package/dist/rel.d.mts +2 -2
  17. package/dist/{relation-helpers-BUsxNNGU.d.mts → relation-helpers-B9MVBrtx.d.mts} +1 -1
  18. package/dist/{relation-helpers-D_8bydc2.d.cts → relation-helpers-D9kMFfum.d.cts} +1 -1
  19. package/dist/shared.cjs +2 -1
  20. package/dist/shared.d.cts +10 -3
  21. package/dist/shared.d.mts +10 -3
  22. package/dist/shared.mjs +2 -2
  23. package/dist/sync.cjs +288 -30
  24. package/dist/sync.d.cts +34 -5
  25. package/dist/sync.d.mts +34 -5
  26. package/dist/sync.mjs +288 -30
  27. package/dist/{validation-utils-DqZAH6aT.mjs → validation-utils-DxHIqa36.mjs} +23 -5
  28. package/dist/{validation-utils-debIYpq0.cjs → validation-utils-_PA7iSLm.cjs} +28 -4
  29. package/dist/{validator-BACewoRj.mjs → validator-BC3nwFi4.mjs} +178 -10
  30. package/dist/{validator-BxWwh3wf.cjs → validator-DHwSyBej.cjs} +201 -9
  31. package/dist/validator.cjs +1 -1
  32. package/dist/validator.mjs +1 -1
  33. package/package.json +6 -6
@@ -1,12 +1,12 @@
1
1
  const require_db_error = require("./db-error-JcfJqqpk.cjs");
2
2
  const require_aggregate_fns = require("./aggregate-fns-DFtKAv7Q.cjs");
3
3
  const require_nested_writer = require("./nested-writer-Bl1SZS7x.cjs");
4
+ const require_validator = require("./validator-DHwSyBej.cjs");
4
5
  const require_search_fields = require("./search-fields-DwrsJUC1.cjs");
5
6
  const require_object = require("./object-Djg28csK.cjs");
6
7
  require("./agg.cjs");
7
8
  const require_aggregate_rules = require("./aggregate-rules-D_bsCpUI.cjs");
8
9
  const require_ops = require("./ops.cjs");
9
- const require_validator = require("./validator-BxWwh3wf.cjs");
10
10
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
11
11
  let _uniqu_core = require("@uniqu/core");
12
12
  let node_async_hooks = require("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 (require_object.findAncestorInSet(path, idx.jsonRoots) === void 0 && require_object.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` or array
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 = require_object.selfOrAncestor(logicalPath, idx.jsonRoots);
300
- const optional = require_object.selfOrAncestor(logicalPath, idx.optional) !== void 0 || jsonRoot !== void 0 && jsonRoot !== logicalPath;
300
+ const presence = require_validator.pathPresence(sourceType, logicalPath);
301
+ const optional = (presence === void 0 ? require_object.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 require_validator.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 = require_validator.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;
@@ -554,6 +587,9 @@ var TableMetadata = class {
554
587
  _alwaysAddressable;
555
588
  _collateMap = /* @__PURE__ */ new Map();
556
589
  _columnFromMap = /* @__PURE__ */ new Map();
590
+ /** The annotated type `build()` ran on — the root of {@link presence}. */
591
+ _rootType;
592
+ _presence = /* @__PURE__ */ new Map();
557
593
  constructor(nestedObjects) {
558
594
  this.nestedObjects = nestedObjects;
559
595
  }
@@ -561,6 +597,72 @@ var TableMetadata = class {
561
597
  return this._built;
562
598
  }
563
599
  /**
600
+ * Whether the value of the LOGICAL field `path` is always there, may be
601
+ * NULL / missing, or is declared by only some members of a union of
602
+ * objects on the way ({@link TPathPresence}); `undefined` inside an array.
603
+ * @since 0.1.155
604
+ */
605
+ presence(path) {
606
+ if (this._presence.has(path)) return this._presence.get(path);
607
+ const root = this._rootType;
608
+ const answer = root ? require_validator.pathPresence(root, path) : void 0;
609
+ this._presence.set(path, answer);
610
+ return answer;
611
+ }
612
+ /**
613
+ * Whether the LOGICAL field `path` can hold NULL (or be missing): it or a
614
+ * parent object is optional or a `| null` union, or only some members of
615
+ * a union of objects declare it. Inside an array: the field or a parent
616
+ * is optional.
617
+ * @since 0.1.155
618
+ */
619
+ isNullable(path) {
620
+ const presence = this.presence(path);
621
+ if (presence !== void 0) return presence !== "required";
622
+ for (let p = path;;) {
623
+ if (this.flatMap.get(p)?.optional === true) return true;
624
+ const dot = p.lastIndexOf(".");
625
+ if (dot === -1) return false;
626
+ p = p.slice(0, dot);
627
+ }
628
+ }
629
+ _objectLeaves;
630
+ /**
631
+ * The stored fields under the object field `path` that a null test on the
632
+ * object reads (`rewriteObjectNullTests`, since 0.1.155): its leaf columns
633
+ * on relational storage, its scalar / array / JSON fields on document
634
+ * storage (not inside an array or a JSON value). `undefined` when `path`
635
+ * is no stored object (a leaf, a `@db.json` / encrypted / ignored field,
636
+ * a navigation relation, inside a JSON value, unknown).
637
+ */
638
+ objectLeaves(path) {
639
+ this._objectLeaves ??= /* @__PURE__ */ new Map();
640
+ if (this._objectLeaves.has(path)) return this._objectLeaves.get(path);
641
+ let leaves;
642
+ const fd = this.descriptorByPath.get(path);
643
+ if (require_object.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))) {
644
+ const prefix = `${path}.`;
645
+ leaves = [];
646
+ for (const [leaf, leafFd] of this.descriptorByPath) {
647
+ if (!leaf.startsWith(prefix) || leafFd.designType === "object") continue;
648
+ if (require_object.findAncestorInSet(leaf, this.jsonValueParents) !== void 0) continue;
649
+ if (this.presence(leaf) === void 0) continue;
650
+ leaves.push(leaf);
651
+ }
652
+ }
653
+ this._objectLeaves.set(path, leaves);
654
+ return leaves;
655
+ }
656
+ /**
657
+ * Whether the column of `path` is nullable: {@link isNullable}, except
658
+ * inside an array (document storage only), where the field's own
659
+ * optionality counts.
660
+ */
661
+ _columnNullable(path, type) {
662
+ const presence = this.presence(path);
663
+ return presence === void 0 ? type.optional === true : presence !== "required";
664
+ }
665
+ /**
564
666
  * {@link documentPath} over this table's `columnMap`. A `@db.column.derived`
565
667
  * field has no stored path of its own: it maps to its source leaf.
566
668
  */
@@ -604,6 +706,7 @@ var TableMetadata = class {
604
706
  build(type, adapter, logger) {
605
707
  if (this._built) return;
606
708
  adapter.onBeforeFlatten?.(type);
709
+ this._rootType = type;
607
710
  const collected = [];
608
711
  this.flatMap = (0, _atscript_typescript_utils.flattenAnnotatedType)(type, {
609
712
  topLevelArrayTag: adapter.getTopLevelArrayTag?.() ?? "db.__topLevelArray",
@@ -662,10 +765,13 @@ var TableMetadata = class {
662
765
  * into the synthetic flat entry of the prop's path, so a member's
663
766
  * annotations would reach the column (DDL type, size, default). Since
664
767
  * atscript 0.1.103 a built-in primitive member carries its built-in
665
- * annotations (`number.timestamp.created | null` → `@db.default.now`,
666
- * `string.char` → `@expect.maxLength 1`). Rebuild such an entry from the
667
- * prop's own annotations plus the non-`db.*` annotations of named members
668
- * (aliases, interfaces), so the column is described as before 0.1.103.
768
+ * annotations (`string.char` → `@expect.maxLength 1`). Rebuild such an
769
+ * entry from the prop's own annotations plus the non-`db.*` annotations of
770
+ * named members (aliases, interfaces), so the column is described as before
771
+ * 0.1.103. Exception: the one non-`null` member of `T | null` is the
772
+ * column's type, so its `db.*` annotations apply too — `createdAt:
773
+ * number.timestamp.created | null` gets `@db.default.now` like
774
+ * `createdAt: number.timestamp.created`.
669
775
  */
670
776
  _dropMemberMetadata(collected) {
671
777
  const byPath = /* @__PURE__ */ new Map();
@@ -678,12 +784,16 @@ var TableMetadata = class {
678
784
  if (!entries.some((e) => e.member)) continue;
679
785
  const flat = this.flatMap.get(path);
680
786
  if (!flat?.__flat_union) continue;
787
+ const prop = entries.find((e) => !e.member);
788
+ const base = prop && require_validator.columnUnionBase(prop.type);
681
789
  const target = flat.metadata;
682
790
  target.clear();
683
791
  for (const entry of entries) {
684
- if (entry.member && isInlinePrimitive(entry.type)) continue;
685
- for (const [key, value] of entry.metadata) if (!entry.member || !key.startsWith("db.")) target.set(key, value);
792
+ const inline = entry.member && isInlinePrimitive(entry.type);
793
+ const isBase = entry.member && entry.type === base;
794
+ for (const [key, value] of entry.metadata) if (!entry.member || (key.startsWith("db.") ? isBase : !inline)) target.set(key, value);
686
795
  }
796
+ if (base && prop) prop.metadata = target;
687
797
  }
688
798
  }
689
799
  /**
@@ -706,7 +816,7 @@ var TableMetadata = class {
706
816
  * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
707
817
  */
708
818
  _scanGenericAnnotations(fieldName, fieldType, metadata, logger) {
709
- if (metadata.has("meta.id")) {
819
+ if (metadata.has("meta.id") && !fieldName.includes(".")) {
710
820
  this.primaryKeys.push(fieldName);
711
821
  this.originalMetaIdFields.push(fieldName);
712
822
  }
@@ -969,7 +1079,7 @@ var TableMetadata = class {
969
1079
  const isJson = this.jsonFields.has(path);
970
1080
  const isArray = designType === "array";
971
1081
  const isObject = designType === "object";
972
- if (isArray) this.jsonFields.add(path);
1082
+ if (isArray || isStructuredMixedUnion(type)) this.jsonFields.add(path);
973
1083
  else if (isObject && isJson) {} else if (isObject && !isJson) this.flattenedParents.add(path);
974
1084
  }
975
1085
  for (const ignoredField of this.ignoredFields) if (this.flattenedParents.has(ignoredField)) {
@@ -1071,6 +1181,7 @@ var TableMetadata = class {
1071
1181
  const isJson = this.jsonFields.has(path);
1072
1182
  const isFlattened = !skipFlattening && require_object.findAncestorInSet(path, this.flattenedParents) !== void 0;
1073
1183
  const designType = isEncrypted ? "string" : isJson ? "json" : resolveDesignType(type);
1184
+ const colType = columnType(type);
1074
1185
  let storage;
1075
1186
  if (skipFlattening) storage = "column";
1076
1187
  else if (isEncrypted) storage = isFlattened ? "flattened" : "column";
@@ -1089,10 +1200,10 @@ var TableMetadata = class {
1089
1200
  if (quantityRef) this.quantityRefByField.set(path, quantityRef);
1090
1201
  descriptors.push({
1091
1202
  path,
1092
- type,
1203
+ type: colType,
1093
1204
  physicalName,
1094
1205
  designType,
1095
- optional: type.optional === true,
1206
+ optional: skipFlattening ? type.optional === true : this._columnNullable(path, type),
1096
1207
  isPrimaryKey: this.primaryKeys.includes(path),
1097
1208
  ignored: this.ignoredFields.has(path),
1098
1209
  defaultValue: this.defaults.get(path),
@@ -1106,7 +1217,7 @@ var TableMetadata = class {
1106
1217
  unitCode,
1107
1218
  unitRefField,
1108
1219
  encrypted: isEncrypted || underEncrypted || void 0,
1109
- isGeoPoint: isGeoPointType(type) || void 0,
1220
+ isGeoPoint: isGeoPointType(colType) || void 0,
1110
1221
  derived: this.derivedFields.get(path),
1111
1222
  computed: computedMeta(rootType, path)
1112
1223
  });
@@ -1234,7 +1345,7 @@ var TableMetadata = class {
1234
1345
  for (const index of this.indexes.values()) for (const field of index.fields) {
1235
1346
  const ftype = this.flatMap.get(field.name);
1236
1347
  if (ftype) {
1237
- field.optional = ftype.optional === true;
1348
+ field.optional = this._columnNullable(field.name, ftype);
1238
1349
  field.designType = ftype.type.tags?.has("objectId") ? "objectId" : resolveDesignType(ftype);
1239
1350
  }
1240
1351
  field.name = this.physicalPath(field.name);
@@ -1647,6 +1758,78 @@ function rewriteIntegerRegex(filter, meta) {
1647
1758
  return out ?? filter;
1648
1759
  }
1649
1760
  //#endregion
1761
+ //#region src/query/object-null.ts
1762
+ /**
1763
+ * Whether a filter entry's value only asks if the value is there: `null`,
1764
+ * `{ $eq: null }`, `{ $ne: null }`, `{ $exists: true | false }` (and
1765
+ * combinations of them).
1766
+ */
1767
+ function isNullTest(value) {
1768
+ if (value === null) return true;
1769
+ if (!require_object.isPlainObject(value)) return false;
1770
+ const ops = Object.entries(value);
1771
+ return ops.length > 0 && ops.every(([op, operand]) => op === "$exists" ? typeof operand === "boolean" : (op === "$eq" || op === "$ne") && operand === null);
1772
+ }
1773
+ /**
1774
+ * The leaf form of one null test on an object (see
1775
+ * {@link rewriteObjectNullTests}) as `$and` members: absent — `$exists: false`
1776
+ * on every leaf; present — one `$or` of `$exists: true` per leaf.
1777
+ */
1778
+ function objectTest(value, leaves) {
1779
+ const ops = value === null ? { $eq: null } : value;
1780
+ const out = [];
1781
+ for (const [op, operand] of Object.entries(ops)) {
1782
+ const present = op === "$exists" ? operand : op === "$ne";
1783
+ const tests = leaves.map((leaf) => ({ [leaf]: { $exists: present } }));
1784
+ if (present) out.push({ $or: tests });
1785
+ else out.push(...tests);
1786
+ }
1787
+ return out;
1788
+ }
1789
+ /**
1790
+ * A null test on a stored object (since 0.1.155) — `{ address: null }`,
1791
+ * `{ address: { $ne: null } }`, `{ address: { $exists: … } }` — on the
1792
+ * object's leaves: the object is null when none of its fields holds a value
1793
+ * (`$exists: false` on every leaf), present when one does. Relational
1794
+ * storage has no column for the object (an all-NULL row reads back as
1795
+ * `null`); document storage answers the same, so an object stored as `{}`
1796
+ * or with only null fields counts as null everywhere. LOGICAL filter;
1797
+ * copy-on-write (the same object comes back when nothing changed).
1798
+ */
1799
+ function rewriteObjectNullTests(filter, meta) {
1800
+ if (!require_object.isPlainObject(filter)) return filter;
1801
+ let out;
1802
+ const extra = [];
1803
+ for (const [key, value] of Object.entries(filter)) {
1804
+ let next = value;
1805
+ if (key === "$and" || key === "$or") {
1806
+ if (Array.isArray(value)) {
1807
+ const mapped = value.map((f) => rewriteObjectNullTests(f, meta));
1808
+ if (mapped.some((m, i) => m !== value[i])) next = mapped;
1809
+ }
1810
+ } else if (key === "$not") next = rewriteObjectNullTests(value, meta);
1811
+ else if (!key.startsWith("$") && isNullTest(value)) {
1812
+ const leaves = meta.objectLeaves(key);
1813
+ if (leaves && leaves.length > 0) {
1814
+ extra.push(...objectTest(value, leaves));
1815
+ out ??= { ...filter };
1816
+ delete out[key];
1817
+ continue;
1818
+ }
1819
+ }
1820
+ if (next !== value) {
1821
+ out ??= { ...filter };
1822
+ out[key] = next;
1823
+ }
1824
+ }
1825
+ if (!out) return filter;
1826
+ if (extra.length > 0) {
1827
+ const and = out.$and;
1828
+ out.$and = [...Array.isArray(and) ? and : [], ...extra];
1829
+ }
1830
+ return out;
1831
+ }
1832
+ //#endregion
1650
1833
  //#region src/query/aggregate-expr.ts
1651
1834
  /**
1652
1835
  * Output aliases of the computed `$select` entries of a grouped query —
@@ -2298,7 +2481,7 @@ var FieldMappingStrategy = class {
2298
2481
  * translate predicate operands at deeper levels).
2299
2482
  */
2300
2483
  translateFilter(filter, meta, depth = 0) {
2301
- filter = rewriteIntegerRegex(filter, meta);
2484
+ filter = rewriteObjectNullTests(rewriteIntegerRegex(filter, meta), meta);
2302
2485
  const has = require_nested_writer.containsRelationFilter(filter);
2303
2486
  const resolved = has ? require_nested_writer.resolveRelationFilterTree(filter, meta, depth) : filter;
2304
2487
  return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
@@ -2640,13 +2823,13 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2640
2823
  current = created;
2641
2824
  } else current = next;
2642
2825
  }
2643
- current[col.last] = value;
2826
+ if (!col.partial || value !== null && value !== void 0) current[col.last] = value;
2644
2827
  }
2645
2828
  for (const parent of plan.parents) collapseNullParent(result, parent);
2646
2829
  return result;
2647
2830
  }
2648
2831
  translateQuery(query, meta) {
2649
- const logical = rewriteIntegerRegex(query.filter, meta);
2832
+ const logical = rewriteObjectNullTests(rewriteIntegerRegex(query.filter, meta), meta);
2650
2833
  const has = require_nested_writer.containsRelationFilter(logical);
2651
2834
  const filter = has ? require_nested_writer.resolveRelationFilterTree(logical, meta, 0) : logical;
2652
2835
  if (!meta.requiresMappings) {
@@ -2701,7 +2884,11 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2701
2884
  return result;
2702
2885
  }
2703
2886
  prepareForWrite(payload, meta, adapter) {
2704
- if (meta.requiresMappings && !meta.onlyColumnRenames) return this.formatWriteValues(this.flattenWritePayload(payload, meta, adapter), meta);
2887
+ if (meta.requiresMappings && !meta.onlyColumnRenames) {
2888
+ const flat = this.flattenWritePayload(payload, meta, adapter);
2889
+ nullAbsentNested(payload, flat, meta);
2890
+ return this.formatWriteValues(flat, meta);
2891
+ }
2705
2892
  const data = { ...payload };
2706
2893
  this.prepareCommon(data, meta, adapter);
2707
2894
  if (meta.onlyColumnRenames) {
@@ -2728,7 +2915,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2728
2915
  const suffix = operatorMatch ? operatorMatch[2] : "";
2729
2916
  const fd = meta.leafByLogical.get(basePath);
2730
2917
  const finalKey = (fd?.physicalName ?? basePath) + suffix;
2731
- if (fd?.storage === "json" && typeof value === "object" && value !== null && !suffix) result[finalKey] = JSON.stringify(value);
2918
+ if (fd?.storage === "json" && value !== null && value !== void 0 && !suffix) result[finalKey] = JSON.stringify(value);
2732
2919
  else result[finalKey] = value;
2733
2920
  }
2734
2921
  return this.formatWriteValues(result, meta);
@@ -2808,6 +2995,49 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2808
2995
  return result;
2809
2996
  }
2810
2997
  };
2998
+ const nestedColumns = /* @__PURE__ */ new WeakMap();
2999
+ function nestedColumnsFor(meta) {
3000
+ let list = nestedColumns.get(meta);
3001
+ if (list === void 0) {
3002
+ list = [];
3003
+ for (const [physical, fd] of meta.leafByPhysical) {
3004
+ if (fd.derived || fd.isPrimaryKey || fd.storage === "column" || !fd.path.includes(".")) continue;
3005
+ const segs = fd.path.split(".");
3006
+ list.push({
3007
+ physical,
3008
+ parents: segs.slice(0, -1),
3009
+ partial: meta.presence(fd.path) === "partial"
3010
+ });
3011
+ }
3012
+ nestedColumns.set(meta, list);
3013
+ }
3014
+ return list;
3015
+ }
3016
+ /**
3017
+ * Every write through `prepareForWrite` stores a whole row (insert, replace).
3018
+ * A column the flattened row leaves out would take its column `DEFAULT` —
3019
+ * a `now` default below an absent optional object would make the object
3020
+ * reappear on read, and one of another union member's leaves would add a
3021
+ * field to the stored member. So a column below an absent / `null` parent
3022
+ * object, or one only some union members declare that the written member
3023
+ * leaves out, is written as `NULL` (since 0.1.155), as the document adapters
3024
+ * store such a row.
3025
+ */
3026
+ function nullAbsentNested(payload, flat, meta) {
3027
+ for (const col of nestedColumnsFor(meta)) {
3028
+ if (col.physical in flat) continue;
3029
+ let absent = false;
3030
+ let current = payload;
3031
+ for (const seg of col.parents) {
3032
+ current = current[seg];
3033
+ if (current === null || current === void 0 || typeof current !== "object") {
3034
+ absent = true;
3035
+ break;
3036
+ }
3037
+ }
3038
+ if (absent || col.partial) flat[col.physical] = null;
3039
+ }
3040
+ }
2811
3041
  /**
2812
3042
  * Per-table read plan of {@link RelationalFieldMapper.reconstructFromRead}:
2813
3043
  * the per-column decisions (coercion, JSON, nesting — the dot-path pre-split)
@@ -2827,12 +3057,14 @@ function compileReadPlan(meta) {
2827
3057
  const fromFmts = meta.fromStorageFormatters;
2828
3058
  const columns = /* @__PURE__ */ new Map();
2829
3059
  for (const [physical, fd] of meta.leafByPhysical) {
2830
- const segs = (fd.storage === "json" || fd.storage === "flattened") && fd.path.includes(".") ? fd.path.split(".") : void 0;
3060
+ const nested = (fd.storage === "json" || fd.storage === "flattened") && fd.path.includes(".");
3061
+ const segs = nested ? fd.path.split(".") : void 0;
2831
3062
  columns.set(physical, {
2832
3063
  path: fd.path,
2833
3064
  parents: segs?.slice(0, -1),
2834
3065
  last: segs ? segs[segs.length - 1] : fd.path,
2835
3066
  json: fd.storage === "json",
3067
+ partial: nested && meta.presence(fd.path) === "partial",
2836
3068
  coerce: fd.designType === "boolean" ? toBool : fd.designType === "decimal" ? toDecimalString : void 0,
2837
3069
  fromFmt: fromFmts?.get(physical)
2838
3070
  });
@@ -2843,15 +3075,16 @@ function compileReadPlan(meta) {
2843
3075
  parents.push({
2844
3076
  ancestors: segs.slice(0, -1),
2845
3077
  last: segs[segs.length - 1],
2846
- optional: !!meta.flatMap?.get(parentPath)?.optional
3078
+ presence: meta.presence(parentPath) ?? "required"
2847
3079
  });
2848
3080
  }
3081
+ parents.sort((a, b) => b.ancestors.length - a.ancestors.length);
2849
3082
  return {
2850
3083
  columns,
2851
3084
  parents
2852
3085
  };
2853
3086
  }
2854
- /** If every child of a flattened parent is null / undefined, collapse it (`null` when optional, else `{}`). */
3087
+ /** If every child of a flattened parent is null / undefined, collapse it (see {@link TReadParent}). */
2855
3088
  function collapseNullParent(obj, parent) {
2856
3089
  let current = obj;
2857
3090
  const ancestors = parent.ancestors;
@@ -2866,7 +3099,8 @@ function collapseNullParent(obj, parent) {
2866
3099
  const v = parentObj[k];
2867
3100
  if (v !== null && v !== void 0) return;
2868
3101
  }
2869
- current[parent.last] = parent.optional ? null : {};
3102
+ if (parent.presence === "partial") delete current[parent.last];
3103
+ else current[parent.last] = parent.presence === "nullable" ? null : {};
2870
3104
  }
2871
3105
  /**
2872
3106
  * Per-table write plan of the relational flatten: each logical path's
@@ -3308,6 +3542,10 @@ function collectQueryPaths(query, aggregate) {
3308
3542
  path,
3309
3543
  predicate,
3310
3544
  relation: relationOpsOf(value)
3545
+ } : isNullTest(value) ? {
3546
+ path,
3547
+ predicate,
3548
+ nullTest: true
3311
3549
  } : {
3312
3550
  path,
3313
3551
  predicate
@@ -3407,10 +3645,11 @@ function pathSourceOf(meta) {
3407
3645
  * `FieldCapabilityIndex`, with `$with` hints and leaf lists) is what clients
3408
3646
  * see and is authoritative — the HTTP gate always answers first.
3409
3647
  */
3410
- function guardPath(meta, adapter, path, op, predicate = "compare") {
3648
+ function guardPath(meta, adapter, path, op, predicate = "compare", nullTest = false) {
3411
3649
  const verb = OP_VERB[op];
3412
3650
  const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
3413
3651
  if (predicate === "relation" && kind !== "nav" && kind !== "unknown") throw pathError(path, `"$some" / "$none" are only valid on a navigation relation — "${path}" is not one`);
3652
+ if (op === "filter" && nullTest && meta.objectLeaves(path)?.length) return;
3414
3653
  switch (kind) {
3415
3654
  case "nav":
3416
3655
  if (op === "filter" && predicate === "relation" && parent === void 0) return;
@@ -3495,7 +3734,7 @@ function guardPaths(meta, adapter, query, aggregate = false, state) {
3495
3734
  if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
3496
3735
  let relState = state;
3497
3736
  for (const ref of refs.filter) {
3498
- guardPath(meta, adapter, ref.path, "filter", ref.predicate);
3737
+ guardPath(meta, adapter, ref.path, "filter", ref.predicate, ref.nullTest);
3499
3738
  if (ref.predicate === "relation") guardRelationRef(meta, adapter, ref, relState ??= require_nested_writer.relGuardState());
3500
3739
  }
3501
3740
  for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
@@ -3637,10 +3876,12 @@ var NullsSource = class {
3637
3876
  }
3638
3877
  /**
3639
3878
  * Whether ordering by the LOGICAL field `path` can meet NULL (or a missing
3640
- * value): the field or one of its parent objects is optional, the field is
3641
- * a derived column, it is not a known field, or the source is a view. A
3642
- * `$nulls` entry on any other field changes nothing and is dropped, so the
3643
- * plain `ORDER BY` keeps using the index.
3879
+ * value): the field can hold NULL (`TableMetadata.isNullable` — it or a
3880
+ * parent object is optional or a `| null` union, or only some members of a
3881
+ * union of objects declare it), the field is a derived column, it is not a
3882
+ * known field, or the source is a view. A `$nulls` entry on any other
3883
+ * field changes nothing and is dropped, so the plain `ORDER BY` keeps
3884
+ * using the index.
3644
3885
  */
3645
3886
  nullable(path) {
3646
3887
  let answer = this._nullable.get(path);
@@ -3654,12 +3895,7 @@ var NullsSource = class {
3654
3895
  const { meta } = this._init;
3655
3896
  const fd = meta.descriptorByPath.get(path);
3656
3897
  if (!fd || fd.derived) return true;
3657
- for (let p = path;;) {
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
- }
3898
+ return meta.isNullable(path);
3663
3899
  }
3664
3900
  /**
3665
3901
  * The effective `$nulls` of the `$sort` keys of one read: the request's
@@ -3774,7 +4010,9 @@ function geoIndexNotFoundMessage(tableName, indexName) {
3774
4010
  * otherwise trips up every adapter author.
3775
4011
  *
3776
4012
  * For union types (e.g., from flattened `{...} | {...}` objects):
3777
- * - If all members resolve to the same type → returns that type (strong type)
4013
+ * - `null` / `undefined` members are left out (since 0.1.155): `T | null`
4014
+ * resolves to the design type of `T`
4015
+ * - If all other members resolve to the same type → returns that type (strong type)
3778
4016
  * - If members disagree → returns `'union'` (out of scope for type management)
3779
4017
  */
3780
4018
  function resolveDesignType(fieldType) {
@@ -3782,9 +4020,9 @@ function resolveDesignType(fieldType) {
3782
4020
  if (fieldType.type.kind === "object") return "object";
3783
4021
  if (fieldType.type.kind === "array") return "array";
3784
4022
  if (fieldType.type.kind === "union") {
3785
- const items = fieldType.type.items;
3786
- if (items.length > 0) {
3787
- const resolved = items.map((item) => resolveDesignType(item));
4023
+ const { members } = require_validator.unionValueMembers(fieldType);
4024
+ if (members.length > 0) {
4025
+ const resolved = members.map((item) => resolveDesignType(item));
3788
4026
  if (resolved.every((type) => type === resolved[0])) return resolved[0];
3789
4027
  }
3790
4028
  return "union";
@@ -4440,21 +4678,13 @@ var AtscriptDbReadable = class {
4440
4678
  _totalOrderKeys() {
4441
4679
  if (this._totalOrderKeysCache) return this._totalOrderKeysCache;
4442
4680
  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
4681
  const sets = [];
4452
4682
  if (meta.primaryKeys.length > 0) sets.push(meta.primaryKeys.map((f) => meta.physicalPath(f)));
4453
4683
  for (const index of meta.indexes.values()) {
4454
4684
  if (index.type !== "unique") continue;
4455
- if (index.fields.every((f) => f.optional !== true && !nullable(meta.physicalToPath.get(f.name) ?? f.name))) sets.push(index.fields.map((f) => f.name));
4685
+ 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
4686
  }
4457
- for (const prop of meta.uniqueProps) if (!nullable(prop)) sets.push([meta.physicalPath(prop)]);
4687
+ for (const prop of meta.uniqueProps) if (!meta.isNullable(prop)) sets.push([meta.physicalPath(prop)]);
4458
4688
  this._totalOrderKeysCache = sets;
4459
4689
  return sets;
4460
4690
  }
@@ -6075,7 +6305,7 @@ var BaseDbAdapter = class {
6075
6305
  }
6076
6306
  /**
6077
6307
  * Whether the table has at least one row. Schema sync uses it in the
6078
- * pre-flight phase to refuse a primary-key change on a populated table.
6308
+ * pre-flight phase to check a primary-key change on a populated table.
6079
6309
  * Override with an EXISTS/LIMIT 1 probe — this default is `count() > 0`,
6080
6310
  * a full scan on some engines, and it can only answer for the adapter's
6081
6311
  * OWN table: for another `tableName` (or on an administrative adapter
@@ -6495,6 +6725,10 @@ function flattenPatchPayload(payload, prefix, update, meta, table) {
6495
6725
  const key = prefix ? `${prefix}.${_key}` : _key;
6496
6726
  if (primaryKeys.includes(key)) continue;
6497
6727
  if (!(typeof value === "object" && value !== null && !Array.isArray(value))) {
6728
+ if (value === null && meta.flattenedParents.has(key)) {
6729
+ for (const lp of meta.optionalLeavesByLogicalParent.get(key) ?? []) update[lp] = null;
6730
+ continue;
6731
+ }
6498
6732
  update[key] = value;
6499
6733
  continue;
6500
6734
  }
@@ -7672,26 +7906,56 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7672
7906
  * full row). Function defaults (`now` / `uuid` / `increment` / custom) the
7673
7907
  * adapter handles natively are NOT filled — the field stays absent so the
7674
7908
  * engine's own default applies. The version column is never touched.
7909
+ * A nested field (`audit.createdAt`) is filled inside its object, only when
7910
+ * that object is present (an absent or `null` embedded object stays so), and
7911
+ * in every object of an array on the way; a field inside a JSON value or an
7912
+ * array has no column default, so the SDK fills its function default too.
7675
7913
  */
7676
7914
  _applyDefaults(data) {
7677
7915
  const nativeFns = this.adapter.nativeDefaultFns();
7678
7916
  const versionField = this._meta.versionField;
7679
7917
  for (const [field, def] of this._meta.defaults.entries()) {
7680
7918
  if (field === versionField) continue;
7681
- if (data[field] === void 0) {
7682
- if (def.kind === "value") data[field] = this._valueDefault(field, def.value);
7683
- else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
7684
- case "now":
7685
- data[field] = Date.now();
7686
- break;
7687
- case "uuid":
7688
- data[field] = crypto.randomUUID();
7689
- break;
7690
- }
7919
+ let make;
7920
+ if (def.kind === "value") make = () => this._valueDefault(field, def.value);
7921
+ else if (def.kind === "fn" && (!nativeFns.has(def.fn) || require_object.findAncestorInSet(field, this._meta.jsonParents) !== void 0)) {
7922
+ if (def.fn === "now") make = Date.now;
7923
+ else if (def.fn === "uuid") make = () => crypto.randomUUID();
7691
7924
  }
7925
+ if (!make) continue;
7926
+ if (!field.includes(".")) {
7927
+ if (data[field] === void 0) data[field] = make();
7928
+ } else if (this._nestedDefaultFillable(field)) fillNested(data, field.split("."), 0, make);
7692
7929
  }
7693
7930
  return data;
7694
7931
  }
7932
+ /** {@link _nestedDefaultFillable} per nested default path. */
7933
+ _fillableDefaults;
7934
+ /**
7935
+ * Whether every parent of the nested default `field` is one object type —
7936
+ * an object, `T | null` of one, or an array of one. Below a tuple or a
7937
+ * union of several types the default cannot be placed (the item it belongs
7938
+ * to is unknown), so it is not filled.
7939
+ */
7940
+ _nestedDefaultFillable(field) {
7941
+ const cache = this._fillableDefaults ??= /* @__PURE__ */ new Map();
7942
+ let fillable = cache.get(field);
7943
+ if (fillable === void 0) {
7944
+ fillable = true;
7945
+ for (let dot = field.indexOf("."); dot !== -1; dot = field.indexOf(".", dot + 1)) {
7946
+ let type = this.flatMap.get(field.slice(0, dot));
7947
+ if (type?.type.kind === "union") type = require_validator.soleUnionMember(type);
7948
+ if (type?.type.kind === "array") type = type.type.of;
7949
+ if (type?.type.kind === "union") type = require_validator.soleUnionMember(type);
7950
+ if (type?.type.kind !== "object") {
7951
+ fillable = false;
7952
+ break;
7953
+ }
7954
+ }
7955
+ cache.set(field, fillable);
7956
+ }
7957
+ return fillable;
7958
+ }
7695
7959
  /**
7696
7960
  * Parsed `@db.default 'literal'` values that are primitives — reused for
7697
7961
  * every row (an object / array default is parsed per row: each row gets
@@ -7857,6 +8121,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7857
8121
  return this.createValidator({ plugins: adapterPlugins });
7858
8122
  }
7859
8123
  };
8124
+ /**
8125
+ * Sets `segs[i..]` to `make()` where it is `undefined`, inside present plain
8126
+ * objects only — fanned out over the objects of every array on the way.
8127
+ */
8128
+ function fillNested(value, segs, i, make) {
8129
+ if (Array.isArray(value)) {
8130
+ for (const item of value) fillNested(item, segs, i, make);
8131
+ return;
8132
+ }
8133
+ if (!require_object.isPlainObject(value)) return;
8134
+ const key = segs[i];
8135
+ if (i === segs.length - 1) {
8136
+ if (value[key] === void 0) value[key] = make();
8137
+ } else fillNested(value[key], segs, i + 1, make);
8138
+ }
7860
8139
  //#endregion
7861
8140
  //#region src/table/view-read-plan.ts
7862
8141
  /** Design types whose `=` compares one scalar to one scalar on every engine. */
@@ -8969,7 +9248,9 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
8969
9248
  });
8970
9249
  }
8971
9250
  const desiredDefault = serializeDefaultValue(field.defaultValue);
8972
- if (existingCol.dflt_value !== void 0 && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
9251
+ const fn = field.defaultValue?.kind === "fn" ? field.defaultValue.fn : void 0;
9252
+ const missingNative = fn !== void 0 && fn !== "increment" && opts?.nativeDefaultFns?.has(fn) === true;
9253
+ if ((existingCol.dflt_value !== void 0 || missingNative) && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
8973
9254
  field,
8974
9255
  oldDefault: existingCol.dflt_value,
8975
9256
  newDefault: desiredDefault
@@ -9366,6 +9647,12 @@ Object.defineProperty(exports, "numericOperandProblem", {
9366
9647
  return numericOperandProblem;
9367
9648
  }
9368
9649
  });
9650
+ Object.defineProperty(exports, "relationalColumnName", {
9651
+ enumerable: true,
9652
+ get: function() {
9653
+ return relationalColumnName;
9654
+ }
9655
+ });
9369
9656
  Object.defineProperty(exports, "resolveCalendarBuckets", {
9370
9657
  enumerable: true,
9371
9658
  get: function() {
@@ -9390,6 +9677,12 @@ Object.defineProperty(exports, "searchTermInteger", {
9390
9677
  return searchTermInteger;
9391
9678
  }
9392
9679
  });
9680
+ Object.defineProperty(exports, "serializeDefaultValue", {
9681
+ enumerable: true,
9682
+ get: function() {
9683
+ return serializeDefaultValue;
9684
+ }
9685
+ });
9393
9686
  Object.defineProperty(exports, "snapshotToExistingColumns", {
9394
9687
  enumerable: true,
9395
9688
  get: function() {