@atscript/db-mysql 0.1.151 → 0.1.153
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/index.cjs +174 -51
- package/dist/index.d.cts +85 -26
- package/dist/index.d.mts +85 -26
- package/dist/index.mjs +175 -52
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -153,6 +153,9 @@ const mysqlDialect = {
|
|
|
153
153
|
return quoteTableName(name);
|
|
154
154
|
},
|
|
155
155
|
unlimitedLimit: "18446744073709551615",
|
|
156
|
+
derivedMergeHint(alias) {
|
|
157
|
+
return `/*+ MERGE(${qi(alias)}) */`;
|
|
158
|
+
},
|
|
156
159
|
toValue: _atscript_db_sql_tools.toSqlValue,
|
|
157
160
|
toParam(value) {
|
|
158
161
|
if (value === void 0) return null;
|
|
@@ -618,9 +621,21 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
618
621
|
get _schema() {
|
|
619
622
|
return this._table?.schema ?? null;
|
|
620
623
|
}
|
|
621
|
-
constructor(driver) {
|
|
624
|
+
constructor(driver, options) {
|
|
622
625
|
super();
|
|
623
626
|
this.driver = driver;
|
|
627
|
+
this.viewJoinPruning = options?.viewJoinPruning !== false;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* The FROM source of a read of this adapter's table or view: a managed
|
|
631
|
+
* view read that needs only some of its LEFT joins reads an inline,
|
|
632
|
+
* pruned definition (`viewReadSource`, since 0.1.153) — MySQL has no
|
|
633
|
+
* outer-join elimination, so every LEFT JOIN of a view is otherwise probed
|
|
634
|
+
* per row, even for a `COUNT(*)`. Off with `viewJoinPruning: false`.
|
|
635
|
+
*/
|
|
636
|
+
_readSource(query, kind, partitionBy) {
|
|
637
|
+
const name = this.resolveTableName();
|
|
638
|
+
return this.viewJoinPruning ? (0, _atscript_db_sql_tools.viewReadSource)(mysqlDialect, this._table, name, query, kind, partitionBy) : name;
|
|
624
639
|
}
|
|
625
640
|
/** Every adapter over this pool shares one transaction (since 0.1.128). */
|
|
626
641
|
_transactionOwner() {
|
|
@@ -722,6 +737,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
722
737
|
supportsAggregateExpressions() {
|
|
723
738
|
return true;
|
|
724
739
|
}
|
|
740
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
741
|
+
supportsNullsPlacement() {
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
725
744
|
/** Computed view columns and first-row joins. */
|
|
726
745
|
viewCapabilities() {
|
|
727
746
|
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
@@ -850,7 +869,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
850
869
|
return this._mapConstraintError(error);
|
|
851
870
|
}
|
|
852
871
|
}
|
|
853
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
872
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock wait timeout, else as is. */
|
|
854
873
|
_mapConstraintError(error) {
|
|
855
874
|
if (error && typeof error === "object" && "errno" in error) {
|
|
856
875
|
const err = error;
|
|
@@ -859,6 +878,10 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
859
878
|
message: err.sqlMessage ?? err.message
|
|
860
879
|
}]);
|
|
861
880
|
if (err.errno === 1451 || err.errno === 1452) throw new _atscript_db.DbError("FK_VIOLATION", this._mapFkError(err.message));
|
|
881
|
+
if (err.errno === 1213 || err.errno === 1205) throw new _atscript_db.DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
|
|
882
|
+
path: "",
|
|
883
|
+
message: err.sqlMessage ?? err.message
|
|
884
|
+
}]);
|
|
862
885
|
}
|
|
863
886
|
throw error;
|
|
864
887
|
}
|
|
@@ -1059,27 +1082,49 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1059
1082
|
return true;
|
|
1060
1083
|
}
|
|
1061
1084
|
/**
|
|
1062
|
-
*
|
|
1063
|
-
* statement). Only when it hits a
|
|
1064
|
-
* SELECT of the chunk's primary /
|
|
1065
|
-
*
|
|
1066
|
-
* INSERT — a dense-duplicate chunk is
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1075
|
-
*
|
|
1085
|
+
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
|
1086
|
+
* INSERT (an all-new batch costs a single statement). Only when it hits a
|
|
1087
|
+
* duplicate key (errno 1062 / 1586) does ONE SELECT of the chunk's primary /
|
|
1088
|
+
* unique key tuples find the stored rows (skipped as conflicts) and the
|
|
1089
|
+
* survivors go in as one more multi-row INSERT — a dense-duplicate chunk is
|
|
1090
|
+
* three statements, never O(rows).
|
|
1091
|
+
*
|
|
1092
|
+
* Inside a CALLER's transaction (`opts.inCallerTransaction`, else
|
|
1093
|
+
* `isInTransaction()` on entry) the key SELECT runs FIRST and only the
|
|
1094
|
+
* missing rows are inserted, in primary-key order: a failed duplicate INSERT
|
|
1095
|
+
* leaves InnoDB's shared lock on the duplicate record until COMMIT, and a
|
|
1096
|
+
* later UPDATE of that row in two such transactions would deadlock (S→X).
|
|
1097
|
+
* The SELECT is a non-locking consistent read, so the common case takes no
|
|
1098
|
+
* lock on a stored row at all. `opts.lockConflicts` then locks the stored
|
|
1099
|
+
* conflicting rows exclusively — `SELECT … WHERE pk IN (found) ORDER BY pk
|
|
1100
|
+
* FOR UPDATE` (by the unique key a row collided on when that was not the
|
|
1101
|
+
* primary key), existing keys only, so record locks and no gap locks —
|
|
1102
|
+
* before inserting the rest.
|
|
1103
|
+
*
|
|
1104
|
+
* Either way, only if an INSERT still collides (a concurrent writer raced in
|
|
1105
|
+
* — committed after the transaction's snapshot or still uncommitted — or a
|
|
1106
|
+
* collation-equal value the exact-match pre-check missed) are the rows
|
|
1107
|
+
* bisected in input order: each half is retried, recursively, and a single
|
|
1108
|
+
* row that still collides is skipped (holding the failed INSERT's shared
|
|
1109
|
+
* lock). A failed statement is rolled back by InnoDB alone, so the
|
|
1110
|
+
* transaction stays usable. A chunk mixing explicit and generated
|
|
1111
|
+
* auto-increment ids is processed as one such sequence per consecutive run
|
|
1112
|
+
* of a kind. Deliberately NOT `INSERT IGNORE` (it would downgrade NOT NULL /
|
|
1113
|
+
* FK / truncation errors to warnings) and not `ON DUPLICATE KEY UPDATE` (a
|
|
1114
|
+
* no-op update is indistinguishable from an insert in the affected-rows
|
|
1115
|
+
* count).
|
|
1076
1116
|
*/
|
|
1077
|
-
async insertManyIgnore(data) {
|
|
1117
|
+
async insertManyIgnore(data, opts) {
|
|
1078
1118
|
if (data.length === 0) return [];
|
|
1079
1119
|
await this._warnNonStrictMode();
|
|
1080
1120
|
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1121
|
+
const inTx = this._txConnection() !== void 0;
|
|
1122
|
+
const mode = {
|
|
1123
|
+
precheck: opts?.inCallerTransaction ?? inTx,
|
|
1124
|
+
lock: opts?.lockConflicts === true
|
|
1125
|
+
};
|
|
1126
|
+
const single = data.length === 1 && !inTx ? this._uniformIdGroup(data) : void 0;
|
|
1127
|
+
if (single) return this._insertIgnoringGroup(this.resolveTableName(), columns, single, this._incrementStep(), mode);
|
|
1083
1128
|
return this.withTransaction(async () => {
|
|
1084
1129
|
const tableName = this.resolveTableName();
|
|
1085
1130
|
const slots = [];
|
|
@@ -1087,7 +1132,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1087
1132
|
const zero = this._zeroIsExplicit();
|
|
1088
1133
|
for (const batch of batches) {
|
|
1089
1134
|
const out = Array.from({ length: batch.length }, () => null);
|
|
1090
|
-
for (const group of await this._idGroups(batch, zero)) (await this._insertIgnoringGroup(tableName, columns, group, step)).forEach((slot, k) => {
|
|
1135
|
+
for (const group of await this._idGroups(batch, zero)) (await this._insertIgnoringGroup(tableName, columns, group, step, mode)).forEach((slot, k) => {
|
|
1091
1136
|
out[group.at[k]] = slot;
|
|
1092
1137
|
});
|
|
1093
1138
|
slots.push(...out);
|
|
@@ -1097,20 +1142,21 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1097
1142
|
}
|
|
1098
1143
|
/**
|
|
1099
1144
|
* Indices of `rows` whose primary / unique-index key tuple is already stored
|
|
1100
|
-
* (a row with a null / missing key component never collides)
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1103
|
-
*
|
|
1145
|
+
* (a row with a null / missing key component never collides), each mapped
|
|
1146
|
+
* to the index (in `uniqueKeySets` order — the primary key first) of the
|
|
1147
|
+
* first key set it collides on. One non-locking SELECT covers every key set;
|
|
1148
|
+
* it is split only to stay under the parameter limit. Skipped entirely when
|
|
1149
|
+
* no row carries a key value (generated PK, no unique index values).
|
|
1104
1150
|
*/
|
|
1105
1151
|
async _findStoredKeyConflicts(tableName, rows) {
|
|
1106
1152
|
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1107
1153
|
const rowTuples = rows.map((row) => keySets.map((fields) => (0, _atscript_db.uniqueKeyTuple)(row, fields)));
|
|
1108
1154
|
const used = keySets.map((_, k) => rowTuples.some((t) => t[k] !== void 0));
|
|
1109
|
-
if (!used.includes(true)) return /* @__PURE__ */ new
|
|
1155
|
+
if (!used.includes(true)) return /* @__PURE__ */ new Map();
|
|
1110
1156
|
const width = keySets.reduce((n, f, k) => n + (used[k] ? f.length : 0), 0);
|
|
1111
1157
|
const sliceSize = Math.max(1, Math.floor(6e4 / width));
|
|
1112
1158
|
const selectCols = [...new Set(keySets.flat())].map((c) => qi(c)).join(", ");
|
|
1113
|
-
const
|
|
1159
|
+
const storedTuples = keySets.map(() => /* @__PURE__ */ new Set());
|
|
1114
1160
|
for (let offset = 0; offset < rows.length; offset += sliceSize) {
|
|
1115
1161
|
const end = Math.min(rows.length, offset + sliceSize);
|
|
1116
1162
|
const clauses = [];
|
|
@@ -1136,30 +1182,103 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1136
1182
|
const found = await this._exec().all(sql, params);
|
|
1137
1183
|
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1138
1184
|
const tuple = (0, _atscript_db.uniqueKeyTuple)(doc, fields);
|
|
1139
|
-
if (tuple !== void 0)
|
|
1185
|
+
if (tuple !== void 0) storedTuples[k].add(tuple);
|
|
1140
1186
|
});
|
|
1141
1187
|
}
|
|
1142
|
-
const skipped = /* @__PURE__ */ new
|
|
1188
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1143
1189
|
rowTuples.forEach((tuples, i) => {
|
|
1144
|
-
|
|
1190
|
+
const k = tuples.findIndex((t, k) => t !== void 0 && storedTuples[k].has(t));
|
|
1191
|
+
if (k >= 0) skipped.set(i, k);
|
|
1145
1192
|
});
|
|
1146
1193
|
return skipped;
|
|
1147
1194
|
}
|
|
1148
|
-
/**
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1195
|
+
/**
|
|
1196
|
+
* Exclusive locks on the stored rows the `skipped` input rows collide with
|
|
1197
|
+
* (found by {@link _findStoredKeyConflicts}, so every key exists): `SELECT …
|
|
1198
|
+
* FOR UPDATE` by the key set each row collided on — the primary key when it
|
|
1199
|
+
* did — ascending, one statement per key set. The keys are the INPUT rows'
|
|
1200
|
+
* own (write-form) values, which the pre-check matched exactly; a full
|
|
1201
|
+
* unique-key equality on existing records takes record locks only, never a
|
|
1202
|
+
* gap lock.
|
|
1203
|
+
*/
|
|
1204
|
+
async _lockStoredRows(tableName, rows, skipped) {
|
|
1205
|
+
if (skipped.size === 0) return;
|
|
1206
|
+
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1207
|
+
const byKeySet = /* @__PURE__ */ new Map();
|
|
1208
|
+
for (const [i, k] of skipped) {
|
|
1209
|
+
let keyed = byKeySet.get(k);
|
|
1210
|
+
if (!keyed) byKeySet.set(k, keyed = /* @__PURE__ */ new Map());
|
|
1211
|
+
keyed.set((0, _atscript_db.uniqueKeyTuple)(rows[i], keySets[k]), rows[i]);
|
|
1212
|
+
}
|
|
1213
|
+
for (const [k, keyed] of byKeySet) {
|
|
1214
|
+
const fields = keySets[k];
|
|
1215
|
+
const keyRows = [...keyed.values()];
|
|
1216
|
+
const sliceSize = Math.max(1, Math.floor(6e4 / fields.length));
|
|
1217
|
+
const cols = fields.map((f) => qi(f)).join(", ");
|
|
1218
|
+
const tuple = fields.length === 1 ? "?" : `(${fields.map(() => "?").join(", ")})`;
|
|
1219
|
+
for (let offset = 0; offset < keyRows.length; offset += sliceSize) {
|
|
1220
|
+
const slice = keyRows.slice(offset, offset + sliceSize);
|
|
1221
|
+
const params = [];
|
|
1222
|
+
for (const row of slice) params.push(...fields.map((f) => mysqlDialect.toValue(row[f])));
|
|
1223
|
+
const target = fields.length === 1 ? cols : `(${cols})`;
|
|
1224
|
+
const sql = `SELECT ${cols} FROM ${quoteTableName(tableName)} WHERE ${target} IN (${slice.map(() => tuple).join(", ")}) ORDER BY ${cols} FOR UPDATE`;
|
|
1225
|
+
this._log(sql, params);
|
|
1226
|
+
await this._wrapConstraintError(() => this._exec().all(sql, params));
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
/** Input positions of `rows` in ascending primary-key order (input order without a complete key). */
|
|
1231
|
+
_pkOrder(rows, at) {
|
|
1232
|
+
const pkCols = this._table.primaryKeys.map((k) => this._table.physicalPath(k));
|
|
1233
|
+
if (pkCols.length === 0 || at.length < 2) return at;
|
|
1234
|
+
if (at.some((i) => pkCols.some((c) => rows[i][c] === void 0 || rows[i][c] === null))) return at;
|
|
1235
|
+
return at.toSorted((a, b) => {
|
|
1236
|
+
for (const c of pkCols) {
|
|
1237
|
+
const x = rows[a][c];
|
|
1238
|
+
const y = rows[b][c];
|
|
1239
|
+
if (x === y) continue;
|
|
1240
|
+
return x < y ? -1 : 1;
|
|
1241
|
+
}
|
|
1242
|
+
return a - b;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* One {@link _idGroups} group. Standalone: optimistic INSERT, then pre-check
|
|
1247
|
+
* + survivor INSERT (+ bisect on a race). `mode.precheck`: pre-check (and
|
|
1248
|
+
* `mode.lock`: lock the stored conflicts) FIRST, then insert the survivors in
|
|
1249
|
+
* primary-key order.
|
|
1250
|
+
*/
|
|
1251
|
+
async _insertIgnoringGroup(tableName, columns, group, step, mode) {
|
|
1252
|
+
if (!mode.precheck && !mode.lock) {
|
|
1253
|
+
const direct = await this._tryInsertGroup(tableName, columns, group, step);
|
|
1254
|
+
if (direct) return direct;
|
|
1255
|
+
if (group.rows.length === 1) return [null];
|
|
1256
|
+
}
|
|
1153
1257
|
const skipped = await this._findStoredKeyConflicts(tableName, group.rows);
|
|
1154
|
-
if (
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1258
|
+
if (mode.lock) await this._lockStoredRows(tableName, group.rows, skipped);
|
|
1259
|
+
if (skipped.size === 0 && !mode.precheck && !mode.lock) return this._bisectGroup(tableName, columns, group, step);
|
|
1260
|
+
const keep = group.rows.map((_, i) => i).filter((i) => !skipped.has(i));
|
|
1261
|
+
const ordered = mode.precheck && !group.generated ? this._pkOrder(group.rows, keep) : keep;
|
|
1262
|
+
const inserted = /* @__PURE__ */ new Map();
|
|
1263
|
+
if (ordered.length > 0) {
|
|
1264
|
+
const sorted = {
|
|
1265
|
+
rows: ordered.map((i) => group.rows[i]),
|
|
1266
|
+
at: [],
|
|
1267
|
+
generated: group.generated
|
|
1268
|
+
};
|
|
1269
|
+
let slots = await this._tryInsertGroup(tableName, columns, sorted, step);
|
|
1270
|
+
if (slots) slots.forEach((slot, k) => inserted.set(ordered[k], slot));
|
|
1271
|
+
else {
|
|
1272
|
+
const survivors = {
|
|
1273
|
+
rows: keep.map((i) => group.rows[i]),
|
|
1274
|
+
at: [],
|
|
1275
|
+
generated: group.generated
|
|
1276
|
+
};
|
|
1277
|
+
slots = keep.length === 1 ? [null] : await this._bisectGroup(tableName, columns, survivors, step);
|
|
1278
|
+
slots.forEach((slot, k) => inserted.set(keep[k], slot));
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return group.rows.map((_, i) => skipped.has(i) ? null : inserted.get(i));
|
|
1163
1282
|
}
|
|
1164
1283
|
/** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
|
|
1165
1284
|
async _bisectGroup(tableName, columns, group, step) {
|
|
@@ -1194,13 +1313,13 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1194
1313
|
...query.controls,
|
|
1195
1314
|
$limit: 1
|
|
1196
1315
|
};
|
|
1197
|
-
const { sql, params } = buildSelect(this.
|
|
1316
|
+
const { sql, params } = buildSelect(this._readSource(query, "rows"), where, controls);
|
|
1198
1317
|
this._log(sql, params);
|
|
1199
1318
|
return this._exec().get(sql, params);
|
|
1200
1319
|
}
|
|
1201
1320
|
async findMany(query) {
|
|
1202
1321
|
const where = buildWhere(query.filter);
|
|
1203
|
-
const { sql, params } = buildSelect(this.
|
|
1322
|
+
const { sql, params } = buildSelect(this._readSource(query, "rows"), where, query.controls);
|
|
1204
1323
|
this._log(sql, params);
|
|
1205
1324
|
return this._exec().all(sql, params);
|
|
1206
1325
|
}
|
|
@@ -1210,20 +1329,21 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1210
1329
|
*/
|
|
1211
1330
|
async findManyPerPartition(query, partitionBy) {
|
|
1212
1331
|
const where = buildWhere(query.filter);
|
|
1213
|
-
const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(mysqlDialect, this.
|
|
1332
|
+
const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(mysqlDialect, this._readSource(query, "rows", partitionBy), where, query.controls, partitionBy);
|
|
1214
1333
|
this._log(sql, params);
|
|
1215
1334
|
return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._exec().all(sql, params));
|
|
1216
1335
|
}
|
|
1217
1336
|
async count(query) {
|
|
1218
1337
|
const where = buildWhere(query.filter);
|
|
1219
|
-
const
|
|
1338
|
+
const source = this._readSource(query, "count");
|
|
1339
|
+
const sql = `SELECT ${(0, _atscript_db_sql_tools.fromSourceHint)(source)}COUNT(*) as cnt FROM ${(0, _atscript_db_sql_tools.fromSourceSql)(mysqlDialect, source)} WHERE ${where.sql}`;
|
|
1220
1340
|
this._log(sql, where.params);
|
|
1221
1341
|
return (await this._exec().get(sql, where.params))?.cnt ?? 0;
|
|
1222
1342
|
}
|
|
1223
1343
|
async aggregate(query) {
|
|
1224
1344
|
const search = (0, _atscript_db_agg.resolveAggregateSearch)(query.controls);
|
|
1225
1345
|
const where = search ? this._buildSearchWhere(search.text, query, search.indexName) : buildWhere(query.filter);
|
|
1226
|
-
const tableName = this.resolveTableName();
|
|
1346
|
+
const tableName = search ? this.resolveTableName() : this._readSource(query, "aggregate");
|
|
1227
1347
|
for (const bucket of query.controls.$select?.buckets ?? []) await this._ensureBucketZone(bucket.tz);
|
|
1228
1348
|
if (query.controls.$count) {
|
|
1229
1349
|
const { sql, params } = buildAggregateCount(tableName, where, query.controls);
|
|
@@ -2269,16 +2389,19 @@ var Mysql2Driver = class {
|
|
|
2269
2389
|
*
|
|
2270
2390
|
* @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
|
|
2271
2391
|
* @param options - Additional pool options passed to mysql2. `strictMode: false`
|
|
2272
|
-
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver})
|
|
2392
|
+
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver});
|
|
2393
|
+
* `viewJoinPruning: false` (not a pool option, since 0.1.153) reads managed views by name
|
|
2394
|
+
* ({@link TMysqlAdapterOptions}).
|
|
2273
2395
|
* @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
|
|
2274
2396
|
*/
|
|
2275
2397
|
function createAdapter(uri, options) {
|
|
2276
|
-
const { strictMode, ...pool } = options ?? {};
|
|
2398
|
+
const { strictMode, viewJoinPruning, ...pool } = options ?? {};
|
|
2277
2399
|
const driver = new Mysql2Driver({
|
|
2278
2400
|
uri,
|
|
2279
2401
|
...pool
|
|
2280
2402
|
}, { strictMode });
|
|
2281
|
-
|
|
2403
|
+
const adapterOptions = { viewJoinPruning };
|
|
2404
|
+
return new _atscript_db.DbSpace(() => new MysqlAdapter(driver, adapterOptions), { onClose: () => driver.close() });
|
|
2282
2405
|
}
|
|
2283
2406
|
//#endregion
|
|
2284
2407
|
exports.Mysql2Driver = Mysql2Driver;
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
3
3
|
import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
|
|
4
4
|
import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
|
|
@@ -73,6 +73,18 @@ interface TMysqlConnection {
|
|
|
73
73
|
/** Release this connection back to the pool. */
|
|
74
74
|
release(): void;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `MysqlAdapter` options.
|
|
78
|
+
* @since 0.1.153
|
|
79
|
+
*/
|
|
80
|
+
interface TMysqlAdapterOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Read a managed view through an inline definition without the LEFT joins
|
|
83
|
+
* the query does not need (default `true`) — see the views guide,
|
|
84
|
+
* "Performance". `false` always reads the stored view by name.
|
|
85
|
+
*/
|
|
86
|
+
viewJoinPruning?: boolean;
|
|
87
|
+
}
|
|
76
88
|
//#endregion
|
|
77
89
|
//#region src/mysql-adapter.d.ts
|
|
78
90
|
/**
|
|
@@ -117,7 +129,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
117
129
|
* (the name-taking schema-sync primitives run on such an adapter).
|
|
118
130
|
*/
|
|
119
131
|
private get _schema();
|
|
120
|
-
constructor(driver: TMysqlDriver);
|
|
132
|
+
constructor(driver: TMysqlDriver, options?: TMysqlAdapterOptions);
|
|
133
|
+
/**
|
|
134
|
+
* The FROM source of a read of this adapter's table or view: a managed
|
|
135
|
+
* view read that needs only some of its LEFT joins reads an inline,
|
|
136
|
+
* pruned definition (`viewReadSource`, since 0.1.153) — MySQL has no
|
|
137
|
+
* outer-join elimination, so every LEFT JOIN of a view is otherwise probed
|
|
138
|
+
* per row, even for a `COUNT(*)`. Off with `viewJoinPruning: false`.
|
|
139
|
+
*/
|
|
140
|
+
private _readSource;
|
|
121
141
|
/** Every adapter over this pool shares one transaction (since 0.1.128). */
|
|
122
142
|
protected _transactionOwner(): unknown;
|
|
123
143
|
/** The dedicated connection of this pool's open transaction, if any. */
|
|
@@ -162,6 +182,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
162
182
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
163
183
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
164
184
|
supportsAggregateExpressions(): boolean;
|
|
185
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
186
|
+
supportsNullsPlacement(): boolean;
|
|
165
187
|
/** Computed view columns and first-row joins. */
|
|
166
188
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
167
189
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -188,7 +210,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
188
210
|
* - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
|
|
189
211
|
*/
|
|
190
212
|
private _wrapConstraintError;
|
|
191
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
213
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock wait timeout, else as is. */
|
|
192
214
|
private _mapConstraintError;
|
|
193
215
|
private _mapFkError;
|
|
194
216
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
@@ -246,31 +268,66 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
246
268
|
private _groupInsertedIds;
|
|
247
269
|
supportsInsertIgnore(): boolean;
|
|
248
270
|
/**
|
|
249
|
-
*
|
|
250
|
-
* statement). Only when it hits a
|
|
251
|
-
* SELECT of the chunk's primary /
|
|
252
|
-
*
|
|
253
|
-
* INSERT — a dense-duplicate chunk is
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
|
272
|
+
* INSERT (an all-new batch costs a single statement). Only when it hits a
|
|
273
|
+
* duplicate key (errno 1062 / 1586) does ONE SELECT of the chunk's primary /
|
|
274
|
+
* unique key tuples find the stored rows (skipped as conflicts) and the
|
|
275
|
+
* survivors go in as one more multi-row INSERT — a dense-duplicate chunk is
|
|
276
|
+
* three statements, never O(rows).
|
|
277
|
+
*
|
|
278
|
+
* Inside a CALLER's transaction (`opts.inCallerTransaction`, else
|
|
279
|
+
* `isInTransaction()` on entry) the key SELECT runs FIRST and only the
|
|
280
|
+
* missing rows are inserted, in primary-key order: a failed duplicate INSERT
|
|
281
|
+
* leaves InnoDB's shared lock on the duplicate record until COMMIT, and a
|
|
282
|
+
* later UPDATE of that row in two such transactions would deadlock (S→X).
|
|
283
|
+
* The SELECT is a non-locking consistent read, so the common case takes no
|
|
284
|
+
* lock on a stored row at all. `opts.lockConflicts` then locks the stored
|
|
285
|
+
* conflicting rows exclusively — `SELECT … WHERE pk IN (found) ORDER BY pk
|
|
286
|
+
* FOR UPDATE` (by the unique key a row collided on when that was not the
|
|
287
|
+
* primary key), existing keys only, so record locks and no gap locks —
|
|
288
|
+
* before inserting the rest.
|
|
289
|
+
*
|
|
290
|
+
* Either way, only if an INSERT still collides (a concurrent writer raced in
|
|
291
|
+
* — committed after the transaction's snapshot or still uncommitted — or a
|
|
292
|
+
* collation-equal value the exact-match pre-check missed) are the rows
|
|
293
|
+
* bisected in input order: each half is retried, recursively, and a single
|
|
294
|
+
* row that still collides is skipped (holding the failed INSERT's shared
|
|
295
|
+
* lock). A failed statement is rolled back by InnoDB alone, so the
|
|
296
|
+
* transaction stays usable. A chunk mixing explicit and generated
|
|
297
|
+
* auto-increment ids is processed as one such sequence per consecutive run
|
|
298
|
+
* of a kind. Deliberately NOT `INSERT IGNORE` (it would downgrade NOT NULL /
|
|
299
|
+
* FK / truncation errors to warnings) and not `ON DUPLICATE KEY UPDATE` (a
|
|
300
|
+
* no-op update is indistinguishable from an insert in the affected-rows
|
|
301
|
+
* count).
|
|
302
|
+
*/
|
|
303
|
+
insertManyIgnore(data: Array<Record<string, unknown>>, opts?: TDbInsertIgnoreOptions): Promise<TDbInsertIgnoreSlot[]>;
|
|
265
304
|
/**
|
|
266
305
|
* Indices of `rows` whose primary / unique-index key tuple is already stored
|
|
267
|
-
* (a row with a null / missing key component never collides)
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
306
|
+
* (a row with a null / missing key component never collides), each mapped
|
|
307
|
+
* to the index (in `uniqueKeySets` order — the primary key first) of the
|
|
308
|
+
* first key set it collides on. One non-locking SELECT covers every key set;
|
|
309
|
+
* it is split only to stay under the parameter limit. Skipped entirely when
|
|
310
|
+
* no row carries a key value (generated PK, no unique index values).
|
|
271
311
|
*/
|
|
272
312
|
private _findStoredKeyConflicts;
|
|
273
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* Exclusive locks on the stored rows the `skipped` input rows collide with
|
|
315
|
+
* (found by {@link _findStoredKeyConflicts}, so every key exists): `SELECT …
|
|
316
|
+
* FOR UPDATE` by the key set each row collided on — the primary key when it
|
|
317
|
+
* did — ascending, one statement per key set. The keys are the INPUT rows'
|
|
318
|
+
* own (write-form) values, which the pre-check matched exactly; a full
|
|
319
|
+
* unique-key equality on existing records takes record locks only, never a
|
|
320
|
+
* gap lock.
|
|
321
|
+
*/
|
|
322
|
+
private _lockStoredRows;
|
|
323
|
+
/** Input positions of `rows` in ascending primary-key order (input order without a complete key). */
|
|
324
|
+
private _pkOrder;
|
|
325
|
+
/**
|
|
326
|
+
* One {@link _idGroups} group. Standalone: optimistic INSERT, then pre-check
|
|
327
|
+
* + survivor INSERT (+ bisect on a race). `mode.precheck`: pre-check (and
|
|
328
|
+
* `mode.lock`: lock the stored conflicts) FIRST, then insert the survivors in
|
|
329
|
+
* primary-key order.
|
|
330
|
+
*/
|
|
274
331
|
private _insertIgnoringGroup;
|
|
275
332
|
/** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
|
|
276
333
|
private _bisectGroup;
|
|
@@ -503,9 +560,11 @@ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions):
|
|
|
503
560
|
*
|
|
504
561
|
* @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
|
|
505
562
|
* @param options - Additional pool options passed to mysql2. `strictMode: false`
|
|
506
|
-
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver})
|
|
563
|
+
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver});
|
|
564
|
+
* `viewJoinPruning: false` (not a pool option, since 0.1.153) reads managed views by name
|
|
565
|
+
* ({@link TMysqlAdapterOptions}).
|
|
507
566
|
* @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
|
|
508
567
|
*/
|
|
509
568
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
510
569
|
//#endregion
|
|
511
|
-
export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
570
|
+
export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlAdapterOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
|
|
3
3
|
import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
|
|
4
4
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
@@ -73,6 +73,18 @@ interface TMysqlConnection {
|
|
|
73
73
|
/** Release this connection back to the pool. */
|
|
74
74
|
release(): void;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* `MysqlAdapter` options.
|
|
78
|
+
* @since 0.1.153
|
|
79
|
+
*/
|
|
80
|
+
interface TMysqlAdapterOptions {
|
|
81
|
+
/**
|
|
82
|
+
* Read a managed view through an inline definition without the LEFT joins
|
|
83
|
+
* the query does not need (default `true`) — see the views guide,
|
|
84
|
+
* "Performance". `false` always reads the stored view by name.
|
|
85
|
+
*/
|
|
86
|
+
viewJoinPruning?: boolean;
|
|
87
|
+
}
|
|
76
88
|
//#endregion
|
|
77
89
|
//#region src/mysql-adapter.d.ts
|
|
78
90
|
/**
|
|
@@ -117,7 +129,15 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
117
129
|
* (the name-taking schema-sync primitives run on such an adapter).
|
|
118
130
|
*/
|
|
119
131
|
private get _schema();
|
|
120
|
-
constructor(driver: TMysqlDriver);
|
|
132
|
+
constructor(driver: TMysqlDriver, options?: TMysqlAdapterOptions);
|
|
133
|
+
/**
|
|
134
|
+
* The FROM source of a read of this adapter's table or view: a managed
|
|
135
|
+
* view read that needs only some of its LEFT joins reads an inline,
|
|
136
|
+
* pruned definition (`viewReadSource`, since 0.1.153) — MySQL has no
|
|
137
|
+
* outer-join elimination, so every LEFT JOIN of a view is otherwise probed
|
|
138
|
+
* per row, even for a `COUNT(*)`. Off with `viewJoinPruning: false`.
|
|
139
|
+
*/
|
|
140
|
+
private _readSource;
|
|
121
141
|
/** Every adapter over this pool shares one transaction (since 0.1.128). */
|
|
122
142
|
protected _transactionOwner(): unknown;
|
|
123
143
|
/** The dedicated connection of this pool's open transaction, if any. */
|
|
@@ -162,6 +182,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
162
182
|
aggregateFns(): ReadonlySet<AggregateFn>;
|
|
163
183
|
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
164
184
|
supportsAggregateExpressions(): boolean;
|
|
185
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
186
|
+
supportsNullsPlacement(): boolean;
|
|
165
187
|
/** Computed view columns and first-row joins. */
|
|
166
188
|
viewCapabilities(): ReadonlySet<TViewCapability>;
|
|
167
189
|
onBeforeFlatten(_type: unknown): void;
|
|
@@ -188,7 +210,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
188
210
|
* - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
|
|
189
211
|
*/
|
|
190
212
|
private _wrapConstraintError;
|
|
191
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
213
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock wait timeout, else as is. */
|
|
192
214
|
private _mapConstraintError;
|
|
193
215
|
private _mapFkError;
|
|
194
216
|
insertOne(data: Record<string, unknown>): Promise<TDbInsertResult>;
|
|
@@ -246,31 +268,66 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
246
268
|
private _groupInsertedIds;
|
|
247
269
|
supportsInsertIgnore(): boolean;
|
|
248
270
|
/**
|
|
249
|
-
*
|
|
250
|
-
* statement). Only when it hits a
|
|
251
|
-
* SELECT of the chunk's primary /
|
|
252
|
-
*
|
|
253
|
-
* INSERT — a dense-duplicate chunk is
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
264
|
-
|
|
271
|
+
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
|
272
|
+
* INSERT (an all-new batch costs a single statement). Only when it hits a
|
|
273
|
+
* duplicate key (errno 1062 / 1586) does ONE SELECT of the chunk's primary /
|
|
274
|
+
* unique key tuples find the stored rows (skipped as conflicts) and the
|
|
275
|
+
* survivors go in as one more multi-row INSERT — a dense-duplicate chunk is
|
|
276
|
+
* three statements, never O(rows).
|
|
277
|
+
*
|
|
278
|
+
* Inside a CALLER's transaction (`opts.inCallerTransaction`, else
|
|
279
|
+
* `isInTransaction()` on entry) the key SELECT runs FIRST and only the
|
|
280
|
+
* missing rows are inserted, in primary-key order: a failed duplicate INSERT
|
|
281
|
+
* leaves InnoDB's shared lock on the duplicate record until COMMIT, and a
|
|
282
|
+
* later UPDATE of that row in two such transactions would deadlock (S→X).
|
|
283
|
+
* The SELECT is a non-locking consistent read, so the common case takes no
|
|
284
|
+
* lock on a stored row at all. `opts.lockConflicts` then locks the stored
|
|
285
|
+
* conflicting rows exclusively — `SELECT … WHERE pk IN (found) ORDER BY pk
|
|
286
|
+
* FOR UPDATE` (by the unique key a row collided on when that was not the
|
|
287
|
+
* primary key), existing keys only, so record locks and no gap locks —
|
|
288
|
+
* before inserting the rest.
|
|
289
|
+
*
|
|
290
|
+
* Either way, only if an INSERT still collides (a concurrent writer raced in
|
|
291
|
+
* — committed after the transaction's snapshot or still uncommitted — or a
|
|
292
|
+
* collation-equal value the exact-match pre-check missed) are the rows
|
|
293
|
+
* bisected in input order: each half is retried, recursively, and a single
|
|
294
|
+
* row that still collides is skipped (holding the failed INSERT's shared
|
|
295
|
+
* lock). A failed statement is rolled back by InnoDB alone, so the
|
|
296
|
+
* transaction stays usable. A chunk mixing explicit and generated
|
|
297
|
+
* auto-increment ids is processed as one such sequence per consecutive run
|
|
298
|
+
* of a kind. Deliberately NOT `INSERT IGNORE` (it would downgrade NOT NULL /
|
|
299
|
+
* FK / truncation errors to warnings) and not `ON DUPLICATE KEY UPDATE` (a
|
|
300
|
+
* no-op update is indistinguishable from an insert in the affected-rows
|
|
301
|
+
* count).
|
|
302
|
+
*/
|
|
303
|
+
insertManyIgnore(data: Array<Record<string, unknown>>, opts?: TDbInsertIgnoreOptions): Promise<TDbInsertIgnoreSlot[]>;
|
|
265
304
|
/**
|
|
266
305
|
* Indices of `rows` whose primary / unique-index key tuple is already stored
|
|
267
|
-
* (a row with a null / missing key component never collides)
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
306
|
+
* (a row with a null / missing key component never collides), each mapped
|
|
307
|
+
* to the index (in `uniqueKeySets` order — the primary key first) of the
|
|
308
|
+
* first key set it collides on. One non-locking SELECT covers every key set;
|
|
309
|
+
* it is split only to stay under the parameter limit. Skipped entirely when
|
|
310
|
+
* no row carries a key value (generated PK, no unique index values).
|
|
271
311
|
*/
|
|
272
312
|
private _findStoredKeyConflicts;
|
|
273
|
-
/**
|
|
313
|
+
/**
|
|
314
|
+
* Exclusive locks on the stored rows the `skipped` input rows collide with
|
|
315
|
+
* (found by {@link _findStoredKeyConflicts}, so every key exists): `SELECT …
|
|
316
|
+
* FOR UPDATE` by the key set each row collided on — the primary key when it
|
|
317
|
+
* did — ascending, one statement per key set. The keys are the INPUT rows'
|
|
318
|
+
* own (write-form) values, which the pre-check matched exactly; a full
|
|
319
|
+
* unique-key equality on existing records takes record locks only, never a
|
|
320
|
+
* gap lock.
|
|
321
|
+
*/
|
|
322
|
+
private _lockStoredRows;
|
|
323
|
+
/** Input positions of `rows` in ascending primary-key order (input order without a complete key). */
|
|
324
|
+
private _pkOrder;
|
|
325
|
+
/**
|
|
326
|
+
* One {@link _idGroups} group. Standalone: optimistic INSERT, then pre-check
|
|
327
|
+
* + survivor INSERT (+ bisect on a race). `mode.precheck`: pre-check (and
|
|
328
|
+
* `mode.lock`: lock the stored conflicts) FIRST, then insert the survivors in
|
|
329
|
+
* primary-key order.
|
|
330
|
+
*/
|
|
274
331
|
private _insertIgnoringGroup;
|
|
275
332
|
/** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
|
|
276
333
|
private _bisectGroup;
|
|
@@ -503,9 +560,11 @@ declare function buildWhere(filter: FilterExpr$1, opts?: TFilterVisitorOptions):
|
|
|
503
560
|
*
|
|
504
561
|
* @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
|
|
505
562
|
* @param options - Additional pool options passed to mysql2. `strictMode: false`
|
|
506
|
-
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver})
|
|
563
|
+
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver});
|
|
564
|
+
* `viewJoinPruning: false` (not a pool option, since 0.1.153) reads managed views by name
|
|
565
|
+
* ({@link TMysqlAdapterOptions}).
|
|
507
566
|
* @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
|
|
508
567
|
*/
|
|
509
568
|
declare function createAdapter(uri: string, options?: Record<string, unknown>): DbSpace;
|
|
510
569
|
//#endregion
|
|
511
|
-
export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
|
|
570
|
+
export { Mysql2Driver, MysqlAdapter, type TMysql2DriverOptions, type TMysqlAdapterOptions, type TMysqlConnection, type TMysqlDriver, type TMysqlRunResult, type TSqlFragment, buildWhere, createAdapter };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, NoopLogger, bucketTimeZoneUnavailable, containsRelationFilter, defaultFulltextIndex, describeFulltext, fkColumns, forEachResolvedRelation, isConflict, searchTermInteger, splitFulltextFields, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
2
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
|
-
import { EMPTY_OR, SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, geoWindowFromControls, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource } from "@atscript/db-sql-tools";
|
|
3
|
+
import { EMPTY_OR, SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, parseRegexString, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource, viewReadSource } from "@atscript/db-sql-tools";
|
|
4
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
5
5
|
//#region src/sql-builder.ts
|
|
6
6
|
/** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
|
|
@@ -152,6 +152,9 @@ const mysqlDialect = {
|
|
|
152
152
|
return quoteTableName(name);
|
|
153
153
|
},
|
|
154
154
|
unlimitedLimit: "18446744073709551615",
|
|
155
|
+
derivedMergeHint(alias) {
|
|
156
|
+
return `/*+ MERGE(${qi(alias)}) */`;
|
|
157
|
+
},
|
|
155
158
|
toValue: toSqlValue,
|
|
156
159
|
toParam(value) {
|
|
157
160
|
if (value === void 0) return null;
|
|
@@ -617,9 +620,21 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
617
620
|
get _schema() {
|
|
618
621
|
return this._table?.schema ?? null;
|
|
619
622
|
}
|
|
620
|
-
constructor(driver) {
|
|
623
|
+
constructor(driver, options) {
|
|
621
624
|
super();
|
|
622
625
|
this.driver = driver;
|
|
626
|
+
this.viewJoinPruning = options?.viewJoinPruning !== false;
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* The FROM source of a read of this adapter's table or view: a managed
|
|
630
|
+
* view read that needs only some of its LEFT joins reads an inline,
|
|
631
|
+
* pruned definition (`viewReadSource`, since 0.1.153) — MySQL has no
|
|
632
|
+
* outer-join elimination, so every LEFT JOIN of a view is otherwise probed
|
|
633
|
+
* per row, even for a `COUNT(*)`. Off with `viewJoinPruning: false`.
|
|
634
|
+
*/
|
|
635
|
+
_readSource(query, kind, partitionBy) {
|
|
636
|
+
const name = this.resolveTableName();
|
|
637
|
+
return this.viewJoinPruning ? viewReadSource(mysqlDialect, this._table, name, query, kind, partitionBy) : name;
|
|
623
638
|
}
|
|
624
639
|
/** Every adapter over this pool shares one transaction (since 0.1.128). */
|
|
625
640
|
_transactionOwner() {
|
|
@@ -721,6 +736,10 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
721
736
|
supportsAggregateExpressions() {
|
|
722
737
|
return true;
|
|
723
738
|
}
|
|
739
|
+
/** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
|
|
740
|
+
supportsNullsPlacement() {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
724
743
|
/** Computed view columns and first-row joins. */
|
|
725
744
|
viewCapabilities() {
|
|
726
745
|
return ALL_VIEW_CAPABILITIES;
|
|
@@ -849,7 +868,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
849
868
|
return this._mapConstraintError(error);
|
|
850
869
|
}
|
|
851
870
|
}
|
|
852
|
-
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
871
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation or a deadlock / lock wait timeout, else as is. */
|
|
853
872
|
_mapConstraintError(error) {
|
|
854
873
|
if (error && typeof error === "object" && "errno" in error) {
|
|
855
874
|
const err = error;
|
|
@@ -858,6 +877,10 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
858
877
|
message: err.sqlMessage ?? err.message
|
|
859
878
|
}]);
|
|
860
879
|
if (err.errno === 1451 || err.errno === 1452) throw new DbError("FK_VIOLATION", this._mapFkError(err.message));
|
|
880
|
+
if (err.errno === 1213 || err.errno === 1205) throw new DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
|
|
881
|
+
path: "",
|
|
882
|
+
message: err.sqlMessage ?? err.message
|
|
883
|
+
}]);
|
|
861
884
|
}
|
|
862
885
|
throw error;
|
|
863
886
|
}
|
|
@@ -1058,27 +1081,49 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1058
1081
|
return true;
|
|
1059
1082
|
}
|
|
1060
1083
|
/**
|
|
1061
|
-
*
|
|
1062
|
-
* statement). Only when it hits a
|
|
1063
|
-
* SELECT of the chunk's primary /
|
|
1064
|
-
*
|
|
1065
|
-
* INSERT — a dense-duplicate chunk is
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
*
|
|
1069
|
-
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
*
|
|
1073
|
-
*
|
|
1074
|
-
*
|
|
1084
|
+
* Standalone (no caller transaction): per chunk ONE optimistic multi-row
|
|
1085
|
+
* INSERT (an all-new batch costs a single statement). Only when it hits a
|
|
1086
|
+
* duplicate key (errno 1062 / 1586) does ONE SELECT of the chunk's primary /
|
|
1087
|
+
* unique key tuples find the stored rows (skipped as conflicts) and the
|
|
1088
|
+
* survivors go in as one more multi-row INSERT — a dense-duplicate chunk is
|
|
1089
|
+
* three statements, never O(rows).
|
|
1090
|
+
*
|
|
1091
|
+
* Inside a CALLER's transaction (`opts.inCallerTransaction`, else
|
|
1092
|
+
* `isInTransaction()` on entry) the key SELECT runs FIRST and only the
|
|
1093
|
+
* missing rows are inserted, in primary-key order: a failed duplicate INSERT
|
|
1094
|
+
* leaves InnoDB's shared lock on the duplicate record until COMMIT, and a
|
|
1095
|
+
* later UPDATE of that row in two such transactions would deadlock (S→X).
|
|
1096
|
+
* The SELECT is a non-locking consistent read, so the common case takes no
|
|
1097
|
+
* lock on a stored row at all. `opts.lockConflicts` then locks the stored
|
|
1098
|
+
* conflicting rows exclusively — `SELECT … WHERE pk IN (found) ORDER BY pk
|
|
1099
|
+
* FOR UPDATE` (by the unique key a row collided on when that was not the
|
|
1100
|
+
* primary key), existing keys only, so record locks and no gap locks —
|
|
1101
|
+
* before inserting the rest.
|
|
1102
|
+
*
|
|
1103
|
+
* Either way, only if an INSERT still collides (a concurrent writer raced in
|
|
1104
|
+
* — committed after the transaction's snapshot or still uncommitted — or a
|
|
1105
|
+
* collation-equal value the exact-match pre-check missed) are the rows
|
|
1106
|
+
* bisected in input order: each half is retried, recursively, and a single
|
|
1107
|
+
* row that still collides is skipped (holding the failed INSERT's shared
|
|
1108
|
+
* lock). A failed statement is rolled back by InnoDB alone, so the
|
|
1109
|
+
* transaction stays usable. A chunk mixing explicit and generated
|
|
1110
|
+
* auto-increment ids is processed as one such sequence per consecutive run
|
|
1111
|
+
* of a kind. Deliberately NOT `INSERT IGNORE` (it would downgrade NOT NULL /
|
|
1112
|
+
* FK / truncation errors to warnings) and not `ON DUPLICATE KEY UPDATE` (a
|
|
1113
|
+
* no-op update is indistinguishable from an insert in the affected-rows
|
|
1114
|
+
* count).
|
|
1075
1115
|
*/
|
|
1076
|
-
async insertManyIgnore(data) {
|
|
1116
|
+
async insertManyIgnore(data, opts) {
|
|
1077
1117
|
if (data.length === 0) return [];
|
|
1078
1118
|
await this._warnNonStrictMode();
|
|
1079
1119
|
const { columns, batches } = chunkInsertRows(data);
|
|
1080
|
-
const
|
|
1081
|
-
|
|
1120
|
+
const inTx = this._txConnection() !== void 0;
|
|
1121
|
+
const mode = {
|
|
1122
|
+
precheck: opts?.inCallerTransaction ?? inTx,
|
|
1123
|
+
lock: opts?.lockConflicts === true
|
|
1124
|
+
};
|
|
1125
|
+
const single = data.length === 1 && !inTx ? this._uniformIdGroup(data) : void 0;
|
|
1126
|
+
if (single) return this._insertIgnoringGroup(this.resolveTableName(), columns, single, this._incrementStep(), mode);
|
|
1082
1127
|
return this.withTransaction(async () => {
|
|
1083
1128
|
const tableName = this.resolveTableName();
|
|
1084
1129
|
const slots = [];
|
|
@@ -1086,7 +1131,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1086
1131
|
const zero = this._zeroIsExplicit();
|
|
1087
1132
|
for (const batch of batches) {
|
|
1088
1133
|
const out = Array.from({ length: batch.length }, () => null);
|
|
1089
|
-
for (const group of await this._idGroups(batch, zero)) (await this._insertIgnoringGroup(tableName, columns, group, step)).forEach((slot, k) => {
|
|
1134
|
+
for (const group of await this._idGroups(batch, zero)) (await this._insertIgnoringGroup(tableName, columns, group, step, mode)).forEach((slot, k) => {
|
|
1090
1135
|
out[group.at[k]] = slot;
|
|
1091
1136
|
});
|
|
1092
1137
|
slots.push(...out);
|
|
@@ -1096,20 +1141,21 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1096
1141
|
}
|
|
1097
1142
|
/**
|
|
1098
1143
|
* Indices of `rows` whose primary / unique-index key tuple is already stored
|
|
1099
|
-
* (a row with a null / missing key component never collides)
|
|
1100
|
-
*
|
|
1101
|
-
*
|
|
1102
|
-
*
|
|
1144
|
+
* (a row with a null / missing key component never collides), each mapped
|
|
1145
|
+
* to the index (in `uniqueKeySets` order — the primary key first) of the
|
|
1146
|
+
* first key set it collides on. One non-locking SELECT covers every key set;
|
|
1147
|
+
* it is split only to stay under the parameter limit. Skipped entirely when
|
|
1148
|
+
* no row carries a key value (generated PK, no unique index values).
|
|
1103
1149
|
*/
|
|
1104
1150
|
async _findStoredKeyConflicts(tableName, rows) {
|
|
1105
1151
|
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1106
1152
|
const rowTuples = rows.map((row) => keySets.map((fields) => uniqueKeyTuple(row, fields)));
|
|
1107
1153
|
const used = keySets.map((_, k) => rowTuples.some((t) => t[k] !== void 0));
|
|
1108
|
-
if (!used.includes(true)) return /* @__PURE__ */ new
|
|
1154
|
+
if (!used.includes(true)) return /* @__PURE__ */ new Map();
|
|
1109
1155
|
const width = keySets.reduce((n, f, k) => n + (used[k] ? f.length : 0), 0);
|
|
1110
1156
|
const sliceSize = Math.max(1, Math.floor(6e4 / width));
|
|
1111
1157
|
const selectCols = [...new Set(keySets.flat())].map((c) => qi(c)).join(", ");
|
|
1112
|
-
const
|
|
1158
|
+
const storedTuples = keySets.map(() => /* @__PURE__ */ new Set());
|
|
1113
1159
|
for (let offset = 0; offset < rows.length; offset += sliceSize) {
|
|
1114
1160
|
const end = Math.min(rows.length, offset + sliceSize);
|
|
1115
1161
|
const clauses = [];
|
|
@@ -1135,30 +1181,103 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1135
1181
|
const found = await this._exec().all(sql, params);
|
|
1136
1182
|
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1137
1183
|
const tuple = uniqueKeyTuple(doc, fields);
|
|
1138
|
-
if (tuple !== void 0)
|
|
1184
|
+
if (tuple !== void 0) storedTuples[k].add(tuple);
|
|
1139
1185
|
});
|
|
1140
1186
|
}
|
|
1141
|
-
const skipped = /* @__PURE__ */ new
|
|
1187
|
+
const skipped = /* @__PURE__ */ new Map();
|
|
1142
1188
|
rowTuples.forEach((tuples, i) => {
|
|
1143
|
-
|
|
1189
|
+
const k = tuples.findIndex((t, k) => t !== void 0 && storedTuples[k].has(t));
|
|
1190
|
+
if (k >= 0) skipped.set(i, k);
|
|
1144
1191
|
});
|
|
1145
1192
|
return skipped;
|
|
1146
1193
|
}
|
|
1147
|
-
/**
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1194
|
+
/**
|
|
1195
|
+
* Exclusive locks on the stored rows the `skipped` input rows collide with
|
|
1196
|
+
* (found by {@link _findStoredKeyConflicts}, so every key exists): `SELECT …
|
|
1197
|
+
* FOR UPDATE` by the key set each row collided on — the primary key when it
|
|
1198
|
+
* did — ascending, one statement per key set. The keys are the INPUT rows'
|
|
1199
|
+
* own (write-form) values, which the pre-check matched exactly; a full
|
|
1200
|
+
* unique-key equality on existing records takes record locks only, never a
|
|
1201
|
+
* gap lock.
|
|
1202
|
+
*/
|
|
1203
|
+
async _lockStoredRows(tableName, rows, skipped) {
|
|
1204
|
+
if (skipped.size === 0) return;
|
|
1205
|
+
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1206
|
+
const byKeySet = /* @__PURE__ */ new Map();
|
|
1207
|
+
for (const [i, k] of skipped) {
|
|
1208
|
+
let keyed = byKeySet.get(k);
|
|
1209
|
+
if (!keyed) byKeySet.set(k, keyed = /* @__PURE__ */ new Map());
|
|
1210
|
+
keyed.set(uniqueKeyTuple(rows[i], keySets[k]), rows[i]);
|
|
1211
|
+
}
|
|
1212
|
+
for (const [k, keyed] of byKeySet) {
|
|
1213
|
+
const fields = keySets[k];
|
|
1214
|
+
const keyRows = [...keyed.values()];
|
|
1215
|
+
const sliceSize = Math.max(1, Math.floor(6e4 / fields.length));
|
|
1216
|
+
const cols = fields.map((f) => qi(f)).join(", ");
|
|
1217
|
+
const tuple = fields.length === 1 ? "?" : `(${fields.map(() => "?").join(", ")})`;
|
|
1218
|
+
for (let offset = 0; offset < keyRows.length; offset += sliceSize) {
|
|
1219
|
+
const slice = keyRows.slice(offset, offset + sliceSize);
|
|
1220
|
+
const params = [];
|
|
1221
|
+
for (const row of slice) params.push(...fields.map((f) => mysqlDialect.toValue(row[f])));
|
|
1222
|
+
const target = fields.length === 1 ? cols : `(${cols})`;
|
|
1223
|
+
const sql = `SELECT ${cols} FROM ${quoteTableName(tableName)} WHERE ${target} IN (${slice.map(() => tuple).join(", ")}) ORDER BY ${cols} FOR UPDATE`;
|
|
1224
|
+
this._log(sql, params);
|
|
1225
|
+
await this._wrapConstraintError(() => this._exec().all(sql, params));
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
/** Input positions of `rows` in ascending primary-key order (input order without a complete key). */
|
|
1230
|
+
_pkOrder(rows, at) {
|
|
1231
|
+
const pkCols = this._table.primaryKeys.map((k) => this._table.physicalPath(k));
|
|
1232
|
+
if (pkCols.length === 0 || at.length < 2) return at;
|
|
1233
|
+
if (at.some((i) => pkCols.some((c) => rows[i][c] === void 0 || rows[i][c] === null))) return at;
|
|
1234
|
+
return at.toSorted((a, b) => {
|
|
1235
|
+
for (const c of pkCols) {
|
|
1236
|
+
const x = rows[a][c];
|
|
1237
|
+
const y = rows[b][c];
|
|
1238
|
+
if (x === y) continue;
|
|
1239
|
+
return x < y ? -1 : 1;
|
|
1240
|
+
}
|
|
1241
|
+
return a - b;
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* One {@link _idGroups} group. Standalone: optimistic INSERT, then pre-check
|
|
1246
|
+
* + survivor INSERT (+ bisect on a race). `mode.precheck`: pre-check (and
|
|
1247
|
+
* `mode.lock`: lock the stored conflicts) FIRST, then insert the survivors in
|
|
1248
|
+
* primary-key order.
|
|
1249
|
+
*/
|
|
1250
|
+
async _insertIgnoringGroup(tableName, columns, group, step, mode) {
|
|
1251
|
+
if (!mode.precheck && !mode.lock) {
|
|
1252
|
+
const direct = await this._tryInsertGroup(tableName, columns, group, step);
|
|
1253
|
+
if (direct) return direct;
|
|
1254
|
+
if (group.rows.length === 1) return [null];
|
|
1255
|
+
}
|
|
1152
1256
|
const skipped = await this._findStoredKeyConflicts(tableName, group.rows);
|
|
1153
|
-
if (
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1257
|
+
if (mode.lock) await this._lockStoredRows(tableName, group.rows, skipped);
|
|
1258
|
+
if (skipped.size === 0 && !mode.precheck && !mode.lock) return this._bisectGroup(tableName, columns, group, step);
|
|
1259
|
+
const keep = group.rows.map((_, i) => i).filter((i) => !skipped.has(i));
|
|
1260
|
+
const ordered = mode.precheck && !group.generated ? this._pkOrder(group.rows, keep) : keep;
|
|
1261
|
+
const inserted = /* @__PURE__ */ new Map();
|
|
1262
|
+
if (ordered.length > 0) {
|
|
1263
|
+
const sorted = {
|
|
1264
|
+
rows: ordered.map((i) => group.rows[i]),
|
|
1265
|
+
at: [],
|
|
1266
|
+
generated: group.generated
|
|
1267
|
+
};
|
|
1268
|
+
let slots = await this._tryInsertGroup(tableName, columns, sorted, step);
|
|
1269
|
+
if (slots) slots.forEach((slot, k) => inserted.set(ordered[k], slot));
|
|
1270
|
+
else {
|
|
1271
|
+
const survivors = {
|
|
1272
|
+
rows: keep.map((i) => group.rows[i]),
|
|
1273
|
+
at: [],
|
|
1274
|
+
generated: group.generated
|
|
1275
|
+
};
|
|
1276
|
+
slots = keep.length === 1 ? [null] : await this._bisectGroup(tableName, columns, survivors, step);
|
|
1277
|
+
slots.forEach((slot, k) => inserted.set(keep[k], slot));
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
return group.rows.map((_, i) => skipped.has(i) ? null : inserted.get(i));
|
|
1162
1281
|
}
|
|
1163
1282
|
/** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
|
|
1164
1283
|
async _bisectGroup(tableName, columns, group, step) {
|
|
@@ -1193,13 +1312,13 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1193
1312
|
...query.controls,
|
|
1194
1313
|
$limit: 1
|
|
1195
1314
|
};
|
|
1196
|
-
const { sql, params } = buildSelect$1(this.
|
|
1315
|
+
const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, controls);
|
|
1197
1316
|
this._log(sql, params);
|
|
1198
1317
|
return this._exec().get(sql, params);
|
|
1199
1318
|
}
|
|
1200
1319
|
async findMany(query) {
|
|
1201
1320
|
const where = buildWhere(query.filter);
|
|
1202
|
-
const { sql, params } = buildSelect$1(this.
|
|
1321
|
+
const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, query.controls);
|
|
1203
1322
|
this._log(sql, params);
|
|
1204
1323
|
return this._exec().all(sql, params);
|
|
1205
1324
|
}
|
|
@@ -1209,20 +1328,21 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1209
1328
|
*/
|
|
1210
1329
|
async findManyPerPartition(query, partitionBy) {
|
|
1211
1330
|
const where = buildWhere(query.filter);
|
|
1212
|
-
const { sql, params } = buildPartitionedSelect(mysqlDialect, this.
|
|
1331
|
+
const { sql, params } = buildPartitionedSelect(mysqlDialect, this._readSource(query, "rows", partitionBy), where, query.controls, partitionBy);
|
|
1213
1332
|
this._log(sql, params);
|
|
1214
1333
|
return stripPartitionRowNumber(await this._exec().all(sql, params));
|
|
1215
1334
|
}
|
|
1216
1335
|
async count(query) {
|
|
1217
1336
|
const where = buildWhere(query.filter);
|
|
1218
|
-
const
|
|
1337
|
+
const source = this._readSource(query, "count");
|
|
1338
|
+
const sql = `SELECT ${fromSourceHint(source)}COUNT(*) as cnt FROM ${fromSourceSql(mysqlDialect, source)} WHERE ${where.sql}`;
|
|
1219
1339
|
this._log(sql, where.params);
|
|
1220
1340
|
return (await this._exec().get(sql, where.params))?.cnt ?? 0;
|
|
1221
1341
|
}
|
|
1222
1342
|
async aggregate(query) {
|
|
1223
1343
|
const search = resolveAggregateSearch(query.controls);
|
|
1224
1344
|
const where = search ? this._buildSearchWhere(search.text, query, search.indexName) : buildWhere(query.filter);
|
|
1225
|
-
const tableName = this.resolveTableName();
|
|
1345
|
+
const tableName = search ? this.resolveTableName() : this._readSource(query, "aggregate");
|
|
1226
1346
|
for (const bucket of query.controls.$select?.buckets ?? []) await this._ensureBucketZone(bucket.tz);
|
|
1227
1347
|
if (query.controls.$count) {
|
|
1228
1348
|
const { sql, params } = buildAggregateCount$1(tableName, where, query.controls);
|
|
@@ -2268,16 +2388,19 @@ var Mysql2Driver = class {
|
|
|
2268
2388
|
*
|
|
2269
2389
|
* @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
|
|
2270
2390
|
* @param options - Additional pool options passed to mysql2. `strictMode: false`
|
|
2271
|
-
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver})
|
|
2391
|
+
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver});
|
|
2392
|
+
* `viewJoinPruning: false` (not a pool option, since 0.1.153) reads managed views by name
|
|
2393
|
+
* ({@link TMysqlAdapterOptions}).
|
|
2272
2394
|
* @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
|
|
2273
2395
|
*/
|
|
2274
2396
|
function createAdapter(uri, options) {
|
|
2275
|
-
const { strictMode, ...pool } = options ?? {};
|
|
2397
|
+
const { strictMode, viewJoinPruning, ...pool } = options ?? {};
|
|
2276
2398
|
const driver = new Mysql2Driver({
|
|
2277
2399
|
uri,
|
|
2278
2400
|
...pool
|
|
2279
2401
|
}, { strictMode });
|
|
2280
|
-
|
|
2402
|
+
const adapterOptions = { viewJoinPruning };
|
|
2403
|
+
return new DbSpace(() => new MysqlAdapter(driver, adapterOptions), { onClose: () => driver.close() });
|
|
2281
2404
|
}
|
|
2282
2405
|
//#endregion
|
|
2283
2406
|
export { Mysql2Driver, MysqlAdapter, buildWhere, createAdapter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mysql",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.153",
|
|
4
4
|
"description": "MySQL adapter for @atscript/db with mysql2 driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@atscript/core": "^0.1.
|
|
45
|
-
"@atscript/typescript": "^0.1.
|
|
46
|
-
"@uniqu/core": "^0.1.
|
|
44
|
+
"@atscript/core": "^0.1.103",
|
|
45
|
+
"@atscript/typescript": "^0.1.103",
|
|
46
|
+
"@uniqu/core": "^0.1.15",
|
|
47
47
|
"mysql2": "^3.11.0",
|
|
48
|
-
"unplugin-atscript": "^0.1.
|
|
48
|
+
"unplugin-atscript": "^0.1.103"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@atscript/core": "^0.1.
|
|
52
|
-
"@atscript/typescript": "^0.1.
|
|
53
|
-
"@uniqu/core": "^0.1.
|
|
51
|
+
"@atscript/core": "^0.1.103",
|
|
52
|
+
"@atscript/typescript": "^0.1.103",
|
|
53
|
+
"@uniqu/core": "^0.1.15",
|
|
54
54
|
"mysql2": ">=3.0.0",
|
|
55
|
-
"@atscript/db": "^0.1.
|
|
56
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
55
|
+
"@atscript/db": "^0.1.153",
|
|
56
|
+
"@atscript/db-sql-tools": "^0.1.153"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"mysql2": {
|