@atscript/db 0.1.82 → 0.1.84

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 +6 -6
@@ -25,7 +25,31 @@ var DepthLimitExceededError = class extends DbError {
25
25
  this.actual = actual;
26
26
  }
27
27
  };
28
+ /**
29
+ * Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
30
+ * without a successful CAS commit — the target row kept changing under
31
+ * the read-modify-write loop. Surfaces the attempt count and the
32
+ * last-observed version so callers can log/report the contention.
33
+ */
34
+ var CasExhaustedError = class extends DbError {
35
+ name = "CasExhaustedError";
36
+ constructor(attempts, lastSeenVersion) {
37
+ const message = `Optimistic concurrency: exhausted ${attempts} attempts; row kept changing under us (last seen version: ${lastSeenVersion ?? "unknown"})`;
38
+ super("CAS_EXHAUSTED", [{
39
+ path: "$cas",
40
+ message
41
+ }], message);
42
+ this.attempts = attempts;
43
+ this.lastSeenVersion = lastSeenVersion;
44
+ }
45
+ };
28
46
  //#endregion
47
+ Object.defineProperty(exports, "CasExhaustedError", {
48
+ enumerable: true,
49
+ get: function() {
50
+ return CasExhaustedError;
51
+ }
52
+ });
29
53
  Object.defineProperty(exports, "DbError", {
30
54
  enumerable: true,
31
55
  get: function() {
@@ -25,5 +25,23 @@ var DepthLimitExceededError = class extends DbError {
25
25
  this.actual = actual;
26
26
  }
27
27
  };
28
+ /**
29
+ * Thrown by {@link withOptimisticRetry} when `maxAttempts` is reached
30
+ * without a successful CAS commit — the target row kept changing under
31
+ * the read-modify-write loop. Surfaces the attempt count and the
32
+ * last-observed version so callers can log/report the contention.
33
+ */
34
+ var CasExhaustedError = class extends DbError {
35
+ name = "CasExhaustedError";
36
+ constructor(attempts, lastSeenVersion) {
37
+ const message = `Optimistic concurrency: exhausted ${attempts} attempts; row kept changing under us (last seen version: ${lastSeenVersion ?? "unknown"})`;
38
+ super("CAS_EXHAUSTED", [{
39
+ path: "$cas",
40
+ message
41
+ }], message);
42
+ this.attempts = attempts;
43
+ this.lastSeenVersion = lastSeenVersion;
44
+ }
45
+ };
28
46
  //#endregion
29
- export { DepthLimitExceededError as n, DbError as t };
47
+ export { DbError as n, DepthLimitExceededError as r, CasExhaustedError as t };
@@ -1,4 +1,4 @@
1
- import { u as TFieldOps } from "./ops-C61kelof.mjs";
1
+ import { f as TFieldOps } from "./ops-Blqr0ipy.mjs";
2
2
  import { FlatOf, FlatOf as FlatOf$1, NavPropsOf, NavPropsOf as NavPropsOf$1, OwnPropsOf, OwnPropsOf as OwnPropsOf$1, PrimaryKeyOf, PrimaryKeyOf as PrimaryKeyOf$1, TAtscriptAnnotatedType, TAtscriptDataType, TAtscriptTypeObject, TMetadataMap, TSerializedAnnotatedType, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
3
3
  import { AggregateControls, AggregateExpr, AggregateExpr as AggregateExpr$1, AggregateFn, AggregateQuery, AggregateQuery as AggregateQuery$1, AggregateResult, FieldOpsFor, FilterExpr, FilterExpr as FilterExpr$1, TypedWithRelation, Uniquery, Uniquery as Uniquery$1, UniqueryControls, UniqueryControls as UniqueryControls$1, UniqueryInsights, WithRelation, WithRelation as WithRelation$1 } from "@uniqu/core";
4
4
 
@@ -83,6 +83,8 @@ declare class TableMetadata {
83
83
  columnMap: Map<string, string>;
84
84
  dimensions: string[];
85
85
  measures: string[];
86
+ /** Logical field name annotated with `@db.column.version`, if any. */
87
+ versionField?: string;
86
88
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
87
89
  quantityRefByField: Map<string, string>;
88
90
  pathToPhysical: Map<string, string>;
@@ -236,6 +238,12 @@ interface TMetaResponse {
236
238
  type: TSerializedAnnotatedType;
237
239
  actions: TDbActionInfo[];
238
240
  crud: TCrudPermissions;
241
+ /**
242
+ * Physical column name annotated with `@db.column.version`, when the table
243
+ * opts into optimistic concurrency control (OCC). Absent for tables without
244
+ * the annotation, i.e. last-write-wins (default) behavior.
245
+ */
246
+ versionColumn?: string;
239
247
  }
240
248
  /** Where the action applies on the UI. */
241
249
  type TDbActionLevel = "table" | "row" | "rows";
@@ -804,7 +812,7 @@ declare abstract class BaseDbAdapter {
804
812
  * @param patch - The patch payload with array operations.
805
813
  * @returns Update result.
806
814
  */
807
- nativePatch(_filter: FilterExpr, _patch: unknown, _ops?: TFieldOps): Promise<TDbUpdateResult>;
815
+ nativePatch(_filter: FilterExpr, _patch: unknown, _ops?: TFieldOps, _expectedVersion?: number): Promise<TDbUpdateResult>;
808
816
  /**
809
817
  * Called before field flattening begins.
810
818
  * Use to extract table-level adapter-specific annotations.
@@ -951,8 +959,8 @@ declare abstract class BaseDbAdapter {
951
959
  aggregate(_query: DbQuery): Promise<Array<Record<string, unknown>>>;
952
960
  abstract insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
953
961
  abstract insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
954
- abstract replaceOne(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
955
- abstract updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
962
+ abstract replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
963
+ abstract updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
956
964
  abstract deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
957
965
  abstract findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
958
966
  abstract findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
@@ -1277,6 +1285,12 @@ declare class AtscriptDbReadable<T extends TAtscriptAnnotatedType = TAtscriptAnn
1277
1285
  * @internal Adapter-facing surface; not part of the consumer API.
1278
1286
  */
1279
1287
  get metaIdPhysical(): string | null;
1288
+ /**
1289
+ * Physical column name of the field annotated with `@db.column.version`, or
1290
+ * `undefined` when the table has no version column. Used by adapters and the
1291
+ * REST integration to drive optimistic concurrency control (OCC).
1292
+ */
1293
+ get versionColumn(): string | undefined;
1280
1294
  /** Dimension fields from `@db.column.dimension`. */
1281
1295
  get dimensions(): readonly string[];
1282
1296
  /** Measure fields from `@db.column.measure`. */
@@ -1,4 +1,4 @@
1
- import { u as TFieldOps } from "./ops-DXJ4Zw0P.cjs";
1
+ import { f as TFieldOps } from "./ops-lzmfzuY9.cjs";
2
2
  import { AggregateControls, AggregateExpr, AggregateExpr as AggregateExpr$1, AggregateFn, AggregateQuery, AggregateQuery as AggregateQuery$1, AggregateResult, FieldOpsFor, FilterExpr, FilterExpr as FilterExpr$1, TypedWithRelation, Uniquery, Uniquery as Uniquery$1, UniqueryControls, UniqueryControls as UniqueryControls$1, UniqueryInsights, WithRelation, WithRelation as WithRelation$1 } from "@uniqu/core";
3
3
  import { FlatOf, FlatOf as FlatOf$1, NavPropsOf, NavPropsOf as NavPropsOf$1, OwnPropsOf, OwnPropsOf as OwnPropsOf$1, PrimaryKeyOf, PrimaryKeyOf as PrimaryKeyOf$1, TAtscriptAnnotatedType, TAtscriptDataType, TAtscriptTypeObject, TMetadataMap, TSerializedAnnotatedType, TValidatorOptions, TValidatorPlugin, Validator } from "@atscript/typescript/utils";
4
4
 
@@ -83,6 +83,8 @@ declare class TableMetadata {
83
83
  columnMap: Map<string, string>;
84
84
  dimensions: string[];
85
85
  measures: string[];
86
+ /** Logical field name annotated with `@db.column.version`, if any. */
87
+ versionField?: string;
86
88
  /** path → sibling-ref path for `@db.amount.currency.ref` / `@db.unit.ref`. */
87
89
  quantityRefByField: Map<string, string>;
88
90
  pathToPhysical: Map<string, string>;
@@ -236,6 +238,12 @@ interface TMetaResponse {
236
238
  type: TSerializedAnnotatedType;
237
239
  actions: TDbActionInfo[];
238
240
  crud: TCrudPermissions;
241
+ /**
242
+ * Physical column name annotated with `@db.column.version`, when the table
243
+ * opts into optimistic concurrency control (OCC). Absent for tables without
244
+ * the annotation, i.e. last-write-wins (default) behavior.
245
+ */
246
+ versionColumn?: string;
239
247
  }
240
248
  /** Where the action applies on the UI. */
241
249
  type TDbActionLevel = "table" | "row" | "rows";
@@ -804,7 +812,7 @@ declare abstract class BaseDbAdapter {
804
812
  * @param patch - The patch payload with array operations.
805
813
  * @returns Update result.
806
814
  */
807
- nativePatch(_filter: FilterExpr, _patch: unknown, _ops?: TFieldOps): Promise<TDbUpdateResult>;
815
+ nativePatch(_filter: FilterExpr, _patch: unknown, _ops?: TFieldOps, _expectedVersion?: number): Promise<TDbUpdateResult>;
808
816
  /**
809
817
  * Called before field flattening begins.
810
818
  * Use to extract table-level adapter-specific annotations.
@@ -951,8 +959,8 @@ declare abstract class BaseDbAdapter {
951
959
  aggregate(_query: DbQuery): Promise<Array<Record<string, unknown>>>;
952
960
  abstract insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
953
961
  abstract insertMany(data: Array<Record<string, unknown>>): Promise<TDbInsertManyResult>;
954
- abstract replaceOne(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
955
- abstract updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps): Promise<TDbUpdateResult>;
962
+ abstract replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
963
+ abstract updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number): Promise<TDbUpdateResult>;
956
964
  abstract deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
957
965
  abstract findOne(query: DbQuery): Promise<Record<string, unknown> | null>;
958
966
  abstract findMany(query: DbQuery): Promise<Array<Record<string, unknown>>>;
@@ -1277,6 +1285,12 @@ declare class AtscriptDbReadable<T extends TAtscriptAnnotatedType = TAtscriptAnn
1277
1285
  * @internal Adapter-facing surface; not part of the consumer API.
1278
1286
  */
1279
1287
  get metaIdPhysical(): string | null;
1288
+ /**
1289
+ * Physical column name of the field annotated with `@db.column.version`, or
1290
+ * `undefined` when the table has no version column. Used by adapters and the
1291
+ * REST integration to drive optimistic concurrency control (OCC).
1292
+ */
1293
+ get versionColumn(): string | undefined;
1280
1294
  /** Dimension fields from `@db.column.dimension`. */
1281
1295
  get dimensions(): readonly string[];
1282
1296
  /** Measure fields from `@db.column.measure`. */
@@ -1,6 +1,6 @@
1
- import { B as TDbInsertResult, J as TFkLookupResolver, N as TDbDeleteResult, W as TDbUpdateResult, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger, t as AtscriptDbReadable, x as TCascadeResolver, z as TDbInsertManyResult } from "./db-readable-BSrIy08e.cjs";
2
- import { FilterExpr } from "@uniqu/core";
1
+ import { B as TDbInsertResult, J as TFkLookupResolver, N as TDbDeleteResult, W as TDbUpdateResult, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger, t as AtscriptDbReadable, x as TCascadeResolver, z as TDbInsertManyResult } from "./db-readable-BUUVDkLf.mjs";
3
2
  import { AtscriptQueryComparison, AtscriptQueryFieldRef, AtscriptQueryFieldRef as AtscriptQueryFieldRef$1, AtscriptQueryNode, AtscriptQueryNode as AtscriptQueryNode$1, AtscriptRef, FlatOf, NavPropsOf, OwnPropsOf, PrimaryKeyOf, TAtscriptAnnotatedType, TAtscriptDataType, Validator } from "@atscript/typescript/utils";
3
+ import { FilterExpr } from "@uniqu/core";
4
4
 
5
5
  //#region src/strategies/integrity.d.ts
6
6
  /**
@@ -1,6 +1,6 @@
1
- import { B as TDbInsertResult, J as TFkLookupResolver, N as TDbDeleteResult, W as TDbUpdateResult, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger, t as AtscriptDbReadable, x as TCascadeResolver, z as TDbInsertManyResult } from "./db-readable-B2TRNZHv.mjs";
2
- import { AtscriptQueryComparison, AtscriptQueryFieldRef, AtscriptQueryFieldRef as AtscriptQueryFieldRef$1, AtscriptQueryNode, AtscriptQueryNode as AtscriptQueryNode$1, AtscriptRef, FlatOf, NavPropsOf, OwnPropsOf, PrimaryKeyOf, TAtscriptAnnotatedType, TAtscriptDataType, Validator } from "@atscript/typescript/utils";
1
+ import { B as TDbInsertResult, J as TFkLookupResolver, N as TDbDeleteResult, W as TDbUpdateResult, dt as TableMetadata, it as TTableResolver, o as BaseDbAdapter, ot as TWriteTableResolver, pt as TGenericLogger, t as AtscriptDbReadable, x as TCascadeResolver, z as TDbInsertManyResult } from "./db-readable-DgIxrEVQ.cjs";
3
2
  import { FilterExpr } from "@uniqu/core";
3
+ import { AtscriptQueryComparison, AtscriptQueryFieldRef, AtscriptQueryFieldRef as AtscriptQueryFieldRef$1, AtscriptQueryNode, AtscriptQueryNode as AtscriptQueryNode$1, AtscriptRef, FlatOf, NavPropsOf, OwnPropsOf, PrimaryKeyOf, TAtscriptAnnotatedType, TAtscriptDataType, Validator } from "@atscript/typescript/utils";
4
4
 
5
5
  //#region src/strategies/integrity.d.ts
6
6
  /**
@@ -1,9 +1,9 @@
1
- import { t as DbError } from "./db-error-Cepx-RsQ.mjs";
1
+ import { n as DbError } from "./db-error-yx2jdg8B.mjs";
2
2
  import { resolveAlias } from "./agg.mjs";
3
3
  import { n as findRemoteFK, t as findFKForRelation } from "./relation-helpers-CLasawQq.mjs";
4
- import { separateFieldOps } from "./ops.mjs";
5
- import { i as forceNavNonOptional, r as dbPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-BB5h1Le3.mjs";
6
- import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, m as remapDeleteFkViolation, n as batchInsertNestedTo, o as batchPatchNestedVia, p as enrichFkViolation, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CT2rLURx.mjs";
4
+ import { separateCas, separateFieldOps } from "./ops.mjs";
5
+ import { i as forceNavNonOptional, r as dbPlugin, s as getKeyProps, t as buildDbValidator } from "./validator-DTDf9yWe.mjs";
6
+ import { a as batchPatchNestedTo, c as batchReplaceNestedTo, d as preValidateNestedFrom, f as validateBatch, i as batchPatchNestedFrom, l as batchReplaceNestedVia, m as remapDeleteFkViolation, n as batchInsertNestedTo, o as batchPatchNestedVia, p as enrichFkViolation, r as batchInsertNestedVia, s as batchReplaceNestedFrom, t as batchInsertNestedFrom, u as checkDepthOverflow } from "./nested-writer-CVMRAPoF.mjs";
7
7
  import { flattenAnnotatedType, isAnnotatedType } from "@atscript/typescript/utils";
8
8
  import { AsyncLocalStorage } from "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 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) checkDepthOverflow(payloads, maxDepth, this._meta);
2621
2673
  return 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] = 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 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) checkDepthOverflow(payloads, maxDepth, this._meta);
2673
2733
  return 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] = 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
- validateBatch(validator, payloads, ctx);
2682
- const originals = canNest ? payloads.map((p) => ({ ...p })) : [];
2748
+ validateBatch(validator, cloned, ctx);
2749
+ const originals = canNest ? cloned.map((p) => ({ ...p })) : [];
2683
2750
  const host = this;
2684
- if (canNest) await batchPatchNestedTo(host, payloads, maxDepth, depth);
2685
- await this._integrity.validateForeignKeys(payloads, this._meta, this._fkLookupResolver, this._writeTableResolver, true);
2751
+ if (canNest) await 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 = 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 = 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 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 = 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, dbPlugin] : [dbPlugin];
3009
+ return this.createValidator({
3010
+ plugins,
3011
+ partial: mode === "patch",
3012
+ replace: (def, path) => {
3013
+ const transformed = forceNavNonOptional(def);
3014
+ if (path === versionField && !transformed.optional) return {
3015
+ ...transformed,
3016
+ optional: true
3017
+ };
3018
+ return transformed;
3019
+ }
3020
+ });
3021
+ }
2925
3022
  return buildDbValidator(this.type, mode, adapterPlugins);
2926
3023
  }
2927
3024
  if (purpose === "bulkUpdate") {
@@ -3078,4 +3175,4 @@ var AtscriptDbView = class extends AtscriptDbReadable {
3078
3175
  }
3079
3176
  };
3080
3177
  //#endregion
3081
- export { BaseDbAdapter as a, AtscriptDbReadable as c, DocumentFieldMapper as d, FieldMappingStrategy as f, NoopLogger as h, ApplicationIntegrity as i, resolveDesignType as l, TableMetadata as m, AtscriptDbTable as n, IntegrityStrategy as o, UniquSelect as p, decomposePatch as r, NativeIntegrity as s, AtscriptDbView as t, RelationalFieldMapper as u };
3178
+ export { ApplicationIntegrity as a, NativeIntegrity as c, RelationalFieldMapper as d, DocumentFieldMapper as f, NoopLogger as g, TableMetadata as h, decomposePatch as i, AtscriptDbReadable as l, UniquSelect as m, AtscriptDbTable as n, BaseDbAdapter as o, FieldMappingStrategy as p, assertNoVersionWrites as r, IntegrityStrategy as s, AtscriptDbView as t, resolveDesignType as u };