@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.
- package/dist/agg.cjs +1 -1
- package/dist/agg.d.cts +1 -1
- package/dist/agg.d.mts +1 -1
- package/dist/agg.mjs +1 -1
- package/dist/{aggregate-fns-B_sgl4mB.mjs → aggregate-fns-DsS9UQVB.mjs} +1 -1
- package/dist/{aggregate-fns-DFtKAv7Q.cjs → aggregate-fns-DupYzFD7.cjs} +1 -1
- package/dist/{buckets-8mG67Mtw.d.mts → buckets-DUTcrT4Z.d.mts} +475 -57
- package/dist/{buckets-B4T_alUe.d.cts → buckets-_iZV39Z3.d.cts} +475 -57
- package/dist/{column-diff-DpeUbhEI.cjs → column-diff-CZsMGQ_w.cjs} +846 -232
- package/dist/{column-diff-DR1glykD.mjs → column-diff-pdn53AIj.mjs} +829 -233
- package/dist/{validator-B3xlbrWf.d.cts → db-error-B_DRL1E9.d.cts} +18 -89
- package/dist/{validator-B3xlbrWf.d.mts → db-error-B_DRL1E9.d.mts} +18 -89
- package/dist/{db-error-JcfJqqpk.cjs → db-error-DmF1_xrz.cjs} +13 -0
- package/dist/{db-error-eSsQX7rt.mjs → db-error-hraC12B9.mjs} +8 -1
- package/dist/{db-validator-plugin-BWy60OvG.d.cts → db-validator-plugin-wRqMYNBI.d.cts} +6 -0
- package/dist/{db-validator-plugin-BWy60OvG.d.mts → db-validator-plugin-wRqMYNBI.d.mts} +6 -0
- package/dist/{fk-diff-5gX42hBt.d.mts → fk-diff-B5jW68T3.d.cts} +34 -10
- package/dist/{fk-diff-BWdQ7kRZ.d.cts → fk-diff-PhVBYohc.d.mts} +34 -10
- package/dist/index.cjs +12 -11
- package/dist/index.d.cts +24 -21
- package/dist/index.d.mts +24 -21
- package/dist/index.mjs +9 -12
- package/dist/{nested-writer-BOj_c_lI.mjs → nested-writer-ClUX0DB0.mjs} +16 -22
- package/dist/{nested-writer-Bl1SZS7x.cjs → nested-writer-yCy6wGuN.cjs} +16 -22
- package/dist/{object-CtPYTIvy.d.cts → object-Czia_qD4.d.cts} +7 -1
- package/dist/{object-CtPYTIvy.d.mts → object-Czia_qD4.d.mts} +7 -1
- package/dist/{object-Djg28csK.cjs → object-GjsuPNqo.cjs} +19 -0
- package/dist/{object-TkiJQ-Dp.mjs → object-PQ1J8jri.mjs} +14 -1
- package/dist/ops.cjs +6 -21
- package/dist/ops.mjs +6 -21
- package/dist/plugin.cjs +263 -323
- package/dist/plugin.mjs +163 -223
- package/dist/rel.cjs +2 -2
- package/dist/rel.d.cts +3 -3
- package/dist/rel.d.mts +3 -3
- package/dist/rel.mjs +2 -2
- package/dist/{relation-helpers-17_2f5hX.d.mts → relation-helpers-BAEkP4bH.d.cts} +1 -1
- package/dist/{relation-helpers-BLBrLQYi.d.cts → relation-helpers-BSYwxOSA.d.mts} +1 -1
- package/dist/{relation-loader-DeulLrAw.cjs → relation-loader-DujL_Mh9.cjs} +1 -1
- package/dist/{relation-loader-CNBMPfRr.mjs → relation-loader-Qv3LrQ42.mjs} +1 -1
- package/dist/{search-fields-CE8cmj9X.mjs → search-fields-0U0FAFGj.mjs} +47 -1
- package/dist/{search-fields-CT_UZ9fg.cjs → search-fields-Bi1n-pB1.cjs} +64 -0
- package/dist/search-fields-CHqqcC3W.d.cts +30 -0
- package/dist/search-fields-CHqqcC3W.d.mts +30 -0
- package/dist/shared.cjs +42 -26
- package/dist/shared.d.cts +98 -4
- package/dist/shared.d.mts +98 -4
- package/dist/shared.mjs +4 -2
- package/dist/sync.cjs +228 -65
- package/dist/sync.d.cts +41 -5
- package/dist/sync.d.mts +41 -5
- package/dist/sync.mjs +228 -66
- package/dist/validator-BcBtg8yW.d.cts +88 -0
- package/dist/validator-BcBtg8yW.d.mts +88 -0
- package/dist/{validator-B4YDLUkj.cjs → validator-DK9Ijpo7.cjs} +3 -2
- package/dist/{validator-B3fPJjS1.mjs → validator-DuDWsIdQ.mjs} +3 -2
- package/dist/validator.cjs +3 -3
- package/dist/validator.d.cts +4 -3
- package/dist/validator.d.mts +4 -3
- package/dist/validator.mjs +3 -3
- package/dist/{validation-utils-CtLNlBtB.cjs → view-validation-D50KWU7O.cjs} +452 -18
- package/dist/{validation-utils-BGjqc2jU.mjs → view-validation-QbqqXEAM.mjs} +364 -20
- package/package.json +6 -6
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const require_db_error = require("./db-error-
|
|
2
|
-
const require_aggregate_fns = require("./aggregate-fns-
|
|
3
|
-
const require_nested_writer = require("./nested-writer-
|
|
4
|
-
const require_validator = require("./validator-
|
|
5
|
-
const require_search_fields = require("./search-fields-
|
|
6
|
-
const require_object = require("./object-
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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, {
|
|
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
|
-
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
6019
|
-
*
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
6037
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
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
|
|
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
|
|
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.
|
|
7233
|
-
|
|
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)
|
|
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
|
|
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
|
|
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
|
|
7448
|
-
for (const [prop, value] of Object.entries(key)) if (value !== void 0 && prop !== versionField && !(prop in pair)) throw
|
|
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
|
|
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 },
|
|
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
|
-
|
|
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
|
-
|
|
7953
|
+
const bound = this._writeBound(filter, opts);
|
|
7526
7954
|
const dataCopy = _cloneWritePayload(data);
|
|
7527
|
-
|
|
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
|
|
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 =
|
|
7540
|
-
const
|
|
7541
|
-
|
|
7542
|
-
|
|
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
|
-
|
|
7550
|
-
|
|
7551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7567
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
7970
|
-
|
|
7971
|
-
|
|
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
|
|
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
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
9303
|
-
|
|
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 ((
|
|
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:
|
|
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() {
|