@atscript/db-mysql 0.1.146 → 0.1.148
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 +423 -66
- package/dist/index.d.cts +140 -7
- package/dist/index.d.mts +140 -7
- package/dist/index.mjs +425 -68
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -105,7 +105,7 @@ function buildColumnDefinition(field, ctx) {
|
|
|
105
105
|
def: `${qi(field.physicalName)} ${sqlType}${mysqlCollateClause(field)} GENERATED ALWAYS AS (${(0, _atscript_db_sql_tools.derivedColumnExpr)(mysqlDialect, field)}) VIRTUAL`,
|
|
106
106
|
inventedDefault: false
|
|
107
107
|
};
|
|
108
|
-
const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.
|
|
108
|
+
const increment = ctx.purpose !== "add" && (ctx.incrementFields?.has(field.path) ?? false);
|
|
109
109
|
let def = `${qi(field.physicalName)} ${sqlType}`;
|
|
110
110
|
if (increment) def += " AUTO_INCREMENT";
|
|
111
111
|
if (ctx.purpose === "create") {
|
|
@@ -165,6 +165,10 @@ const mysqlDialect = {
|
|
|
165
165
|
params: [pattern]
|
|
166
166
|
};
|
|
167
167
|
},
|
|
168
|
+
mapQueryError(error, arithmetic) {
|
|
169
|
+
if (error?.errno !== 1690) return void 0;
|
|
170
|
+
return arithmetic ? (0, _atscript_db_sql_tools.arithOverflowError)() : (0, _atscript_db_sql_tools.numericOutOfRangeError)();
|
|
171
|
+
},
|
|
168
172
|
geoWithin(quotedCol, circle) {
|
|
169
173
|
const dist = mysqlGeoDistanceExpr(quotedCol, circle.center);
|
|
170
174
|
return {
|
|
@@ -175,6 +179,7 @@ const mysqlDialect = {
|
|
|
175
179
|
calendarBucket: mysqlCalendarBucket,
|
|
176
180
|
bucketAliasInHaving: true,
|
|
177
181
|
jsonExtract: mysqlJsonExtract,
|
|
182
|
+
castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
|
|
178
183
|
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
179
184
|
};
|
|
180
185
|
/** `JSON_TYPE()` names of JSON numbers (MariaDB reports only INTEGER / DOUBLE). */
|
|
@@ -210,7 +215,8 @@ function isMysqlTimestampColumn(fd) {
|
|
|
210
215
|
const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
|
|
211
216
|
/**
|
|
212
217
|
* Calendar-bucket label: TEXT `'YYYY-MM-DD'` — the local date of the
|
|
213
|
-
* bucket's first day in `b.tz` — or
|
|
218
|
+
* bucket's first day in `b.tz` — or, for `hour`, `'YYYY-MM-DDTHH:00'` — the
|
|
219
|
+
* local wall-clock hour — or NULL for a NULL source or one outside
|
|
214
220
|
* `[BUCKET_MIN_INSTANT, BUCKET_MAX_INSTANT)`.
|
|
215
221
|
*
|
|
216
222
|
* The source's UTC wall time `U` (a DATETIME) depends on storage:
|
|
@@ -222,9 +228,12 @@ const MYSQL_EPOCH = "CAST('1970-01-01 00:00:00' AS DATETIME)";
|
|
|
222
228
|
* return whatever the server's session zone is (`UNIX_TIMESTAMP(col)` would
|
|
223
229
|
* agree only under a UTC session zone).
|
|
224
230
|
*
|
|
225
|
-
* The local
|
|
226
|
-
*
|
|
227
|
-
*
|
|
231
|
+
* The local wall time is `CONVERT_TZ(U, '+00:00', '<tz>')`, or `U` for `UTC`
|
|
232
|
+
* (no time zone tables needed) — UTC → local is never ambiguous, so a
|
|
233
|
+
* `+05:30` zone's hour starts at :30 UTC and a DST fall-back's repeated hour
|
|
234
|
+
* is one label. The hour is formatted from it; the other units truncate
|
|
235
|
+
* its `DATE()` with calendar arithmetic. `CONVERT_TZ`
|
|
236
|
+
* returns NULL when the zone is unknown to the
|
|
228
237
|
* server and its input unchanged outside its range — `MysqlAdapter` probes
|
|
229
238
|
* each zone before running the query (`BUCKET_TZ_UNAVAILABLE`).
|
|
230
239
|
*
|
|
@@ -241,24 +250,33 @@ function mysqlCalendarBucket(quotedCol, b) {
|
|
|
241
250
|
utc = `(${MYSQL_EPOCH} + INTERVAL FLOOR(${quotedCol} / 1000) SECOND)`;
|
|
242
251
|
inRange = `${quotedCol} >= ${_uniqu_core.BUCKET_MIN_INSTANT} AND ${quotedCol} < ${_uniqu_core.BUCKET_MAX_INSTANT}`;
|
|
243
252
|
}
|
|
244
|
-
const
|
|
245
|
-
|
|
253
|
+
const wall = b.tz === "UTC" ? utc : `CONVERT_TZ(${utc}, '+00:00', ${(0, _atscript_db_sql_tools.sqlTimeZoneLiteral)(b.tz)})`;
|
|
254
|
+
const local = `DATE(${wall})`;
|
|
255
|
+
let value;
|
|
256
|
+
let format = "%Y-%m-%d";
|
|
246
257
|
switch (b.unit) {
|
|
258
|
+
case "hour":
|
|
259
|
+
value = wall;
|
|
260
|
+
format = "%Y-%m-%dT%H:00";
|
|
261
|
+
break;
|
|
247
262
|
case "day":
|
|
248
|
-
|
|
263
|
+
value = local;
|
|
249
264
|
break;
|
|
250
265
|
case "week":
|
|
251
|
-
|
|
266
|
+
value = `DATE_SUB(${local}, INTERVAL ((WEEKDAY(${local}) + 1 - ${b.weekStartIso} + 7) % 7) DAY)`;
|
|
252
267
|
break;
|
|
253
268
|
case "month":
|
|
254
|
-
|
|
269
|
+
value = `DATE_SUB(${local}, INTERVAL DAYOFMONTH(${local}) - 1 DAY)`;
|
|
255
270
|
break;
|
|
256
271
|
case "quarter":
|
|
257
|
-
|
|
272
|
+
value = `(MAKEDATE(YEAR(${local}), 1) + INTERVAL (QUARTER(${local}) - 1) QUARTER)`;
|
|
273
|
+
break;
|
|
274
|
+
case "year":
|
|
275
|
+
value = `MAKEDATE(YEAR(${local}), 1)`;
|
|
258
276
|
break;
|
|
259
|
-
default:
|
|
277
|
+
default: throw new Error(`Unhandled calendar bucket unit "${String(b.unit)}"`);
|
|
260
278
|
}
|
|
261
|
-
return `CASE WHEN ${inRange} THEN DATE_FORMAT(${
|
|
279
|
+
return `CASE WHEN ${inRange} THEN DATE_FORMAT(${value}, '${format}') END`;
|
|
262
280
|
}
|
|
263
281
|
/**
|
|
264
282
|
* Spherical distance in meters from a POINT column to a query point.
|
|
@@ -446,12 +464,7 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
446
464
|
}
|
|
447
465
|
if (foreignKeys) for (const fk of foreignKeys.values()) {
|
|
448
466
|
if (options?.deferForeignKeysTo?.has(fk.targetTable)) continue;
|
|
449
|
-
|
|
450
|
-
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
451
|
-
let constraint = `FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
452
|
-
if (fk.onDelete) constraint += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
|
|
453
|
-
if (fk.onUpdate) constraint += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
|
|
454
|
-
constraints.push(constraint);
|
|
467
|
+
constraints.push((0, _atscript_db_sql_tools.foreignKeySql)(qi, fk));
|
|
455
468
|
}
|
|
456
469
|
const body = [...colDefs.map((c) => c.def), ...constraints].join(", ");
|
|
457
470
|
let sql = `CREATE TABLE IF NOT EXISTS ${quoteTableName(table)} (${body})`;
|
|
@@ -469,9 +482,15 @@ function buildCreateTable(table, fields, foreignKeys, options) {
|
|
|
469
482
|
*
|
|
470
483
|
* @returns `{ sql, params }` — the WHERE clause (without "WHERE") and bound params.
|
|
471
484
|
* Returns `{ sql: '1=1', params: [] }` for empty/null filters.
|
|
485
|
+
*
|
|
486
|
+
* Relational predicates (`$some` / `$none`) render as correlated `EXISTS`
|
|
487
|
+
* subqueries referencing the outer table by `opts.qualifier` (default: the
|
|
488
|
+
* source table) — a statement with an aliased FROM must pass its alias.
|
|
489
|
+
*
|
|
490
|
+
* @param opts - since 0.1.147
|
|
472
491
|
*/
|
|
473
|
-
function buildWhere(filter) {
|
|
474
|
-
return (0, _atscript_db_sql_tools.buildWhere)(mysqlDialect, filter);
|
|
492
|
+
function buildWhere(filter, opts) {
|
|
493
|
+
return (0, _atscript_db_sql_tools.buildWhere)(mysqlDialect, filter, opts);
|
|
475
494
|
}
|
|
476
495
|
//#endregion
|
|
477
496
|
//#region src/mysql-adapter.ts
|
|
@@ -503,6 +522,9 @@ function epochMsToUtcDatetime(ms) {
|
|
|
503
522
|
* "unchanged" named-zone result is not evidence of a limited range.
|
|
504
523
|
*/
|
|
505
524
|
const TZ_PROBE_SQL = "SELECT CONVERT_TZ('2040-06-01 12:00:00', '+00:00', ?) IS NULL AS missing, TIMESTAMPDIFF(MINUTE, '2040-06-01 12:00:00', CONVERT_TZ('2040-06-01 12:00:00', '+00:00', '+01:00')) AS shift";
|
|
525
|
+
const isZero = (v) => v === 0 || v === "0";
|
|
526
|
+
/** Drivers whose session `sql_mode` was already checked for strictness (see `_warnNonStrictMode`). */
|
|
527
|
+
const nonStrictChecked = /* @__PURE__ */ new WeakSet();
|
|
506
528
|
/**
|
|
507
529
|
* Zones `CONVERT_TZ` is known to handle, per driver (adapters are per table
|
|
508
530
|
* and share a driver). Positives only: a failed zone is probed again, so
|
|
@@ -615,6 +637,17 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
615
637
|
if (!tx) conn.release();
|
|
616
638
|
}
|
|
617
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Relational predicates (`$some` / `$none`) render as correlated
|
|
642
|
+
* `[NOT] EXISTS` subqueries — in reads and in mutation filters alike.
|
|
643
|
+
* An UPDATE / DELETE whose predicate reads the mutated table itself is
|
|
644
|
+
* rewritten through a materialized derived table (MySQL error 1093).
|
|
645
|
+
*
|
|
646
|
+
* @since 0.1.147
|
|
647
|
+
*/
|
|
648
|
+
supportsRelationFilters(_mode) {
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
618
651
|
/** MySQL InnoDB enforces FK constraints natively. */
|
|
619
652
|
supportsNativeForeignKeys() {
|
|
620
653
|
return true;
|
|
@@ -635,10 +668,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
635
668
|
calendarBucketUnits() {
|
|
636
669
|
return _atscript_db.ALL_BUCKET_UNITS;
|
|
637
670
|
}
|
|
638
|
-
/** Every aggregate function
|
|
671
|
+
/** Every aggregate function: `countDistinct`, `first` and `last` included. */
|
|
639
672
|
aggregateFns() {
|
|
640
673
|
return _atscript_db.ALL_AGGREGATE_FNS;
|
|
641
674
|
}
|
|
675
|
+
/** Arithmetic in an aggregate `$select` (`{ $expr }`, `{ $fn, $expr }`). */
|
|
676
|
+
supportsAggregateExpressions() {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
/** Computed view columns and first-row joins. */
|
|
680
|
+
viewCapabilities() {
|
|
681
|
+
return _atscript_db.ALL_VIEW_CAPABILITIES;
|
|
682
|
+
}
|
|
642
683
|
onBeforeFlatten(_type) {
|
|
643
684
|
const meta = _type.metadata;
|
|
644
685
|
const engine = meta.get("db.mysql.engine");
|
|
@@ -749,6 +790,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
749
790
|
*
|
|
750
791
|
* MySQL uses numeric error codes:
|
|
751
792
|
* - 1062 = ER_DUP_ENTRY (unique constraint violation)
|
|
793
|
+
* - 1586 = ER_DUP_ENTRY_WITH_KEY_NAME (same, for a multi-row INSERT)
|
|
752
794
|
* - 1451 = ER_ROW_IS_REFERENCED_2 (FK violation on delete)
|
|
753
795
|
* - 1452 = ER_NO_REFERENCED_ROW_2 (FK violation on insert/update)
|
|
754
796
|
*/
|
|
@@ -756,16 +798,20 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
756
798
|
try {
|
|
757
799
|
return await fn();
|
|
758
800
|
} catch (error) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
801
|
+
return this._mapConstraintError(error);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
/** Rethrows `error` as a structured `DbError` when it is a unique / FK violation, else as is. */
|
|
805
|
+
_mapConstraintError(error) {
|
|
806
|
+
if (error && typeof error === "object" && "errno" in error) {
|
|
807
|
+
const err = error;
|
|
808
|
+
if (err.errno === 1062 || err.errno === 1586) throw new _atscript_db.DbError("CONFLICT", [{
|
|
809
|
+
path: (err.message?.match(/^[\s\S]*for key '(?:[^']*\.)?([^'.]+)'/))?.[1] ?? "",
|
|
810
|
+
message: err.sqlMessage ?? err.message
|
|
811
|
+
}]);
|
|
812
|
+
if (err.errno === 1451 || err.errno === 1452) throw new _atscript_db.DbError("FK_VIOLATION", this._mapFkError(err.message));
|
|
768
813
|
}
|
|
814
|
+
throw error;
|
|
769
815
|
}
|
|
770
816
|
_mapFkError(message) {
|
|
771
817
|
const fkMatch = message.match(/FOREIGN KEY \(`(\w+)`\)/);
|
|
@@ -794,16 +840,21 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
794
840
|
};
|
|
795
841
|
return this.withTransaction(async () => {
|
|
796
842
|
const tableName = this.resolveTableName();
|
|
797
|
-
const columns = (0, _atscript_db_sql_tools.
|
|
798
|
-
const maxRowsPerBatch = columns.length > 0 ? Math.floor(6e4 / columns.length) : data.length;
|
|
843
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
799
844
|
const allIds = [];
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const
|
|
805
|
-
|
|
806
|
-
|
|
845
|
+
const step = this._incrementStep();
|
|
846
|
+
const zero = this._zeroIsExplicit();
|
|
847
|
+
for (const batch of batches) {
|
|
848
|
+
const ids = Array.from({ length: batch.length });
|
|
849
|
+
for (const group of await this._idGroups(batch, zero)) {
|
|
850
|
+
const { sql, params } = buildInsertMany(tableName, group.rows, columns);
|
|
851
|
+
this._log(sql, params);
|
|
852
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
853
|
+
(await this._groupInsertedIds(group, result.insertId, step)).forEach((id, k) => {
|
|
854
|
+
ids[group.at[k]] = id;
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
allIds.push(...ids);
|
|
807
858
|
}
|
|
808
859
|
return {
|
|
809
860
|
insertedCount: allIds.length,
|
|
@@ -811,6 +862,230 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
811
862
|
};
|
|
812
863
|
});
|
|
813
864
|
}
|
|
865
|
+
/** Physical column of the single-column AUTO_INCREMENT primary key, if the table has one. */
|
|
866
|
+
_autoIncrementPk() {
|
|
867
|
+
const pks = this._table.primaryKeys;
|
|
868
|
+
if (pks.length !== 1 || !this._incrementFields.has(pks[0])) return void 0;
|
|
869
|
+
return this._table.getMetadata().physicalPath(pks[0]);
|
|
870
|
+
}
|
|
871
|
+
/**
|
|
872
|
+
* Splits `rows` into runs that each map to ONE statement with a derivable
|
|
873
|
+
* id sequence. MySQL reports only the first GENERATED id of a statement, and
|
|
874
|
+
* an explicit auto-increment value above the counter bumps the counter, so a
|
|
875
|
+
* statement mixing explicit and generated PKs cannot be mapped by
|
|
876
|
+
* `insertId + i * step`: the chunk is cut into CONSECUTIVE runs of one kind,
|
|
877
|
+
* executed in input order (so "an earlier row wins" a unique collision, even
|
|
878
|
+
* under a case-insensitive collation). A table without an AUTO_INCREMENT PK,
|
|
879
|
+
* or a chunk of one kind, stays one group.
|
|
880
|
+
*/
|
|
881
|
+
async _idGroups(rows, zeroIsExplicit) {
|
|
882
|
+
const col = this._autoIncrementPk();
|
|
883
|
+
if (!col) return [{
|
|
884
|
+
rows,
|
|
885
|
+
at: rows.map((_, i) => i),
|
|
886
|
+
generated: false
|
|
887
|
+
}];
|
|
888
|
+
const zeroExplicit = rows.some((r) => isZero(r[col])) && await zeroIsExplicit();
|
|
889
|
+
const groups = [];
|
|
890
|
+
rows.forEach((row, i) => {
|
|
891
|
+
const v = row[col];
|
|
892
|
+
const generated = v === void 0 || v === null || isZero(v) && !zeroExplicit;
|
|
893
|
+
let last = groups.at(-1);
|
|
894
|
+
if (!last || last.generated !== generated) {
|
|
895
|
+
last = {
|
|
896
|
+
rows: [],
|
|
897
|
+
at: [],
|
|
898
|
+
generated
|
|
899
|
+
};
|
|
900
|
+
groups.push(last);
|
|
901
|
+
}
|
|
902
|
+
last.rows.push(row);
|
|
903
|
+
last.at.push(i);
|
|
904
|
+
});
|
|
905
|
+
return groups;
|
|
906
|
+
}
|
|
907
|
+
/** The session `sql_mode` text on the current connection (`undefined` when the server returns no row). */
|
|
908
|
+
async _sessionSqlMode() {
|
|
909
|
+
const row = await this._exec().get("SELECT @@SESSION.sql_mode AS mode", []);
|
|
910
|
+
if (!row) return void 0;
|
|
911
|
+
return typeof row.mode === "string" ? row.mode : "";
|
|
912
|
+
}
|
|
913
|
+
/** The {@link TZeroIsExplicit} of one `insertMany` / `insertManyIgnore` call. */
|
|
914
|
+
_zeroIsExplicit() {
|
|
915
|
+
let mode;
|
|
916
|
+
return () => mode ??= this._sessionSqlMode().then((text) => (text ?? "").includes("NO_AUTO_VALUE_ON_ZERO"));
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Warns ONCE per driver when the session `sql_mode` is not strict: writes
|
|
920
|
+
* (insert-ignore included) assume `STRICT_TRANS_TABLES` / `STRICT_ALL_TABLES`
|
|
921
|
+
* (the MySQL 8 default); a non-strict mode coerces a NOT NULL violation to the
|
|
922
|
+
* column default instead of failing. Only probes when the adapter has a logger.
|
|
923
|
+
*/
|
|
924
|
+
async _warnNonStrictMode() {
|
|
925
|
+
if (this.logger === _atscript_db.NoopLogger || nonStrictChecked.has(this.driver)) return;
|
|
926
|
+
nonStrictChecked.add(this.driver);
|
|
927
|
+
try {
|
|
928
|
+
const mode = await this._sessionSqlMode();
|
|
929
|
+
if (mode !== void 0 && !/STRICT_(TRANS|ALL)_TABLES/.test(mode)) this.logger.warn(`MySQL session sql_mode lacks STRICT_TRANS_TABLES / STRICT_ALL_TABLES (${mode}): writes assume a strict mode and a non-strict server silently coerces NOT NULL violations`);
|
|
930
|
+
} catch {
|
|
931
|
+
nonStrictChecked.delete(this.driver);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/** The {@link TIncrementStep} of one `insertMany` / `insertManyIgnore` call. */
|
|
935
|
+
_incrementStep() {
|
|
936
|
+
let step;
|
|
937
|
+
return () => step ??= (async () => {
|
|
938
|
+
const row = await this._exec().get("SELECT @@auto_increment_increment AS step", []);
|
|
939
|
+
const n = Number(row?.step);
|
|
940
|
+
return Number.isInteger(n) && n > 0 ? n : 1;
|
|
941
|
+
})();
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* Ids of the rows of ONE successful multi-row INSERT of a homogeneous
|
|
945
|
+
* {@link _idGroups} group: generated rows get ids from `insertId` stepping by
|
|
946
|
+
* the session's `@@auto_increment_increment` (consecutive within one
|
|
947
|
+
* statement under `innodb_autoinc_lock_mode` 0 / 1, and — for a known row
|
|
948
|
+
* count — mode 2, the 8.0 default), explicit rows keep their own value.
|
|
949
|
+
*/
|
|
950
|
+
async _groupInsertedIds(group, insertId, step) {
|
|
951
|
+
const first = Number(insertId);
|
|
952
|
+
const stride = group.generated && group.rows.length > 1 && first > 0 ? await step() : 1;
|
|
953
|
+
return group.rows.map((row, i) => {
|
|
954
|
+
const generatedId = first > 0 ? first + i * stride : 0;
|
|
955
|
+
return group.generated ? generatedId : this._resolveInsertedId(row, generatedId);
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
supportsInsertIgnore() {
|
|
959
|
+
return true;
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* Per chunk: ONE optimistic multi-row INSERT (an all-new batch costs a single
|
|
963
|
+
* statement). Only when it hits a duplicate key (errno 1062 / 1586) does ONE
|
|
964
|
+
* SELECT of the chunk's primary / unique key tuples find the stored rows
|
|
965
|
+
* (skipped as conflicts) and the survivors go in as one more multi-row
|
|
966
|
+
* INSERT — a dense-duplicate chunk is three statements, never O(rows). Only
|
|
967
|
+
* if that INSERT still collides (a concurrent writer raced in, or a
|
|
968
|
+
* collation-equal value the exact-match pre-check missed) are the survivors
|
|
969
|
+
* bisected: each half is retried, recursively, and a single row that still
|
|
970
|
+
* collides is skipped. A failed statement is rolled back by InnoDB alone, so
|
|
971
|
+
* the transaction stays usable. A chunk mixing explicit and generated
|
|
972
|
+
* auto-increment ids is processed as one such sequence per consecutive run of a kind. Deliberately
|
|
973
|
+
* NOT `INSERT IGNORE` (it would downgrade NOT NULL / FK / truncation errors
|
|
974
|
+
* to warnings) and not `ON DUPLICATE KEY UPDATE` (a no-op update is
|
|
975
|
+
* indistinguishable from an insert in the affected-rows count).
|
|
976
|
+
*/
|
|
977
|
+
async insertManyIgnore(data) {
|
|
978
|
+
if (data.length === 0) return [];
|
|
979
|
+
await this._warnNonStrictMode();
|
|
980
|
+
return this.withTransaction(async () => {
|
|
981
|
+
const tableName = this.resolveTableName();
|
|
982
|
+
const { columns, batches } = (0, _atscript_db_sql_tools.chunkInsertRows)(data);
|
|
983
|
+
const slots = [];
|
|
984
|
+
const step = this._incrementStep();
|
|
985
|
+
const zero = this._zeroIsExplicit();
|
|
986
|
+
for (const batch of batches) {
|
|
987
|
+
const out = Array.from({ length: batch.length }, () => null);
|
|
988
|
+
for (const group of await this._idGroups(batch, zero)) (await this._insertIgnoringGroup(tableName, columns, group, step)).forEach((slot, k) => {
|
|
989
|
+
out[group.at[k]] = slot;
|
|
990
|
+
});
|
|
991
|
+
slots.push(...out);
|
|
992
|
+
}
|
|
993
|
+
return slots;
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Indices of `rows` whose primary / unique-index key tuple is already stored
|
|
998
|
+
* (a row with a null / missing key component never collides). One SELECT
|
|
999
|
+
* covers every key set; it is split only to stay under the parameter limit.
|
|
1000
|
+
* Skipped entirely when no row carries a key value (generated PK, no unique
|
|
1001
|
+
* index values).
|
|
1002
|
+
*/
|
|
1003
|
+
async _findStoredKeyConflicts(tableName, rows) {
|
|
1004
|
+
const keySets = this._table.uniqueKeySets.filter((f) => f.length > 0);
|
|
1005
|
+
const rowTuples = rows.map((row) => keySets.map((fields) => (0, _atscript_db.uniqueKeyTuple)(row, fields)));
|
|
1006
|
+
const used = keySets.map((_, k) => rowTuples.some((t) => t[k] !== void 0));
|
|
1007
|
+
if (!used.includes(true)) return /* @__PURE__ */ new Set();
|
|
1008
|
+
const width = keySets.reduce((n, f, k) => n + (used[k] ? f.length : 0), 0);
|
|
1009
|
+
const sliceSize = Math.max(1, Math.floor(6e4 / width));
|
|
1010
|
+
const selectCols = [...new Set(keySets.flat())].map((c) => qi(c)).join(", ");
|
|
1011
|
+
const stored = keySets.map(() => /* @__PURE__ */ new Set());
|
|
1012
|
+
for (let offset = 0; offset < rows.length; offset += sliceSize) {
|
|
1013
|
+
const end = Math.min(rows.length, offset + sliceSize);
|
|
1014
|
+
const clauses = [];
|
|
1015
|
+
const params = [];
|
|
1016
|
+
keySets.forEach((fields, k) => {
|
|
1017
|
+
if (!used[k]) return;
|
|
1018
|
+
const group = [];
|
|
1019
|
+
for (let i = offset; i < end; i++) if (rowTuples[i][k] !== void 0) group.push(rows[i]);
|
|
1020
|
+
if (group.length === 0) return;
|
|
1021
|
+
const values = (row) => fields.map((f) => mysqlDialect.toValue(row[f]));
|
|
1022
|
+
if (fields.length === 1) {
|
|
1023
|
+
clauses.push(`${qi(fields[0])} IN (${group.map(() => "?").join(", ")})`);
|
|
1024
|
+
for (const row of group) params.push(...values(row));
|
|
1025
|
+
} else {
|
|
1026
|
+
const tuple = `(${fields.map(() => "?").join(", ")})`;
|
|
1027
|
+
clauses.push(`(${fields.map((f) => qi(f)).join(", ")}) IN (${group.map(() => tuple).join(", ")})`);
|
|
1028
|
+
for (const row of group) params.push(...values(row));
|
|
1029
|
+
}
|
|
1030
|
+
});
|
|
1031
|
+
if (clauses.length === 0) continue;
|
|
1032
|
+
const sql = `SELECT ${selectCols} FROM ${quoteTableName(tableName)} WHERE ${clauses.join(" OR ")}`;
|
|
1033
|
+
this._log(sql, params);
|
|
1034
|
+
const found = await this._exec().all(sql, params);
|
|
1035
|
+
for (const doc of found) keySets.forEach((fields, k) => {
|
|
1036
|
+
const tuple = (0, _atscript_db.uniqueKeyTuple)(doc, fields);
|
|
1037
|
+
if (tuple !== void 0) stored[k].add(tuple);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
const skipped = /* @__PURE__ */ new Set();
|
|
1041
|
+
rowTuples.forEach((tuples, i) => {
|
|
1042
|
+
if (tuples.some((t, k) => t !== void 0 && stored[k].has(t))) skipped.add(i);
|
|
1043
|
+
});
|
|
1044
|
+
return skipped;
|
|
1045
|
+
}
|
|
1046
|
+
/** Optimistic INSERT of a group, then pre-check + survivor INSERT (+ bisect on a race). */
|
|
1047
|
+
async _insertIgnoringGroup(tableName, columns, group, step) {
|
|
1048
|
+
const direct = await this._tryInsertGroup(tableName, columns, group, step);
|
|
1049
|
+
if (direct) return direct;
|
|
1050
|
+
if (group.rows.length === 1) return [null];
|
|
1051
|
+
const skipped = await this._findStoredKeyConflicts(tableName, group.rows);
|
|
1052
|
+
if (skipped.size === 0) return this._bisectGroup(tableName, columns, group, step);
|
|
1053
|
+
const survivors = {
|
|
1054
|
+
rows: group.rows.filter((_, i) => !skipped.has(i)),
|
|
1055
|
+
at: [],
|
|
1056
|
+
generated: group.generated
|
|
1057
|
+
};
|
|
1058
|
+
const inserted = survivors.rows.length > 0 ? await this._tryInsertGroup(tableName, columns, survivors, step) ?? (survivors.rows.length === 1 ? [null] : await this._bisectGroup(tableName, columns, survivors, step)) : [];
|
|
1059
|
+
let next = 0;
|
|
1060
|
+
return group.rows.map((_, i) => skipped.has(i) ? null : inserted[next++]);
|
|
1061
|
+
}
|
|
1062
|
+
/** Retries the halves of a group whose one INSERT is known to collide; a lone colliding row is skipped. */
|
|
1063
|
+
async _bisectGroup(tableName, columns, group, step) {
|
|
1064
|
+
const mid = group.rows.length >> 1;
|
|
1065
|
+
const out = [];
|
|
1066
|
+
for (const rows of [group.rows.slice(0, mid), group.rows.slice(mid)]) {
|
|
1067
|
+
const half = {
|
|
1068
|
+
rows,
|
|
1069
|
+
at: [],
|
|
1070
|
+
generated: group.generated
|
|
1071
|
+
};
|
|
1072
|
+
out.push(...await this._tryInsertGroup(tableName, columns, half, step) ?? (rows.length === 1 ? [null] : await this._bisectGroup(tableName, columns, half, step)));
|
|
1073
|
+
}
|
|
1074
|
+
return out;
|
|
1075
|
+
}
|
|
1076
|
+
/** ONE INSERT of a group's rows; `undefined` on a duplicate key (errno 1062 / 1586). */
|
|
1077
|
+
async _tryInsertGroup(tableName, columns, group, step) {
|
|
1078
|
+
const { sql, params } = buildInsertMany(tableName, group.rows, columns);
|
|
1079
|
+
this._log(sql, params);
|
|
1080
|
+
try {
|
|
1081
|
+
const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
|
|
1082
|
+
return (await this._groupInsertedIds(group, result.insertId, step)).map((insertedId) => ({ insertedId }));
|
|
1083
|
+
} catch (error) {
|
|
1084
|
+
if (!(0, _atscript_db.isConflict)(error)) throw error;
|
|
1085
|
+
if (group.generated && error.errors.some((e) => e.path === "PRIMARY")) throw error;
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
814
1089
|
async findOne(query) {
|
|
815
1090
|
const where = buildWhere(query.filter);
|
|
816
1091
|
const controls = {
|
|
@@ -827,6 +1102,16 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
827
1102
|
this._log(sql, params);
|
|
828
1103
|
return this._exec().all(sql, params);
|
|
829
1104
|
}
|
|
1105
|
+
/**
|
|
1106
|
+
* `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
|
|
1107
|
+
* window over `partitionBy` (the generic `$with` loader's per-parent page).
|
|
1108
|
+
*/
|
|
1109
|
+
async findManyPerPartition(query, partitionBy) {
|
|
1110
|
+
const where = buildWhere(query.filter);
|
|
1111
|
+
const { sql, params } = (0, _atscript_db_sql_tools.buildPartitionedSelect)(mysqlDialect, this.resolveTableName(), where, query.controls, partitionBy);
|
|
1112
|
+
this._log(sql, params);
|
|
1113
|
+
return (0, _atscript_db_sql_tools.stripPartitionRowNumber)(await this._exec().all(sql, params));
|
|
1114
|
+
}
|
|
830
1115
|
async count(query) {
|
|
831
1116
|
const where = buildWhere(query.filter);
|
|
832
1117
|
const sql = `SELECT COUNT(*) as cnt FROM ${quoteTableName(this.resolveTableName())} WHERE ${where.sql}`;
|
|
@@ -841,11 +1126,11 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
841
1126
|
if (query.controls.$count) {
|
|
842
1127
|
const { sql, params } = buildAggregateCount(tableName, where, query.controls);
|
|
843
1128
|
this._log(sql, params);
|
|
844
|
-
return [{ count: (await this._exec().get(sql, params))?.count ?? 0 }];
|
|
1129
|
+
return [{ count: (await (0, _atscript_db_sql_tools.mapQueryErrors)(mysqlDialect, () => this._exec().get(sql, params), query.controls))?.count ?? 0 }];
|
|
845
1130
|
}
|
|
846
1131
|
const { sql, params } = buildAggregateSelect(tableName, where, query.controls);
|
|
847
1132
|
this._log(sql, params);
|
|
848
|
-
return this._exec().all(sql, params);
|
|
1133
|
+
return (0, _atscript_db_sql_tools.mapQueryErrors)(mysqlDialect, () => this._exec().all(sql, params), query.controls);
|
|
849
1134
|
}
|
|
850
1135
|
/**
|
|
851
1136
|
* Verifies that `CONVERT_TZ` can convert to a calendar bucket's zone before
|
|
@@ -871,8 +1156,50 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
871
1156
|
}
|
|
872
1157
|
zones.add(tz);
|
|
873
1158
|
}
|
|
874
|
-
|
|
1159
|
+
/**
|
|
1160
|
+
* The WHERE of an UPDATE / DELETE on this table. MySQL rejects a statement
|
|
1161
|
+
* whose WHERE reads the mutated table in a subquery (error 1093,
|
|
1162
|
+
* `ER_UPDATE_TABLE_USED`) — what a relational predicate (`$some` / `$none`)
|
|
1163
|
+
* does when its target or junction table, at any nesting level, is this
|
|
1164
|
+
* table (a self relation such as `parent`). Such a filter is re-keyed on the
|
|
1165
|
+
* primary key through a materialized derived table:
|
|
1166
|
+
*
|
|
1167
|
+
* ```sql
|
|
1168
|
+
* WHERE (<pk…>) IN (SELECT * FROM (SELECT DISTINCT <pk…> FROM t WHERE <where>) AS `_rfm`)
|
|
1169
|
+
* ```
|
|
1170
|
+
*
|
|
1171
|
+
* `DISTINCT` keeps the optimizer from merging the derived table back into
|
|
1172
|
+
* the statement. Every other filter renders as-is.
|
|
1173
|
+
*
|
|
1174
|
+
* @since 0.1.147
|
|
1175
|
+
*/
|
|
1176
|
+
_mutationWhere(filter) {
|
|
875
1177
|
const where = buildWhere(filter);
|
|
1178
|
+
if (!this._filterReadsOwnTable(filter)) return where;
|
|
1179
|
+
const tableName = this.resolveTableName();
|
|
1180
|
+
const keys = this._table.primaryKeys.map((key) => this._table.physicalPath(key));
|
|
1181
|
+
if (keys.length === 0) throw new _atscript_db.DbError("REL_FILTER_NOT_SUPPORTED", [{
|
|
1182
|
+
path: "",
|
|
1183
|
+
message: "MySQL cannot update or delete by a relational predicate that reads the table itself when the table has no primary key"
|
|
1184
|
+
}]);
|
|
1185
|
+
const cols = keys.map((key) => qi(key)).join(", ");
|
|
1186
|
+
return {
|
|
1187
|
+
sql: `${keys.length === 1 ? cols : `(${cols})`} IN (SELECT * FROM (SELECT DISTINCT ${cols} FROM ${quoteTableName(tableName)} WHERE ${where.sql}) AS ${qi("_rfm")})`,
|
|
1188
|
+
params: where.params
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
/** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
|
|
1192
|
+
_filterReadsOwnTable(filter) {
|
|
1193
|
+
if (!(0, _atscript_db.containsRelationFilter)(filter)) return false;
|
|
1194
|
+
const own = this.resolveTableName();
|
|
1195
|
+
let hit = false;
|
|
1196
|
+
(0, _atscript_db.forEachResolvedRelation)(filter, (node) => {
|
|
1197
|
+
if (node.target.table === own || node.junction?.table === own) hit = true;
|
|
1198
|
+
});
|
|
1199
|
+
return hit;
|
|
1200
|
+
}
|
|
1201
|
+
async updateOne(filter, data, ops, expectedVersion) {
|
|
1202
|
+
const where = this._mutationWhere(filter);
|
|
876
1203
|
const versionColumn = this._table.versionColumnPhysical;
|
|
877
1204
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, 1, ops, versionColumn, expectedVersion);
|
|
878
1205
|
this._log(sql, params);
|
|
@@ -883,7 +1210,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
883
1210
|
};
|
|
884
1211
|
}
|
|
885
1212
|
async updateMany(filter, data, ops) {
|
|
886
|
-
const where =
|
|
1213
|
+
const where = this._mutationWhere(filter);
|
|
887
1214
|
const versionColumn = this._table.versionColumnPhysical;
|
|
888
1215
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, ops, versionColumn);
|
|
889
1216
|
this._log(sql, params);
|
|
@@ -894,7 +1221,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
894
1221
|
};
|
|
895
1222
|
}
|
|
896
1223
|
async replaceOne(filter, data, expectedVersion) {
|
|
897
|
-
const where =
|
|
1224
|
+
const where = this._mutationWhere(filter);
|
|
898
1225
|
const versionColumn = this._table.versionColumnPhysical;
|
|
899
1226
|
const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, (0, _atscript_db_sql_tools.replaceColumnsFor)(this._table.fieldDescriptors, this.nativeDefaultFns()), versionColumn);
|
|
900
1227
|
const { sql, params } = buildUpdate(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
|
|
@@ -906,7 +1233,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
906
1233
|
};
|
|
907
1234
|
}
|
|
908
1235
|
async replaceMany(filter, data) {
|
|
909
|
-
const where =
|
|
1236
|
+
const where = this._mutationWhere(filter);
|
|
910
1237
|
const versionColumn = this._table.versionColumnPhysical;
|
|
911
1238
|
const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
|
|
912
1239
|
this._log(sql, params);
|
|
@@ -917,18 +1244,19 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
917
1244
|
};
|
|
918
1245
|
}
|
|
919
1246
|
async deleteOne(filter) {
|
|
920
|
-
const where =
|
|
1247
|
+
const where = this._mutationWhere(filter);
|
|
921
1248
|
const { sql, params } = buildDelete(this.resolveTableName(), where, 1);
|
|
922
1249
|
this._log(sql, params);
|
|
923
1250
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
924
1251
|
}
|
|
925
1252
|
async deleteMany(filter) {
|
|
926
|
-
const where =
|
|
1253
|
+
const where = this._mutationWhere(filter);
|
|
927
1254
|
const { sql, params } = buildDelete(this.resolveTableName(), where);
|
|
928
1255
|
this._log(sql, params);
|
|
929
1256
|
return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
|
|
930
1257
|
}
|
|
931
1258
|
async prepareTypeMapper() {
|
|
1259
|
+
await this._warnNonStrictMode();
|
|
932
1260
|
if (this._supportsVector === void 0 && this._vectorFields.size > 0) await this._detectVectorSupport();
|
|
933
1261
|
}
|
|
934
1262
|
async ensureTable(opts) {
|
|
@@ -1295,7 +1623,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1295
1623
|
async syncForeignKeys() {
|
|
1296
1624
|
const existingByName = await this._getExistingFkConstraints();
|
|
1297
1625
|
const desiredFkKeys = /* @__PURE__ */ new Set();
|
|
1298
|
-
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fk.fields].toSorted().join(","));
|
|
1626
|
+
for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(","));
|
|
1299
1627
|
for (const [constraintName, columns] of existingByName) {
|
|
1300
1628
|
const key = columns.toSorted().join(",");
|
|
1301
1629
|
if (!desiredFkKeys.has(key)) {
|
|
@@ -1306,13 +1634,9 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1306
1634
|
}
|
|
1307
1635
|
const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
|
|
1308
1636
|
for (const fk of this._table.foreignKeys.values()) {
|
|
1309
|
-
const key = [...fk.fields].toSorted().join(",");
|
|
1637
|
+
const key = [...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(",");
|
|
1310
1638
|
if (!existingKeys.has(key)) {
|
|
1311
|
-
const
|
|
1312
|
-
const targetCols = fk.targetFields.map((f) => qi(f)).join(", ");
|
|
1313
|
-
let ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD FOREIGN KEY (${localCols}) REFERENCES ${qi(fk.targetTable)} (${targetCols})`;
|
|
1314
|
-
if (fk.onDelete) ddl += ` ON DELETE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onDelete)}`;
|
|
1315
|
-
if (fk.onUpdate) ddl += ` ON UPDATE ${(0, _atscript_db_sql_tools.refActionToSql)(fk.onUpdate)}`;
|
|
1639
|
+
const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
|
|
1316
1640
|
this._log(ddl);
|
|
1317
1641
|
await this._exec().exec(ddl);
|
|
1318
1642
|
}
|
|
@@ -1472,7 +1796,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1472
1796
|
vec = first.value[1];
|
|
1473
1797
|
}
|
|
1474
1798
|
const distanceFn = similarityToMysqlFn(vec.similarity);
|
|
1475
|
-
const where = buildWhere(query.filter);
|
|
1799
|
+
const where = buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) });
|
|
1476
1800
|
const controls = query.controls || {};
|
|
1477
1801
|
const threshold = this._resolveVectorThreshold(controls, vec.indexName);
|
|
1478
1802
|
return {
|
|
@@ -1550,7 +1874,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1550
1874
|
const controls = query.controls ?? {};
|
|
1551
1875
|
return {
|
|
1552
1876
|
tableName: this.resolveTableName(),
|
|
1553
|
-
where: buildWhere(query.filter),
|
|
1877
|
+
where: buildWhere(query.filter, { qualifier: mysqlDialect.quoteTable(_atscript_db_sql_tools.SEARCH_SOURCE_ALIAS) }),
|
|
1554
1878
|
dist: mysqlGeoDistanceExpr(qi(column), point),
|
|
1555
1879
|
window: (0, _atscript_db_sql_tools.geoWindowFromControls)(controls),
|
|
1556
1880
|
controls
|
|
@@ -1634,6 +1958,26 @@ function atscriptTypeCast(field, next) {
|
|
|
1634
1958
|
}
|
|
1635
1959
|
return next();
|
|
1636
1960
|
}
|
|
1961
|
+
/** Appends `STRICT_TRANS_TABLES` to the session `sql_mode` unless a strict mode is already set. */
|
|
1962
|
+
const ENSURE_STRICT_SQL = "SET SESSION sql_mode = IF(FIND_IN_SET('STRICT_TRANS_TABLES', @@SESSION.sql_mode) OR FIND_IN_SET('STRICT_ALL_TABLES', @@SESSION.sql_mode), @@SESSION.sql_mode, CONCAT_WS(',', NULLIF(@@SESSION.sql_mode, ''), 'STRICT_TRANS_TABLES'))";
|
|
1963
|
+
/**
|
|
1964
|
+
* Runs {@link ENSURE_STRICT_SQL} once on every connection of the pool: on
|
|
1965
|
+
* `connection` for the ones the pool opens, and on `acquire` for a connection
|
|
1966
|
+
* opened before the driver was constructed (a pre-used pool). The statement is
|
|
1967
|
+
* queued on the connection before the pool hands it out, so it precedes every
|
|
1968
|
+
* query; a failure leaves the server's mode in place.
|
|
1969
|
+
*/
|
|
1970
|
+
function ensureStrict(pool) {
|
|
1971
|
+
const raw = pool;
|
|
1972
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1973
|
+
const strict = (conn) => {
|
|
1974
|
+
if (seen.has(conn)) return;
|
|
1975
|
+
seen.add(conn);
|
|
1976
|
+
conn.query(ENSURE_STRICT_SQL, () => {});
|
|
1977
|
+
};
|
|
1978
|
+
raw.on("connection", strict);
|
|
1979
|
+
raw.on("acquire", strict);
|
|
1980
|
+
}
|
|
1637
1981
|
/**
|
|
1638
1982
|
* {@link TMysqlDriver} implementation backed by `mysql2/promise`.
|
|
1639
1983
|
*
|
|
@@ -1661,6 +2005,11 @@ function atscriptTypeCast(field, next) {
|
|
|
1661
2005
|
* const driver = new Mysql2Driver(pool)
|
|
1662
2006
|
* ```
|
|
1663
2007
|
*
|
|
2008
|
+
* Every new pool connection gets `STRICT_TRANS_TABLES` appended to its session
|
|
2009
|
+
* `sql_mode` (since 0.1.148); pass `{ strictMode: false }` as the second
|
|
2010
|
+
* argument to opt out. A pre-created `Pool` is covered too: connections it
|
|
2011
|
+
* opened earlier get the statement when they are first acquired through it.
|
|
2012
|
+
*
|
|
1664
2013
|
* Requires `mysql2` to be installed:
|
|
1665
2014
|
* ```bash
|
|
1666
2015
|
* pnpm add mysql2
|
|
@@ -1669,9 +2018,12 @@ function atscriptTypeCast(field, next) {
|
|
|
1669
2018
|
var Mysql2Driver = class {
|
|
1670
2019
|
pool;
|
|
1671
2020
|
poolInit;
|
|
1672
|
-
constructor(poolOrConfig) {
|
|
1673
|
-
|
|
1674
|
-
|
|
2021
|
+
constructor(poolOrConfig, options = {}) {
|
|
2022
|
+
const strict = options.strictMode !== false;
|
|
2023
|
+
if (typeof poolOrConfig === "object" && "execute" in poolOrConfig) {
|
|
2024
|
+
this.pool = poolOrConfig;
|
|
2025
|
+
if (strict) ensureStrict(this.pool);
|
|
2026
|
+
} else this.poolInit = import("mysql2/promise").then((mysql) => {
|
|
1675
2027
|
if (typeof poolOrConfig === "string") this.pool = mysql.createPool({
|
|
1676
2028
|
uri: poolOrConfig,
|
|
1677
2029
|
timezone: "+00:00",
|
|
@@ -1686,6 +2038,7 @@ var Mysql2Driver = class {
|
|
|
1686
2038
|
bigNumberStrings: false,
|
|
1687
2039
|
typeCast: atscriptTypeCast
|
|
1688
2040
|
});
|
|
2041
|
+
if (strict) ensureStrict(this.pool);
|
|
1689
2042
|
return this.pool;
|
|
1690
2043
|
});
|
|
1691
2044
|
}
|
|
@@ -1740,8 +2093,10 @@ var Mysql2Driver = class {
|
|
|
1740
2093
|
}
|
|
1741
2094
|
};
|
|
1742
2095
|
}
|
|
1743
|
-
|
|
1744
|
-
|
|
2096
|
+
_closing;
|
|
2097
|
+
/** Idempotent: every call returns the first call's promise. */
|
|
2098
|
+
close() {
|
|
2099
|
+
return this._closing ??= Promise.resolve(this.getPool()).then((pool) => pool.end());
|
|
1745
2100
|
}
|
|
1746
2101
|
};
|
|
1747
2102
|
//#endregion
|
|
@@ -1750,15 +2105,17 @@ var Mysql2Driver = class {
|
|
|
1750
2105
|
* Creates a {@link DbSpace} backed by a MySQL connection pool.
|
|
1751
2106
|
*
|
|
1752
2107
|
* @param uri - MySQL connection URI (e.g., `mysql://root@localhost:3306/mydb`)
|
|
1753
|
-
* @param options - Additional pool options passed to mysql2.
|
|
2108
|
+
* @param options - Additional pool options passed to mysql2. `strictMode: false`
|
|
2109
|
+
* (not a pool option) opts out of the per-session `STRICT_TRANS_TABLES` ({@link Mysql2Driver}).
|
|
1754
2110
|
* @returns A `DbSpace` that creates `MysqlAdapter` instances per table.
|
|
1755
2111
|
*/
|
|
1756
2112
|
function createAdapter(uri, options) {
|
|
2113
|
+
const { strictMode, ...pool } = options ?? {};
|
|
1757
2114
|
const driver = new Mysql2Driver({
|
|
1758
2115
|
uri,
|
|
1759
|
-
...
|
|
1760
|
-
});
|
|
1761
|
-
return new _atscript_db.DbSpace(() => new MysqlAdapter(driver));
|
|
2116
|
+
...pool
|
|
2117
|
+
}, { strictMode });
|
|
2118
|
+
return new _atscript_db.DbSpace(() => new MysqlAdapter(driver), { onClose: () => driver.close() });
|
|
1762
2119
|
}
|
|
1763
2120
|
//#endregion
|
|
1764
2121
|
exports.Mysql2Driver = Mysql2Driver;
|