@atscript/db 0.1.83 → 0.1.85

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 (37) hide show
  1. package/dist/{db-error-D8tQhNgM.cjs → db-error-CrtPzaZ-.cjs} +24 -0
  2. package/dist/{db-error-Cepx-RsQ.mjs → db-error-yx2jdg8B.mjs} +19 -1
  3. package/dist/{db-readable-B2TRNZHv.d.mts → db-readable-BUUVDkLf.d.mts} +18 -4
  4. package/dist/{db-readable-BSrIy08e.d.cts → db-readable-DgIxrEVQ.d.cts} +18 -4
  5. package/dist/{db-space-Dx8x-Ke1.d.cts → db-space-CZooARt_.d.mts} +2 -2
  6. package/dist/{db-space-SUFqMvKz.d.mts → db-space-DiErv7Q1.d.cts} +2 -2
  7. package/dist/{db-view-CqEIhTFa.mjs → db-view-Dfw-FP5o.mjs} +126 -29
  8. package/dist/{db-view-Bf5P3_7k.cjs → db-view-da5OTSRH.cjs} +130 -27
  9. package/dist/index.cjs +57 -3
  10. package/dist/index.d.cts +76 -5
  11. package/dist/index.d.mts +76 -5
  12. package/dist/index.mjs +54 -5
  13. package/dist/{nested-writer-CT2rLURx.mjs → nested-writer-CVMRAPoF.mjs} +1 -1
  14. package/dist/{nested-writer-v_LPR1yJ.cjs → nested-writer-Chl_zySG.cjs} +1 -1
  15. package/dist/{ops-C61kelof.d.mts → ops-Blqr0ipy.d.mts} +48 -1
  16. package/dist/{ops-DXJ4Zw0P.d.cts → ops-lzmfzuY9.d.cts} +48 -1
  17. package/dist/ops.cjs +73 -0
  18. package/dist/ops.d.cts +2 -2
  19. package/dist/ops.d.mts +2 -2
  20. package/dist/ops.mjs +72 -1
  21. package/dist/plugin.cjs +24 -1
  22. package/dist/plugin.mjs +24 -1
  23. package/dist/rel.cjs +1 -1
  24. package/dist/rel.d.cts +1 -1
  25. package/dist/rel.d.mts +1 -1
  26. package/dist/rel.mjs +1 -1
  27. package/dist/sync.cjs +1 -1
  28. package/dist/sync.d.cts +2 -2
  29. package/dist/sync.d.mts +2 -2
  30. package/dist/sync.mjs +1 -1
  31. package/dist/{validator-BB5h1Le3.mjs → validator-DTDf9yWe.mjs} +1 -1
  32. package/dist/{validator-BIuw_T0k.cjs → validator-DrmUaZA3.cjs} +1 -1
  33. package/dist/validator.cjs +2 -1
  34. package/dist/validator.d.cts +2 -2
  35. package/dist/validator.d.mts +2 -2
  36. package/dist/validator.mjs +3 -3
  37. package/package.json +1 -1
@@ -1,9 +1,9 @@
1
- const require_db_error = require("./db-error-D8tQhNgM.cjs");
1
+ const require_db_error = require("./db-error-CrtPzaZ-.cjs");
2
2
  const require_agg = require("./agg.cjs");
3
3
  const require_relation_helpers = require("./relation-helpers-BYvsE1tR.cjs");
4
4
  const require_ops = require("./ops.cjs");
5
- const require_validator = require("./validator-BIuw_T0k.cjs");
6
- const require_nested_writer = require("./nested-writer-v_LPR1yJ.cjs");
5
+ const require_validator = require("./validator-DrmUaZA3.cjs");
6
+ const require_nested_writer = require("./nested-writer-Chl_zySG.cjs");
7
7
  let _atscript_typescript_utils = require("@atscript/typescript/utils");
8
8
  let node_async_hooks = require("node:async_hooks");
9
9
  //#region src/logger.ts
@@ -64,6 +64,8 @@ var TableMetadata = class {
64
64
  columnMap = /* @__PURE__ */ new Map();
65
65
  dimensions = [];
66
66
  measures = [];
67
+ /** Logical field name annotated with `@db.column.version`, if any. */
68
+ versionField;
67
69
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
68
70
  quantityRefByField = /* @__PURE__ */ new Map();
69
71
  pathToPhysical = /* @__PURE__ */ new Map();
@@ -258,6 +260,14 @@ var TableMetadata = class {
258
260
  if (!hasExplicitIndex) this._addIndexField("plain", fieldName, fieldName);
259
261
  }
260
262
  if (metadata.has("db.column.measure")) this.measures.push(fieldName);
263
+ if (metadata.has("db.column.version")) if (this.versionField !== void 0) logger.warn(`@db.column.version declared on multiple fields ("${this.versionField}" and "${fieldName}") — only one is allowed; using "${this.versionField}"`);
264
+ else {
265
+ this.versionField = fieldName;
266
+ if (!this.defaults.has(fieldName)) this.defaults.set(fieldName, {
267
+ kind: "value",
268
+ value: "0"
269
+ });
270
+ }
261
271
  }
262
272
  _addIndexField(type, name, field, opts) {
263
273
  const key = indexKey(type, name);
@@ -1249,6 +1259,17 @@ var AtscriptDbReadable = class {
1249
1259
  }
1250
1260
  return this._metaIdPhysical;
1251
1261
  }
1262
+ /**
1263
+ * Physical column name of the field annotated with `@db.column.version`, or
1264
+ * `undefined` when the table has no version column. Used by adapters and the
1265
+ * REST integration to drive optimistic concurrency control (OCC).
1266
+ */
1267
+ get versionColumn() {
1268
+ this._ensureBuilt();
1269
+ const field = this._meta.versionField;
1270
+ if (field === void 0) return void 0;
1271
+ return this._meta.columnMap.get(field) ?? field;
1272
+ }
1252
1273
  /** Dimension fields from `@db.column.dimension`. */
1253
1274
  get dimensions() {
1254
1275
  this._ensureBuilt();
@@ -1940,7 +1961,7 @@ var BaseDbAdapter = class {
1940
1961
  * @param patch - The patch payload with array operations.
1941
1962
  * @returns Update result.
1942
1963
  */
1943
- async nativePatch(_filter, _patch, _ops) {
1964
+ async nativePatch(_filter, _patch, _ops, _expectedVersion) {
1944
1965
  throw new Error("Native patch not supported by this adapter");
1945
1966
  }
1946
1967
  /**
@@ -2473,6 +2494,37 @@ function decomposeArrayPatch(key, value, fieldType, update, _table) {
2473
2494
  if (value.$remove !== void 0) update[`${key}.__$remove`] = value.$remove;
2474
2495
  if (keyProps.size > 0 && (value.$upsert !== void 0 || value.$update !== void 0 || value.$remove !== void 0)) update[`${key}.__$keys`] = [...keyProps];
2475
2496
  }
2497
+ /**
2498
+ * Throws if the payload attempts to write the version column directly.
2499
+ *
2500
+ * The version column is server-managed: the adapter auto-increments it
2501
+ * on every update. Direct writes (plain SET, `$inc`, `$mul`) would
2502
+ * either silently no-op (overwritten by the auto-bump) or corrupt the
2503
+ * OCC invariant. Reject them at the patch layer so the failure is
2504
+ * loud and adapter-agnostic.
2505
+ *
2506
+ * Inspects the payload AFTER `separateCas` has stripped `$cas` (so
2507
+ * that the legitimate `$cas` → expectedVersion path is not flagged).
2508
+ *
2509
+ * The check is intentionally narrow: only the physical column name at
2510
+ * the top level of the (already-decomposed) payload. Nested objects
2511
+ * cannot reach the version column through dot-paths because the
2512
+ * decomposer flattens those into top-level dot-keys; the version
2513
+ * column, being a scalar at the root, cannot be nested.
2514
+ *
2515
+ * Zero-allocation when the version column is not present.
2516
+ *
2517
+ * @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
2518
+ * column appears as a top-level key in `data` or as the target of a
2519
+ * `$inc`/`$mul` field op.
2520
+ */
2521
+ function assertNoVersionWrites(data, versionColumn) {
2522
+ if (!(versionColumn in data)) return;
2523
+ throw new require_db_error.DbError("VERSION_COLUMN_WRITE", [{
2524
+ path: versionColumn,
2525
+ message: `Cannot write to version column "${versionColumn}" directly; omit it from the payload or use $cas for conflict detection`
2526
+ }]);
2527
+ }
2476
2528
  //#endregion
2477
2529
  //#region src/table/db-table.ts
2478
2530
  /**
@@ -2619,7 +2671,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2619
2671
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
2620
2672
  if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
2621
2673
  return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
2622
- const items = payloads.map((p) => this._applyDefaults({ ...p }));
2674
+ const versionColumn = this.versionColumn;
2675
+ const expectedVersions = Array.from({ length: payloads.length });
2676
+ const items = payloads.map((p, i) => {
2677
+ const clone = { ...p };
2678
+ expectedVersions[i] = require_ops.separateCas(clone, versionColumn);
2679
+ return this._applyDefaults(clone);
2680
+ });
2623
2681
  const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
2624
2682
  const validator = this.getValidator("bulkReplace");
2625
2683
  const ctx = {
@@ -2634,11 +2692,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2634
2692
  if (canNest) await require_nested_writer.preValidateNestedFrom(host, originals);
2635
2693
  let matchedCount = 0;
2636
2694
  let modifiedCount = 0;
2637
- for (const data of items) {
2695
+ for (let i = 0; i < items.length; i++) {
2696
+ const data = items[i];
2638
2697
  for (const navField of this._meta.navFields) delete data[navField];
2698
+ if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
2639
2699
  const filter = this._extractRecordFilter(data);
2640
2700
  const prepared = this._fieldMapper.prepareForWrite(data, this._meta, this.adapter);
2641
- const result = await this.adapter.replaceOne(this._fieldMapper.translateFilter(filter, this._meta), prepared);
2701
+ const result = await this.adapter.replaceOne(this._fieldMapper.translateFilter(filter, this._meta), prepared, expectedVersions[i]);
2642
2702
  matchedCount += result.matchedCount;
2643
2703
  modifiedCount += result.modifiedCount;
2644
2704
  }
@@ -2671,6 +2731,13 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2671
2731
  const canNest = depth < maxDepth && this._writeTableResolver && this._meta.navFields.size > 0;
2672
2732
  if (!canNest && this._meta.navFields.size > 0) require_nested_writer.checkDepthOverflow(payloads, maxDepth, this._meta);
2673
2733
  return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.withTransaction(async () => {
2734
+ const versionColumn = this.versionColumn;
2735
+ const expectedVersions = Array.from({ length: payloads.length });
2736
+ const cloned = payloads.map((p, i) => {
2737
+ const c = { ...p };
2738
+ expectedVersions[i] = require_ops.separateCas(c, versionColumn);
2739
+ return c;
2740
+ });
2674
2741
  const validator = this.getValidator("bulkUpdate");
2675
2742
  const ctx = {
2676
2743
  mode: "patch",
@@ -2678,18 +2745,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2678
2745
  navFields: this._meta.navFields
2679
2746
  };
2680
2747
  this._applyDepthCtx(ctx, depth);
2681
- require_nested_writer.validateBatch(validator, payloads, ctx);
2682
- const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
2748
+ require_nested_writer.validateBatch(validator, cloned, ctx);
2749
+ const originals = canNest ? cloned.map((p) => ({ ...p })) : [];
2683
2750
  const host = this;
2684
- if (canNest) await require_nested_writer.batchPatchNestedTo(host, payloads, maxDepth, depth);
2685
- await this._integrity.validateForeignKeys(payloads, this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2751
+ if (canNest) await require_nested_writer.batchPatchNestedTo(host, cloned, maxDepth, depth);
2752
+ await this._integrity.validateForeignKeys(cloned, this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2686
2753
  let matchedCount = 0;
2687
2754
  let modifiedCount = 0;
2688
- for (const payload of payloads) {
2755
+ for (let i = 0; i < cloned.length; i++) {
2756
+ const payload = cloned[i];
2757
+ const expectedVersion = expectedVersions[i];
2689
2758
  const data = { ...payload };
2690
2759
  for (const navField of this._meta.navFields) delete data[navField];
2691
2760
  const filter = this._extractRecordFilter(data);
2692
2761
  for (const key of Object.keys(filter)) delete data[key];
2762
+ if (versionColumn !== void 0) assertNoVersionWrites(data, versionColumn);
2693
2763
  if (_isEmptyObj(data)) {
2694
2764
  matchedCount += 1;
2695
2765
  modifiedCount += 0;
@@ -2701,7 +2771,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2701
2771
  const ops = require_ops.separateFieldOps(data);
2702
2772
  const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
2703
2773
  const translatedData = this._fieldMapper.translatePatchKeys(data, this._meta);
2704
- result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps);
2774
+ result = await this.adapter.nativePatch(translatedFilter, translatedData, translatedOps, expectedVersion);
2705
2775
  } else {
2706
2776
  const update = decomposePatch(data, this);
2707
2777
  const ops = require_ops.separateFieldOps(update);
@@ -2712,8 +2782,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2712
2782
  filter: translatedFilter,
2713
2783
  controls: {}
2714
2784
  }), this);
2715
- result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps);
2716
- } else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps);
2785
+ result = await this.adapter.updateOne(translatedFilter, resolved, translatedOps, expectedVersion);
2786
+ } else result = await this.adapter.updateOne(translatedFilter, translatedUpdate, translatedOps, expectedVersion);
2717
2787
  }
2718
2788
  matchedCount += result.matchedCount;
2719
2789
  modifiedCount += result.modifiedCount;
@@ -2746,7 +2816,14 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2746
2816
  async updateMany(filter, data) {
2747
2817
  this._ensureBuilt();
2748
2818
  await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2749
- const update = decomposePatch({ ...data }, this);
2819
+ const dataCopy = { ...data };
2820
+ const versionColumn = this.versionColumn;
2821
+ if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
2822
+ path: "$cas",
2823
+ message: "$cas is not supported on updateMany — use bulkUpdate with per-row $cas for version-locked batch updates"
2824
+ }]);
2825
+ if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
2826
+ const update = decomposePatch(dataCopy, this);
2750
2827
  const ops = require_ops.separateFieldOps(update);
2751
2828
  const translatedOps = ops ? _translateOpsKeys(ops, this._meta) : void 0;
2752
2829
  const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
@@ -2787,17 +2864,21 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2787
2864
  _applyDefaults(data) {
2788
2865
  const nativeValues = this.adapter.supportsNativeValueDefaults();
2789
2866
  const nativeFns = this.adapter.nativeDefaultFns();
2790
- for (const [field, def] of this._meta.defaults.entries()) if (data[field] === void 0) {
2791
- if (def.kind === "value" && !nativeValues) {
2792
- const fieldType = this._meta.flatMap?.get(field);
2793
- data[field] = (fieldType?.type.kind === "" && fieldType.type.designType) === "string" ? def.value : JSON.parse(def.value);
2794
- } else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
2795
- case "now":
2796
- data[field] = Date.now();
2797
- break;
2798
- case "uuid":
2799
- data[field] = crypto.randomUUID();
2800
- break;
2867
+ const versionField = this._meta.versionField;
2868
+ for (const [field, def] of this._meta.defaults.entries()) {
2869
+ if (field === versionField) continue;
2870
+ if (data[field] === void 0) {
2871
+ if (def.kind === "value" && !nativeValues) {
2872
+ const fieldType = this._meta.flatMap?.get(field);
2873
+ data[field] = (fieldType?.type.kind === "" && fieldType.type.designType) === "string" ? def.value : JSON.parse(def.value);
2874
+ } else if (def.kind === "fn" && !nativeFns.has(def.fn)) switch (def.fn) {
2875
+ case "now":
2876
+ data[field] = Date.now();
2877
+ break;
2878
+ case "uuid":
2879
+ data[field] = crypto.randomUUID();
2880
+ break;
2881
+ }
2801
2882
  }
2802
2883
  }
2803
2884
  return data;
@@ -2922,6 +3003,22 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
2922
3003
  const adapterPlugins = this.adapter.getValidatorPlugins();
2923
3004
  if (purpose === "insert" || purpose === "patch" || purpose === "bulkReplace") {
2924
3005
  const mode = purpose === "bulkReplace" ? "replace" : purpose;
3006
+ const versionField = this._meta.versionField;
3007
+ if (versionField !== void 0) {
3008
+ const plugins = adapterPlugins.length ? [...adapterPlugins, require_validator.dbPlugin] : [require_validator.dbPlugin];
3009
+ return this.createValidator({
3010
+ plugins,
3011
+ partial: mode === "patch",
3012
+ replace: (def, path) => {
3013
+ const transformed = require_validator.forceNavNonOptional(def);
3014
+ if (path === versionField && !transformed.optional) return {
3015
+ ...transformed,
3016
+ optional: true
3017
+ };
3018
+ return transformed;
3019
+ }
3020
+ });
3021
+ }
2925
3022
  return require_validator.buildDbValidator(this.type, mode, adapterPlugins);
2926
3023
  }
2927
3024
  if (purpose === "bulkUpdate") {
@@ -3156,6 +3253,12 @@ Object.defineProperty(exports, "UniquSelect", {
3156
3253
  return UniquSelect;
3157
3254
  }
3158
3255
  });
3256
+ Object.defineProperty(exports, "assertNoVersionWrites", {
3257
+ enumerable: true,
3258
+ get: function() {
3259
+ return assertNoVersionWrites;
3260
+ }
3261
+ });
3159
3262
  Object.defineProperty(exports, "decomposePatch", {
3160
3263
  enumerable: true,
3161
3264
  get: function() {
package/dist/index.cjs CHANGED
@@ -1,9 +1,58 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_db_error = require("./db-error-D8tQhNgM.cjs");
3
- const require_db_view = require("./db-view-Bf5P3_7k.cjs");
2
+ const require_db_error = require("./db-error-CrtPzaZ-.cjs");
3
+ const require_db_view = require("./db-view-da5OTSRH.cjs");
4
4
  const require_ops = require("./ops.cjs");
5
- const require_validator = require("./validator-BIuw_T0k.cjs");
5
+ const require_validator = require("./validator-DrmUaZA3.cjs");
6
6
  let _uniqu_core = require("@uniqu/core");
7
+ //#region src/with-optimistic-retry.ts
8
+ /**
9
+ * Runs a read-modify-write loop under optimistic concurrency control (OCC).
10
+ *
11
+ * Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
12
+ * the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
13
+ * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
14
+ * mutator with the fresh state, and retries — up to `maxAttempts` times.
15
+ *
16
+ * The filter (typically the primary key) is threaded into the update payload
17
+ * so the table layer can extract the row identity. If `mutator` returns
18
+ * fields that overlap with the filter, the patch wins (last-write semantics
19
+ * inside a single object spread).
20
+ *
21
+ * @throws {DbError} with code `INVALID_QUERY` if `table` has no
22
+ * `@db.column.version` column — the helper would have no version to thread
23
+ * into `$cas` and would silently degrade to last-write-wins.
24
+ * @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
25
+ * `null`. The mutator is not invoked with a fabricated row.
26
+ * @throws {CasExhaustedError} if `maxAttempts` is reached without a
27
+ * successful commit.
28
+ */
29
+ async function withOptimisticRetry(table, filter, mutator, opts) {
30
+ const versionColumn = table.versionColumn;
31
+ if (versionColumn === void 0) throw new require_db_error.DbError("INVALID_QUERY", [{
32
+ path: "$cas",
33
+ message: `withOptimisticRetry: table "${table.tableName}" has no @db.column.version column — CAS cannot be applied`
34
+ }]);
35
+ const maxAttempts = opts?.maxAttempts ?? 5;
36
+ let lastSeenVersion;
37
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
38
+ const row = await table.findOne({ filter });
39
+ if (row === null) throw new require_db_error.DbError("NOT_FOUND", [{
40
+ path: "$cas",
41
+ message: `withOptimisticRetry: row not found in "${table.tableName}" for filter ${JSON.stringify(filter)}`
42
+ }]);
43
+ lastSeenVersion = row[versionColumn];
44
+ const patch = await mutator(row);
45
+ const result = await table.updateOne({
46
+ ...filter,
47
+ ...patch,
48
+ $cas: { [versionColumn]: lastSeenVersion }
49
+ });
50
+ if (result.matchedCount > 0) return result;
51
+ if (attempt < maxAttempts && opts?.delay !== void 0) await opts.delay(attempt);
52
+ }
53
+ throw new require_db_error.CasExhaustedError(maxAttempts, lastSeenVersion);
54
+ }
55
+ //#endregion
7
56
  //#region src/table/db-space.ts
8
57
  /**
9
58
  * A database space — a registry of tables and views sharing the same adapter type and driver.
@@ -139,6 +188,7 @@ function translateQueryTree(node, resolveField) {
139
188
  return { [leftField]: { [comp.op]: comp.right } };
140
189
  }
141
190
  //#endregion
191
+ exports.$cas = require_ops.$cas;
142
192
  exports.$dec = require_ops.$dec;
143
193
  exports.$inc = require_ops.$inc;
144
194
  exports.$insert = require_ops.$insert;
@@ -152,6 +202,7 @@ exports.AtscriptDbReadable = require_db_view.AtscriptDbReadable;
152
202
  exports.AtscriptDbTable = require_db_view.AtscriptDbTable;
153
203
  exports.AtscriptDbView = require_db_view.AtscriptDbView;
154
204
  exports.BaseDbAdapter = require_db_view.BaseDbAdapter;
205
+ exports.CasExhaustedError = require_db_error.CasExhaustedError;
155
206
  exports.DbError = require_db_error.DbError;
156
207
  exports.DbSpace = DbSpace;
157
208
  exports.DocumentFieldMapper = require_db_view.DocumentFieldMapper;
@@ -162,6 +213,7 @@ exports.NoopLogger = require_db_view.NoopLogger;
162
213
  exports.RelationalFieldMapper = require_db_view.RelationalFieldMapper;
163
214
  exports.TableMetadata = require_db_view.TableMetadata;
164
215
  exports.UniquSelect = require_db_view.UniquSelect;
216
+ exports.assertNoVersionWrites = require_db_view.assertNoVersionWrites;
165
217
  exports.buildDbValidator = require_validator.buildDbValidator;
166
218
  exports.buildValidationContext = require_validator.buildValidationContext;
167
219
  Object.defineProperty(exports, "computeInsights", {
@@ -184,6 +236,7 @@ Object.defineProperty(exports, "isPrimitive", {
184
236
  }
185
237
  });
186
238
  exports.resolveDesignType = require_db_view.resolveDesignType;
239
+ exports.separateCas = require_ops.separateCas;
187
240
  exports.separateFieldOps = require_ops.separateFieldOps;
188
241
  exports.translateQueryTree = translateQueryTree;
189
242
  Object.defineProperty(exports, "walkFilter", {
@@ -192,3 +245,4 @@ Object.defineProperty(exports, "walkFilter", {
192
245
  return _uniqu_core.walkFilter;
193
246
  }
194
247
  });
248
+ exports.withOptimisticRetry = withOptimisticRetry;
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-BSrIy08e.cjs";
2
- import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-DXJ4Zw0P.cjs";
3
- import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-Dx8x-Ke1.cjs";
1
+ import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-DgIxrEVQ.cjs";
2
+ import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-lzmfzuY9.cjs";
3
+ import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-DiErv7Q1.cjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DRGMCEn3.cjs";
5
5
  import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-_z_A3cKa.cjs";
6
6
  import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
81
81
  }
82
82
  //#endregion
83
83
  //#region src/db-error.d.ts
84
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED";
84
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
85
85
  declare class DbError extends Error {
86
86
  readonly code: DbErrorCode;
87
87
  readonly errors: Array<{
@@ -94,6 +94,52 @@ declare class DbError extends Error {
94
94
  message: string;
95
95
  }>, message?: string);
96
96
  }
97
+ /**
98
+ * Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
99
+ * without a successful CAS commit — the target row kept changing under
100
+ * the read-modify-write loop. Surfaces the attempt count and the
101
+ * last-observed version so callers can log/report the contention.
102
+ */
103
+ declare class CasExhaustedError extends DbError {
104
+ readonly attempts: number;
105
+ readonly lastSeenVersion: number | undefined;
106
+ name: string;
107
+ constructor(attempts: number, lastSeenVersion: number | undefined);
108
+ }
109
+ //#endregion
110
+ //#region src/with-optimistic-retry.d.ts
111
+ interface WithOptimisticRetryOptions {
112
+ /** Maximum number of attempts before giving up. Defaults to `5`. */
113
+ maxAttempts?: number;
114
+ /**
115
+ * Optional hook invoked between failed attempts. Receives the 1-based
116
+ * attempt number that just failed. Useful for exponential backoff with
117
+ * jitter. Default behavior is to retry immediately.
118
+ */
119
+ delay?: (attempt: number) => Promise<void>;
120
+ }
121
+ /**
122
+ * Runs a read-modify-write loop under optimistic concurrency control (OCC).
123
+ *
124
+ * Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
125
+ * the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
126
+ * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
127
+ * mutator with the fresh state, and retries — up to `maxAttempts` times.
128
+ *
129
+ * The filter (typically the primary key) is threaded into the update payload
130
+ * so the table layer can extract the row identity. If `mutator` returns
131
+ * fields that overlap with the filter, the patch wins (last-write semantics
132
+ * inside a single object spread).
133
+ *
134
+ * @throws {DbError} with code `INVALID_QUERY` if `table` has no
135
+ * `@db.column.version` column — the helper would have no version to thread
136
+ * into `$cas` and would silently degrade to last-write-wins.
137
+ * @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
138
+ * `null`. The mutator is not invoked with a fabricated row.
139
+ * @throws {CasExhaustedError} if `maxAttempts` is reached without a
140
+ * successful commit.
141
+ */
142
+ declare function withOptimisticRetry<TRow extends Record<string, unknown>>(table: AtscriptDbTable, filter: Record<string, unknown>, mutator: (row: TRow) => Promise<Record<string, unknown>> | Record<string, unknown>, opts?: WithOptimisticRetryOptions): Promise<TDbUpdateResult>;
97
143
  //#endregion
98
144
  //#region src/patch/patch-decomposer.d.ts
99
145
  /**
@@ -113,5 +159,30 @@ declare class DbError extends Error {
113
159
  * @returns A flat update object suitable for a basic `updateOne` call.
114
160
  */
115
161
  declare function decomposePatch(payload: Record<string, unknown>, table: AtscriptDbTable): Record<string, unknown>;
162
+ /**
163
+ * Throws if the payload attempts to write the version column directly.
164
+ *
165
+ * The version column is server-managed: the adapter auto-increments it
166
+ * on every update. Direct writes (plain SET, `$inc`, `$mul`) would
167
+ * either silently no-op (overwritten by the auto-bump) or corrupt the
168
+ * OCC invariant. Reject them at the patch layer so the failure is
169
+ * loud and adapter-agnostic.
170
+ *
171
+ * Inspects the payload AFTER `separateCas` has stripped `$cas` (so
172
+ * that the legitimate `$cas` → expectedVersion path is not flagged).
173
+ *
174
+ * The check is intentionally narrow: only the physical column name at
175
+ * the top level of the (already-decomposed) payload. Nested objects
176
+ * cannot reach the version column through dot-paths because the
177
+ * decomposer flattens those into top-level dot-keys; the version
178
+ * column, being a scalar at the root, cannot be nested.
179
+ *
180
+ * Zero-allocation when the version column is not present.
181
+ *
182
+ * @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
183
+ * column appears as a top-level key in `data` or as the target of a
184
+ * `$inc`/`$mul` field op.
185
+ */
186
+ declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
116
187
  //#endregion
117
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithRelation, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateFieldOps, translateQueryTree, walkFilter };
188
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
- import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-B2TRNZHv.mjs";
2
- import { a as $remove, c as $upsert, d as getDbFieldOp, f as isDbFieldOp, i as $mul, l as TDbFieldOp, n as $inc, o as $replace, p as separateFieldOps, r as $insert, s as $update, t as $dec, u as TFieldOps } from "./ops-C61kelof.mjs";
3
- import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-SUFqMvKz.mjs";
1
+ import { $ as TMetadataOverrides, A as TDbCollation, B as TDbInsertResult, C as TColumnDiff, D as TDbActionIntent, E as TDbActionInfo, F as TDbForeignKey, G as TExistingColumn, H as TDbRelation, I as TDbIndex, J as TFkLookupResolver, K as TExistingTableOption, L as TDbIndexField, M as TDbDefaultValue, N as TDbDeleteResult, O as TDbActionLevel, P as TDbFieldMeta, Q as TMetaResponse, R as TDbIndexType, S as TCascadeTarget, T as TCrudPermissions, U as TDbStorageType, V as TDbReferentialAction, W as TDbUpdateResult, X as TIdDescriptor, Y as TFkLookupTarget, Z as TIdentification, _ as FlatOf, a as FieldMappingStrategy, at as TValueFormatterPair, b as PrimaryKeyOf, c as AggregateExpr, ct as Uniquery, d as AggregateResult, dt as TableMetadata, et as TRelationInfo, f as AtscriptDbWritable, ft as NoopLogger, g as FilterExpr, h as FieldOpsFor, i as DocumentFieldMapper, it as TTableResolver, j as TDbDefaultFn, k as TDbActionProcessor, l as AggregateFn, lt as UniqueryControls, m as DbQuery, mt as UniquSelect, n as DbResponse, nt as TSyncColumnResult, o as BaseDbAdapter, ot as TWriteTableResolver, p as DbControls, pt as TGenericLogger, q as TFieldMeta, r as resolveDesignType, rt as TTableOptionDiff, s as AggregateControls, st as TypedWithRelation, t as AtscriptDbReadable, tt as TSearchIndexInfo, u as AggregateQuery, ut as WithRelation, v as NavPropsOf, w as TCrudOp, x as TCascadeResolver, y as OwnPropsOf, z as TDbInsertManyResult } from "./db-readable-BUUVDkLf.mjs";
2
+ import { a as $mul, c as $update, d as TDbFieldOp, f as TFieldOps, g as separateFieldOps, h as separateCas, i as $insert, l as $upsert, m as isDbFieldOp, n as $dec, o as $remove, p as getDbFieldOp, r as $inc, s as $replace, t as $cas, u as TDbCas } from "./ops-Blqr0ipy.mjs";
3
+ import { a as AtscriptQueryComparison, c as AtscriptRef, d as translateQueryTree, f as AtscriptDbTable, i as TViewColumnMapping, l as TViewJoin, m as NativeIntegrity, n as TAdapterFactory, o as AtscriptQueryFieldRef, p as IntegrityStrategy, r as AtscriptDbView, s as AtscriptQueryNode, t as DbSpace, u as TViewPlan } from "./db-space-CZooARt_.mjs";
4
4
  import { n as createDbValidatorPlugin, t as DbValidationContext } from "./db-validator-plugin-DDvYyv5t.mjs";
5
5
  import { c as TArrayPatch, i as buildValidationContext, l as TDbPatch, n as ValidatorMode, o as forceNavNonOptional, r as buildDbValidator, s as isNavRelation, t as ValidationContext, u as getKeyProps } from "./validator-DttN2e5_.mjs";
6
6
  import { AggregateQuery as AggregateQuery$1, FilterExpr as FilterExpr$1, FilterVisitor, Uniquery as Uniquery$1, computeInsights, isPrimitive, walkFilter } from "@uniqu/core";
@@ -81,7 +81,7 @@ declare class ApplicationIntegrity extends IntegrityStrategy {
81
81
  }
82
82
  //#endregion
83
83
  //#region src/db-error.d.ts
84
- type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED";
84
+ type DbErrorCode = "CONFLICT" | "FK_VIOLATION" | "NOT_FOUND" | "CASCADE_CYCLE" | "INVALID_QUERY" | "DEPTH_EXCEEDED" | "VERSION_COLUMN_WRITE" | "CAS_EXHAUSTED";
85
85
  declare class DbError extends Error {
86
86
  readonly code: DbErrorCode;
87
87
  readonly errors: Array<{
@@ -94,6 +94,52 @@ declare class DbError extends Error {
94
94
  message: string;
95
95
  }>, message?: string);
96
96
  }
97
+ /**
98
+ * Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
99
+ * without a successful CAS commit — the target row kept changing under
100
+ * the read-modify-write loop. Surfaces the attempt count and the
101
+ * last-observed version so callers can log/report the contention.
102
+ */
103
+ declare class CasExhaustedError extends DbError {
104
+ readonly attempts: number;
105
+ readonly lastSeenVersion: number | undefined;
106
+ name: string;
107
+ constructor(attempts: number, lastSeenVersion: number | undefined);
108
+ }
109
+ //#endregion
110
+ //#region src/with-optimistic-retry.d.ts
111
+ interface WithOptimisticRetryOptions {
112
+ /** Maximum number of attempts before giving up. Defaults to `5`. */
113
+ maxAttempts?: number;
114
+ /**
115
+ * Optional hook invoked between failed attempts. Receives the 1-based
116
+ * attempt number that just failed. Useful for exponential backoff with
117
+ * jitter. Default behavior is to retry immediately.
118
+ */
119
+ delay?: (attempt: number) => Promise<void>;
120
+ }
121
+ /**
122
+ * Runs a read-modify-write loop under optimistic concurrency control (OCC).
123
+ *
124
+ * Reads the row via `findOne({ filter })`, hands it to `mutator`, then applies
125
+ * the returned patch with `$cas: { [versionColumn]: row[versionColumn] }`. On
126
+ * a version conflict (`matchedCount === 0`) it re-reads the row, calls the
127
+ * mutator with the fresh state, and retries — up to `maxAttempts` times.
128
+ *
129
+ * The filter (typically the primary key) is threaded into the update payload
130
+ * so the table layer can extract the row identity. If `mutator` returns
131
+ * fields that overlap with the filter, the patch wins (last-write semantics
132
+ * inside a single object spread).
133
+ *
134
+ * @throws {DbError} with code `INVALID_QUERY` if `table` has no
135
+ * `@db.column.version` column — the helper would have no version to thread
136
+ * into `$cas` and would silently degrade to last-write-wins.
137
+ * @throws {DbError} with code `NOT_FOUND` if the initial `findOne` returns
138
+ * `null`. The mutator is not invoked with a fabricated row.
139
+ * @throws {CasExhaustedError} if `maxAttempts` is reached without a
140
+ * successful commit.
141
+ */
142
+ declare function withOptimisticRetry<TRow extends Record<string, unknown>>(table: AtscriptDbTable, filter: Record<string, unknown>, mutator: (row: TRow) => Promise<Record<string, unknown>> | Record<string, unknown>, opts?: WithOptimisticRetryOptions): Promise<TDbUpdateResult>;
97
143
  //#endregion
98
144
  //#region src/patch/patch-decomposer.d.ts
99
145
  /**
@@ -113,5 +159,30 @@ declare class DbError extends Error {
113
159
  * @returns A flat update object suitable for a basic `updateOne` call.
114
160
  */
115
161
  declare function decomposePatch(payload: Record<string, unknown>, table: AtscriptDbTable): Record<string, unknown>;
162
+ /**
163
+ * Throws if the payload attempts to write the version column directly.
164
+ *
165
+ * The version column is server-managed: the adapter auto-increments it
166
+ * on every update. Direct writes (plain SET, `$inc`, `$mul`) would
167
+ * either silently no-op (overwritten by the auto-bump) or corrupt the
168
+ * OCC invariant. Reject them at the patch layer so the failure is
169
+ * loud and adapter-agnostic.
170
+ *
171
+ * Inspects the payload AFTER `separateCas` has stripped `$cas` (so
172
+ * that the legitimate `$cas` → expectedVersion path is not flagged).
173
+ *
174
+ * The check is intentionally narrow: only the physical column name at
175
+ * the top level of the (already-decomposed) payload. Nested objects
176
+ * cannot reach the version column through dot-paths because the
177
+ * decomposer flattens those into top-level dot-keys; the version
178
+ * column, being a scalar at the root, cannot be nested.
179
+ *
180
+ * Zero-allocation when the version column is not present.
181
+ *
182
+ * @throws {DbError} with code `VERSION_COLUMN_WRITE` when the version
183
+ * column appears as a top-level key in `data` or as the target of a
184
+ * `$inc`/`$mul` field op.
185
+ */
186
+ declare function assertNoVersionWrites(data: Record<string, unknown>, versionColumn: string): void;
116
187
  //#endregion
117
- export { $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithRelation, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateFieldOps, translateQueryTree, walkFilter };
188
+ export { $cas, $dec, $inc, $insert, $mul, $remove, $replace, $update, $upsert, type AggregateControls, type AggregateExpr, type AggregateFn, type AggregateQuery, type AggregateResult, ApplicationIntegrity, AtscriptDbReadable, AtscriptDbTable, AtscriptDbView, type AtscriptDbWritable, type AtscriptQueryComparison, type AtscriptQueryFieldRef, type AtscriptQueryNode, type AtscriptRef, BaseDbAdapter, CasExhaustedError, type DbControls, DbError, type DbErrorCode, type DbQuery, type DbResponse, DbSpace, type DbValidationContext, DocumentFieldMapper, FieldMappingStrategy, type FieldOpsFor, type FilterExpr, type FilterVisitor, type FlatOf, IntegrityStrategy, NativeIntegrity, type NavPropsOf, NoopLogger, type OwnPropsOf, type PrimaryKeyOf, RelationalFieldMapper, type TAdapterFactory, type TArrayPatch, type TCascadeResolver, type TCascadeTarget, type TColumnDiff, type TCrudOp, type TCrudPermissions, type TDbActionInfo, type TDbActionIntent, type TDbActionLevel, type TDbActionProcessor, type TDbCas, type TDbCollation, type TDbDefaultFn, type TDbDefaultValue, type TDbDeleteResult, type TDbFieldMeta, type TDbFieldOp, type TDbForeignKey, type TDbIndex, type TDbIndexField, type TDbIndexType, type TDbInsertManyResult, type TDbInsertResult, type TDbPatch, type TDbReferentialAction, type TDbRelation, type TDbStorageType, type TDbUpdateResult, type TExistingColumn, type TExistingTableOption, type TFieldMeta, type TFieldOps, type TFkLookupResolver, type TFkLookupTarget, type TGenericLogger, type TIdDescriptor, type TIdentification, type TMetaResponse, type TMetadataOverrides, type TRelationInfo, type TSearchIndexInfo, type TSyncColumnResult, type TTableOptionDiff, type TTableResolver, type TValueFormatterPair, type TViewColumnMapping, type TViewJoin, type TViewPlan, type TWriteTableResolver, TableMetadata, type TypedWithRelation, UniquSelect, type Uniquery, type UniqueryControls, type ValidationContext, type ValidatorMode, type WithOptimisticRetryOptions, type WithRelation, assertNoVersionWrites, buildDbValidator, buildValidationContext, computeInsights, createDbValidatorPlugin, decomposePatch, forceNavNonOptional, getDbFieldOp, getKeyProps, isDbFieldOp, isNavRelation, isPrimitive, resolveDesignType, separateCas, separateFieldOps, translateQueryTree, walkFilter, withOptimisticRetry };