@atscript/db 0.1.163 → 0.1.165

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 (55) hide show
  1. package/dist/agg.cjs +1 -1
  2. package/dist/agg.d.cts +1 -1
  3. package/dist/agg.d.mts +1 -1
  4. package/dist/agg.mjs +1 -1
  5. package/dist/{aggregate-fns-DupYzFD7.cjs → aggregate-fns-CNtrSxGH.cjs} +1 -1
  6. package/dist/{aggregate-fns-DsS9UQVB.mjs → aggregate-fns-CmtSNIHm.mjs} +1 -1
  7. package/dist/{buckets-B8UOod-v.d.cts → buckets-BDxKfCNo.d.cts} +143 -13
  8. package/dist/{buckets-CHyspjYP.d.mts → buckets-CHUgky8T.d.mts} +143 -13
  9. package/dist/{column-diff-BfNIlMwX.mjs → column-diff-9pF0d8JP.mjs} +354 -31
  10. package/dist/{column-diff-BIGX3sRH.cjs → column-diff-DjeUj9kb.cjs} +371 -30
  11. package/dist/control-B5BJypqL.cjs +16 -0
  12. package/dist/control-CevYIJC4.mjs +16 -0
  13. package/dist/{db-error-hraC12B9.mjs → db-error-BMy4-OWq.mjs} +23 -1
  14. package/dist/{db-error-B_DRL1E9.d.cts → db-error-DQUw2cED.d.cts} +65 -9
  15. package/dist/{db-error-B_DRL1E9.d.mts → db-error-DQUw2cED.d.mts} +65 -9
  16. package/dist/{db-error-DmF1_xrz.cjs → db-error-G3sgvL87.cjs} +28 -0
  17. package/dist/{fk-diff-BNGawZi9.d.cts → fk-diff-DSM7O7hs.d.cts} +1 -1
  18. package/dist/{fk-diff-rj9N2QLw.d.mts → fk-diff-Dapr5Dk-.d.mts} +1 -1
  19. package/dist/index.cjs +17 -11
  20. package/dist/index.d.cts +38 -12
  21. package/dist/index.d.mts +38 -12
  22. package/dist/index.mjs +14 -12
  23. package/dist/{nested-writer-ClUX0DB0.mjs → nested-writer-BraWjS8a.mjs} +177 -26
  24. package/dist/{nested-writer-yCy6wGuN.cjs → nested-writer-DOvdbYG-.cjs} +194 -31
  25. package/dist/ops.cjs +1 -1
  26. package/dist/ops.mjs +1 -1
  27. package/dist/plugin.cjs +15 -12
  28. package/dist/plugin.mjs +15 -12
  29. package/dist/rel.cjs +2 -2
  30. package/dist/rel.d.cts +2 -2
  31. package/dist/rel.d.mts +2 -2
  32. package/dist/rel.mjs +2 -2
  33. package/dist/{relation-helpers-DloMywNs.d.cts → relation-helpers-C4UNWYQu.d.cts} +1 -1
  34. package/dist/{relation-helpers-zwt19E7r.d.mts → relation-helpers-hdbuqwjV.d.mts} +1 -1
  35. package/dist/{relation-loader-Qv3LrQ42.mjs → relation-loader-BOEVhcBe.mjs} +1 -1
  36. package/dist/{relation-loader-DujL_Mh9.cjs → relation-loader-BrkakVF_.cjs} +1 -1
  37. package/dist/shared.cjs +3 -1
  38. package/dist/shared.d.cts +13 -2
  39. package/dist/shared.d.mts +13 -2
  40. package/dist/shared.mjs +2 -2
  41. package/dist/sync.cjs +107 -25
  42. package/dist/sync.d.cts +53 -6
  43. package/dist/sync.d.mts +53 -6
  44. package/dist/sync.mjs +107 -25
  45. package/dist/{validator-DK9Ijpo7.cjs → validator-D7E7T0Oy.cjs} +1 -1
  46. package/dist/{validator-DuDWsIdQ.mjs → validator-UpgPhwvH.mjs} +1 -1
  47. package/dist/validator.cjs +2 -2
  48. package/dist/validator.d.cts +1 -1
  49. package/dist/validator.d.mts +1 -1
  50. package/dist/validator.mjs +2 -2
  51. package/dist/{view-validation-BiDLCYMw.cjs → view-validation-C0UVLx_H.cjs} +38 -10
  52. package/dist/{view-validation-BcgDq7M9.mjs → view-validation-KW77xIKn.mjs} +28 -12
  53. package/package.json +6 -6
  54. package/dist/control-D5cQmzFj.mjs +0 -14
  55. package/dist/control-jIVU2hkf.cjs +0 -14
@@ -1,7 +1,7 @@
1
- import { a as aggregateExpressionsNotSupported, c as invalidQuery, d as spaceClosedError, n as CasMismatchError, r as DbError } from "./db-error-hraC12B9.mjs";
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-DsS9UQVB.mjs";
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-ClUX0DB0.mjs";
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-DuDWsIdQ.mjs";
1
+ import { a as aggregateExpressionsNotSupported, f as spaceClosedError, l as invalidQuery, n as CasMismatchError, r as DbError } from "./db-error-BMy4-OWq.mjs";
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-CmtSNIHm.mjs";
3
+ import { A as createRelationFilterHost, B as findFKForRelation, C as sameKey, F as relGuardState, G as computeOf, J as isFieldRef, K as computedOperands, L as resolveRelationFilterTree, M as hasRelationOp, P as noteRelationFilter, R as compositeKey, S as rowMatchesKey, U as slicePerGroup, V as findRemoteFK, W as tableNameOf, X as viewExprNullable, Z as walkViewExpr, _ as fkRestrictEntry, a as batchPatchNestedFrom, b as findRowsByKeys, c as batchReplaceNestedFrom, d as checkDepthOverflow, f as planNestedFromVia, g as fkMissingEntries, h as validateBatch, i as batchInsertNestedVia, k as containsRelationFilter, 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 normalizeWriteErrors, w as uniqueKeyTuple, x as pkTupleKey, y as remapDeleteFkViolation, z as findFKEntryForRelation } from "./nested-writer-BraWjS8a.mjs";
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-UpgPhwvH.mjs";
5
5
  import { a as searchMemberKind, c as DERIVED_INCOMPATIBLE, n as ENCRYPTED_SEARCH_FEATURES, o as numericTypeProblem, t as ENCRYPTED_INCOMPATIBLE, u as isJsonLeafType } from "./search-fields-BPNsxrCk.mjs";
6
6
  import { a as isPlainObject, i as isEmptyObject, n as findAncestorInSet, o as selfOrAncestor, r as getPath, s as stableJson, t as deletePath } from "./object-PQ1J8jri.mjs";
7
7
  import { resolveAlias as resolveAlias$1 } from "./agg.mjs";
@@ -10,6 +10,74 @@ import { isDbFieldOp, separateCas, separateFieldOps } from "./ops.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";
13
+ //#region src/shared/with-nesting.ts
14
+ /**
15
+ * Rewrites dotted `$with` names into nesting (since 0.1.165):
16
+ * `{ name: "a.b", filter, controls }` becomes
17
+ * `{ name: "a", controls: { $with: [{ name: "b", filter, controls }] } }`,
18
+ * at every depth. Entries naming the same relation at one level are merged
19
+ * into one — the first one's `filter` / `controls` win, the nested `$with`
20
+ * lists are concatenated — so `$with=a($select=x),a.b` loads `a` once with
21
+ * `b` nested in it. Before, a dotted name was silently skipped.
22
+ *
23
+ * Returns the input array itself when no entry at any level is dotted or a
24
+ * string (the common case allocates nothing). String entries (`"a"`, `"a.b"`
25
+ * — the shorthand the `$with` type admits) are read as `{ name }`.
26
+ */
27
+ function nestDottedWith(withRelations) {
28
+ if (!withRelations.some(needsRewrite)) return withRelations;
29
+ const out = [];
30
+ const byName = /* @__PURE__ */ new Map();
31
+ for (const raw of withRelations) {
32
+ const rel = typeof raw === "string" ? { name: raw } : raw;
33
+ const dot = rel.name.indexOf(".");
34
+ const head = dot < 0 ? rel.name : rel.name.slice(0, dot);
35
+ const entry = dot < 0 ? { ...rel } : {
36
+ name: head,
37
+ controls: { $with: [{
38
+ ...rel,
39
+ name: rel.name.slice(dot + 1)
40
+ }] }
41
+ };
42
+ const existing = byName.get(head);
43
+ if (!existing) {
44
+ byName.set(head, entry);
45
+ out.push(entry);
46
+ continue;
47
+ }
48
+ const extra = entry.controls?.$with;
49
+ existing.filter ??= entry.filter;
50
+ const controls = {
51
+ ...entry.controls,
52
+ ...existing.controls
53
+ };
54
+ const nested = [...existing.controls?.$with ?? [], ...extra ?? []];
55
+ if (nested.length > 0) controls.$with = nested;
56
+ existing.controls = controls;
57
+ }
58
+ for (const entry of out) {
59
+ const nested = entry.controls?.$with;
60
+ if (nested?.length) {
61
+ const next = nestDottedWith(nested);
62
+ if (next !== nested) entry.controls = {
63
+ ...entry.controls,
64
+ $with: next
65
+ };
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * An entry is rewritten when it (or a nested one) is dotted, or is the
72
+ * string shorthand (typed as allowed, but the loaders read `.name`).
73
+ */
74
+ function needsRewrite(raw) {
75
+ if (typeof raw === "string") return true;
76
+ if (raw.name.includes(".")) return true;
77
+ const nested = raw.controls?.$with;
78
+ return Array.isArray(nested) && nested.some(needsRewrite);
79
+ }
80
+ //#endregion
13
81
  //#region src/logger.ts
14
82
  const NoopLogger = {
15
83
  error: () => {},
@@ -511,6 +579,12 @@ var TableMetadata = class {
511
579
  */
512
580
  onUpdateNow = /* @__PURE__ */ new Set();
513
581
  /**
582
+ * Top-level `number.timestamp.created` fields (since 0.1.165): set by the
583
+ * insert only — a replace keeps the stored value, a replace or patch
584
+ * payload's value is dropped.
585
+ */
586
+ insertOnly = /* @__PURE__ */ new Set();
587
+ /**
514
588
  * Top-level fields annotated with `@db.onUpdate.inc`: `0` on insert, `+1` in
515
589
  * the same statement on every update and replace.
516
590
  * @since 0.1.162
@@ -848,6 +922,7 @@ var TableMetadata = class {
848
922
  const resolvedDefault = resolveDefaultFromMetadata(metadata);
849
923
  if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
850
924
  if (metadata.has("db.onUpdate.now")) this.onUpdateNow.add(fieldName);
925
+ else if (!fieldName.includes(".") && hasPrimitiveTag(fieldType, "created")) this.insertOnly.add(fieldName);
851
926
  if (metadata.has("db.onUpdate.inc")) {
852
927
  if (fieldName.includes(".")) throw new Error(`@db.onUpdate.inc on "${fieldName}" must sit on a top-level field of the table`);
853
928
  this.onUpdateInc.add(fieldName);
@@ -880,9 +955,10 @@ var TableMetadata = class {
880
955
  if (metadata.has("db.rel.FK")) {
881
956
  const raw = metadata.get("db.rel.FK");
882
957
  const alias = raw === true ? void 0 : raw;
883
- if (fieldType.ref) {
884
- const targetTable = tableNameOf(fieldType.ref.type());
885
- const targetField = fieldType.ref.field;
958
+ const fkRef = fieldType.ref ?? soleUnionMember(fieldType)?.ref;
959
+ if (fkRef) {
960
+ const targetTable = tableNameOf(fkRef.type());
961
+ const targetField = fkRef.field;
886
962
  const key = alias || `__auto_${fieldName}`;
887
963
  const existing = this.foreignKeys.get(key);
888
964
  if (existing) {
@@ -892,7 +968,7 @@ var TableMetadata = class {
892
968
  fields: [fieldName],
893
969
  targetTable,
894
970
  targetFields: [targetField],
895
- targetTypeRef: fieldType.ref.type,
971
+ targetTypeRef: fkRef.type,
896
972
  alias
897
973
  });
898
974
  }
@@ -1242,7 +1318,8 @@ var TableMetadata = class {
1242
1318
  encrypted: isEncrypted || underEncrypted || void 0,
1243
1319
  isGeoPoint: isGeoPointType(colType) || void 0,
1244
1320
  derived: this.derivedFields.get(path),
1245
- computed: computedMeta(rootType, path)
1321
+ computed: computedMeta(rootType, path),
1322
+ insertOnly: this.insertOnly.has(path) || void 0
1246
1323
  });
1247
1324
  }
1248
1325
  const flatCache = /* @__PURE__ */ new Map();
@@ -1452,6 +1529,16 @@ function computedMeta(rootType, path) {
1452
1529
  via: Object.freeze(computed.via)
1453
1530
  } : void 0;
1454
1531
  }
1532
+ /**
1533
+ * Whether the column of `type` is the tagged primitive: the type itself, or
1534
+ * the one value member of `T | null` ({@link columnUnionBase}). Not a
1535
+ * reference to another field (`a: Other.createdAt` does not share its
1536
+ * semantics), nor a wider union (`number.timestamp.created | string`).
1537
+ */
1538
+ function hasPrimitiveTag(type, tag) {
1539
+ if (type.ref?.field) return false;
1540
+ return (columnUnionBase(type) ?? type).type.tags?.has(tag) === true;
1541
+ }
1455
1542
  //#endregion
1456
1543
  //#region src/shared/search-term.ts
1457
1544
  /**
@@ -1504,6 +1591,16 @@ function describeFulltext(index, textLabel) {
1504
1591
  function defaultFulltextIndex(indexes) {
1505
1592
  return indexes.find((i) => splitFulltextFields(i).text.length > 0) ?? indexes[0];
1506
1593
  }
1594
+ /**
1595
+ * The fulltext index a search names (since 0.1.165): the one DECLARED with
1596
+ * that name (`@db.index.fulltext 'name'`), else the one whose internal key
1597
+ * (`atscript__fulltext__<name>`) it is — a deprecated alias kept for callers
1598
+ * that copied the key from an older `getSearchIndexes()`. `undefined` when
1599
+ * none matches; a search naming no index uses {@link defaultFulltextIndex}.
1600
+ */
1601
+ function resolveFulltextIndex(indexes, indexName) {
1602
+ return indexes.find((i) => i.name === indexName) ?? indexes.find((i) => i.key === indexName);
1603
+ }
1507
1604
  //#endregion
1508
1605
  //#region src/query/filter-values.ts
1509
1606
  const OPAQUE = {
@@ -4444,10 +4541,26 @@ var AtscriptDbReadable = class {
4444
4541
  */
4445
4542
  get onUpdateIncPhysical() {
4446
4543
  this._ensureBuilt();
4447
- return this._onUpdateIncPhysical ??= [...this._meta.onUpdateInc].map((field) => this._meta.columnMap.get(field) ?? field);
4544
+ return this._onUpdateIncPhysical ??= this._physicalNames(this._meta.onUpdateInc);
4448
4545
  }
4449
4546
  _onUpdateIncPhysical;
4450
4547
  /**
4548
+ * Physical column names of the insert-only fields (`number.timestamp.created`,
4549
+ * since 0.1.165): a full replace keeps their stored values. Empty when
4550
+ * there are none.
4551
+ *
4552
+ * @internal Adapter-facing surface; not part of the consumer API.
4553
+ */
4554
+ get insertOnlyPhysical() {
4555
+ this._ensureBuilt();
4556
+ return this._insertOnlyPhysical ??= this._physicalNames(this._meta.insertOnly);
4557
+ }
4558
+ _insertOnlyPhysical;
4559
+ /** The physical columns of a set of top-level logical fields. */
4560
+ _physicalNames(fields) {
4561
+ return [...fields].map((field) => this._meta.columnMap.get(field) ?? field);
4562
+ }
4563
+ /**
4451
4564
  * Logical paths of the `@db.column.version.exempt` fields, including objects
4452
4565
  * whose direct children are all exempt. A patch that writes only these (and
4453
4566
  * no `$cas`) does not bump the version column. Empty when the table has no
@@ -4606,8 +4719,22 @@ var AtscriptDbReadable = class {
4606
4719
  */
4607
4720
  _translateRead(query) {
4608
4721
  let readQuery = query ?? {};
4609
- const controls = readQuery.controls;
4610
- const withRelations = controls?.$with;
4722
+ let controls = readQuery.controls;
4723
+ let withRelations = controls?.$with;
4724
+ if (withRelations?.length) {
4725
+ const nested = nestDottedWith(withRelations);
4726
+ if (nested !== withRelations) {
4727
+ withRelations = nested;
4728
+ controls = {
4729
+ ...controls,
4730
+ $with: nested
4731
+ };
4732
+ readQuery = {
4733
+ ...readQuery,
4734
+ controls
4735
+ };
4736
+ }
4737
+ }
4611
4738
  let widened = [];
4612
4739
  if (withRelations?.length && controls?.$select) {
4613
4740
  const widen = this._widenSelectForWith(controls.$select, withRelations);
@@ -5534,8 +5661,8 @@ var AtscriptDbReadable = class {
5534
5661
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
5535
5662
  */
5536
5663
  async loadRelations(rows, withRelations) {
5537
- const { loadRelationsImpl } = await import("./relation-loader-Qv3LrQ42.mjs").then((n) => n.n);
5538
- return loadRelationsImpl(rows, withRelations, this);
5664
+ const { loadRelationsImpl } = await import("./relation-loader-BOEVhcBe.mjs").then((n) => n.n);
5665
+ return loadRelationsImpl(rows, nestDottedWith(withRelations), this);
5539
5666
  }
5540
5667
  /**
5541
5668
  * Finds the FK entry that connects a `@db.rel.to` relation to its target.
@@ -6779,10 +6906,7 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
6779
6906
  if (await target.count(filter) < expectedCount) {
6780
6907
  const sample = firstValues.slice(0, 3).join(", ");
6781
6908
  const suffix = firstValues.length > 3 ? `, ... (${firstValues.length} total)` : "";
6782
- throw new DbError("FK_VIOLATION", [{
6783
- path: fk.fields.join(", "),
6784
- message: `FK constraint violation: "${fk.fields.join(", ")}" references non-existent record in "${fk.targetTable}" (values: ${sample}${suffix})`
6785
- }]);
6909
+ throw new DbError("FK_VIOLATION", fkMissingEntries(fk.fields, fk.targetTable, `values: ${sample}${suffix}`));
6786
6910
  }
6787
6911
  });
6788
6912
  }
@@ -6860,10 +6984,11 @@ var ApplicationIntegrity = class extends IntegrityStrategy {
6860
6984
  const childFilter = this.buildCascadeChildFilter(records, target.fk);
6861
6985
  if (!childFilter) continue;
6862
6986
  restrictChecks.push(target.count(childFilter).then((count) => {
6863
- if (count > 0) throw new DbError("CONFLICT", [{
6864
- path: tableName,
6865
- message: `Cannot delete from "${tableName}": ${count} record(s) in "${target.childTable}" (${target.fk.fields.join(", ")}) reference it (RESTRICT)`
6866
- }]);
6987
+ if (count > 0) throw new DbError("CONFLICT", [fkRestrictEntry(tableName, {
6988
+ referencedBy: target.childTable,
6989
+ count,
6990
+ fields: target.fk.fields
6991
+ })]);
6867
6992
  }));
6868
6993
  }
6869
6994
  if (restrictChecks.length > 0) await Promise.all(restrictChecks);
@@ -6953,6 +7078,22 @@ function pinByPrimaryKey(rows, pk) {
6953
7078
  return out;
6954
7079
  }
6955
7080
  //#endregion
7081
+ //#region src/table/fk-lookup.ts
7082
+ /**
7083
+ * The FK lookup target of a table: `count` and the key projection `find`
7084
+ * (`$select` of the target fields) that `validateRows({ fk: true })` diffs.
7085
+ * One wrapper for the space's resolver and a table's writable fallback.
7086
+ */
7087
+ function fkLookupTargetOf(table) {
7088
+ return {
7089
+ count: (filter) => table.count({ filter }),
7090
+ find: (filter, fields) => table.findMany({
7091
+ filter,
7092
+ controls: { $select: [...fields] }
7093
+ })
7094
+ };
7095
+ }
7096
+ //#endregion
6956
7097
  //#region src/patch/array-ops-resolver.ts
6957
7098
  /**
6958
7099
  * Resolves array patch operator keys (`__$insert`, `__$remove`, `__$upsert`,
@@ -7493,6 +7634,23 @@ const UPDATE_RESULT = {
7493
7634
  };
7494
7635
  /** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
7495
7636
  const TOUCH_MANY_CHUNK = 500;
7637
+ /** Distinct FK key tuples per `validateRows` lookup query (well under every bind limit). */
7638
+ const FK_LOOKUP_CHUNK = 500;
7639
+ /** A key tuple's identity for the in-memory diff — scalar-text, so `1` and `"1"` agree like the engine compares them. */
7640
+ function fkTupleKey(values) {
7641
+ return JSON.stringify(values.map((v) => v instanceof Date ? v.getTime() : String(v)));
7642
+ }
7643
+ /** The write mode a standard validator purpose validates in when called without a context. */
7644
+ /** The validator purpose a write mode validates with (`validateRows`, the write paths). */
7645
+ const MODE_PURPOSE = {
7646
+ insert: "insert",
7647
+ replace: "bulkReplace",
7648
+ patch: "bulkUpdate"
7649
+ };
7650
+ const PURPOSE_MODES = {
7651
+ ...Object.fromEntries(Object.entries(MODE_PURPOSE).map(([mode, purpose]) => [purpose, mode])),
7652
+ patch: "patch"
7653
+ };
7496
7654
  var AtscriptDbTable = class extends AtscriptDbReadable {
7497
7655
  _cascadeResolver;
7498
7656
  _fkLookupResolver;
@@ -7526,13 +7684,175 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7526
7684
  * Adapters may define additional purposes.
7527
7685
  */
7528
7686
  getValidator(purpose) {
7529
- if (!this.validators.has(purpose)) {
7530
- const validator = this._buildValidator(purpose);
7687
+ let validator = this.validators.get(purpose);
7688
+ if (!validator) {
7689
+ validator = this._withDefaultContext(this._buildValidator(purpose), purpose);
7531
7690
  this.validators.set(purpose, validator);
7532
7691
  }
7533
- return this.validators.get(purpose);
7692
+ return validator;
7693
+ }
7694
+ /**
7695
+ * A standard purpose's validator called WITHOUT a context validates in that
7696
+ * purpose's write mode (since 0.1.165): `getValidator("insert").validate(row, true)`
7697
+ * accepts what an insert accepts — omitted primary key, defaulted, FK and
7698
+ * navigation fields. Before, the db plugin saw no mode and ran inert while
7699
+ * navigation fields were forced required. An explicit context still wins.
7700
+ */
7701
+ _withDefaultContext(validator, purpose) {
7702
+ const mode = PURPOSE_MODES[purpose];
7703
+ if (!mode) return validator;
7704
+ const validate = (value, safe, context) => validator.validate(value, safe, context ?? this._validationCtx(mode));
7705
+ return new Proxy(validator, { get: (target, key) => key === "validate" ? validate : Reflect.get(target, key, target) });
7706
+ }
7707
+ /** A fresh validation context of a write `mode` — the one its write paths build. */
7708
+ _validationCtx(mode) {
7709
+ this._ensureBuilt();
7710
+ return {
7711
+ mode,
7712
+ navFields: this._meta.navFields,
7713
+ ...mode === "patch" ? { flatMap: this.flatMap } : {}
7714
+ };
7715
+ }
7716
+ /** {@link normalizeWriteErrors} for this table: its constraint errors normalized against its metadata. */
7717
+ _normalizeWrite(fn) {
7718
+ return normalizeWriteErrors(this._meta, this.tableName, fn);
7719
+ }
7720
+ /**
7721
+ * Validates rows exactly as a write in `mode` would, without writing
7722
+ * (since 0.1.165): each row is cloned, given the defaults / stamps that
7723
+ * write applies, and validated with that write's validator and context.
7724
+ * Returns one `{ index, path, message }` per problem — `[]` when every row
7725
+ * is valid; the caller's rows are not modified.
7726
+ *
7727
+ * `fk: true` also checks that every foreign key names an existing row
7728
+ * (by lookup, on every adapter — native-FK adapters otherwise leave that to
7729
+ * the engine at write time); a missing target is reported on each of the
7730
+ * key's fields. Rows that fail type validation are not looked up.
7731
+ *
7732
+ * @param mode - `'insert'` (default), `'replace'` or `'patch'`.
7733
+ */
7734
+ async validateRows(rows, mode = "insert", opts) {
7735
+ this._ensureBuilt();
7736
+ const validator = this.getValidator(MODE_PURPOSE[mode]);
7737
+ const ctx = this._validationCtx(mode);
7738
+ this._applyDepthCtx(ctx, 0);
7739
+ const now = Date.now();
7740
+ const errors = [];
7741
+ const valid = [];
7742
+ for (let index = 0; index < rows.length; index++) {
7743
+ const raw = rows[index];
7744
+ let row = raw;
7745
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw)) {
7746
+ const clone = _cloneWritePayload(raw);
7747
+ delete clone.$cas;
7748
+ if (mode !== "patch") this._applyDefaults(clone, now);
7749
+ if (mode !== "insert") this._stampOnUpdate(clone, now, mode === "replace");
7750
+ row = clone;
7751
+ }
7752
+ try {
7753
+ if (validator.validate(row, true, ctx)) {
7754
+ valid.push({
7755
+ index,
7756
+ row
7757
+ });
7758
+ continue;
7759
+ }
7760
+ for (const e of validator.errors) errors.push({
7761
+ index,
7762
+ path: e.path,
7763
+ message: e.message
7764
+ });
7765
+ } catch (error) {
7766
+ errors.push({
7767
+ index,
7768
+ path: error.field ?? "",
7769
+ message: error instanceof Error ? error.message : String(error)
7770
+ });
7771
+ }
7772
+ }
7773
+ if (opts?.fk && valid.length > 0) {
7774
+ errors.push(...await this._missingForeignKeys(valid, mode === "patch"));
7775
+ errors.sort((a, b) => a.index - b.index);
7776
+ }
7777
+ return errors;
7778
+ }
7779
+ /**
7780
+ * {@link validateRows}' FK lookup: one entry per FK field of each row naming
7781
+ * a missing target. Distinct key tuples are looked up in chunks of
7782
+ * {@link FK_LOOKUP_CHUNK} — reading the target keys that exist (`$in` for a
7783
+ * single-column key, `$or` for a composite one) and diffing in memory, so a
7784
+ * batch never exceeds a bind-parameter limit and a miss costs no extra
7785
+ * query. A lookup target without `find` (a custom resolver) is counted per
7786
+ * chunk, and per tuple only in a chunk that comes up short.
7787
+ */
7788
+ async _missingForeignKeys(rows, partial) {
7789
+ const out = [];
7790
+ for (const fk of this._meta.foreignKeys.values()) {
7791
+ const target = this._fkTargetReader(fk);
7792
+ if (!target) continue;
7793
+ const byKey = /* @__PURE__ */ new Map();
7794
+ for (const { index, row } of rows) {
7795
+ if (partial && !fk.fields.some((f) => f in row)) continue;
7796
+ const values = fk.fields.map((f) => row[f]);
7797
+ if (values.some((v) => v === null || v === void 0)) continue;
7798
+ const key = fkTupleKey(values);
7799
+ const entry = byKey.get(key);
7800
+ if (entry) entry.indexes.push(index);
7801
+ else byKey.set(key, {
7802
+ values,
7803
+ indexes: [index]
7804
+ });
7805
+ }
7806
+ const tuples = [...byKey.values()];
7807
+ const tf = fk.targetFields;
7808
+ const filterOf = (values) => Object.fromEntries(tf.map((f, i) => [f, values[i]]));
7809
+ for (let at = 0; at < tuples.length; at += FK_LOOKUP_CHUNK) {
7810
+ const chunk = tuples.slice(at, at + FK_LOOKUP_CHUNK);
7811
+ const filter = tf.length === 1 ? { [tf[0]]: { $in: chunk.map((t) => t.values[0]) } } : { $or: chunk.map((t) => filterOf(t.values)) };
7812
+ let missing;
7813
+ if (target.find) {
7814
+ const found = new Set((await target.find(filter, tf)).map((r) => fkTupleKey(tf.map((f) => r[f]))));
7815
+ missing = chunk.filter((t) => !found.has(fkTupleKey(t.values)));
7816
+ } else {
7817
+ if (await target.count(filter) >= chunk.length) continue;
7818
+ missing = [];
7819
+ for (const t of chunk) if (await target.count(filterOf(t.values)) === 0) missing.push(t);
7820
+ }
7821
+ for (const tuple of missing) {
7822
+ const shown = tuple.values.map((v) => String(v)).join(", ");
7823
+ for (const index of tuple.indexes) for (const field of fk.fields) out.push({
7824
+ index,
7825
+ path: field,
7826
+ message: `References a non-existent record in "${fk.targetTable}" (${shown})`
7827
+ });
7828
+ }
7829
+ }
7830
+ }
7831
+ return out;
7832
+ }
7833
+ /** How {@link _missingForeignKeys} reads an FK's target table, if it can. */
7834
+ _fkTargetReader(fk) {
7835
+ const lookup = this._fkLookupResolver?.(fk.targetTable);
7836
+ if (lookup) return lookup;
7837
+ if (!fk.targetTypeRef || !this._writeTableResolver) return void 0;
7838
+ const table = this._writeTableResolver(fk.targetTypeRef());
7839
+ return table ? fkLookupTargetOf(table) : void 0;
7534
7840
  }
7535
7841
  async insertOne(payload, opts) {
7842
+ const { returning, ...insertOpts } = opts ?? {};
7843
+ if (!returning) return this._insertOneRow(payload, insertOpts);
7844
+ return this.adapter.withTransaction(async () => {
7845
+ const result = await this._insertOneRow(payload, insertOpts);
7846
+ if (result.insertedId === void 0) return result;
7847
+ const controls = returning === true ? void 0 : { $select: [...returning] };
7848
+ const row = await this.findById(result.insertedId, controls ? { controls } : void 0);
7849
+ return {
7850
+ ...result,
7851
+ row
7852
+ };
7853
+ });
7854
+ }
7855
+ async _insertOneRow(payload, opts) {
7536
7856
  const result = await this._insertMany([payload], {
7537
7857
  ...opts,
7538
7858
  _action: "insert"
@@ -7562,7 +7882,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7562
7882
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
7563
7883
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
7564
7884
  const singleStatement = !ignore && this._isSingleStatementWrite(payloads, depth, guard, check);
7565
- return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
7885
+ return this._normalizeWrite(() => this._inWriteTransaction(singleStatement, async () => {
7566
7886
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
7567
7887
  const now = Date.now();
7568
7888
  let items = payloads.map((p) => this._applyDefaults(clone(p), now));
@@ -7637,7 +7957,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7637
7957
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
7638
7958
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
7639
7959
  const singleStatement = this._isSingleStatementWrite(payloads, depth, guard, check);
7640
- return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
7960
+ return this._normalizeWrite(() => this._inWriteTransaction(singleStatement, async () => {
7641
7961
  const versionColumn = this.versionColumn;
7642
7962
  const expectedVersions = Array.from({ length: payloads.length });
7643
7963
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
@@ -7727,7 +8047,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7727
8047
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
7728
8048
  if (!canNest && this._meta.navFields.size > 0) checkDepthOverflow(payloads, maxDepth, this._meta);
7729
8049
  const singleStatement = this._isSingleStatementWrite(payloads, depth, guard, check);
7730
- return enrichFkViolation(this._meta, () => this._inWriteTransaction(singleStatement, async () => {
8050
+ return this._normalizeWrite(() => this._inWriteTransaction(singleStatement, async () => {
7731
8051
  const versionColumn = this.versionColumn;
7732
8052
  const expectedVersions = Array.from({ length: payloads.length });
7733
8053
  const clone = depth === 0 ? _cloneWritePayload : _shallowPrunedClone;
@@ -7987,7 +8307,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7987
8307
  modifiedCount: 0
7988
8308
  };
7989
8309
  }
7990
- return enrichFkViolation(this._meta, () => {
8310
+ return this._normalizeWrite(() => {
7991
8311
  if (!bound) return this.adapter.updateMany(translatedFilter, update, ops, ...updateOptsArg(keepVersion));
7992
8312
  return this._runBounded(bound, UPDATE_RESULT, (f, limit) => this.adapter.updateMany(f, update, ops, ...updateOptsArg(keepVersion, limit)));
7993
8313
  });
@@ -8013,7 +8333,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
8013
8333
  const versionColumn = this.versionColumn;
8014
8334
  if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
8015
8335
  await this._encryptItems([dataCopy], "write");
8016
- return enrichFkViolation(this._meta, () => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite(dataCopy, this._meta, this.adapter)));
8336
+ return this._normalizeWrite(() => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite(dataCopy, this._meta, this.adapter)));
8017
8337
  }
8018
8338
  /**
8019
8339
  * Deletes every row matching `filter` (application-level cascades run first
@@ -8532,6 +8852,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
8532
8852
  */
8533
8853
  _stampOnUpdate(data, now, topLevel) {
8534
8854
  for (const field of this._meta.onUpdateInc) delete data[field];
8855
+ for (const field of this._meta.insertOnly) delete data[field];
8535
8856
  for (const field of this._meta.onUpdateNow) if (!field.includes(".")) if (topLevel) data[field] = now;
8536
8857
  else delete data[field];
8537
8858
  else if (this._nestedDefaultFillable(field)) stampNested(data, field.split("."), 0, now);
@@ -8702,6 +9023,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
8702
9023
  * Used by parent tables to validate FROM children before the main insert,
8703
9024
  * ensuring errors are caught before the parent is committed.
8704
9025
  *
9026
+ * @internal Nested-write plumbing — its FK step is a no-op on native-FK
9027
+ * adapters. To validate rows yourself, use {@link validateRows}.
8705
9028
  * @param opts.excludeFkTargetTable - Skip FK validation to this table (the parent).
8706
9029
  */
8707
9030
  async preValidateItems(items, opts) {
@@ -9953,4 +10276,4 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
9953
10276
  return diff;
9954
10277
  }
9955
10278
  //#endregion
9956
- export { narrowerFilterOps as $, hasForeignKeyChanges as A, acceptedOperatorsHint as B, ALL_VIEW_CAPABILITIES as C, computeForeignKeyDiff as D, createFailureCollector as E, geoIndexNotFoundMessage as F, classifyQueryPath as G, bucketSourceVerdict as H, searchIndexNotFoundMessage as I, guardAggregate as J, collectQueryPaths as K, vectorIndexNotFoundMessage as L, NativeIntegrity as M, AtscriptDbReadable as N, fkColumns as O, resolveDesignType as P, guardQuery as Q, ADAPTER_FILTER_REASON as R, ALL_ISOLATION_LEVELS as S, NoopLogger as St, createChangeRecorder as T, canFilterLeaf as U, assertGeoPoint as V, checkHavingKeys as W, guardPath as X, guardFilter as Y, guardPaths as Z, isVersionExemptPatch as _, isBucketableField as _t, computeSchemaHash as a, UniquSelect as at, ApplicationIntegrity as b, normalizeComputedSelect as bt, computeViewSnapshot as c, defaultFulltextIndex as ct, snapshotToExistingTableOptions as d, splitFulltextFields as dt, sortFieldNames as et, viewSnapshotSources as f, TableMetadata as ft, AtscriptDbTable as g, aliasTargetOf as gt, isViewType as h, relationalColumnName as ht, canonicalizeQueryNode as i, FieldMappingStrategy as it, IntegrityStrategy as j, fkKey as k, serializeDefaultValue as l, describeFulltext as lt, isAtscriptDbView as m, isGeoPointType as mt, isColumnTypeChanged as n, RelationalFieldMapper as nt, computeTableHash as o, numericOperandProblem as ot, AtscriptDbView as p, isGeoIndexableType as pt, groupSourceVerdict as q, sameDefaultValue as r, DocumentFieldMapper as rt, computeTableSnapshot as s, INTEGER_REGEX_OP as st, computeColumnDiff as t, unsupportedOperatorMessage as tt, snapshotToExistingColumns as u, searchTermInteger as ut, assertNoVersionWrites as v, isJsonValueField as vt, BaseDbAdapter as w, ALL_BUCKET_UNITS as x, resolveCalendarBuckets as xt, decomposePatch as y, jsonValueAncestor as yt, ENCRYPTED_REASON as z };
10279
+ export { guardQuery as $, fkKey as A, ENCRYPTED_REASON as B, ALL_ISOLATION_LEVELS as C, resolveCalendarBuckets as Ct, createFailureCollector as D, createChangeRecorder as E, resolveDesignType as F, checkHavingKeys as G, assertGeoPoint as H, geoIndexNotFoundMessage as I, groupSourceVerdict as J, classifyQueryPath as K, searchIndexNotFoundMessage as L, IntegrityStrategy as M, NativeIntegrity as N, computeForeignKeyDiff as O, AtscriptDbReadable as P, guardPaths as Q, vectorIndexNotFoundMessage as R, ALL_BUCKET_UNITS as S, normalizeComputedSelect as St, BaseDbAdapter as T, nestDottedWith as Tt, bucketSourceVerdict as U, acceptedOperatorsHint as V, canFilterLeaf as W, guardFilter as X, guardAggregate as Y, guardPath as Z, isVersionExemptPatch as _, relationalColumnName as _t, computeSchemaHash as a, FieldMappingStrategy as at, fkLookupTargetOf as b, isJsonValueField as bt, computeViewSnapshot as c, INTEGER_REGEX_OP as ct, snapshotToExistingTableOptions as d, resolveFulltextIndex as dt, narrowerFilterOps as et, viewSnapshotSources as f, searchTermInteger as ft, AtscriptDbTable as g, isGeoPointType as gt, isViewType as h, isGeoIndexableType as ht, canonicalizeQueryNode as i, DocumentFieldMapper as it, hasForeignKeyChanges as j, fkColumns as k, serializeDefaultValue as l, defaultFulltextIndex as lt, isAtscriptDbView as m, TableMetadata as mt, isColumnTypeChanged as n, unsupportedOperatorMessage as nt, computeTableHash as o, UniquSelect as ot, AtscriptDbView as p, splitFulltextFields as pt, collectQueryPaths as q, sameDefaultValue as r, RelationalFieldMapper as rt, computeTableSnapshot as s, numericOperandProblem as st, computeColumnDiff as t, sortFieldNames as tt, snapshotToExistingColumns as u, describeFulltext as ut, assertNoVersionWrites as v, aliasTargetOf as vt, ALL_VIEW_CAPABILITIES as w, NoopLogger as wt, ApplicationIntegrity as x, jsonValueAncestor as xt, decomposePatch as y, isBucketableField as yt, ADAPTER_FILTER_REASON as z };