@atscript/db 0.1.161 → 0.1.162

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 (63) 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-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
  6. package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
  7. package/dist/{buckets-8mG67Mtw.d.mts → buckets-DUTcrT4Z.d.mts} +475 -57
  8. package/dist/{buckets-B4T_alUe.d.cts → buckets-_iZV39Z3.d.cts} +475 -57
  9. package/dist/{column-diff-DpeUbhEI.cjs → column-diff-CZsMGQ_w.cjs} +846 -232
  10. package/dist/{column-diff-DR1glykD.mjs → column-diff-pdn53AIj.mjs} +829 -233
  11. package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
  12. package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
  13. package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
  14. package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
  15. package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
  16. package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
  17. package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-B5jW68T3.d.cts} +34 -10
  18. package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-PhVBYohc.d.mts} +34 -10
  19. package/dist/index.cjs +12 -11
  20. package/dist/index.d.cts +24 -21
  21. package/dist/index.d.mts +24 -21
  22. package/dist/index.mjs +9 -12
  23. package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
  24. package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
  25. package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
  26. package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
  27. package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
  28. package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
  29. package/dist/ops.cjs +6 -21
  30. package/dist/ops.mjs +6 -21
  31. package/dist/plugin.cjs +263 -323
  32. package/dist/plugin.mjs +163 -223
  33. package/dist/rel.cjs +2 -2
  34. package/dist/rel.d.cts +3 -3
  35. package/dist/rel.d.mts +3 -3
  36. package/dist/rel.mjs +2 -2
  37. package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-BAEkP4bH.d.cts} +1 -1
  38. package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-BSYwxOSA.d.mts} +1 -1
  39. package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
  40. package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
  41. package/dist/{search-fields-CE8cmj9X.mjs → search-fields-0U0FAFGj.mjs} +47 -1
  42. package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-Bi1n-pB1.cjs} +64 -0
  43. package/dist/search-fields-CHqqcC3W.d.cts +30 -0
  44. package/dist/search-fields-CHqqcC3W.d.mts +30 -0
  45. package/dist/shared.cjs +42 -26
  46. package/dist/shared.d.cts +98 -4
  47. package/dist/shared.d.mts +98 -4
  48. package/dist/shared.mjs +4 -2
  49. package/dist/sync.cjs +228 -65
  50. package/dist/sync.d.cts +41 -5
  51. package/dist/sync.d.mts +41 -5
  52. package/dist/sync.mjs +228 -66
  53. package/dist/validator-BcBtg8yW.d.cts +88 -0
  54. package/dist/validator-BcBtg8yW.d.mts +88 -0
  55. package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
  56. package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
  57. package/dist/validator.cjs +3 -3
  58. package/dist/validator.d.cts +4 -3
  59. package/dist/validator.d.mts +4 -3
  60. package/dist/validator.mjs +3 -3
  61. package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-D50KWU7O.cjs} +452 -18
  62. package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-QbqqXEAM.mjs} +364 -20
  63. package/package.json +6 -6
@@ -1,9 +1,9 @@
1
- const require_db_error = require("./db-error-JcfJqqpk.cjs");
2
- const require_aggregate_fns = require("./aggregate-fns-DFtKAv7Q.cjs");
3
- const require_nested_writer = require("./nested-writer-Bl1SZS7x.cjs");
4
- const require_validator = require("./validator-B4YDLUkj.cjs");
5
- const require_search_fields = require("./search-fields-CT_UZ9fg.cjs");
6
- const require_object = require("./object-Djg28csK.cjs");
1
+ const require_db_error = require("./db-error-DmF1_xrz.cjs");
2
+ const require_aggregate_fns = require("./aggregate-fns-DupYzFD7.cjs");
3
+ const require_nested_writer = require("./nested-writer-yCy6wGuN.cjs");
4
+ const require_validator = require("./validator-DK9Ijpo7.cjs");
5
+ const require_search_fields = require("./search-fields-Bi1n-pB1.cjs");
6
+ const require_object = require("./object-GjsuPNqo.cjs");
7
7
  require("./agg.cjs");
8
8
  const require_aggregate_rules = require("./aggregate-rules-D_bsCpUI.cjs");
9
9
  const require_ops = require("./ops.cjs");
@@ -319,7 +319,8 @@ function resolveViewSource(sourceType, logicalPath, nestedObjects) {
319
319
  return jsonRoot === logicalPath ? {
320
320
  column,
321
321
  designType,
322
- optional
322
+ optional,
323
+ jsonColumn: true
323
324
  } : {
324
325
  column,
325
326
  jsonPath: logicalPath.slice(jsonRoot.length + 1).split("."),
@@ -509,6 +510,12 @@ var TableMetadata = class {
509
510
  * @since 0.1.156
510
511
  */
511
512
  onUpdateNow = /* @__PURE__ */ new Set();
513
+ /**
514
+ * Top-level fields annotated with `@db.onUpdate.inc`: `0` on insert, `+1` in
515
+ * the same statement on every update and replace.
516
+ * @since 0.1.162
517
+ */
518
+ onUpdateInc = /* @__PURE__ */ new Set();
512
519
  /** Logical path → `@db.column` override (top-level keys only on document storage). */
513
520
  columnMap = /* @__PURE__ */ new Map();
514
521
  dimensions = [];
@@ -739,6 +746,13 @@ var TableMetadata = class {
739
746
  adapter.onFieldScanned?.(entry.path, entry.type, entry.metadata);
740
747
  }
741
748
  for (const path of this.encryptedFields) if (require_object.findAncestorInSet(path, this.encryptedFields) !== void 0) this.encryptedFields.delete(path);
749
+ if (this.encryptedFields.size > 0) for (const entry of collected) {
750
+ if (entry.member || this.ignoredFields.has(entry.path)) continue;
751
+ const feature = require_search_fields.ENCRYPTED_SEARCH_FEATURES.find((key) => entry.metadata.has(key));
752
+ if (!feature) continue;
753
+ const encrypted = require_object.findAncestorInSet(entry.path, this.encryptedFields);
754
+ if (encrypted !== void 0) throw new Error(`@${feature} on "${entry.path}" — it is inside the @db.encrypted field "${encrypted}"; search over ciphertext is impossible`);
755
+ }
742
756
  if (!this.nestedObjects) this._classifyFields();
743
757
  const overrides = adapter.getMetadataOverrides?.(this);
744
758
  if (overrides) this._applyOverrides(overrides);
@@ -834,6 +848,14 @@ var TableMetadata = class {
834
848
  const resolvedDefault = resolveDefaultFromMetadata(metadata);
835
849
  if (resolvedDefault) this.defaults.set(fieldName, resolvedDefault);
836
850
  if (metadata.has("db.onUpdate.now")) this.onUpdateNow.add(fieldName);
851
+ if (metadata.has("db.onUpdate.inc")) {
852
+ if (fieldName.includes(".")) throw new Error(`@db.onUpdate.inc on "${fieldName}" must sit on a top-level field of the table`);
853
+ this.onUpdateInc.add(fieldName);
854
+ if (!this.defaults.has(fieldName)) this.defaults.set(fieldName, {
855
+ kind: "value",
856
+ value: "0"
857
+ });
858
+ }
837
859
  if (metadata.has("db.ignore")) this.ignoredFields.add(fieldName);
838
860
  if (isNavRelation(metadata)) {
839
861
  this.navFields.add(fieldName);
@@ -997,14 +1019,7 @@ var TableMetadata = class {
997
1019
  const reject = (what, why) => {
998
1020
  throw new Error(`@db.encrypted on "${fieldName}" cannot coexist with ${what} — ${why}`);
999
1021
  };
1000
- if (metadata.has("meta.id")) reject("@meta.id", "the primary key must be addressable");
1001
- if (metadata.has("db.rel.FK")) reject("@db.rel.FK", "joins are impossible over ciphertext");
1002
- if (metadata.has("db.index.plain") || metadata.has("db.index.unique") || metadata.has("db.index.fulltext") || metadata.has("db.index.geo")) reject("@db.index.*", "indexes over ciphertext are meaningless");
1003
- if (metadata.has("db.search.vector") || metadata.has("db.search.filter")) reject("@db.search.*", "search over ciphertext is impossible");
1004
- if (metadata.has("db.mongo.search.text") || metadata.has("db.mongo.search.autocomplete")) reject("@db.mongo.search.*", "Atlas Search over ciphertext is impossible");
1005
- if (metadata.has("db.column.version")) reject("@db.column.version", "the OCC filter needs cleartext equality");
1006
- if (metadata.has("db.onUpdate.now")) reject("@db.onUpdate.now", "the update time is set after the encryption transform");
1007
- if (metadata.has("db.default.increment") || metadata.has("db.default.now")) reject("@db.default.increment / @db.default.now", "engine-side defaults bypass the encryption transform");
1022
+ for (const [name, why] of require_search_fields.ENCRYPTED_INCOMPATIBLE) if (metadata.has(name)) reject(`@${name}`, why);
1008
1023
  if (metadata.get("db.patch.strategy") === "merge") reject("@db.patch.strategy \"merge\"", "ciphertext is opaque — partial merges would silently drop omitted keys");
1009
1024
  }
1010
1025
  /**
@@ -4064,6 +4079,19 @@ function resolveDefaultFromMetadata(metadata) {
4064
4079
  fn: "now"
4065
4080
  };
4066
4081
  }
4082
+ const LOCK_WAITS = new Set([
4083
+ "block",
4084
+ "nowait",
4085
+ "skipLocked"
4086
+ ]);
4087
+ /** A plain row read — not grouped, aggregated or `$count`. */
4088
+ function isRowRead(controls) {
4089
+ if (!controls) return true;
4090
+ if (controls.$count) return false;
4091
+ if (Array.isArray(controls.$groupBy) && controls.$groupBy.length > 0) return false;
4092
+ const select = controls.$select;
4093
+ return !(Array.isArray(select) && select.some((item) => (0, _uniqu_core.isAggregateExpr)(item) || (0, _uniqu_core.isAggregateOfExpr)(item)));
4094
+ }
4067
4095
  /**
4068
4096
  * `query` with `$groupBy` defaulted to `[]` — an aggregate without one is the
4069
4097
  * ungrouped query (one group), as `$groupBy: []` documents it.
@@ -4072,10 +4100,7 @@ function resolveDefaultFromMetadata(metadata) {
4072
4100
  function withGroupBy(query) {
4073
4101
  const controls = query.controls ?? {};
4074
4102
  if (Array.isArray(controls.$groupBy)) return query;
4075
- if (controls.$groupBy !== void 0 && controls.$groupBy !== null) throw new require_db_error.DbError("INVALID_QUERY", [{
4076
- path: "$groupBy",
4077
- message: "$groupBy must be an array of field names"
4078
- }]);
4103
+ if (controls.$groupBy !== void 0 && controls.$groupBy !== null) throw require_db_error.invalidQuery("$groupBy", "$groupBy must be an array of field names");
4079
4104
  return {
4080
4105
  ...query,
4081
4106
  controls: {
@@ -4183,10 +4208,7 @@ var AtscriptDbReadable = class {
4183
4208
  _ensureSearchable() {
4184
4209
  if (this.adapter.isSearchable()) return;
4185
4210
  const hasVectorIndex = this.adapter.getSearchIndexes().some((i) => i.type === "vector");
4186
- throw new require_db_error.DbError("INVALID_QUERY", [{
4187
- path: "$search",
4188
- message: `Table "${this.tableName}" has no text search index` + (hasVectorIndex ? " — a @db.search.vector index only answers vectorSearch()" : "")
4189
- }]);
4211
+ throw require_db_error.invalidQuery("$search", `Table "${this.tableName}" has no text search index` + (hasVectorIndex ? " — a @db.search.vector index only answers vectorSearch()" : ""));
4190
4212
  }
4191
4213
  /** Engine-agnostic query-time guards (encrypted-field refs, $geoWithin shape). */
4192
4214
  _guardQuery(query) {
@@ -4415,6 +4437,17 @@ var AtscriptDbReadable = class {
4415
4437
  return this._meta.columnMap.get(field) ?? field;
4416
4438
  }
4417
4439
  /**
4440
+ * Physical column names of the `@db.onUpdate.inc` fields (since 0.1.162):
4441
+ * adapters add `col = col + 1` to every replace. Empty when there are none.
4442
+ *
4443
+ * @internal Adapter-facing surface; not part of the consumer API.
4444
+ */
4445
+ get onUpdateIncPhysical() {
4446
+ this._ensureBuilt();
4447
+ return this._onUpdateIncPhysical ??= [...this._meta.onUpdateInc].map((field) => this._meta.columnMap.get(field) ?? field);
4448
+ }
4449
+ _onUpdateIncPhysical;
4450
+ /**
4418
4451
  * Logical paths of the `@db.column.version.exempt` fields, including objects
4419
4452
  * whose direct children are all exempt. A patch that writes only these (and
4420
4453
  * no `$cas`) does not bump the version column. Empty when the table has no
@@ -4663,10 +4696,7 @@ var AtscriptDbReadable = class {
4663
4696
  const sort = controls?.$sort;
4664
4697
  if (!sort || typeof sort !== "object") return void 0;
4665
4698
  const keys = Object.keys(sort);
4666
- if (keys.length === 0 || controls.$count) return void 0;
4667
- if (Array.isArray(controls.$groupBy) && controls.$groupBy.length > 0) return void 0;
4668
- const select = controls.$select;
4669
- if (Array.isArray(select) && select.some((item) => (0, _uniqu_core.isAggregateExpr)(item) || (0, _uniqu_core.isAggregateOfExpr)(item))) return;
4699
+ if (keys.length === 0 || !isRowRead(controls)) return void 0;
4670
4700
  const pk = this._meta.primaryKeys;
4671
4701
  if (pk.length === 0) return void 0;
4672
4702
  const sorted = new Set(keys.map((key) => this._meta.physicalPath(key)));
@@ -4820,15 +4850,62 @@ var AtscriptDbReadable = class {
4820
4850
  return this._type.validator(opts);
4821
4851
  }
4822
4852
  /**
4853
+ * The row lock a `findOne` / `findMany` asked for (`opts.lock`, since
4854
+ * 0.1.162), validated — `undefined` when none was asked for.
4855
+ *
4856
+ * @throws DbError `INVALID_QUERY` — bad `lock` / `wait` value, a view, a
4857
+ * grouped / aggregate / `$count` read, or no `withTransaction` scope of
4858
+ * this table's adapter; `LOCK_NOT_SUPPORTED` — the adapter cannot lock.
4859
+ */
4860
+ _readLock(query, opts) {
4861
+ const mode = opts?.lock;
4862
+ const wait = opts?.wait;
4863
+ if (mode === void 0) {
4864
+ if (wait !== void 0) throw require_db_error.invalidQuery("wait", "`wait` requires `lock`");
4865
+ return;
4866
+ }
4867
+ if (mode !== "update" && mode !== "share") throw require_db_error.invalidQuery("lock", `Unknown lock mode "${String(mode)}" — use 'update' or 'share'`);
4868
+ if (wait !== void 0 && !LOCK_WAITS.has(wait)) throw require_db_error.invalidQuery("wait", `Unknown lock wait "${String(wait)}" — use 'block', 'nowait' or 'skipLocked'`);
4869
+ if (this.isView) throw require_db_error.invalidQuery("lock", `Locking reads run on tables only — "${this.tableName}" is a view`);
4870
+ if (!isRowRead(query.controls)) throw require_db_error.invalidQuery("lock", "A locking read cannot group, aggregate or $count rows");
4871
+ if (!this.adapter.supportsReadLocks()) throw new require_db_error.DbError("LOCK_NOT_SUPPORTED", [{
4872
+ path: "lock",
4873
+ message: `The adapter behind "${this.tableName}" cannot lock rows on read — use $cas / withOptimisticRetry`
4874
+ }]);
4875
+ if (!this.adapter.inTransactionScope()) throw require_db_error.invalidQuery("lock", "A locking read requires a transaction — run it inside withTransaction() of this table's adapter");
4876
+ return {
4877
+ mode,
4878
+ wait: wait ?? "block"
4879
+ };
4880
+ }
4881
+ /**
4823
4882
  * Finds a single record matching the query.
4824
4883
  * The return type automatically excludes nav props unless they are
4825
4884
  * explicitly requested via `$with`.
4885
+ *
4886
+ * `opts.lock` (since 0.1.162) makes it a locking read — see
4887
+ * {@link TReadLockOptions}:
4888
+ *
4889
+ * ```typescript
4890
+ * await adapter.withTransaction(async () => {
4891
+ * const order = await orders.findOne({ filter: { id }, controls: {} }, { lock: 'update' })
4892
+ * // … the row stays locked until the transaction ends
4893
+ * })
4894
+ * ```
4826
4895
  */
4827
- async findOne(query) {
4896
+ async findOne(query, opts) {
4828
4897
  this._ensureBuilt();
4829
4898
  this._guardQuery(query);
4899
+ const lock = this._readLock(query, opts);
4900
+ return this._findOne(query, lock);
4901
+ }
4902
+ /** {@link findOne} of a guarded query with an already validated lock. */
4903
+ async _findOne(query, lock) {
4830
4904
  const read = this._translateRead(query);
4831
- const result = await this.adapter.findOne(read.translated);
4905
+ const result = await this.adapter.findOne(lock ? {
4906
+ ...read.translated,
4907
+ lock
4908
+ } : read.translated);
4832
4909
  if (!result) return null;
4833
4910
  const [row] = await this._finishRead([result], read);
4834
4911
  return row;
@@ -4837,12 +4914,20 @@ var AtscriptDbReadable = class {
4837
4914
  * Finds all records matching the query.
4838
4915
  * The return type automatically excludes nav props unless they are
4839
4916
  * explicitly requested via `$with`.
4917
+ *
4918
+ * `opts.lock` (since 0.1.162) makes it a locking read — see
4919
+ * {@link TReadLockOptions}. Sort by the primary key when locking several
4920
+ * rows, so concurrent transactions take the locks in the same order.
4840
4921
  */
4841
- async findMany(query) {
4922
+ async findMany(query, opts) {
4842
4923
  this._ensureBuilt();
4843
4924
  this._guardQuery(query);
4925
+ const lock = this._readLock(query, opts);
4844
4926
  const read = this._translateRead(query);
4845
- return await this._finishRead(await this.adapter.findMany(read.translated), read);
4927
+ return await this._finishRead(await this.adapter.findMany(lock ? {
4928
+ ...read.translated,
4929
+ lock
4930
+ } : read.translated), read);
4846
4931
  }
4847
4932
  /**
4848
4933
  * `findMany` for the generic `$with` loader. With `partitionBy` (logical
@@ -4912,10 +4997,7 @@ var AtscriptDbReadable = class {
4912
4997
  const { buckets } = computed;
4913
4998
  if ($select) {
4914
4999
  const groupBySet = new Set($groupBy);
4915
- for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw new require_db_error.DbError("INVALID_QUERY", [{
4916
- path: "$select",
4917
- message: `Plain field "${item}" in $select must also appear in $groupBy`
4918
- }]);
5000
+ for (const item of $select) if (typeof item === "string" && !groupBySet.has(item)) throw require_db_error.invalidQuery("$select", `Plain field "${item}" in $select must also appear in $groupBy`);
4919
5001
  }
4920
5002
  const { dimensions, measures } = this._meta;
4921
5003
  if (isStrictTable(this._meta)) {
@@ -4927,24 +5009,15 @@ var AtscriptDbReadable = class {
4927
5009
  if (bucketAliases.has(field)) continue;
4928
5010
  const fd = descriptors.get(field);
4929
5011
  const verdict = fd && groupSourceVerdict(fd, this._meta, this.adapter);
4930
- if (verdict ? !verdict.ok && verdict.code === "notDimension" : !dimSet.has(field)) throw new require_db_error.DbError("INVALID_QUERY", [{
4931
- path: "$groupBy",
4932
- message: `Field "${field}" is not a dimension`
4933
- }]);
5012
+ if (verdict ? !verdict.ok && verdict.code === "notDimension" : !dimSet.has(field)) throw require_db_error.invalidQuery("$groupBy", `Field "${field}" is not a dimension`);
4934
5013
  }
4935
5014
  if ($select) for (const item of $select) {
4936
5015
  if (!(0, _uniqu_core.isAggregateExpr)(item) || item.$field === "*" || measSet.has(item.$field)) continue;
4937
5016
  if (item.$fn === "countDistinct" || require_aggregate_fns.isFirstLast(item)) {
4938
5017
  if (dimSet.has(item.$field)) continue;
4939
- throw new require_db_error.DbError("INVALID_QUERY", [{
4940
- path: "$select",
4941
- message: `Aggregate field "${item.$field}" is not a dimension or measure`
4942
- }]);
5018
+ throw require_db_error.invalidQuery("$select", `Aggregate field "${item.$field}" is not a dimension or measure`);
4943
5019
  }
4944
- throw new require_db_error.DbError("INVALID_QUERY", [{
4945
- path: "$select",
4946
- message: `Aggregate field "${item.$field}" is not a measure`
4947
- }]);
5020
+ throw require_db_error.invalidQuery("$select", `Aggregate field "${item.$field}" is not a measure`);
4948
5021
  }
4949
5022
  }
4950
5023
  const { quantityRefByField } = this._meta;
@@ -4953,10 +5026,7 @@ var AtscriptDbReadable = class {
4953
5026
  for (const item of $select) {
4954
5027
  if (!(0, _uniqu_core.isAggregateExpr)(item) || item.$field === "*") continue;
4955
5028
  const refField = quantityRefByField.get(item.$field);
4956
- if (refField && !groupBySet.has(refField)) throw new require_db_error.DbError("INVALID_QUERY", [{
4957
- path: "$select",
4958
- message: `Aggregate "${item.$fn}(${item.$field})" requires "${refField}" in $groupBy — quantity-ref-tagged fields must be grouped by their dimension`
4959
- }]);
5029
+ if (refField && !groupBySet.has(refField)) throw require_db_error.invalidQuery("$select", `Aggregate "${item.$fn}(${item.$field})" requires "${refField}" in $groupBy — quantity-ref-tagged fields must be grouped by their dimension`);
4960
5030
  }
4961
5031
  }
4962
5032
  const searchTerm = query.controls.$search;
@@ -5186,16 +5256,10 @@ var AtscriptDbReadable = class {
5186
5256
  }]);
5187
5257
  assertGeoPoint(point, "$center");
5188
5258
  const controls = query?.controls ?? {};
5189
- if (controls.$sort) throw new require_db_error.DbError("INVALID_QUERY", [{
5190
- path: "$sort",
5191
- message: "geoSearch results are distance-ordered — $sort is not allowed on this path"
5192
- }]);
5259
+ if (controls.$sort) throw require_db_error.invalidQuery("$sort", "geoSearch results are distance-ordered — $sort is not allowed on this path");
5193
5260
  for (const key of ["$maxDistance", "$minDistance"]) {
5194
5261
  const v = controls[key];
5195
- if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0)) throw new require_db_error.DbError("INVALID_QUERY", [{
5196
- path: key,
5197
- message: `${key} must be a non-negative number of meters`
5198
- }]);
5262
+ if (v !== void 0 && (typeof v !== "number" || !Number.isFinite(v) || v < 0)) throw require_db_error.invalidQuery(key, `${key} must be a non-negative number of meters`);
5199
5263
  }
5200
5264
  this._guardQuery(query);
5201
5265
  return this._translateRead(query);
@@ -5215,10 +5279,17 @@ var AtscriptDbReadable = class {
5215
5279
  *
5216
5280
  * // With relations — only requested nav props appear
5217
5281
  * const user = await table.findById('123', { controls: { $with: [{ name: 'posts' }] } })
5282
+ *
5283
+ * // Locking read inside a transaction (since 0.1.162) — see TReadLockOptions
5284
+ * const user = await table.findById('123', { lock: 'update' })
5218
5285
  * ```
5219
5286
  */
5220
5287
  async findById(id, query) {
5221
- return this.findOneByRow(id, { controls: query?.controls });
5288
+ return this.findOneByRow(id, {
5289
+ controls: query?.controls,
5290
+ lock: query?.lock,
5291
+ wait: query?.wait
5292
+ });
5222
5293
  }
5223
5294
  /**
5224
5295
  * Reads the ONE row an id addresses — resolved exactly like
@@ -5229,14 +5300,37 @@ var AtscriptDbReadable = class {
5229
5300
  * must also match `opts.scope` (an out-of-scope row answers `null` like a
5230
5301
  * missing one).
5231
5302
  *
5303
+ * `opts.lock` / `opts.wait` (since 0.1.162) lock the row found — see
5304
+ * {@link TReadLockOptions}. The id is then resolved to its one row first
5305
+ * (an unlocked read, exactly like {@link resolveRowFilter}) and that row is
5306
+ * read with the lock: with `wait: 'skipLocked'` a row another transaction
5307
+ * holds answers `null` — never a lower-precedence identification naming a
5308
+ * different row.
5309
+ *
5232
5310
  * @since 0.1.143
5233
5311
  */
5234
5312
  async findOneByRow(id, opts) {
5235
5313
  this._ensureBuilt();
5236
5314
  const controls = opts?.controls ?? {};
5237
- for (const candidate of this._idCandidates(id, opts)) {
5315
+ const candidates = this._idCandidates(id, opts);
5316
+ if (opts?.lock !== void 0 || opts?.wait !== void 0) {
5317
+ const lock = this._readLock({
5318
+ filter: {},
5319
+ controls
5320
+ }, opts);
5321
+ const pinned = await this._pinIdCandidates(candidates, opts?.scope);
5322
+ if (!pinned) return null;
5323
+ const query = {
5324
+ filter: this._andScope(pinned, opts?.scope),
5325
+ controls
5326
+ };
5327
+ this._guardQuery(query);
5328
+ return this._findOne(query, lock);
5329
+ }
5330
+ for (const candidate of candidates) {
5331
+ const filter = this._andScope(candidate, opts?.scope);
5238
5332
  const row = await this.findOne({
5239
- filter: this._andScope(candidate, opts?.scope),
5333
+ filter,
5240
5334
  controls
5241
5335
  });
5242
5336
  if (row) return row;
@@ -5440,7 +5534,7 @@ var AtscriptDbReadable = class {
5440
5534
  * Public entry point for relation loading. Used by adapters for nested $with delegation.
5441
5535
  */
5442
5536
  async loadRelations(rows, withRelations) {
5443
- const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-DeulLrAw.cjs")).then((n) => n.relation_loader_exports);
5537
+ const { loadRelationsImpl } = await Promise.resolve().then(() => require("./relation-loader-DujL_Mh9.cjs")).then((n) => n.relation_loader_exports);
5444
5538
  return loadRelationsImpl(rows, withRelations, this);
5445
5539
  }
5446
5540
  /**
@@ -5478,6 +5572,62 @@ var NativeIntegrity = class extends IntegrityStrategy {
5478
5572
  }
5479
5573
  };
5480
5574
  //#endregion
5575
+ //#region src/schema/fk-diff.ts
5576
+ /** Canonical key for an FK: sorted local field names, comma-joined. */
5577
+ function fkKey(fields) {
5578
+ return [...fields].toSorted().join(",");
5579
+ }
5580
+ /**
5581
+ * Physical local / target column names of a desired FK (`@db.column` renames
5582
+ * applied) — what DDL, constraint sync, the FK diff and the snapshot compare.
5583
+ */
5584
+ function fkColumns(fk) {
5585
+ return {
5586
+ fields: fk.physicalFields ?? fk.fields,
5587
+ targetFields: fk.physicalTargetFields ?? fk.targetFields
5588
+ };
5589
+ }
5590
+ /**
5591
+ * Compares desired FK constraints against stored snapshot to detect
5592
+ * additions, removals, and property changes (target table, target fields,
5593
+ * onDelete, onUpdate).
5594
+ */
5595
+ function computeForeignKeyDiff(desired, existingSnapshot) {
5596
+ const added = [];
5597
+ const removed = [];
5598
+ const changed = [];
5599
+ const existingByKey = /* @__PURE__ */ new Map();
5600
+ for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
5601
+ const desiredKeys = /* @__PURE__ */ new Set();
5602
+ for (const fk of desired.values()) {
5603
+ const key = fkKey(fkColumns(fk).fields);
5604
+ desiredKeys.add(key);
5605
+ const existing = existingByKey.get(key);
5606
+ if (!existing) added.push(fk);
5607
+ else if (fkPropertiesDiffer(fk, existing)) changed.push({
5608
+ desired: fk,
5609
+ existing
5610
+ });
5611
+ }
5612
+ for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
5613
+ return {
5614
+ added,
5615
+ removed,
5616
+ changed
5617
+ };
5618
+ }
5619
+ /** Whether the FK diff contains any changes. */
5620
+ function hasForeignKeyChanges(diff) {
5621
+ return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
5622
+ }
5623
+ function fkPropertiesDiffer(desired, existing) {
5624
+ if (desired.targetTable !== existing.targetTable) return true;
5625
+ if (fkKey(fkColumns(desired).targetFields) !== fkKey(existing.targetFields)) return true;
5626
+ if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
5627
+ if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
5628
+ return false;
5629
+ }
5630
+ //#endregion
5481
5631
  //#region src/shared/failure-collector.ts
5482
5632
  /**
5483
5633
  * Collects failures from a batch of independent operations so one failing
@@ -5493,8 +5643,10 @@ function createFailureCollector(what) {
5493
5643
  attempt: async (label, op) => {
5494
5644
  try {
5495
5645
  await op();
5646
+ return true;
5496
5647
  } catch (error) {
5497
5648
  failures.push(`${label}: ${error.message}`);
5649
+ return false;
5498
5650
  }
5499
5651
  },
5500
5652
  throwIfAny: () => {
@@ -5502,6 +5654,27 @@ function createFailureCollector(what) {
5502
5654
  }
5503
5655
  };
5504
5656
  }
5657
+ /**
5658
+ * Records the changes a sync pass makes — or, in a dry run, would make — so
5659
+ * planning and syncing share one code path. `apply(change, label, op)`
5660
+ * records `change` straight away in a dry run; otherwise it runs `op` first:
5661
+ * through `attempt` when given (a {@link createFailureCollector} collector —
5662
+ * a failure is collected and the change not recorded), else directly (a
5663
+ * failure throws).
5664
+ * @since 0.1.162
5665
+ */
5666
+ function createChangeRecorder(dryRun, attempt) {
5667
+ const changes = [];
5668
+ return {
5669
+ changes,
5670
+ apply: async (change, label, op) => {
5671
+ if (!dryRun) if (attempt) {
5672
+ if (!await attempt(label, async () => op())) return;
5673
+ } else await op();
5674
+ changes.push(change);
5675
+ }
5676
+ };
5677
+ }
5505
5678
  //#endregion
5506
5679
  //#region src/base-adapter.ts
5507
5680
  const EMPTY_DEFAULT_FNS = /* @__PURE__ */ new Set();
@@ -5512,6 +5685,32 @@ const EMPTY_BUCKET_UNITS = /* @__PURE__ */ new Set();
5512
5685
  */
5513
5686
  const ALL_BUCKET_UNITS = new Set(_uniqu_core.BUCKET_UNITS);
5514
5687
  const NO_VIEW_CAPABILITIES = /* @__PURE__ */ new Set();
5688
+ /**
5689
+ * Every isolation level, weakest first — what an adapter that provides them
5690
+ * all returns from `isolationLevels()`. @since 0.1.162
5691
+ */
5692
+ const ALL_ISOLATION_LEVELS = new Set([
5693
+ "read committed",
5694
+ "repeatable read",
5695
+ "serializable"
5696
+ ]);
5697
+ const NO_ISOLATION_LEVELS = /* @__PURE__ */ new Set();
5698
+ /** Strength of each isolation level (higher = stricter). */
5699
+ const ISOLATION_RANK = {
5700
+ "read committed": 1,
5701
+ "repeatable read": 2,
5702
+ serializable: 3
5703
+ };
5704
+ /**
5705
+ * Validates `opts.isolation`: `undefined` when none is requested.
5706
+ * @throws DbError `INVALID_QUERY` for an unknown level.
5707
+ */
5708
+ function requestedIsolation(opts) {
5709
+ const level = opts?.isolation;
5710
+ if (level === void 0) return void 0;
5711
+ if (!ALL_ISOLATION_LEVELS.has(level)) throw require_db_error.invalidQuery("isolation", `Unknown isolation level "${String(level)}" — use 'read committed', 'repeatable read' or 'serializable'`);
5712
+ return level;
5713
+ }
5515
5714
  /** Every view capability — what the bundled adapters return from `viewCapabilities()`. @since 0.1.147 */
5516
5715
  const ALL_VIEW_CAPABILITIES = new Set(["compute", "firstJoin"]);
5517
5716
  const txStorage = new node_async_hooks.AsyncLocalStorage();
@@ -5621,6 +5820,41 @@ var BaseDbAdapter = class {
5621
5820
  if (expectedVersion !== void 0) throw new Error("keepVersion cannot combine with expectedVersion");
5622
5821
  }
5623
5822
  /**
5823
+ * `@db.onUpdate.inc` columns (physical names, since 0.1.162) as a field-op
5824
+ * increment by one, or `undefined` when the table declares none. A replace
5825
+ * path renders it next to the replacement data (the core leaves these
5826
+ * columns out of the payload); the patch paths get the `$inc` from the core.
5827
+ */
5828
+ _onUpdateIncOps() {
5829
+ const cols = this._table.onUpdateIncPhysical;
5830
+ if (cols.length === 0) return void 0;
5831
+ const inc = {};
5832
+ for (const col of cols) inc[col] = 1;
5833
+ return { inc };
5834
+ }
5835
+ /**
5836
+ * Whether `updateMany` / `deleteMany` render a row bound
5837
+ * ({@link TDbWriteLimit}) natively in one statement (since 0.1.162). When
5838
+ * `false` (the default) the core emulates a bounded write: it reads the
5839
+ * primary keys of the first `limit` matching rows in order, then writes the
5840
+ * rows matching the filter AND those keys.
5841
+ */
5842
+ supportsWriteLimit() {
5843
+ return false;
5844
+ }
5845
+ /**
5846
+ * Whether a native bounded write ({@link supportsWriteLimit}) picks and
5847
+ * writes its rows atomically — a row a concurrent transaction deletes or
5848
+ * moves out of the filter is passed over and the next match taken, so `0`
5849
+ * affected rows always means nothing matched (MySQL's
5850
+ * `UPDATE/DELETE … ORDER BY … LIMIT`). When `false` (the default) the core
5851
+ * probes for a remaining match after a bounded write that affected no rows
5852
+ * and retries it a few times (since 0.1.162).
5853
+ */
5854
+ writeLimitIsAtomic() {
5855
+ return false;
5856
+ }
5857
+ /**
5624
5858
  * Logs a debug message if verbose mode is enabled.
5625
5859
  * Adapters call this to log DB operations with zero overhead when disabled.
5626
5860
  */
@@ -5637,16 +5871,29 @@ var BaseDbAdapter = class {
5637
5871
  * The generic layer handles nesting detection via `AsyncLocalStorage`.
5638
5872
  * Adapters override `_beginTransaction`, `_commitTransaction`, and
5639
5873
  * `_rollbackTransaction` to provide raw DB-specific transaction primitives.
5640
- */
5641
- async withTransaction(fn) {
5874
+ *
5875
+ * `opts.isolation` (since 0.1.162) sets the isolation level of the
5876
+ * transaction this call opens — "at least this level". An adapter whose
5877
+ * {@link isolationLevels} lacks it throws `ISOLATION_NOT_SUPPORTED`. A
5878
+ * nested call joins the open transaction; it may request a level only when
5879
+ * that transaction was opened with the same or a stricter one, else it
5880
+ * throws `INVALID_QUERY` (the level of a running transaction cannot change).
5881
+ */
5882
+ async withTransaction(fn, opts) {
5642
5883
  const owner = this._transactionOwner();
5643
- if (findTxContext(owner)) return fn();
5884
+ const isolation = this._checkIsolation(opts);
5885
+ const open = findTxContext(owner);
5886
+ if (open) {
5887
+ if (isolation) this._assertJoinIsolation(open.isolation, isolation);
5888
+ return fn();
5889
+ }
5644
5890
  const ctx = {
5645
5891
  owner,
5646
5892
  state: void 0,
5647
- parent: txStorage.getStore()
5893
+ parent: txStorage.getStore(),
5894
+ isolation: this._inherentIsolation() ?? isolation
5648
5895
  };
5649
- ctx.state = await this._beginTransaction();
5896
+ ctx.state = await this._beginTransaction(isolation ? { isolation } : void 0);
5650
5897
  return txStorage.run(ctx, async () => {
5651
5898
  try {
5652
5899
  const result = await fn();
@@ -5661,6 +5908,67 @@ var BaseDbAdapter = class {
5661
5908
  });
5662
5909
  }
5663
5910
  /**
5911
+ * The isolation levels {@link withTransaction} can open a transaction at
5912
+ * (since 0.1.162). Default: none — a requested level throws
5913
+ * `ISOLATION_NOT_SUPPORTED`. Override and apply the level in
5914
+ * {@link _beginTransaction} (`opts.isolation`).
5915
+ */
5916
+ isolationLevels() {
5917
+ return NO_ISOLATION_LEVELS;
5918
+ }
5919
+ /**
5920
+ * The level every transaction of this adapter runs at regardless of the
5921
+ * request (since 0.1.162) — SQLite's `BEGIN IMMEDIATE` is always
5922
+ * serializable. A nested `withTransaction` asking for any level then joins
5923
+ * without error. Default `undefined` (the engine default / what was asked).
5924
+ */
5925
+ _inherentIsolation() {}
5926
+ /**
5927
+ * Validates a `withTransaction` isolation request against
5928
+ * {@link isolationLevels} — the requested level, or `undefined`.
5929
+ * @throws DbError `INVALID_QUERY` (unknown level) / `ISOLATION_NOT_SUPPORTED`.
5930
+ */
5931
+ _checkIsolation(opts) {
5932
+ const level = requestedIsolation(opts);
5933
+ if (level && !this.isolationLevels().has(level)) throw this._isolationNotSupported(`${this.constructor.name} cannot run a transaction at isolation level '${level}'`);
5934
+ return level;
5935
+ }
5936
+ /** The `ISOLATION_NOT_SUPPORTED` error of a `withTransaction` isolation request. */
5937
+ _isolationNotSupported(message) {
5938
+ return new require_db_error.DbError("ISOLATION_NOT_SUPPORTED", [{
5939
+ path: "isolation",
5940
+ message
5941
+ }]);
5942
+ }
5943
+ /**
5944
+ * A nested `withTransaction` asked for `requested` while a transaction
5945
+ * opened at `open` runs: fine when `open` is at least as strict.
5946
+ * @throws DbError `INVALID_QUERY` otherwise.
5947
+ */
5948
+ _assertJoinIsolation(open, requested) {
5949
+ if (open && ISOLATION_RANK[open] >= ISOLATION_RANK[requested]) return;
5950
+ throw require_db_error.invalidQuery("isolation", `A nested withTransaction cannot request isolation '${requested}': the open transaction runs at ${open ? `'${open}'` : "the engine default"} — set the level on the outermost call`);
5951
+ }
5952
+ /**
5953
+ * Whether the readable layer may pass a row lock on `DbQuery.lock` to
5954
+ * `findOne` / `findMany` (since 0.1.162). Default `false` — a locking read
5955
+ * throws `LOCK_NOT_SUPPORTED`. An adapter whose transaction already
5956
+ * serialises writers may return `true` and ignore the lock.
5957
+ */
5958
+ supportsReadLocks() {
5959
+ return false;
5960
+ }
5961
+ /**
5962
+ * `true` inside a `withTransaction` scope of this adapter's owner — a real
5963
+ * transaction or a pass-through one (adapters without transaction
5964
+ * primitives, such as the in-memory one). Unlike {@link isInTransaction} a
5965
+ * pass-through scope counts. Used to require a scope for locking reads.
5966
+ * @since 0.1.162
5967
+ */
5968
+ inTransactionScope() {
5969
+ return findTxContext(this._transactionOwner()) !== void 0;
5970
+ }
5971
+ /**
5664
5972
  * The object a transaction state is branded with (since 0.1.128). Every
5665
5973
  * adapter instance that returns the same owner shares one transaction —
5666
5974
  * override to return the driver / pool / client the adapter was constructed
@@ -5707,9 +6015,10 @@ var BaseDbAdapter = class {
5707
6015
  }
5708
6016
  /**
5709
6017
  * Starts a raw transaction. Returns opaque state stored in the async context.
5710
- * Override in adapters that support transactions.
6018
+ * Override in adapters that support transactions. `opts.isolation` (since
6019
+ * 0.1.162) is set only to a level of {@link isolationLevels}.
5711
6020
  */
5712
- async _beginTransaction() {}
6021
+ async _beginTransaction(_opts) {}
5713
6022
  /** Commits the raw transaction. Override in adapters that support transactions. */
5714
6023
  async _commitTransaction(_state) {}
5715
6024
  /** Rolls back the raw transaction. Override in adapters that support transactions. */
@@ -6015,46 +6324,87 @@ var BaseDbAdapter = class {
6015
6324
  *
6016
6325
  * @example
6017
6326
  * ```typescript
6018
- * async syncIndexes() {
6019
- * await this.syncIndexesWithDiff({
6327
+ * protected override _syncIndexes(dryRun: boolean) {
6328
+ * return this.syncIndexesWithDiff({
6329
+ * dryRun,
6020
6330
  * listExisting: async () => this.driver.all('PRAGMA index_list(...)'),
6021
6331
  * createIndex: async (index) => this.driver.exec('CREATE INDEX ...'),
6022
6332
  * dropIndex: async (name) => this.driver.exec('DROP INDEX ...'),
6023
- * shouldSkipType: (type) => type === 'fulltext',
6333
+ * skipIndex: (index) => (index.type === 'fulltext' ? null : undefined),
6024
6334
  * })
6025
6335
  * }
6026
6336
  * ```
6027
6337
  */
6028
6338
  async syncIndexesWithDiff(opts) {
6029
6339
  const prefix = opts.prefix ?? "atscript__";
6030
- const managed = (await opts.listExisting()).filter((i) => i.name.startsWith(prefix));
6340
+ const dryRun = opts.dryRun === true;
6341
+ const managed = (await opts.listExisting()).filter((i) => i.name.startsWith(prefix) && !(opts.isUnmanaged?.(i.name) ?? false));
6031
6342
  const existingNames = new Set(managed.map((i) => i.name));
6032
6343
  const existingColumns = new Map(managed.filter((i) => i.columns).map((i) => [i.name, i.columns]));
6033
6344
  const desiredNames = /* @__PURE__ */ new Set();
6034
6345
  const { attempt, throwIfAny } = createFailureCollector("index sync");
6346
+ const { changes, apply } = createChangeRecorder(dryRun, attempt);
6035
6347
  for (const index of this._table.indexes.values()) {
6036
- if (opts.warnUnsupportedTypes?.types.includes(index.type)) {
6037
- this.logger.warn(`[${opts.warnUnsupportedTypes.adapter}] ${index.type} index "${index.name}" declared but not supported by this adapter — skipped`);
6348
+ if (index.type === "fulltext" && !index.fields.some((f) => !f.integer)) continue;
6349
+ const skipReason = opts.skipIndex?.(index);
6350
+ if (skipReason !== void 0) {
6351
+ if (skipReason !== null && !dryRun) this.logger.warn(skipReason);
6038
6352
  continue;
6039
6353
  }
6040
- if (opts.shouldSkipType?.(index.type)) continue;
6041
- if (index.type === "fulltext" && !index.fields.some((f) => !f.integer)) continue;
6042
6354
  desiredNames.add(index.key);
6043
6355
  if (!existingNames.has(index.key)) {
6044
- await attempt(`create index "${index.key}"`, () => opts.createIndex(index));
6356
+ await apply({
6357
+ name: index.key,
6358
+ action: "add"
6359
+ }, `create index "${index.key}"`, () => opts.createIndex(index));
6045
6360
  continue;
6046
6361
  }
6047
6362
  if (index.type === "plain" || index.type === "unique") {
6048
6363
  const liveColumns = existingColumns.get(index.key);
6049
6364
  const desiredColumns = index.fields.map((f) => opts.renderDesiredColumn?.(index, f) ?? f.name);
6050
- if (liveColumns && (liveColumns.length !== desiredColumns.length || liveColumns.some((c, i) => c !== desiredColumns[i]))) await attempt(`rebuild index "${index.key}"`, async () => {
6365
+ if (liveColumns && (liveColumns.length !== desiredColumns.length || liveColumns.some((c, i) => c !== desiredColumns[i]))) await apply({
6366
+ name: index.key,
6367
+ action: "rebuild"
6368
+ }, `rebuild index "${index.key}"`, async () => {
6051
6369
  await opts.dropIndex(index.key);
6052
6370
  await opts.createIndex(index);
6053
6371
  });
6054
6372
  }
6055
6373
  }
6056
- for (const name of existingNames) if (!desiredNames.has(name)) await attempt(`drop index "${name}"`, () => opts.dropIndex(name));
6374
+ for (const name of existingNames) if (!desiredNames.has(name)) await apply({
6375
+ name,
6376
+ action: "drop"
6377
+ }, `drop index "${name}"`, () => opts.dropIndex(name));
6057
6378
  throwIfAny();
6379
+ return changes;
6380
+ }
6381
+ /**
6382
+ * Template method for foreign-key constraint synchronization: drops the
6383
+ * live constraints the model does not declare and adds the declared ones
6384
+ * the live table lacks — matched by their sorted local columns
6385
+ * ({@link fkKey}). A failing statement throws. Returns the changes (made,
6386
+ * or with `dryRun` only computed).
6387
+ * @since 0.1.162
6388
+ */
6389
+ async syncForeignKeysWithDiff(opts) {
6390
+ const { changes, apply } = createChangeRecorder(opts.dryRun === true);
6391
+ const existing = await opts.listExisting();
6392
+ const desired = [...this._table.foreignKeys.values()];
6393
+ const desiredKeys = new Set(desired.map((fk) => fkKey(fkColumns(fk).fields)));
6394
+ for (const [name, columns] of existing) if (!desiredKeys.has(fkKey(columns))) await apply({
6395
+ fields: columns,
6396
+ action: "drop"
6397
+ }, `drop foreign key "${name}"`, () => opts.dropConstraint(name));
6398
+ const existingKeys = new Set([...existing.values()].map((columns) => fkKey(columns)));
6399
+ for (const fk of desired) {
6400
+ const { fields } = fkColumns(fk);
6401
+ if (!existingKeys.has(fkKey(fields))) await apply({
6402
+ fields,
6403
+ targetTable: fk.targetTable,
6404
+ action: "add"
6405
+ }, `add foreign key (${fields.join(", ")})`, () => opts.addConstraint(fk));
6406
+ }
6407
+ return changes;
6058
6408
  }
6059
6409
  /**
6060
6410
  * Returns available search indexes for this adapter.
@@ -6273,6 +6623,38 @@ var BaseDbAdapter = class {
6273
6623
  return false;
6274
6624
  }
6275
6625
  /**
6626
+ * Synchronizes indexes between the Atscript definitions and the database.
6627
+ * Uses `this._table.indexes` for the full index definitions. Returns the
6628
+ * changes it made (since 0.1.162) — schema sync reports them on the run
6629
+ * entry; `void` reports none. Schema sync
6630
+ * passes `opts` when safe mode skipped work the indexes depend on (see
6631
+ * {@link TSyncIndexesOptions}); an adapter whose indexes do not depend on it
6632
+ * ignores them. Default: {@link _syncIndexes} without `dryRun`.
6633
+ */
6634
+ async syncIndexes(opts) {
6635
+ return this._syncIndexes(false, opts);
6636
+ }
6637
+ /**
6638
+ * The index changes {@link syncIndexes} would make now — no DDL. Schema
6639
+ * sync's plan reports them (`entry.indexChanges`) and marks the entry
6640
+ * `alter`; a run reports what `syncIndexes` returns.
6641
+ * @since 0.1.162
6642
+ */
6643
+ async planIndexes(opts) {
6644
+ return this._syncIndexes(true, opts);
6645
+ }
6646
+ /**
6647
+ * The one index pass behind {@link syncIndexes} (`dryRun: false`) and
6648
+ * {@link planIndexes} (`dryRun: true` — compute the changes, no DDL).
6649
+ * Adapters implement this (usually over {@link syncIndexesWithDiff}). An
6650
+ * adapter that overrides `syncIndexes` instead plans no index work.
6651
+ * @since 0.1.162
6652
+ */
6653
+ async _syncIndexes(dryRun, _opts) {
6654
+ if (dryRun) return [];
6655
+ throw new Error(`${this.constructor.name} implements neither syncIndexes nor _syncIndexes`);
6656
+ }
6657
+ /**
6276
6658
  * When true, the adapter can handle column type changes in-place
6277
6659
  * (e.g. MySQL's ALTER TABLE MODIFY COLUMN) without requiring table recreation.
6278
6660
  * The generic sync layer will delegate type changes to {@link syncColumns}
@@ -6817,7 +7199,7 @@ function assertNoVersionWrites(data, versionColumn) {
6817
7199
  * Whether a logical (pre-decomposition) patch writes ONLY version-exempt
6818
7200
  * fields (`@db.column.version.exempt`). `data` has nav fields, filter keys and
6819
7201
  * derived fields removed and `$cas` separated. Mirrors `flattenPatchPayload`
6820
- * (keep the two side by side): PK keys and `@db.onUpdate.now` fields are
7202
+ * (keep the two side by side): PK keys and `@db.onUpdate.now` / `.inc` fields are
6821
7203
  * skipped; an exempt path (or one under an exempt ancestor) is covered
6822
7204
  * whatever its value (scalar, field op, array ops, object); a non-exempt
6823
7205
  * merge-strategy object recurses (only the supplied children are written);
@@ -6831,11 +7213,12 @@ function isVersionExemptPatch(data, table) {
6831
7213
  const primaryKeys = meta.primaryKeys;
6832
7214
  const flatMap = meta.flatMap;
6833
7215
  const stamps = meta.onUpdateNow;
7216
+ const counters = meta.onUpdateInc;
6834
7217
  const walk = (obj, prefix) => {
6835
7218
  let result = "none";
6836
7219
  for (const [k, value] of Object.entries(obj)) {
6837
7220
  const key = prefix ? `${prefix}.${k}` : k;
6838
- if (primaryKeys.includes(key) || stamps.has(key)) continue;
7221
+ if (primaryKeys.includes(key) || stamps.has(key) || counters.has(key)) continue;
6839
7222
  if (meta.isVersionExemptPath(key)) {
6840
7223
  result = "exempt";
6841
7224
  continue;
@@ -7062,15 +7445,47 @@ function concurrentChange() {
7062
7445
  }
7063
7446
  /** Hand-off for a version-exempt patch (since 0.1.150) — shared, never mutated. */
7064
7447
  const KEEP_VERSION = Object.freeze({ keepVersion: true });
7065
- /** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
7066
- const TOUCH_MANY_CHUNK = 500;
7067
- /** `touchMany` input rejection — always `INVALID_QUERY`, path names the key. */
7068
- function invalidTouchKey(path, message) {
7069
- return new require_db_error.DbError("INVALID_QUERY", [{
7070
- path,
7071
- message
7448
+ /**
7449
+ * The trailing `TDbUpdateOptions` rest-arg of an adapter update: `[]` when
7450
+ * there is nothing to pass, so the common call shape stays unchanged.
7451
+ */
7452
+ function updateOptsArg(keepVersion, limit) {
7453
+ if (limit) return [keepVersion ? {
7454
+ keepVersion,
7455
+ limit
7456
+ } : { limit }];
7457
+ return keepVersion ? [KEEP_VERSION] : [];
7458
+ }
7459
+ /** Extra attempts of a bounded write that affected no rows while matches remain. */
7460
+ const BOUNDED_WRITE_RETRIES = 3;
7461
+ /**
7462
+ * A bounded write lost its picked rows to concurrent writers on every
7463
+ * attempt while rows still matched — retryable, like other row contention.
7464
+ */
7465
+ function boundedWriteContention() {
7466
+ return new require_db_error.DbError("SERIALIZATION_FAILURE", [{
7467
+ path: "limit",
7468
+ message: `Bounded write: concurrent writers took the picked rows on 4 attempts while rows still matched — retry`
7072
7469
  }]);
7073
7470
  }
7471
+ const DELETE_RESULT = {
7472
+ affected: (r) => r.deletedCount,
7473
+ add: (a, b) => ({ deletedCount: a.deletedCount + b.deletedCount }),
7474
+ none: () => ({ deletedCount: 0 })
7475
+ };
7476
+ const UPDATE_RESULT = {
7477
+ affected: (r) => r.matchedCount,
7478
+ add: (a, b) => ({
7479
+ matchedCount: a.matchedCount + b.matchedCount,
7480
+ modifiedCount: a.modifiedCount + b.modifiedCount
7481
+ }),
7482
+ none: () => ({
7483
+ matchedCount: 0,
7484
+ modifiedCount: 0
7485
+ })
7486
+ };
7487
+ /** Upper bound of keys per `touchMany` UPDATE statement (parameter-count safety). */
7488
+ const TOUCH_MANY_CHUNK = 500;
7074
7489
  var AtscriptDbTable = class extends AtscriptDbReadable {
7075
7490
  _cascadeResolver;
7076
7491
  _fkLookupResolver;
@@ -7130,15 +7545,9 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7130
7545
  async _insertMany(payloads, opts) {
7131
7546
  this._ensureBuilt();
7132
7547
  const { _depth, _action, maxDepth: userMax, guard, check, onConflict, lockConflicts } = opts ?? {};
7133
- if (onConflict !== void 0 && onConflict !== "error" && onConflict !== "ignore") throw new require_db_error.DbError("INVALID_QUERY", [{
7134
- path: "onConflict",
7135
- message: `Unknown onConflict mode "${String(onConflict)}"`
7136
- }]);
7548
+ if (onConflict !== void 0 && onConflict !== "error" && onConflict !== "ignore") throw require_db_error.invalidQuery("onConflict", `Unknown onConflict mode "${String(onConflict)}"`);
7137
7549
  const ignore = onConflict === "ignore";
7138
- if (lockConflicts && !ignore) throw new require_db_error.DbError("INVALID_QUERY", [{
7139
- path: "lockConflicts",
7140
- message: "lockConflicts requires onConflict \"ignore\""
7141
- }]);
7550
+ if (lockConflicts && !ignore) throw require_db_error.invalidQuery("lockConflicts", "lockConflicts requires onConflict \"ignore\"");
7142
7551
  const ignoreOpts = ignore ? { inCallerTransaction: this.adapter.isInTransaction() } : void 0;
7143
7552
  if (ignoreOpts?.inCallerTransaction && lockConflicts) ignoreOpts.lockConflicts = true;
7144
7553
  const maxDepth = userMax ?? 3;
@@ -7229,8 +7638,9 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7229
7638
  const items = payloads.map((p, i) => {
7230
7639
  const c = clone(p);
7231
7640
  expectedVersions[i] = require_ops.separateCas(c, versionColumn);
7232
- this._stampOnUpdate(c, now, true);
7233
- return this._applyDefaults(c, now);
7641
+ const d = this._applyDefaults(c, now);
7642
+ this._stampOnUpdate(d, now, true);
7643
+ return d;
7234
7644
  });
7235
7645
  const originals = canNest ? items.map((item) => ({ ...item })) : [];
7236
7646
  const validator = this.getValidator("bulkReplace");
@@ -7364,8 +7774,8 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7364
7774
  if (!matched[i] && _ownedBy?.strict?.[i]) throw concurrentChange();
7365
7775
  continue;
7366
7776
  }
7367
- const updateOpts = expectedVersion === void 0 && versionColumn !== void 0 && isVersionExemptPatch(data, this) ? [KEEP_VERSION] : [];
7368
- this._stampTopLevel(data, now);
7777
+ const updateOpts = updateOptsArg(expectedVersion === void 0 && versionColumn !== void 0 && isVersionExemptPatch(data, this));
7778
+ this._stampTopLevel(data, now, expectedVersion !== void 0);
7369
7779
  let result;
7370
7780
  if (this.adapter.supportsNativePatch()) {
7371
7781
  const ops = require_ops.separateFieldOps(data);
@@ -7429,7 +7839,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7429
7839
  async touchMany(keys, opts) {
7430
7840
  this._ensureBuilt();
7431
7841
  const versionField = this._meta.versionField;
7432
- if (versionField === void 0) throw invalidTouchKey("", "touchMany requires @db.column.version");
7842
+ if (versionField === void 0) throw require_db_error.invalidQuery("", "touchMany requires @db.column.version");
7433
7843
  if (keys.length === 0) return {
7434
7844
  matchedCount: 0,
7435
7845
  modifiedCount: 0
@@ -7440,14 +7850,14 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7440
7850
  for (const [i, key] of keys.entries()) {
7441
7851
  const pair = {};
7442
7852
  for (const pk of pkFields) {
7443
- if (key[pk] === void 0) throw invalidTouchKey(`[${i}].${pk}`, `touchMany: each key must carry its "${pk}"`);
7853
+ if (key[pk] === void 0) throw require_db_error.invalidQuery(`[${i}].${pk}`, `touchMany: each key must carry its "${pk}"`);
7444
7854
  pair[pk] = key[pk];
7445
7855
  }
7446
7856
  const version = key[versionField];
7447
- if (typeof version !== "number" || !Number.isFinite(version)) throw invalidTouchKey(`[${i}].${versionField}`, `touchMany: each key must carry its expected "${versionField}" (number)`);
7448
- for (const [prop, value] of Object.entries(key)) if (value !== void 0 && prop !== versionField && !(prop in pair)) throw invalidTouchKey(`[${i}].${prop}`, `touchMany: a touch carries no payload — keys hold the primary key and "${versionField}" only, got "${prop}"`);
7857
+ if (typeof version !== "number" || !Number.isFinite(version)) throw require_db_error.invalidQuery(`[${i}].${versionField}`, `touchMany: each key must carry its expected "${versionField}" (number)`);
7858
+ for (const [prop, value] of Object.entries(key)) if (value !== void 0 && prop !== versionField && !(prop in pair)) throw require_db_error.invalidQuery(`[${i}].${prop}`, `touchMany: a touch carries no payload — keys hold the primary key and "${versionField}" only, got "${prop}"`);
7449
7859
  const identity = JSON.stringify(pair);
7450
- if (seen.has(identity)) throw invalidTouchKey(`[${i}]`, `touchMany: duplicate key ${identity}`);
7860
+ if (seen.has(identity)) throw require_db_error.invalidQuery(`[${i}]`, `touchMany: duplicate key ${identity}`);
7451
7861
  seen.add(identity);
7452
7862
  pair[versionField] = version;
7453
7863
  pairs.push(pair);
@@ -7463,12 +7873,18 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7463
7873
  });
7464
7874
  if (matched < keys.length) throw new require_db_error.CasMismatchError(matched, keys.length);
7465
7875
  }
7876
+ const stamps = {};
7877
+ this._stampTopLevel(stamps, Date.now(), true);
7878
+ const { update, ops } = require_object.isEmptyObject(stamps) ? {
7879
+ update: stamps,
7880
+ ops: void 0
7881
+ } : this._translatePatch(stamps);
7466
7882
  return this.adapter.withTransaction(async () => {
7467
7883
  let matchedCount = 0;
7468
7884
  let modifiedCount = 0;
7469
7885
  for (let start = 0; start < translated.length; start += TOUCH_MANY_CHUNK) {
7470
7886
  const chunk = translated.slice(start, start + TOUCH_MANY_CHUNK);
7471
- const result = await this.adapter.updateMany({ $or: chunk }, {}, void 0);
7887
+ const result = await this.adapter.updateMany({ $or: chunk }, update, ops);
7472
7888
  matchedCount += result.matchedCount;
7473
7889
  modifiedCount += result.modifiedCount;
7474
7890
  }
@@ -7519,66 +7935,227 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7519
7935
  };
7520
7936
  return require_nested_writer.remapDeleteFkViolation(this.tableName, () => guard || needsCascade || candidates.length > 1 ? this.adapter.withTransaction(run) : run());
7521
7937
  }
7522
- async updateMany(filter, data) {
7938
+ /**
7939
+ * Patches every row matching `filter` with `data` (own fields only — no
7940
+ * relations, no `$cas`). Since 0.1.162 the payload is validated in `patch`
7941
+ * mode, exactly like {@link updateOne}: a type mismatch, `null` on a
7942
+ * non-nullable field, `@meta.required` or `@expect.*` failures throw a
7943
+ * `ValidatorError` before any statement runs.
7944
+ *
7945
+ * `opts.limit` (since 0.1.162) bounds the write to the first `limit` matching
7946
+ * rows in `opts.sort` order (default: primary key ascending; the primary key
7947
+ * always breaks ties). Loop until `matchedCount === 0` to process every
7948
+ * match in batches. Not exposed over HTTP.
7949
+ */
7950
+ async updateMany(filter, data, opts) {
7523
7951
  this._ensureBuilt();
7524
7952
  this._guardMutationFilter(filter);
7525
- await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
7953
+ const bound = this._writeBound(filter, opts);
7526
7954
  const dataCopy = _cloneWritePayload(data);
7527
- this._meta.stripDerived(dataCopy);
7955
+ if ("$cas" in dataCopy) throw require_db_error.invalidQuery("$cas", "$cas is not supported on updateMany — use bulkUpdate with per-row $cas for version-locked batch updates");
7956
+ this._rejectNavWrite(dataCopy, "updateMany");
7528
7957
  const now = Date.now();
7529
7958
  this._stampOnUpdate(dataCopy, now, false);
7959
+ require_nested_writer.validateBatch(this.getValidator("bulkUpdate"), [dataCopy], {
7960
+ mode: "patch",
7961
+ flatMap: this.flatMap,
7962
+ navFields: this._meta.navFields
7963
+ });
7964
+ await this._integrity.validateForeignKeys([dataCopy], this._meta, this._fkLookupResolver, this._writeTableResolver, true);
7965
+ this._meta.stripDerived(dataCopy);
7530
7966
  const versionColumn = this.versionColumn;
7531
- if ("$cas" in dataCopy) throw new require_db_error.DbError("INVALID_QUERY", [{
7532
- path: "$cas",
7533
- message: "$cas is not supported on updateMany — use bulkUpdate with per-row $cas for version-locked batch updates"
7534
- }]);
7535
7967
  if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
7536
- const updateOpts = versionColumn !== void 0 && isVersionExemptPatch(dataCopy, this) ? [KEEP_VERSION] : [];
7968
+ const keepVersion = versionColumn !== void 0 && isVersionExemptPatch(dataCopy, this);
7537
7969
  this._stampTopLevel(dataCopy, now);
7538
7970
  await this._encryptItems([dataCopy], "patch");
7539
- const update = decomposePatch(dataCopy, this);
7540
- const ops = require_ops.separateFieldOps(update);
7541
- const translatedOps = ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0;
7542
- const translatedUpdate = this._fieldMapper.translatePatchKeys(update, this._meta);
7543
- const translatedFilter = this._fieldMapper.translateFilter(filter, this._meta);
7544
- if (translatedOps === void 0 && require_object.isEmptyObject(translatedUpdate)) return {
7545
- matchedCount: await this.adapter.count({
7971
+ const { update, ops } = this._translatePatch(dataCopy);
7972
+ const translatedFilter = bound?.query.filter ?? this._fieldMapper.translateFilter(filter, this._meta);
7973
+ if (ops === void 0 && require_object.isEmptyObject(update)) {
7974
+ const matchedCount = await this.adapter.count({
7546
7975
  filter: translatedFilter,
7547
7976
  controls: {}
7548
- }),
7549
- modifiedCount: 0
7550
- };
7551
- return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.updateMany(translatedFilter, translatedUpdate, translatedOps, ...updateOpts));
7977
+ });
7978
+ return {
7979
+ matchedCount: bound ? Math.min(matchedCount, bound.writeLimit.limit) : matchedCount,
7980
+ modifiedCount: 0
7981
+ };
7982
+ }
7983
+ return require_nested_writer.enrichFkViolation(this._meta, () => {
7984
+ if (!bound) return this.adapter.updateMany(translatedFilter, update, ops, ...updateOptsArg(keepVersion));
7985
+ return this._runBounded(bound, UPDATE_RESULT, (f, limit) => this.adapter.updateMany(f, update, ops, ...updateOptsArg(keepVersion, limit)));
7986
+ });
7552
7987
  }
7988
+ /**
7989
+ * Replaces the own fields of every row matching `filter` with `data`. The
7990
+ * payload is validated in `replace` mode (since 0.1.162) — every required
7991
+ * field must be present — except the primary key, which the filter
7992
+ * replaces as a row picker.
7993
+ */
7553
7994
  async replaceMany(filter, data) {
7554
7995
  this._ensureBuilt();
7555
7996
  this._guardMutationFilter(filter);
7556
- await this._integrity.validateForeignKeys([data], this._meta, this._fkLookupResolver, this._writeTableResolver);
7557
7997
  const dataCopy = _cloneWritePayload(data);
7998
+ this._rejectNavWrite(dataCopy, "replaceMany");
7558
7999
  this._stampOnUpdate(dataCopy, Date.now(), true);
8000
+ require_nested_writer.validateBatch(this.getValidator("bulkReplace"), [dataCopy], {
8001
+ mode: "replace",
8002
+ navFields: this._meta.navFields,
8003
+ keyless: true
8004
+ });
8005
+ await this._integrity.validateForeignKeys([dataCopy], this._meta, this._fkLookupResolver, this._writeTableResolver);
8006
+ const versionColumn = this.versionColumn;
8007
+ if (versionColumn !== void 0) assertNoVersionWrites(dataCopy, versionColumn);
7559
8008
  await this._encryptItems([dataCopy], "write");
7560
8009
  return require_nested_writer.enrichFkViolation(this._meta, () => this.adapter.replaceMany(this._fieldMapper.translateFilter(filter, this._meta), this._fieldMapper.prepareForWrite(dataCopy, this._meta, this.adapter)));
7561
8010
  }
7562
- async deleteMany(filter) {
8011
+ /**
8012
+ * Deletes every row matching `filter` (application-level cascades run first
8013
+ * where the adapter has no native foreign keys).
8014
+ *
8015
+ * `opts.limit` (since 0.1.162) bounds the delete to the first `limit`
8016
+ * matching rows in `opts.sort` order (default: primary key ascending; the
8017
+ * primary key always breaks ties) — for retention sweeps that keep each
8018
+ * statement small. Loop until `deletedCount === 0`, not until it is below
8019
+ * `limit`: a concurrent writer can make one round delete fewer rows while
8020
+ * matches remain. Not exposed over HTTP.
8021
+ */
8022
+ async deleteMany(filter, opts) {
7563
8023
  this._ensureBuilt();
7564
8024
  this._guardMutationFilter(filter);
8025
+ const bound = this._writeBound(filter, opts);
8026
+ const translate = (f) => this._fieldMapper.translateFilter(f, this._meta);
7565
8027
  if (this._integrity.needsCascade(this._cascadeResolver, this.tableName)) return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.withTransaction(async () => {
7566
- const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, (f) => this._fieldMapper.translateFilter(f, this._meta), this.adapter);
7567
- if (pin) {
7568
- let deletedCount = 0;
7569
- for (const batch of pin) deletedCount += (await this.adapter.deleteMany(batch)).deletedCount;
7570
- return { deletedCount };
7571
- }
7572
- return this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta));
8028
+ if (!bound) return this._cascadeDelete(filter, translate);
8029
+ return this._runBounded(bound, DELETE_RESULT, (f) => this._cascadeDelete(f, (pinned) => pinned), false);
7573
8030
  }));
7574
- return require_nested_writer.remapDeleteFkViolation(this.tableName, () => this.adapter.deleteMany(this._fieldMapper.translateFilter(filter, this._meta)));
8031
+ return require_nested_writer.remapDeleteFkViolation(this.tableName, () => bound ? this._runBounded(bound, DELETE_RESULT, (f, limit) => limit ? this.adapter.deleteMany(f, { limit }) : this.adapter.deleteMany(f)) : this.adapter.deleteMany(translate(filter)));
8032
+ }
8033
+ /**
8034
+ * Runs a bounded write. `native` (default: `supportsWriteLimit()`) hands
8035
+ * `write` the physical filter and the bound; otherwise the primary keys of
8036
+ * the picked rows are read first and `write` gets `filter AND keys` per
8037
+ * key batch (no bound) — a pick that finds nothing writes nothing.
8038
+ *
8039
+ * When the write affected no rows although a row still matches — a
8040
+ * concurrent transaction deleted or changed the picked rows between the
8041
+ * pick and the write — it runs again, at most
8042
+ * {@link BOUNDED_WRITE_RETRIES} more times; an emulated write's next pick
8043
+ * is that check. Rows still matching after the last attempt throw a
8044
+ * retryable `SERIALIZATION_FAILURE`. So `0` means nothing matched (a loop
8045
+ * until `0` cannot stop early); fewer than `limit` stays possible.
8046
+ * A native write whose adapter reports `writeLimitIsAtomic()` is never
8047
+ * re-checked.
8048
+ */
8049
+ async _runBounded(bound, result, write, native = this.adapter.supportsWriteLimit()) {
8050
+ const atomic = native && this.adapter.writeLimitIsAtomic();
8051
+ const filter = bound.query.filter;
8052
+ const pkColumns = this._meta.primaryKeys.map((field) => this._meta.physicalPath(field));
8053
+ for (let attempt = 0;; attempt++) {
8054
+ let out;
8055
+ if (native) out = await write(filter, bound.writeLimit);
8056
+ else {
8057
+ const rows = await this.adapter.findMany(bound.query);
8058
+ if (rows.length === 0) return result.none();
8059
+ out = result.none();
8060
+ for (const keys of pinByPrimaryKey(rows, pkColumns)) out = result.add(out, await write({ $and: [filter, keys] }));
8061
+ }
8062
+ if (atomic || result.affected(out) > 0) return out;
8063
+ const last = attempt === BOUNDED_WRITE_RETRIES;
8064
+ if (native || last) {
8065
+ if (!await this._boundStillMatches(bound)) return out;
8066
+ if (last) throw boundedWriteContention();
8067
+ }
8068
+ }
8069
+ }
8070
+ /** Whether any row still matches a bounded write's filter (a one-row key read). */
8071
+ async _boundStillMatches(bound) {
8072
+ const { $sort: _sort, $nulls: _nulls, ...controls } = bound.query.controls;
8073
+ return (await this.adapter.findMany({
8074
+ ...bound.query,
8075
+ controls: {
8076
+ ...controls,
8077
+ $limit: 1
8078
+ }
8079
+ })).length > 0;
8080
+ }
8081
+ /**
8082
+ * Application-level cascade, then the delete. `translate` maps `filter` to
8083
+ * the adapter's physical filter (identity for an already-physical one).
8084
+ */
8085
+ async _cascadeDelete(filter, translate) {
8086
+ const pin = await this._integrity.cascadeBeforeDelete(filter, this.tableName, this._meta, this._cascadeResolver, translate, this.adapter);
8087
+ if (pin) {
8088
+ let deletedCount = 0;
8089
+ for (const batch of pin) deletedCount += (await this.adapter.deleteMany(batch)).deletedCount;
8090
+ return { deletedCount };
8091
+ }
8092
+ return this.adapter.deleteMany(translate(filter));
8093
+ }
8094
+ /**
8095
+ * Validates `opts` of a bounded `updateMany` / `deleteMany` and returns the
8096
+ * translated read of the rows it picks (filter, order with the primary-key
8097
+ * tie-breaker, `limit`, primary-key projection), or `undefined` when the
8098
+ * write is unbounded.
8099
+ */
8100
+ _writeBound(filter, opts) {
8101
+ if (opts?.limit === void 0) {
8102
+ if (opts?.sort !== void 0) throw require_db_error.invalidQuery("sort", "sort applies to a bounded write only — set limit");
8103
+ return;
8104
+ }
8105
+ const limit = opts.limit;
8106
+ if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1) throw require_db_error.invalidQuery("limit", `limit must be a positive integer, got ${String(limit)}`);
8107
+ const pk = this._meta.primaryKeys;
8108
+ if (pk.length === 0) throw require_db_error.invalidQuery("limit", "a bounded write needs a primary key to order and pin its rows");
8109
+ let sort;
8110
+ if (opts.sort !== void 0 && Object.keys(opts.sort).length > 0) {
8111
+ sort = {};
8112
+ for (const [field, dir] of Object.entries(opts.sort)) {
8113
+ if (dir !== 1 && dir !== -1) throw require_db_error.invalidQuery(`sort.${field}`, "sort direction must be 1 or -1");
8114
+ sort[field] = dir;
8115
+ }
8116
+ guardPaths(this._meta, this.adapter, { controls: { $sort: sort } });
8117
+ } else sort = Object.fromEntries(pk.map((field) => [field, 1]));
8118
+ const query = this._fieldMapper.translateQuery(this._withStableSort({
8119
+ filter,
8120
+ controls: {
8121
+ $sort: sort,
8122
+ $limit: limit,
8123
+ $select: [...pk]
8124
+ }
8125
+ }), this._meta);
8126
+ const writeLimit = {
8127
+ limit,
8128
+ sort: query.controls.$sort
8129
+ };
8130
+ if (query.controls.$nulls) writeLimit.nulls = query.controls.$nulls;
8131
+ return {
8132
+ query,
8133
+ writeLimit
8134
+ };
8135
+ }
8136
+ /** `updateMany` / `replaceMany` write the table's own fields only. */
8137
+ _rejectNavWrite(data, method) {
8138
+ for (const navField of this._meta.navFields) if (data[navField] !== void 0) throw require_db_error.invalidQuery(navField, `${method} writes the table's own fields only — write relations through the keyed methods (updateOne / bulkUpdate / replaceOne / bulkReplace)`);
8139
+ }
8140
+ /**
8141
+ * A logical patch → the adapter's physical `{ update, ops }`: nested
8142
+ * merge-strategy objects decomposed into dot-paths (so nested field ops are
8143
+ * caught), field ops separated, keys translated.
8144
+ */
8145
+ _translatePatch(data) {
8146
+ const decomposed = decomposePatch(data, this);
8147
+ const ops = require_ops.separateFieldOps(decomposed);
8148
+ return {
8149
+ update: this._fieldMapper.translatePatchKeys(decomposed, this._meta),
8150
+ ops: ops ? this._fieldMapper.translateOpsKeys(ops, this._meta) : void 0
8151
+ };
7575
8152
  }
7576
8153
  /**
7577
8154
  * Synchronizes indexes between Atscript definitions and the database.
7578
8155
  */
7579
8156
  async syncIndexes() {
7580
8157
  this._ensureBuilt();
7581
- return this.adapter.syncIndexes();
8158
+ await this.adapter.syncIndexes();
7582
8159
  }
7583
8160
  /**
7584
8161
  * Ensures the table/collection exists in the database.
@@ -7809,10 +8386,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7809
8386
  if (!(keys.length === fk.targetFields.length && fk.targetFields.every((f) => keys.includes(f) && target[f] != null))) continue;
7810
8387
  fk.localFields.forEach((local, i) => {
7811
8388
  const value = target[fk.targetFields[i]];
7812
- if (item[local] !== void 0 && item[local] !== null && !require_nested_writer.sameKey(item[local], value)) throw new require_db_error.DbError("INVALID_QUERY", [{
7813
- path: navField,
7814
- message: `"${navField}" and "${local}" name different parents`
7815
- }]);
8389
+ if (item[local] !== void 0 && item[local] !== null && !require_nested_writer.sameKey(item[local], value)) throw require_db_error.invalidQuery(navField, `"${navField}" and "${local}" name different parents`);
7816
8390
  item[local] = value;
7817
8391
  });
7818
8392
  delete item[navField];
@@ -7823,10 +8397,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7823
8397
  _rejectNestedToInIgnoreMode(items) {
7824
8398
  for (const [navField, relation] of this._meta.relations) {
7825
8399
  if (relation.direction !== "to") continue;
7826
- if (items.some((item) => item[navField] !== void 0 && item[navField] !== null)) throw new require_db_error.DbError("INVALID_QUERY", [{
7827
- path: navField,
7828
- message: "onConflict \"ignore\" cannot create a related parent record — insert it first or reference it by key"
7829
- }]);
8400
+ if (items.some((item) => item[navField] !== void 0 && item[navField] !== null)) throw require_db_error.invalidQuery(navField, "onConflict \"ignore\" cannot create a related parent record — insert it first or reference it by key");
7830
8401
  }
7831
8402
  }
7832
8403
  /**
@@ -7852,10 +8423,7 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7852
8423
  send.push(i);
7853
8424
  }
7854
8425
  const slots = send.length ? await this.adapter.insertManyIgnore(send.map((i) => prepared[i]), opts) : [];
7855
- if (slots.length !== send.length) throw new require_db_error.DbError("INVALID_QUERY", [{
7856
- path: "",
7857
- message: "Adapter insertManyIgnore must return one slot per input row"
7858
- }]);
8426
+ if (slots.length !== send.length) throw require_db_error.invalidQuery("", "Adapter insertManyIgnore must return one slot per input row");
7859
8427
  const insertedIds = [];
7860
8428
  const inserted = [];
7861
8429
  send.forEach((rowIndex, k) => {
@@ -7880,20 +8448,14 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7880
8448
  */
7881
8449
  _insertedPkFilters(items, prepared, insertedIds) {
7882
8450
  const pkFields = this.primaryKeys;
7883
- if (pkFields.length === 0 && items.length > 0) throw new require_db_error.DbError("INVALID_QUERY", [{
7884
- path: "",
7885
- message: "Write check requires a primary key on the table"
7886
- }]);
8451
+ if (pkFields.length === 0 && items.length > 0) throw require_db_error.invalidQuery("", "Write check requires a primary key on the table");
7887
8452
  return items.map((item, i) => {
7888
8453
  const key = {};
7889
8454
  for (const field of pkFields) {
7890
8455
  let value = item[field];
7891
8456
  if (value === void 0) value = prepared[i]?.[this._meta.columnMap.get(field) ?? field];
7892
8457
  if (value === void 0 && pkFields.length === 1) value = insertedIds[i];
7893
- if (value === void 0 || value === null) throw new require_db_error.DbError("INVALID_QUERY", [{
7894
- path: field,
7895
- message: `Write check: cannot resolve the primary key of inserted row [${i}]`
7896
- }]);
8458
+ if (value === void 0 || value === null) throw require_db_error.invalidQuery(field, `Write check: cannot resolve the primary key of inserted row [${i}]`);
7897
8459
  key[field] = value;
7898
8460
  }
7899
8461
  return this._pkFilterFrom(key);
@@ -7962,14 +8524,20 @@ var AtscriptDbTable = class extends AtscriptDbReadable {
7962
8524
  * them once the patch is known to write something.
7963
8525
  */
7964
8526
  _stampOnUpdate(data, now, topLevel) {
8527
+ for (const field of this._meta.onUpdateInc) delete data[field];
7965
8528
  for (const field of this._meta.onUpdateNow) if (!field.includes(".")) if (topLevel) data[field] = now;
7966
8529
  else delete data[field];
7967
8530
  else if (this._nestedDefaultFillable(field)) stampNested(data, field.split("."), 0, now);
7968
8531
  }
7969
- /** Sets the top-level `@db.onUpdate.now` columns of a non-empty patch to `now`. */
7970
- _stampTopLevel(data, now) {
7971
- if (require_object.isEmptyObject(data)) return;
8532
+ /**
8533
+ * Sets the top-level `@db.onUpdate.now` columns of a non-empty patch to
8534
+ * `now` and increments its `@db.onUpdate.inc` counters (`$inc: 1`).
8535
+ * `force`: stamp an empty patch too (a touch writes the row).
8536
+ */
8537
+ _stampTopLevel(data, now, force = false) {
8538
+ if (!force && require_object.isEmptyObject(data)) return;
7972
8539
  for (const field of this._meta.onUpdateNow) if (!field.includes(".")) data[field] = now;
8540
+ for (const field of this._meta.onUpdateInc) data[field] = { $inc: 1 };
7973
8541
  }
7974
8542
  /** {@link _nestedDefaultFillable} per nested default path. */
7975
8543
  _fillableDefaults;
@@ -8852,7 +9420,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
8852
9420
  continue;
8853
9421
  }
8854
9422
  if (source.flattened) fail(fieldName, "source is a flattened object — remove @db.json from the view field");
8855
- const mapping = this._leafMapping(fieldName, ownColumn ?? fieldName, sourceTable, source, joinNullable);
9423
+ const mapping = this._leafMapping(fieldName, ownColumn ?? fieldName, sourceTable, source, joinNullable, fieldType.metadata.has("db.json"));
8856
9424
  mappings.push(agg ? {
8857
9425
  ...mapping,
8858
9426
  ...agg
@@ -8890,7 +9458,7 @@ var AtscriptDbView = class extends AtscriptDbReadable {
8890
9458
  for (const m of computed) visit(m, []);
8891
9459
  }
8892
9460
  /** One view column over one physical source (a column or a JSON leaf). */
8893
- _leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable) {
9461
+ _leafMapping(viewPath, viewColumn, sourceTable, source, joinNullable, viewJson = false) {
8894
9462
  const mapping = {
8895
9463
  viewColumn,
8896
9464
  viewPath,
@@ -8904,7 +9472,10 @@ var AtscriptDbView = class extends AtscriptDbReadable {
8904
9472
  path: source.jsonPath,
8905
9473
  type: source.designType
8906
9474
  };
8907
- }
9475
+ } else if (source.jsonColumn && !viewJson && require_search_fields.isJsonLeafType(source.designType)) mapping.json = {
9476
+ path: [],
9477
+ type: source.designType
9478
+ };
8908
9479
  return mapping;
8909
9480
  }
8910
9481
  };
@@ -8921,62 +9492,6 @@ function isAtscriptDbView(readable) {
8921
9492
  return readable.isView;
8922
9493
  }
8923
9494
  //#endregion
8924
- //#region src/schema/fk-diff.ts
8925
- /** Canonical key for an FK: sorted local field names, comma-joined. */
8926
- function fkKey(fields) {
8927
- return [...fields].toSorted().join(",");
8928
- }
8929
- /**
8930
- * Physical local / target column names of a desired FK (`@db.column` renames
8931
- * applied) — what DDL, constraint sync, the FK diff and the snapshot compare.
8932
- */
8933
- function fkColumns(fk) {
8934
- return {
8935
- fields: fk.physicalFields ?? fk.fields,
8936
- targetFields: fk.physicalTargetFields ?? fk.targetFields
8937
- };
8938
- }
8939
- /**
8940
- * Compares desired FK constraints against stored snapshot to detect
8941
- * additions, removals, and property changes (target table, target fields,
8942
- * onDelete, onUpdate).
8943
- */
8944
- function computeForeignKeyDiff(desired, existingSnapshot) {
8945
- const added = [];
8946
- const removed = [];
8947
- const changed = [];
8948
- const existingByKey = /* @__PURE__ */ new Map();
8949
- for (const fk of existingSnapshot) existingByKey.set(fkKey(fk.fields), fk);
8950
- const desiredKeys = /* @__PURE__ */ new Set();
8951
- for (const fk of desired.values()) {
8952
- const key = fkKey(fkColumns(fk).fields);
8953
- desiredKeys.add(key);
8954
- const existing = existingByKey.get(key);
8955
- if (!existing) added.push(fk);
8956
- else if (fkPropertiesDiffer(fk, existing)) changed.push({
8957
- desired: fk,
8958
- existing
8959
- });
8960
- }
8961
- for (const [key, fk] of existingByKey) if (!desiredKeys.has(key)) removed.push(fk);
8962
- return {
8963
- added,
8964
- removed,
8965
- changed
8966
- };
8967
- }
8968
- /** Whether the FK diff contains any changes. */
8969
- function hasForeignKeyChanges(diff) {
8970
- return diff.added.length > 0 || diff.removed.length > 0 || diff.changed.length > 0;
8971
- }
8972
- function fkPropertiesDiffer(desired, existing) {
8973
- if (desired.targetTable !== existing.targetTable) return true;
8974
- if (fkKey(fkColumns(desired).targetFields) !== fkKey(existing.targetFields)) return true;
8975
- if ((desired.onDelete ?? void 0) !== (existing.onDelete ?? void 0)) return true;
8976
- if ((desired.onUpdate ?? void 0) !== (existing.onUpdate ?? void 0)) return true;
8977
- return false;
8978
- }
8979
- //#endregion
8980
9495
  //#region src/schema/schema-hash.ts
8981
9496
  /**
8982
9497
  * The physical sources a stored view snapshot reads: its entry table and the
@@ -9244,6 +9759,90 @@ function isColumnTypeChanged(existingType, expectedType) {
9244
9759
  return existingType.toUpperCase() !== expectedType.toUpperCase();
9245
9760
  }
9246
9761
  /**
9762
+ * Whether a live column default (as the adapter's introspection normalized
9763
+ * it) and a model default (`serializeDefaultValue`) denote the same value for
9764
+ * `field` — compared in a canonical, type-aware form, because the DDL writes a
9765
+ * literal that differs from the annotation text and the engine reformats it
9766
+ * again: booleans as `1` / `0` (`true` / `1` → `1`), numbers numerically
9767
+ * (`1.0` = `1`, a DECIMAL's `0.00` = `0`), JSON-stored values as parsed,
9768
+ * key-sorted JSON (`{"a": 1}` = `{"a":1}`). With a column type `liveType`
9769
+ * of a fixed scale (`DECIMAL(10,2)`, `NUMERIC(8,3)`) — the type the column
9770
+ * has once synced, i.e. the model's — a model value is also the value the
9771
+ * column stores for it (`2.345` = `2.35`).
9772
+ * Function defaults (`fn:now`, `fn:uuid`, …) and every other value compare as
9773
+ * strings. Exported for adapters that must agree with schema sync.
9774
+ * @since 0.1.162
9775
+ */
9776
+ function sameDefaultValue(field, live, desired, liveType) {
9777
+ if (live === desired) return true;
9778
+ if (live === void 0 || desired === void 0) return false;
9779
+ if (live.startsWith("fn:") || desired.startsWith("fn:")) return false;
9780
+ if (isJsonDefault(field)) {
9781
+ const a = canonicalJson(live);
9782
+ return a !== void 0 && a === canonicalJson(desired);
9783
+ }
9784
+ switch (field.designType) {
9785
+ case "boolean": {
9786
+ const a = canonicalBoolean(live);
9787
+ return a !== void 0 && a === canonicalBoolean(desired);
9788
+ }
9789
+ case "number":
9790
+ case "integer":
9791
+ case "decimal": return sameNumber(live, desired, liveType);
9792
+ default: return false;
9793
+ }
9794
+ }
9795
+ function isJsonDefault(field) {
9796
+ return field.storage === "json" || field.designType === "json" || field.designType === "array" || field.designType === "object";
9797
+ }
9798
+ function canonicalBoolean(value) {
9799
+ const v = value.trim().toLowerCase();
9800
+ if (v === "true" || v === "1") return "1";
9801
+ if (v === "false" || v === "0") return "0";
9802
+ }
9803
+ /** Parsed JSON re-serialized with object keys sorted; `undefined` when not JSON. */
9804
+ function canonicalJson(value) {
9805
+ try {
9806
+ return require_object.stableJson(JSON.parse(value));
9807
+ } catch {
9808
+ return;
9809
+ }
9810
+ }
9811
+ const DECIMAL_RE = /^([+-]?)(\d*)(?:\.(\d*))?$/;
9812
+ const SCALED_TYPE_RE = /^(?:DECIMAL|NUMERIC|DEC|FIXED)\s*\(\s*\d+\s*,\s*(\d+)\s*\)/i;
9813
+ /**
9814
+ * Numeric equality of two default literals — or, on a column of a fixed
9815
+ * scale (`liveType` `DECIMAL(p,s)` / `NUMERIC(p,s)`), of the live value and
9816
+ * the model value rounded half-up to that scale, as the engine stores it.
9817
+ */
9818
+ function sameNumber(live, desired, liveType) {
9819
+ const l = live.trim();
9820
+ const d = desired.trim();
9821
+ if (l === "" || d === "") return false;
9822
+ const a = Number(l);
9823
+ const b = Number(d);
9824
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return false;
9825
+ if (a === b) return true;
9826
+ const scale = liveType ? SCALED_TYPE_RE.exec(liveType.trim())?.[1] : void 0;
9827
+ if (scale === void 0) return false;
9828
+ const rounded = roundDecimal(d, Number(scale));
9829
+ return rounded !== void 0 && Number(rounded) === a;
9830
+ }
9831
+ /** `value` (a plain decimal literal) rounded half-up to `scale` fraction digits, exactly. */
9832
+ function roundDecimal(value, scale) {
9833
+ const m = DECIMAL_RE.exec(value);
9834
+ if (!m) return;
9835
+ const [, sign, int, frac = ""] = m;
9836
+ if (frac.length <= scale) return value;
9837
+ const digits = `${int || "0"}${frac.slice(0, scale)}`;
9838
+ let n = BigInt(digits);
9839
+ if (frac.charCodeAt(scale) >= 53) n += 1n;
9840
+ const str = n.toString().padStart(scale + 1, "0");
9841
+ const head = str.slice(0, str.length - scale);
9842
+ const tail = str.slice(str.length - scale);
9843
+ return `${sign}${head}${scale > 0 ? `.${tail}` : ""}`;
9844
+ }
9845
+ /**
9247
9846
  * Computes the difference between desired schema fields and existing database columns.
9248
9847
  *
9249
9848
  * @param desired - Field descriptors from the Atscript type (after flattening) —
@@ -9253,10 +9852,7 @@ function isColumnTypeChanged(existingType, expectedType) {
9253
9852
  * Receives the full field meta (design type, annotations, PK status, etc.)
9254
9853
  * so adapters can produce context-aware types (e.g., `VARCHAR(255)` from maxLength).
9255
9854
  * Required for type change detection.
9256
- * @param opts.snapshot - The table's stored snapshot (since 0.1.141) — the baseline a
9257
- * derived column's expression is compared with (the engine normalizes
9258
- * the expression it stores, so the live column cannot be). Without
9259
- * one, only a kind or type drift is seen.
9855
+ * @param opts - See {@link TColumnDiffOptions}.
9260
9856
  */
9261
9857
  function computeColumnDiff(desired, existing, typeMapper, opts) {
9262
9858
  const existingByName = new Map(existing.map((c) => [c.name, c]));
@@ -9297,12 +9893,11 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
9297
9893
  });
9298
9894
  renamedOldNames.add(field.renamedFrom);
9299
9895
  } else if (!checkDerived(field, existingCol, field.physicalName)) {
9300
- if (typeMapper) {
9301
- if (isColumnTypeChanged(existingCol.type, typeMapper(field))) typeChanged.push({
9302
- field,
9303
- existingType: existingCol.type
9304
- });
9305
- }
9896
+ const mappedType = typeMapper?.(field);
9897
+ if (mappedType !== void 0 && isColumnTypeChanged(existingCol.type, mappedType)) typeChanged.push({
9898
+ field,
9899
+ existingType: existingCol.type
9900
+ });
9306
9901
  if (!field.isPrimaryKey && !existingCol.pk) {
9307
9902
  const desiredNotNull = !field.optional;
9308
9903
  if (existingCol.notnull !== desiredNotNull) nullableChanged.push({
@@ -9310,12 +9905,13 @@ function computeColumnDiff(desired, existing, typeMapper, opts) {
9310
9905
  wasNullable: !existingCol.notnull
9311
9906
  });
9312
9907
  }
9908
+ const liveDefault = existingCol.dflt_value;
9313
9909
  const desiredDefault = serializeDefaultValue(field.defaultValue);
9314
9910
  const fn = field.defaultValue?.kind === "fn" ? field.defaultValue.fn : void 0;
9315
9911
  const missingNative = fn !== void 0 && fn !== "increment" && opts?.nativeDefaultFns?.has(fn) === true;
9316
- if ((existingCol.dflt_value !== void 0 || missingNative) && existingCol.dflt_value !== desiredDefault) defaultChanged.push({
9912
+ if ((liveDefault !== void 0 || missingNative) && liveDefault !== desiredDefault && !sameDefaultValue(field, liveDefault, desiredDefault, mappedType ?? existingCol.type) && !(liveDefault !== void 0 && desiredDefault !== void 0 && opts?.reportedDefault?.(desiredDefault, field) === liveDefault)) defaultChanged.push({
9317
9913
  field,
9318
- oldDefault: existingCol.dflt_value,
9914
+ oldDefault: liveDefault,
9319
9915
  newDefault: desiredDefault
9320
9916
  });
9321
9917
  }
@@ -9362,6 +9958,12 @@ Object.defineProperty(exports, "ALL_BUCKET_UNITS", {
9362
9958
  return ALL_BUCKET_UNITS;
9363
9959
  }
9364
9960
  });
9961
+ Object.defineProperty(exports, "ALL_ISOLATION_LEVELS", {
9962
+ enumerable: true,
9963
+ get: function() {
9964
+ return ALL_ISOLATION_LEVELS;
9965
+ }
9966
+ });
9365
9967
  Object.defineProperty(exports, "ALL_VIEW_CAPABILITIES", {
9366
9968
  enumerable: true,
9367
9969
  get: function() {
@@ -9554,6 +10156,12 @@ Object.defineProperty(exports, "computeViewSnapshot", {
9554
10156
  return computeViewSnapshot;
9555
10157
  }
9556
10158
  });
10159
+ Object.defineProperty(exports, "createChangeRecorder", {
10160
+ enumerable: true,
10161
+ get: function() {
10162
+ return createChangeRecorder;
10163
+ }
10164
+ });
9557
10165
  Object.defineProperty(exports, "createFailureCollector", {
9558
10166
  enumerable: true,
9559
10167
  get: function() {
@@ -9728,6 +10336,12 @@ Object.defineProperty(exports, "resolveDesignType", {
9728
10336
  return resolveDesignType;
9729
10337
  }
9730
10338
  });
10339
+ Object.defineProperty(exports, "sameDefaultValue", {
10340
+ enumerable: true,
10341
+ get: function() {
10342
+ return sameDefaultValue;
10343
+ }
10344
+ });
9731
10345
  Object.defineProperty(exports, "searchIndexNotFoundMessage", {
9732
10346
  enumerable: true,
9733
10347
  get: function() {