@atscript/db 0.1.153 → 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-DrE4ho_m.mjs → column-diff-C68V_eYM.mjs} +348 -69
  6. package/dist/{column-diff-CbdWi_uZ.cjs → column-diff-DMkGYAGM.cjs} +359 -68
  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,14 +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;
681
- const metadata = /* @__PURE__ */ new Map();
682
- for (const entry of entries) {
683
- if (entry.member && isInlinePrimitive(entry.type)) continue;
684
- for (const [key, value] of entry.metadata) if (!entry.member || !key.startsWith("db.")) metadata.set(key, value);
685
- }
787
+ const prop = entries.find((e) => !e.member);
788
+ const base = prop && require_validator.columnUnionBase(prop.type);
686
789
  const target = flat.metadata;
687
790
  target.clear();
688
- for (const [key, value] of metadata) target.set(key, value);
791
+ for (const entry of entries) {
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);
795
+ }
796
+ if (base && prop) prop.metadata = target;
689
797
  }
690
798
  }
691
799
  /**
@@ -708,7 +816,7 @@ var TableMetadata = class {
708
816
  * Scans `@db.*` and `@meta.id` annotations on a field during flattening.
709
817
  */
710
818
  _scanGenericAnnotations(fieldName, fieldType, metadata, logger) {
711
- if (metadata.has("meta.id")) {
819
+ if (metadata.has("meta.id") && !fieldName.includes(".")) {
712
820
  this.primaryKeys.push(fieldName);
713
821
  this.originalMetaIdFields.push(fieldName);
714
822
  }
@@ -971,7 +1079,7 @@ var TableMetadata = class {
971
1079
  const isJson = this.jsonFields.has(path);
972
1080
  const isArray = designType === "array";
973
1081
  const isObject = designType === "object";
974
- if (isArray) this.jsonFields.add(path);
1082
+ if (isArray || isStructuredMixedUnion(type)) this.jsonFields.add(path);
975
1083
  else if (isObject && isJson) {} else if (isObject && !isJson) this.flattenedParents.add(path);
976
1084
  }
977
1085
  for (const ignoredField of this.ignoredFields) if (this.flattenedParents.has(ignoredField)) {
@@ -1073,6 +1181,7 @@ var TableMetadata = class {
1073
1181
  const isJson = this.jsonFields.has(path);
1074
1182
  const isFlattened = !skipFlattening && require_object.findAncestorInSet(path, this.flattenedParents) !== void 0;
1075
1183
  const designType = isEncrypted ? "string" : isJson ? "json" : resolveDesignType(type);
1184
+ const colType = columnType(type);
1076
1185
  let storage;
1077
1186
  if (skipFlattening) storage = "column";
1078
1187
  else if (isEncrypted) storage = isFlattened ? "flattened" : "column";
@@ -1091,10 +1200,10 @@ var TableMetadata = class {
1091
1200
  if (quantityRef) this.quantityRefByField.set(path, quantityRef);
1092
1201
  descriptors.push({
1093
1202
  path,
1094
- type,
1203
+ type: colType,
1095
1204
  physicalName,
1096
1205
  designType,
1097
- optional: type.optional === true,
1206
+ optional: skipFlattening ? type.optional === true : this._columnNullable(path, type),
1098
1207
  isPrimaryKey: this.primaryKeys.includes(path),
1099
1208
  ignored: this.ignoredFields.has(path),
1100
1209
  defaultValue: this.defaults.get(path),
@@ -1108,7 +1217,7 @@ var TableMetadata = class {
1108
1217
  unitCode,
1109
1218
  unitRefField,
1110
1219
  encrypted: isEncrypted || underEncrypted || void 0,
1111
- isGeoPoint: isGeoPointType(type) || void 0,
1220
+ isGeoPoint: isGeoPointType(colType) || void 0,
1112
1221
  derived: this.derivedFields.get(path),
1113
1222
  computed: computedMeta(rootType, path)
1114
1223
  });
@@ -1236,7 +1345,7 @@ var TableMetadata = class {
1236
1345
  for (const index of this.indexes.values()) for (const field of index.fields) {
1237
1346
  const ftype = this.flatMap.get(field.name);
1238
1347
  if (ftype) {
1239
- field.optional = ftype.optional === true;
1348
+ field.optional = this._columnNullable(field.name, ftype);
1240
1349
  field.designType = ftype.type.tags?.has("objectId") ? "objectId" : resolveDesignType(ftype);
1241
1350
  }
1242
1351
  field.name = this.physicalPath(field.name);
@@ -1649,6 +1758,78 @@ function rewriteIntegerRegex(filter, meta) {
1649
1758
  return out ?? filter;
1650
1759
  }
1651
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
1652
1833
  //#region src/query/aggregate-expr.ts
1653
1834
  /**
1654
1835
  * Output aliases of the computed `$select` entries of a grouped query —
@@ -2300,7 +2481,7 @@ var FieldMappingStrategy = class {
2300
2481
  * translate predicate operands at deeper levels).
2301
2482
  */
2302
2483
  translateFilter(filter, meta, depth = 0) {
2303
- filter = rewriteIntegerRegex(filter, meta);
2484
+ filter = rewriteObjectNullTests(rewriteIntegerRegex(filter, meta), meta);
2304
2485
  const has = require_nested_writer.containsRelationFilter(filter);
2305
2486
  const resolved = has ? require_nested_writer.resolveRelationFilterTree(filter, meta, depth) : filter;
2306
2487
  return this.noteTranslated(filter, this.translateResolvedFilter(resolved, meta), has);
@@ -2642,13 +2823,13 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2642
2823
  current = created;
2643
2824
  } else current = next;
2644
2825
  }
2645
- current[col.last] = value;
2826
+ if (!col.partial || value !== null && value !== void 0) current[col.last] = value;
2646
2827
  }
2647
2828
  for (const parent of plan.parents) collapseNullParent(result, parent);
2648
2829
  return result;
2649
2830
  }
2650
2831
  translateQuery(query, meta) {
2651
- const logical = rewriteIntegerRegex(query.filter, meta);
2832
+ const logical = rewriteObjectNullTests(rewriteIntegerRegex(query.filter, meta), meta);
2652
2833
  const has = require_nested_writer.containsRelationFilter(logical);
2653
2834
  const filter = has ? require_nested_writer.resolveRelationFilterTree(logical, meta, 0) : logical;
2654
2835
  if (!meta.requiresMappings) {
@@ -2703,7 +2884,11 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2703
2884
  return result;
2704
2885
  }
2705
2886
  prepareForWrite(payload, meta, adapter) {
2706
- 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
+ }
2707
2892
  const data = { ...payload };
2708
2893
  this.prepareCommon(data, meta, adapter);
2709
2894
  if (meta.onlyColumnRenames) {
@@ -2730,7 +2915,7 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2730
2915
  const suffix = operatorMatch ? operatorMatch[2] : "";
2731
2916
  const fd = meta.leafByLogical.get(basePath);
2732
2917
  const finalKey = (fd?.physicalName ?? basePath) + suffix;
2733
- 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);
2734
2919
  else result[finalKey] = value;
2735
2920
  }
2736
2921
  return this.formatWriteValues(result, meta);
@@ -2810,6 +2995,49 @@ var RelationalFieldMapper = class extends FieldMappingStrategy {
2810
2995
  return result;
2811
2996
  }
2812
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
+ }
2813
3041
  /**
2814
3042
  * Per-table read plan of {@link RelationalFieldMapper.reconstructFromRead}:
2815
3043
  * the per-column decisions (coercion, JSON, nesting — the dot-path pre-split)
@@ -2829,12 +3057,14 @@ function compileReadPlan(meta) {
2829
3057
  const fromFmts = meta.fromStorageFormatters;
2830
3058
  const columns = /* @__PURE__ */ new Map();
2831
3059
  for (const [physical, fd] of meta.leafByPhysical) {
2832
- 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;
2833
3062
  columns.set(physical, {
2834
3063
  path: fd.path,
2835
3064
  parents: segs?.slice(0, -1),
2836
3065
  last: segs ? segs[segs.length - 1] : fd.path,
2837
3066
  json: fd.storage === "json",
3067
+ partial: nested && meta.presence(fd.path) === "partial",
2838
3068
  coerce: fd.designType === "boolean" ? toBool : fd.designType === "decimal" ? toDecimalString : void 0,
2839
3069
  fromFmt: fromFmts?.get(physical)
2840
3070
  });
@@ -2845,15 +3075,16 @@ function compileReadPlan(meta) {
2845
3075
  parents.push({
2846
3076
  ancestors: segs.slice(0, -1),
2847
3077
  last: segs[segs.length - 1],
2848
- optional: !!meta.flatMap?.get(parentPath)?.optional
3078
+ presence: meta.presence(parentPath) ?? "required"
2849
3079
  });
2850
3080
  }
3081
+ parents.sort((a, b) => b.ancestors.length - a.ancestors.length);
2851
3082
  return {
2852
3083
  columns,
2853
3084
  parents
2854
3085
  };
2855
3086
  }
2856
- /** 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}). */
2857
3088
  function collapseNullParent(obj, parent) {
2858
3089
  let current = obj;
2859
3090
  const ancestors = parent.ancestors;
@@ -2868,7 +3099,8 @@ function collapseNullParent(obj, parent) {
2868
3099
  const v = parentObj[k];
2869
3100
  if (v !== null && v !== void 0) return;
2870
3101
  }
2871
- current[parent.last] = parent.optional ? null : {};
3102
+ if (parent.presence === "partial") delete current[parent.last];
3103
+ else current[parent.last] = parent.presence === "nullable" ? null : {};
2872
3104
  }
2873
3105
  /**
2874
3106
  * Per-table write plan of the relational flatten: each logical path's
@@ -3310,6 +3542,10 @@ function collectQueryPaths(query, aggregate) {
3310
3542
  path,
3311
3543
  predicate,
3312
3544
  relation: relationOpsOf(value)
3545
+ } : isNullTest(value) ? {
3546
+ path,
3547
+ predicate,
3548
+ nullTest: true
3313
3549
  } : {
3314
3550
  path,
3315
3551
  predicate
@@ -3409,10 +3645,11 @@ function pathSourceOf(meta) {
3409
3645
  * `FieldCapabilityIndex`, with `$with` hints and leaf lists) is what clients
3410
3646
  * see and is authoritative — the HTTP gate always answers first.
3411
3647
  */
3412
- function guardPath(meta, adapter, path, op, predicate = "compare") {
3648
+ function guardPath(meta, adapter, path, op, predicate = "compare", nullTest = false) {
3413
3649
  const verb = OP_VERB[op];
3414
3650
  const { kind, parent } = classifyQueryPath(pathSourceOf(meta), path);
3415
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;
3416
3653
  switch (kind) {
3417
3654
  case "nav":
3418
3655
  if (op === "filter" && predicate === "relation" && parent === void 0) return;
@@ -3497,7 +3734,7 @@ function guardPaths(meta, adapter, query, aggregate = false, state) {
3497
3734
  if (refs.unsupportedOperator !== void 0) throw pathError(refs.unsupportedOperator, unsupportedOperatorMessage(refs.unsupportedOperator));
3498
3735
  let relState = state;
3499
3736
  for (const ref of refs.filter) {
3500
- guardPath(meta, adapter, ref.path, "filter", ref.predicate);
3737
+ guardPath(meta, adapter, ref.path, "filter", ref.predicate, ref.nullTest);
3501
3738
  if (ref.predicate === "relation") guardRelationRef(meta, adapter, ref, relState ??= require_nested_writer.relGuardState());
3502
3739
  }
3503
3740
  for (const path of refs.sort) guardPath(meta, adapter, path, "sort");
@@ -3639,10 +3876,12 @@ var NullsSource = class {
3639
3876
  }
3640
3877
  /**
3641
3878
  * Whether ordering by the LOGICAL field `path` can meet NULL (or a missing
3642
- * value): the field or one of its parent objects is optional, the field is
3643
- * a derived column, it is not a known field, or the source is a view. A
3644
- * `$nulls` entry on any other field changes nothing and is dropped, so the
3645
- * 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.
3646
3885
  */
3647
3886
  nullable(path) {
3648
3887
  let answer = this._nullable.get(path);
@@ -3656,12 +3895,7 @@ var NullsSource = class {
3656
3895
  const { meta } = this._init;
3657
3896
  const fd = meta.descriptorByPath.get(path);
3658
3897
  if (!fd || fd.derived) return true;
3659
- for (let p = path;;) {
3660
- if (meta.flatMap.get(p)?.optional === true) return true;
3661
- const dot = p.lastIndexOf(".");
3662
- if (dot === -1) return false;
3663
- p = p.slice(0, dot);
3664
- }
3898
+ return meta.isNullable(path);
3665
3899
  }
3666
3900
  /**
3667
3901
  * The effective `$nulls` of the `$sort` keys of one read: the request's
@@ -3776,7 +4010,9 @@ function geoIndexNotFoundMessage(tableName, indexName) {
3776
4010
  * otherwise trips up every adapter author.
3777
4011
  *
3778
4012
  * For union types (e.g., from flattened `{...} | {...}` objects):
3779
- * - 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)
3780
4016
  * - If members disagree → returns `'union'` (out of scope for type management)
3781
4017
  */
3782
4018
  function resolveDesignType(fieldType) {
@@ -3784,9 +4020,9 @@ function resolveDesignType(fieldType) {
3784
4020
  if (fieldType.type.kind === "object") return "object";
3785
4021
  if (fieldType.type.kind === "array") return "array";
3786
4022
  if (fieldType.type.kind === "union") {
3787
- const items = fieldType.type.items;
3788
- if (items.length > 0) {
3789
- 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));
3790
4026
  if (resolved.every((type) => type === resolved[0])) return resolved[0];
3791
4027
  }
3792
4028
  return "union";
@@ -4442,21 +4678,13 @@ var AtscriptDbReadable = class {
4442
4678
  _totalOrderKeys() {
4443
4679
  if (this._totalOrderKeysCache) return this._totalOrderKeysCache;
4444
4680
  const meta = this._meta;
4445
- const nullable = (logical) => {
4446
- for (let path = logical;;) {
4447
- if (meta.flatMap.get(path)?.optional === true) return true;
4448
- const dot = path.lastIndexOf(".");
4449
- if (dot === -1) return false;
4450
- path = path.slice(0, dot);
4451
- }
4452
- };
4453
4681
  const sets = [];
4454
4682
  if (meta.primaryKeys.length > 0) sets.push(meta.primaryKeys.map((f) => meta.physicalPath(f)));
4455
4683
  for (const index of meta.indexes.values()) {
4456
4684
  if (index.type !== "unique") continue;
4457
- 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));
4458
4686
  }
4459
- 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)]);
4460
4688
  this._totalOrderKeysCache = sets;
4461
4689
  return sets;
4462
4690
  }
@@ -6077,7 +6305,7 @@ var BaseDbAdapter = class {
6077
6305
  }
6078
6306
  /**
6079
6307
  * Whether the table has at least one row. Schema sync uses it in the
6080
- * 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.
6081
6309
  * Override with an EXISTS/LIMIT 1 probe — this default is `count() > 0`,
6082
6310
  * a full scan on some engines, and it can only answer for the adapter's
6083
6311
  * OWN table: for another `tableName` (or on an administrative adapter
@@ -6497,6 +6725,10 @@ function flattenPatchPayload(payload, prefix, update, meta, table) {
6497
6725
  const key = prefix ? `${prefix}.${_key}` : _key;
6498
6726
  if (primaryKeys.includes(key)) continue;
6499
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
+ }
6500
6732
  update[key] = value;
6501
6733
  continue;
6502
6734
  }
@@ -7674,26 +7906,56 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7674
7906
  * full row). Function defaults (`now` / `uuid` / `increment` / custom) the
7675
7907
  * adapter handles natively are NOT filled — the field stays absent so the
7676
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.
7677
7913
  */
7678
7914
  _applyDefaults(data) {
7679
7915
  const nativeFns = this.adapter.nativeDefaultFns();
7680
7916
  const versionField = this._meta.versionField;
7681
7917
  for (const [field, def] of this._meta.defaults.entries()) {
7682
7918
  if (field === versionField) continue;
7683
- if (data[field] === void 0) {
7684
- if (def.kind === "value") data[field] = this._valueDefault(field, def.value);
7685
- else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
7686
- case "now":
7687
- data[field] = Date.now();
7688
- break;
7689
- case "uuid":
7690
- data[field] = crypto.randomUUID();
7691
- break;
7692
- }
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();
7693
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);
7694
7929
  }
7695
7930
  return data;
7696
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
+ }
7697
7959
  /**
7698
7960
  * Parsed `@db.default 'literal'` values that are primitives — reused for
7699
7961
  * every row (an object / array default is parsed per row: each row gets
@@ -7859,6 +8121,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7859
8121
  return this.createValidator({ plugins: adapterPlugins });
7860
8122
  }
7861
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
+ }
7862
8139
  //#endregion
7863
8140
  //#region src/table/view-read-plan.ts
7864
8141
  /** Design types whose `=` compares one scalar to one scalar on every engine. */
@@ -8971,7 +9248,9 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
8971
9248
  });
8972
9249
  }
8973
9250
  const desiredDefault = serializeDefaultValue(field.defaultValue);
8974
- 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({
8975
9254
  field,
8976
9255
  oldDefault: existingCol.dflt_value,
8977
9256
  newDefault: desiredDefault
@@ -9368,6 +9647,12 @@ Object.defineProperty(exports, "numericOperandProblem", {
9368
9647
  return numericOperandProblem;
9369
9648
  }
9370
9649
  });
9650
+ Object.defineProperty(exports, "relationalColumnName", {
9651
+ enumerable: true,
9652
+ get: function() {
9653
+ return relationalColumnName;
9654
+ }
9655
+ });
9371
9656
  Object.defineProperty(exports, "resolveCalendarBuckets", {
9372
9657
  enumerable: true,
9373
9658
  get: function() {
@@ -9392,6 +9677,12 @@ Object.defineProperty(exports, "searchTermInteger", {
9392
9677
  return searchTermInteger;
9393
9678
  }
9394
9679
  });
9680
+ Object.defineProperty(exports, "serializeDefaultValue", {
9681
+ enumerable: true,
9682
+ get: function() {
9683
+ return serializeDefaultValue;
9684
+ }
9685
+ });
9395
9686
  Object.defineProperty(exports, "snapshotToExistingColumns", {
9396
9687
  enumerable: true,
9397
9688
  get: function() {