@atscript/db-mysql 0.1.161 → 0.1.163

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 CHANGED
@@ -4,6 +4,16 @@ let _atscript_db_agg = require("@atscript/db/agg");
4
4
  let _atscript_db_sql_tools = require("@atscript/db-sql-tools");
5
5
  let _uniqu_core = require("@uniqu/core");
6
6
  //#region src/sql-builder.ts
7
+ /**
8
+ * A MySQL string literal: quotes doubled and backslashes escaped — MySQL
9
+ * reads `\` as an escape in a string literal (unless the server runs with
10
+ * `NO_BACKSLASH_ESCAPES`), so the shared `sqlStringLiteral` would drop or
11
+ * reinterpret them (`'a\b'` stores a backspace, `'x\'` is unterminated).
12
+ * @since 0.1.162
13
+ */
14
+ function mysqlStringLiteral(value) {
15
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
16
+ }
7
17
  /** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
8
18
  const TEXT_BLOB_RE = /^(TINY|MEDIUM|LONG)?(TEXT|BLOB)\b/i;
9
19
  /** Spatial types (`POINT SRID 4326`, `GEOMETRY`, `MULTI*`, …). */
@@ -13,11 +23,19 @@ function needsExpressionDefault(sqlType) {
13
23
  return TEXT_BLOB_RE.test(sqlType) || /^JSON\b/i.test(sqlType) || GEOMETRY_RE.test(sqlType);
14
24
  }
15
25
  /**
26
+ * `defaultValueToSqlLiteral` for MySQL: a string literal also escapes
27
+ * backslashes, which MySQL reads as escapes (unless `NO_BACKSLASH_ESCAPES`) —
28
+ * `'a\\b'` would store `a\b` minus one backslash otherwise.
29
+ */
30
+ function mysqlValueLiteral(designType, value) {
31
+ return (0, _atscript_db_sql_tools.defaultValueToSqlLiteral)(designType, value, mysqlStringLiteral);
32
+ }
33
+ /**
16
34
  * Renders a model value default for a column of `sqlType`: the SQL literal,
17
35
  * wrapped in the expression form for the types that require it.
18
36
  */
19
37
  function mysqlDefaultLiteral(sqlType, designType, value) {
20
- const literal = (0, _atscript_db_sql_tools.defaultValueToSqlLiteral)(designType, value);
38
+ const literal = mysqlValueLiteral(designType, value);
21
39
  return needsExpressionDefault(sqlType) ? `(${literal})` : literal;
22
40
  }
23
41
  /**
@@ -153,6 +171,7 @@ const mysqlDialect = {
153
171
  return quoteTableName(name);
154
172
  },
155
173
  unlimitedLimit: "18446744073709551615",
174
+ stringLiteral: mysqlStringLiteral,
156
175
  derivedMergeHint(alias) {
157
176
  return `/*+ MERGE(${qi(alias)}) */`;
158
177
  },
@@ -184,11 +203,11 @@ const mysqlDialect = {
184
203
  bucketAliasInHaving: true,
185
204
  jsonExtract: mysqlJsonExtract,
186
205
  jsonExtractText: (quotedCol, path) => {
187
- const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path)})`;
206
+ const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path, mysqlStringLiteral)})`;
188
207
  return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE JSON_UNQUOTE(${e}) END`;
189
208
  },
190
209
  jsonExtractValue: (quotedCol, path) => {
191
- const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path)})`;
210
+ const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path, mysqlStringLiteral)})`;
192
211
  return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE ${e} END`;
193
212
  },
194
213
  jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
@@ -206,7 +225,7 @@ const MYSQL_JSON_NUMBER_TYPES = "'INTEGER', 'UNSIGNED INTEGER', 'DOUBLE', 'DECIM
206
225
  * `CAST(… AS JSON)` (not in MariaDB).
207
226
  */
208
227
  function mysqlJsonExtract(quotedCol, path, type) {
209
- const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path)})`;
228
+ const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path, mysqlStringLiteral)})`;
210
229
  const t = `JSON_TYPE(${e})`;
211
230
  switch (type) {
212
231
  case "string": return `CASE ${t} WHEN 'STRING' THEN JSON_UNQUOTE(${e}) END`;
@@ -372,21 +391,28 @@ function buildInsertMany(table, rows, columns) {
372
391
  /**
373
392
  * Builds a SELECT statement with optional sort, limit, offset, projection.
374
393
  */
375
- function buildSelect(table, where, controls) {
376
- return (0, _atscript_db_sql_tools.buildSelect)(mysqlDialect, table, where, controls);
394
+ function buildSelect(table, where, controls, lock) {
395
+ return (0, _atscript_db_sql_tools.buildSelect)(mysqlDialect, table, where, controls, lock);
377
396
  }
378
397
  /**
379
- * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
398
+ * Builds an UPDATE ... SET ... WHERE statement with optional [ORDER BY …] LIMIT.
380
399
  */
381
400
  function buildUpdate(table, data, where, limit, ops, versionColumn, expectedVersion) {
382
401
  return (0, _atscript_db_sql_tools.buildUpdate)(mysqlDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
383
402
  }
384
403
  /**
385
- * Builds a DELETE ... WHERE statement with optional LIMIT.
404
+ * Builds a DELETE ... WHERE statement with optional [ORDER BY …] LIMIT.
386
405
  */
387
406
  function buildDelete(table, where, limit) {
388
407
  return (0, _atscript_db_sql_tools.buildDelete)(mysqlDialect, table, where, limit);
389
408
  }
409
+ /** `ORDER BY … LIMIT n` of a bounded write (since 0.1.162), `undefined` when unbounded. */
410
+ function writeLimitClause(bound) {
411
+ return bound && {
412
+ count: (0, _atscript_db_sql_tools.writeLimitCount)(bound),
413
+ orderBy: (0, _atscript_db_sql_tools.orderByList)(mysqlDialect, bound.sort, bound.nulls)
414
+ };
415
+ }
390
416
  /**
391
417
  * Builds a CREATE OR REPLACE VIEW statement from a view plan and column mappings.
392
418
  */
@@ -680,11 +706,24 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
680
706
  _txConnection() {
681
707
  return this._getTransactionState();
682
708
  }
683
- async _beginTransaction() {
709
+ async _beginTransaction(opts) {
684
710
  const conn = await this.driver.getConnection();
685
- await conn.exec("START TRANSACTION");
686
- this._log("START TRANSACTION");
687
- return conn;
711
+ let levelPending = false;
712
+ try {
713
+ if (opts?.isolation) {
714
+ const sql = `SET TRANSACTION ISOLATION LEVEL ${opts.isolation.toUpperCase()}`;
715
+ this._log(sql);
716
+ await conn.exec(sql);
717
+ levelPending = true;
718
+ }
719
+ await conn.exec("START TRANSACTION");
720
+ this._log("START TRANSACTION");
721
+ return conn;
722
+ } catch (err) {
723
+ if (levelPending && conn.destroy) conn.destroy();
724
+ else conn.release();
725
+ throw err;
726
+ }
688
727
  }
689
728
  async _commitTransaction(state) {
690
729
  const conn = state;
@@ -776,6 +815,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
776
815
  supportsNullsPlacement() {
777
816
  return true;
778
817
  }
818
+ /**
819
+ * Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
820
+ * `SKIP LOCKED`] (MySQL 8). A locking read is a current read — it sees the
821
+ * latest committed row even under REPEATABLE READ. @since 0.1.162
822
+ */
823
+ supportsReadLocks() {
824
+ return true;
825
+ }
826
+ /** Every level, set by `SET TRANSACTION ISOLATION LEVEL` before `START TRANSACTION`. @since 0.1.162 */
827
+ isolationLevels() {
828
+ return _atscript_db.ALL_ISOLATION_LEVELS;
829
+ }
779
830
  /** Computed view columns and first-row joins. */
780
831
  viewCapabilities() {
781
832
  return _atscript_db.ALL_VIEW_CAPABILITIES;
@@ -913,7 +964,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
913
964
  message: err.sqlMessage ?? err.message
914
965
  }]);
915
966
  if (err.errno === 1451 || err.errno === 1452) throw new _atscript_db.DbError("FK_VIOLATION", this._mapFkError(err.message));
916
- if (err.errno === 1213 || err.errno === 1205) throw new _atscript_db.DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
967
+ if (err.errno === 1213 || err.errno === 1205 || err.errno === 3572) throw new _atscript_db.DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
917
968
  path: "",
918
969
  message: err.sqlMessage ?? err.message
919
970
  }]);
@@ -1258,7 +1309,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1258
1309
  const target = fields.length === 1 ? cols : `(${cols})`;
1259
1310
  const sql = `SELECT ${cols} FROM ${quoteTableName(tableName)} WHERE ${target} IN (${slice.map(() => tuple).join(", ")}) ORDER BY ${cols} FOR UPDATE`;
1260
1311
  this._log(sql, params);
1261
- await this._wrapConstraintError(() => this._exec().all(sql, params));
1312
+ await this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
1262
1313
  }
1263
1314
  }
1264
1315
  }
@@ -1348,15 +1399,15 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1348
1399
  ...query.controls,
1349
1400
  $limit: 1
1350
1401
  };
1351
- const { sql, params } = buildSelect(this._readSource(query, "rows"), where, controls);
1402
+ const { sql, params } = buildSelect(this._readSource(query, "rows"), where, controls, query.lock);
1352
1403
  this._log(sql, params);
1353
- return this._exec().get(sql, params);
1404
+ return this._exec().get(sql, params).catch((error) => this._mapConstraintError(error));
1354
1405
  }
1355
1406
  async findMany(query) {
1356
1407
  const where = buildWhere(query.filter);
1357
- const { sql, params } = buildSelect(this._readSource(query, "rows"), where, query.controls);
1408
+ const { sql, params } = buildSelect(this._readSource(query, "rows"), where, query.controls, query.lock);
1358
1409
  this._log(sql, params);
1359
- return this._exec().all(sql, params);
1410
+ return this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
1360
1411
  }
1361
1412
  /**
1362
1413
  * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
@@ -1466,10 +1517,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1466
1517
  modifiedCount: result.changedRows
1467
1518
  };
1468
1519
  }
1520
+ /** Native `UPDATE/DELETE … ORDER BY … LIMIT n` (since 0.1.162). */
1521
+ supportsWriteLimit() {
1522
+ return true;
1523
+ }
1524
+ /** The `ORDER BY … LIMIT` scan locks as it goes and skips rows gone meanwhile. */
1525
+ writeLimitIsAtomic() {
1526
+ return true;
1527
+ }
1469
1528
  async updateMany(filter, data, ops, opts) {
1470
1529
  const where = this._mutationWhere(filter);
1471
1530
  const versionColumn = this._versionColumnFor(opts);
1472
- const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, ops, versionColumn);
1531
+ const { sql, params } = buildUpdate(this.resolveTableName(), data, where, writeLimitClause(opts?.limit), ops, versionColumn);
1473
1532
  this._log(sql, params);
1474
1533
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1475
1534
  return {
@@ -1480,8 +1539,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1480
1539
  async replaceOne(filter, data, expectedVersion) {
1481
1540
  const where = this._mutationWhere(filter);
1482
1541
  const versionColumn = this._table.versionColumnPhysical;
1483
- const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, this._replaceColumns(), versionColumn);
1484
- const { sql, params } = buildUpdate(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
1542
+ const full = (0, _atscript_db_sql_tools.fillReplacePayload)(data, this._replaceColumns(), versionColumn, this._table.onUpdateIncPhysical);
1543
+ const { sql, params } = buildUpdate(this.resolveTableName(), full, where, 1, this._onUpdateIncOps(), versionColumn, expectedVersion);
1485
1544
  this._log(sql, params);
1486
1545
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1487
1546
  return {
@@ -1502,7 +1561,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1502
1561
  async replaceMany(filter, data) {
1503
1562
  const where = this._mutationWhere(filter);
1504
1563
  const versionColumn = this._table.versionColumnPhysical;
1505
- const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
1564
+ const { sql, params } = buildUpdate(this.resolveTableName(), data, where, void 0, this._onUpdateIncOps(), versionColumn);
1506
1565
  this._log(sql, params);
1507
1566
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1508
1567
  return {
@@ -1516,9 +1575,9 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1516
1575
  this._log(sql, params);
1517
1576
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
1518
1577
  }
1519
- async deleteMany(filter) {
1578
+ async deleteMany(filter, opts) {
1520
1579
  const where = this._mutationWhere(filter);
1521
- const { sql, params } = buildDelete(this.resolveTableName(), where);
1580
+ const { sql, params } = buildDelete(this.resolveTableName(), where, writeLimitClause(opts?.limit));
1522
1581
  this._log(sql, params);
1523
1582
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
1524
1583
  }
@@ -1660,6 +1719,13 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1660
1719
  this._log(sql);
1661
1720
  await this._exec().exec(sql);
1662
1721
  }
1722
+ /**
1723
+ * INFORMATION_SCHEMA reports a 4-byte character (an emoji) of a literal
1724
+ * default as `?` — the column stores it intact.
1725
+ */
1726
+ reportedDefault(desired) {
1727
+ return desired.replace(/[\u{10000}-\u{10FFFF}]/gu, "?");
1728
+ }
1663
1729
  async getExistingColumns() {
1664
1730
  return this.getExistingColumnsForTable(this._table.tableName);
1665
1731
  }
@@ -1681,7 +1747,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1681
1747
  type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1682
1748
  notnull: r.IS_NULLABLE === "NO",
1683
1749
  pk: Boolean(Number(r.IS_PK ?? 0)),
1684
- dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1750
+ dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT, r.EXTRA)
1685
1751
  };
1686
1752
  if (/\b(VIRTUAL|STORED) GENERATED\b/i.test(r.EXTRA ?? "")) column.generated = true;
1687
1753
  return column;
@@ -1789,7 +1855,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1789
1855
  const selectExprs = commonCols.map((c) => {
1790
1856
  const field = fieldsByName.get(c);
1791
1857
  if (field && !field.optional && !field.isPrimaryKey) {
1792
- const fallback = field.defaultValue?.kind === "value" ? (0, _atscript_db_sql_tools.defaultValueToSqlLiteral)(field.designType, field.defaultValue.value) : (0, _atscript_db_sql_tools.defaultValueForType)(field.designType);
1858
+ const fallback = field.defaultValue?.kind === "value" ? mysqlValueLiteral(field.designType, field.defaultValue.value) : (0, _atscript_db_sql_tools.defaultValueForType)(field.designType);
1793
1859
  return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1794
1860
  }
1795
1861
  return qi(c);
@@ -1880,7 +1946,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1880
1946
  }
1881
1947
  return mysqlTypeFromField(field);
1882
1948
  }
1883
- async syncIndexes() {
1949
+ async _syncIndexes(dryRun) {
1884
1950
  const tableName = this._table.tableName;
1885
1951
  const schema = this._schema;
1886
1952
  const bytesPerChar = mysqlBytesPerChar(this._charset);
@@ -1894,7 +1960,13 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1894
1960
  return type === void 0 ? void 0 : mysqlIndexPrefix(type, bytesPerChar);
1895
1961
  };
1896
1962
  const renderPart = (column, prefix) => prefix === void 0 ? column : `${column}(${prefix})`;
1897
- await this.syncIndexesWithDiff({
1963
+ return this.syncIndexesWithDiff({
1964
+ dryRun,
1965
+ skipIndex: (index) => {
1966
+ if (index.type !== "geo") return void 0;
1967
+ const fieldMeta = this._table.fieldDescriptors.find((f) => f.physicalName === index.fields[0]?.name);
1968
+ return fieldMeta?.optional ? `[mysql] geo index "${index.name}" skipped — SPATIAL indexes require a NOT NULL column; make "${fieldMeta.path}" required to index it` : void 0;
1969
+ },
1898
1970
  listExisting: async () => {
1899
1971
  const rows = await this._exec().all(`SELECT INDEX_NAME, COLUMN_NAME, SUB_PART, SEQ_IN_INDEX
1900
1972
  FROM INFORMATION_SCHEMA.STATISTICS
@@ -1917,11 +1989,6 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1917
1989
  renderDesiredColumn: (_index, f) => renderPart(f.name, desiredPrefix(f.name)),
1918
1990
  createIndex: async (index) => {
1919
1991
  if (index.type === "geo") {
1920
- const fieldMeta = this._table.fieldDescriptors.find((f) => f.physicalName === index.fields[0]?.name);
1921
- if (fieldMeta?.optional) {
1922
- this.logger.warn(`[mysql] geo index "${index.name}" skipped — SPATIAL indexes require a NOT NULL column; make "${fieldMeta.path}" required to index it`);
1923
- return;
1924
- }
1925
1992
  const sql = `CREATE SPATIAL INDEX ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${qi(index.fields[0].name)})`;
1926
1993
  this._log(sql);
1927
1994
  await this._exec().exec(sql);
@@ -1950,39 +2017,33 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
1950
2017
  });
1951
2018
  }
1952
2019
  async syncForeignKeys() {
1953
- const existingByName = await this._getExistingFkConstraints();
1954
- const desiredFkKeys = /* @__PURE__ */ new Set();
1955
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(","));
1956
- for (const [constraintName, columns] of existingByName) {
1957
- const key = columns.toSorted().join(",");
1958
- if (!desiredFkKeys.has(key)) {
1959
- const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
1960
- this._log(ddl);
1961
- await this._exec().exec(ddl);
1962
- }
1963
- }
1964
- const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1965
- for (const fk of this._table.foreignKeys.values()) {
1966
- const key = [...(0, _atscript_db.fkColumns)(fk).fields].toSorted().join(",");
1967
- if (!existingKeys.has(key)) {
2020
+ return this._syncForeignKeys(false);
2021
+ }
2022
+ /** The FK changes {@link syncForeignKeys} would make — no DDL. */
2023
+ async planForeignKeys() {
2024
+ return this._syncForeignKeys(true);
2025
+ }
2026
+ _syncForeignKeys(dryRun) {
2027
+ return this.syncForeignKeysWithDiff({
2028
+ dryRun,
2029
+ listExisting: () => this._getExistingFkConstraints(),
2030
+ dropConstraint: (name) => this._dropFkConstraint(name),
2031
+ addConstraint: async (fk) => {
1968
2032
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${(0, _atscript_db_sql_tools.foreignKeySql)(qi, fk)}`;
1969
2033
  this._log(ddl);
1970
2034
  await this._exec().exec(ddl);
1971
2035
  }
1972
- }
2036
+ });
1973
2037
  }
1974
2038
  async dropForeignKeys(fkFieldKeys) {
1975
2039
  if (fkFieldKeys.length === 0) return;
1976
2040
  const keySet = new Set(fkFieldKeys);
1977
- const existingByName = await this._getExistingFkConstraints();
1978
- for (const [constraintName, cols] of existingByName) {
1979
- const key = cols.toSorted().join(",");
1980
- if (keySet.has(key)) {
1981
- const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
1982
- this._log(ddl);
1983
- await this._exec().exec(ddl);
1984
- }
1985
- }
2041
+ for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has((0, _atscript_db.fkKey)(cols))) await this._dropFkConstraint(constraintName);
2042
+ }
2043
+ async _dropFkConstraint(name) {
2044
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(name)}`;
2045
+ this._log(ddl);
2046
+ await this._exec().exec(ddl);
1986
2047
  }
1987
2048
  /** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
1988
2049
  async _getExistingFkConstraints() {
@@ -2320,14 +2381,46 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
2320
2381
  * `uuid()`), but the diff engine compares against serialized form (`fn:now`,
2321
2382
  * `fn:uuid`). Without normalization, every table with function defaults
2322
2383
  * produces phantom ALTER diffs on re-plan.
2384
+ *
2385
+ * An expression default (`EXTRA` has `DEFAULT_GENERATED` — the form TEXT /
2386
+ * BLOB / JSON / GEOMETRY columns require) is reported as the printed
2387
+ * expression: possibly parenthesised, a string literal with a charset
2388
+ * introducer, escaped once more as a whole (`_utf8mb4\'it\\\'s\'` for
2389
+ * `'it''s'`). It is unwrapped to the literal's value. The value is compared type-aware by
2390
+ * `sameDefaultValue` (core), so `1` / `0.00` need no rewriting here.
2323
2391
  */
2324
- function normalizeMysqlDefault(value) {
2392
+ function normalizeMysqlDefault(value, extra) {
2325
2393
  if (value === null) return;
2326
- const lower = value.toLowerCase();
2394
+ let v = value;
2395
+ const expression = /\bDEFAULT_GENERATED\b/i.test(extra ?? "");
2396
+ if (expression) while (v.startsWith("(") && v.endsWith(")")) v = v.slice(1, -1).trim();
2397
+ const lower = v.toLowerCase();
2327
2398
  if (/^current_timestamp(?:\(\d?\))?$/.test(lower)) return "fn:now";
2328
2399
  if (lower === "uuid()") return "fn:uuid";
2329
- if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
2330
- return value;
2400
+ if (expression) {
2401
+ v = decodeLatin1Utf8(v);
2402
+ const printed = /^(?:_\w+)?\\'/.test(v) ? v.replace(/\\([\s\S])/g, "$1") : v;
2403
+ const literal = /^(?:_\w+)?'([\s\S]*)'$/.exec(printed);
2404
+ return literal ? unescapeMysqlString(literal[1]) : printed;
2405
+ }
2406
+ return (0, _atscript_db_sql_tools.unquoteSqlStringLiteral)(v) ?? v;
2407
+ }
2408
+ /** `value`'s code units (all ≤ U+00FF) as bytes, decoded as UTF-8 — `value` unchanged otherwise. */
2409
+ function decodeLatin1Utf8(value) {
2410
+ if (!/[\u0080-\u00ff]/.test(value) || /[^\u0000-\u00ff]/.test(value)) return value;
2411
+ const decoded = Buffer.from(value, "latin1").toString("utf8");
2412
+ return decoded.includes("�") ? value : decoded;
2413
+ }
2414
+ const MYSQL_ESCAPES = {
2415
+ n: "\n",
2416
+ r: "\r",
2417
+ t: " ",
2418
+ "0": "\0",
2419
+ Z: ""
2420
+ };
2421
+ /** The body of a printed MySQL string literal: `\x` escapes and doubled `''` resolved. */
2422
+ function unescapeMysqlString(body) {
2423
+ return body.replace(/\\([\s\S])|''/g, (match, ch) => ch === void 0 ? "'" : MYSQL_ESCAPES[ch] ?? ch);
2331
2424
  }
2332
2425
  /** Maps generic similarity metric to MySQL 9+ distance function name. */
2333
2426
  function similarityToMysqlFn(similarity) {
@@ -2511,6 +2604,9 @@ var Mysql2Driver = class {
2511
2604
  },
2512
2605
  release() {
2513
2606
  conn.release();
2607
+ },
2608
+ destroy() {
2609
+ conn.destroy();
2514
2610
  }
2515
2611
  };
2516
2612
  }
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TForeignKeyChange, TIndexChange, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TTransactionOptions, 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";
@@ -72,6 +72,13 @@ interface TMysqlConnection {
72
72
  exec(sql: string): Promise<void>;
73
73
  /** Release this connection back to the pool. */
74
74
  release(): void;
75
+ /**
76
+ * Close this connection instead of returning it to the pool (since
77
+ * 0.1.162) — used when its session state cannot be trusted, e.g. a
78
+ * one-shot `SET TRANSACTION` left pending by a failed
79
+ * `START TRANSACTION`. Optional: a driver without it gets `release()`.
80
+ */
81
+ destroy?(): void;
75
82
  }
76
83
  /**
77
84
  * `MysqlAdapter` options.
@@ -154,7 +161,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
154
161
  protected _transactionOwner(): unknown;
155
162
  /** The dedicated connection of this pool's open transaction, if any. */
156
163
  private _txConnection;
157
- protected _beginTransaction(): Promise<TMysqlConnection>;
164
+ protected _beginTransaction(opts?: TTransactionOptions): Promise<TMysqlConnection>;
158
165
  protected _commitTransaction(state: unknown): Promise<void>;
159
166
  protected _rollbackTransaction(state: unknown): Promise<void>;
160
167
  /**
@@ -196,6 +203,14 @@ declare class MysqlAdapter extends BaseDbAdapter {
196
203
  supportsAggregateExpressions(): boolean;
197
204
  /** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
198
205
  supportsNullsPlacement(): boolean;
206
+ /**
207
+ * Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
208
+ * `SKIP LOCKED`] (MySQL 8). A locking read is a current read — it sees the
209
+ * latest committed row even under REPEATABLE READ. @since 0.1.162
210
+ */
211
+ supportsReadLocks(): boolean;
212
+ /** Every level, set by `SET TRANSACTION ISOLATION LEVEL` before `START TRANSACTION`. @since 0.1.162 */
213
+ isolationLevels(): ReadonlySet<TDbIsolationLevel>;
199
214
  /** Computed view columns and first-row joins. */
200
215
  viewCapabilities(): ReadonlySet<TViewCapability>;
201
216
  onBeforeFlatten(_type: unknown): void;
@@ -385,13 +400,17 @@ declare class MysqlAdapter extends BaseDbAdapter {
385
400
  /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
386
401
  private _filterReadsOwnTable;
387
402
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
403
+ /** Native `UPDATE/DELETE … ORDER BY … LIMIT n` (since 0.1.162). */
404
+ supportsWriteLimit(): boolean;
405
+ /** The `ORDER BY … LIMIT` scan locks as it goes and skips rows gone meanwhile. */
406
+ writeLimitIsAtomic(): boolean;
388
407
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
389
408
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
390
409
  /** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
391
410
  private _replaceColumns;
392
411
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
393
412
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
394
- deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
413
+ deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
395
414
  prepareTypeMapper(): Promise<void>;
396
415
  ensureTable(opts?: TEnsureTableOptions): Promise<void>;
397
416
  /** The CREATE TABLE options (engine/charset/collation/increment/ON UPDATE/type mapper). */
@@ -419,6 +438,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
419
438
  */
420
439
  rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
421
440
  private _ensureView;
441
+ /**
442
+ * INFORMATION_SCHEMA reports a 4-byte character (an emoji) of a literal
443
+ * default as `?` — the column stores it intact.
444
+ */
445
+ reportedDefault(desired: string): string;
422
446
  getExistingColumns(): Promise<TExistingColumn[]>;
423
447
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
424
448
  /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
@@ -434,9 +458,13 @@ declare class MysqlAdapter extends BaseDbAdapter {
434
458
  dropViewByName(viewName: string): Promise<void>;
435
459
  renameTable(oldName: string): Promise<void>;
436
460
  typeMapper(field: TDbFieldMeta): string;
437
- syncIndexes(): Promise<void>;
438
- syncForeignKeys(): Promise<void>;
461
+ protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
462
+ syncForeignKeys(): Promise<TForeignKeyChange[]>;
463
+ /** The FK changes {@link syncForeignKeys} would make — no DDL. */
464
+ planForeignKeys(): Promise<TForeignKeyChange[]>;
465
+ private _syncForeignKeys;
439
466
  dropForeignKeys(fkFieldKeys: string[]): Promise<void>;
467
+ private _dropFkConstraint;
440
468
  /** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
441
469
  private _getExistingFkConstraints;
442
470
  getSearchIndexes(): TSearchIndexInfo[];
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
1
+ import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteManyOptions, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbIsolationLevel, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TForeignKeyChange, TIndexChange, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TTransactionOptions, 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";
@@ -72,6 +72,13 @@ interface TMysqlConnection {
72
72
  exec(sql: string): Promise<void>;
73
73
  /** Release this connection back to the pool. */
74
74
  release(): void;
75
+ /**
76
+ * Close this connection instead of returning it to the pool (since
77
+ * 0.1.162) — used when its session state cannot be trusted, e.g. a
78
+ * one-shot `SET TRANSACTION` left pending by a failed
79
+ * `START TRANSACTION`. Optional: a driver without it gets `release()`.
80
+ */
81
+ destroy?(): void;
75
82
  }
76
83
  /**
77
84
  * `MysqlAdapter` options.
@@ -154,7 +161,7 @@ declare class MysqlAdapter extends BaseDbAdapter {
154
161
  protected _transactionOwner(): unknown;
155
162
  /** The dedicated connection of this pool's open transaction, if any. */
156
163
  private _txConnection;
157
- protected _beginTransaction(): Promise<TMysqlConnection>;
164
+ protected _beginTransaction(opts?: TTransactionOptions): Promise<TMysqlConnection>;
158
165
  protected _commitTransaction(state: unknown): Promise<void>;
159
166
  protected _rollbackTransaction(state: unknown): Promise<void>;
160
167
  /**
@@ -196,6 +203,14 @@ declare class MysqlAdapter extends BaseDbAdapter {
196
203
  supportsAggregateExpressions(): boolean;
197
204
  /** `$nulls` / `@db.sort.nulls` NULL placement — a leading `(col IS NULL)` key where the native order differs. */
198
205
  supportsNullsPlacement(): boolean;
206
+ /**
207
+ * Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
208
+ * `SKIP LOCKED`] (MySQL 8). A locking read is a current read — it sees the
209
+ * latest committed row even under REPEATABLE READ. @since 0.1.162
210
+ */
211
+ supportsReadLocks(): boolean;
212
+ /** Every level, set by `SET TRANSACTION ISOLATION LEVEL` before `START TRANSACTION`. @since 0.1.162 */
213
+ isolationLevels(): ReadonlySet<TDbIsolationLevel>;
199
214
  /** Computed view columns and first-row joins. */
200
215
  viewCapabilities(): ReadonlySet<TViewCapability>;
201
216
  onBeforeFlatten(_type: unknown): void;
@@ -385,13 +400,17 @@ declare class MysqlAdapter extends BaseDbAdapter {
385
400
  /** `true` when a relational predicate of `filter` (nested ones included) reads this table. */
386
401
  private _filterReadsOwnTable;
387
402
  updateOne(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, expectedVersion?: number, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
403
+ /** Native `UPDATE/DELETE … ORDER BY … LIMIT n` (since 0.1.162). */
404
+ supportsWriteLimit(): boolean;
405
+ /** The `ORDER BY … LIMIT` scan locks as it goes and skips rows gone meanwhile. */
406
+ writeLimitIsAtomic(): boolean;
388
407
  updateMany(filter: FilterExpr, data: Record<string, unknown>, ops?: TFieldOps, opts?: TDbUpdateOptions): Promise<TDbUpdateResult>;
389
408
  replaceOne(filter: FilterExpr, data: Record<string, unknown>, expectedVersion?: number): Promise<TDbUpdateResult>;
390
409
  /** The columns a full replace assigns (`replaceColumnsFor`), built once per table. */
391
410
  private _replaceColumns;
392
411
  replaceMany(filter: FilterExpr, data: Record<string, unknown>): Promise<TDbUpdateResult>;
393
412
  deleteOne(filter: FilterExpr): Promise<TDbDeleteResult>;
394
- deleteMany(filter: FilterExpr): Promise<TDbDeleteResult>;
413
+ deleteMany(filter: FilterExpr, opts?: TDbDeleteManyOptions): Promise<TDbDeleteResult>;
395
414
  prepareTypeMapper(): Promise<void>;
396
415
  ensureTable(opts?: TEnsureTableOptions): Promise<void>;
397
416
  /** The CREATE TABLE options (engine/charset/collation/increment/ON UPDATE/type mapper). */
@@ -419,6 +438,11 @@ declare class MysqlAdapter extends BaseDbAdapter {
419
438
  */
420
439
  rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
421
440
  private _ensureView;
441
+ /**
442
+ * INFORMATION_SCHEMA reports a 4-byte character (an emoji) of a literal
443
+ * default as `?` — the column stores it intact.
444
+ */
445
+ reportedDefault(desired: string): string;
422
446
  getExistingColumns(): Promise<TExistingColumn[]>;
423
447
  getExistingColumnsForTable(tableName: string): Promise<TExistingColumn[]>;
424
448
  /** Live columns of `tableName`, read through `exec` (a held connection, or the pool). */
@@ -434,9 +458,13 @@ declare class MysqlAdapter extends BaseDbAdapter {
434
458
  dropViewByName(viewName: string): Promise<void>;
435
459
  renameTable(oldName: string): Promise<void>;
436
460
  typeMapper(field: TDbFieldMeta): string;
437
- syncIndexes(): Promise<void>;
438
- syncForeignKeys(): Promise<void>;
461
+ protected _syncIndexes(dryRun: boolean): Promise<TIndexChange[]>;
462
+ syncForeignKeys(): Promise<TForeignKeyChange[]>;
463
+ /** The FK changes {@link syncForeignKeys} would make — no DDL. */
464
+ planForeignKeys(): Promise<TForeignKeyChange[]>;
465
+ private _syncForeignKeys;
439
466
  dropForeignKeys(fkFieldKeys: string[]): Promise<void>;
467
+ private _dropFkConstraint;
440
468
  /** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
441
469
  private _getExistingFkConstraints;
442
470
  getSearchIndexes(): TSearchIndexInfo[];
package/dist/index.mjs CHANGED
@@ -1,8 +1,18 @@
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";
1
+ import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_ISOLATION_LEVELS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, NoopLogger, bucketTimeZoneUnavailable, containsRelationFilter, defaultFulltextIndex, describeFulltext, fkKey, 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, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, 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";
3
+ import { EMPTY_OR, SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildJsonColumnCopy, buildJsonifyText, buildKeyViolationCount, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, fromSourceHint, fromSourceSql, geoWindowFromControls, jsonDollarPath, mapQueryErrors, normalizeGeoPointValue, numericOutOfRangeError, orFragment, orderByList, parseRegexString, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, unquoteSqlStringLiteral, vectorDistanceSource, viewReadSource, writeLimitCount } 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
+ /**
7
+ * A MySQL string literal: quotes doubled and backslashes escaped — MySQL
8
+ * reads `\` as an escape in a string literal (unless the server runs with
9
+ * `NO_BACKSLASH_ESCAPES`), so the shared `sqlStringLiteral` would drop or
10
+ * reinterpret them (`'a\b'` stores a backspace, `'x\'` is unterminated).
11
+ * @since 0.1.162
12
+ */
13
+ function mysqlStringLiteral(value) {
14
+ return `'${value.replace(/\\/g, "\\\\").replace(/'/g, "''")}'`;
15
+ }
6
16
  /** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
7
17
  const TEXT_BLOB_RE = /^(TINY|MEDIUM|LONG)?(TEXT|BLOB)\b/i;
8
18
  /** Spatial types (`POINT SRID 4326`, `GEOMETRY`, `MULTI*`, …). */
@@ -12,11 +22,19 @@ function needsExpressionDefault(sqlType) {
12
22
  return TEXT_BLOB_RE.test(sqlType) || /^JSON\b/i.test(sqlType) || GEOMETRY_RE.test(sqlType);
13
23
  }
14
24
  /**
25
+ * `defaultValueToSqlLiteral` for MySQL: a string literal also escapes
26
+ * backslashes, which MySQL reads as escapes (unless `NO_BACKSLASH_ESCAPES`) —
27
+ * `'a\\b'` would store `a\b` minus one backslash otherwise.
28
+ */
29
+ function mysqlValueLiteral(designType, value) {
30
+ return defaultValueToSqlLiteral(designType, value, mysqlStringLiteral);
31
+ }
32
+ /**
15
33
  * Renders a model value default for a column of `sqlType`: the SQL literal,
16
34
  * wrapped in the expression form for the types that require it.
17
35
  */
18
36
  function mysqlDefaultLiteral(sqlType, designType, value) {
19
- const literal = defaultValueToSqlLiteral(designType, value);
37
+ const literal = mysqlValueLiteral(designType, value);
20
38
  return needsExpressionDefault(sqlType) ? `(${literal})` : literal;
21
39
  }
22
40
  /**
@@ -152,6 +170,7 @@ const mysqlDialect = {
152
170
  return quoteTableName(name);
153
171
  },
154
172
  unlimitedLimit: "18446744073709551615",
173
+ stringLiteral: mysqlStringLiteral,
155
174
  derivedMergeHint(alias) {
156
175
  return `/*+ MERGE(${qi(alias)}) */`;
157
176
  },
@@ -183,11 +202,11 @@ const mysqlDialect = {
183
202
  bucketAliasInHaving: true,
184
203
  jsonExtract: mysqlJsonExtract,
185
204
  jsonExtractText: (quotedCol, path) => {
186
- const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path)})`;
205
+ const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path, mysqlStringLiteral)})`;
187
206
  return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE JSON_UNQUOTE(${e}) END`;
188
207
  },
189
208
  jsonExtractValue: (quotedCol, path) => {
190
- const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path)})`;
209
+ const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path, mysqlStringLiteral)})`;
191
210
  return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE ${e} END`;
192
211
  },
193
212
  jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
@@ -205,7 +224,7 @@ const MYSQL_JSON_NUMBER_TYPES = "'INTEGER', 'UNSIGNED INTEGER', 'DOUBLE', 'DECIM
205
224
  * `CAST(… AS JSON)` (not in MariaDB).
206
225
  */
207
226
  function mysqlJsonExtract(quotedCol, path, type) {
208
- const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path)})`;
227
+ const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path, mysqlStringLiteral)})`;
209
228
  const t = `JSON_TYPE(${e})`;
210
229
  switch (type) {
211
230
  case "string": return `CASE ${t} WHEN 'STRING' THEN JSON_UNQUOTE(${e}) END`;
@@ -371,21 +390,28 @@ function buildInsertMany$1(table, rows, columns) {
371
390
  /**
372
391
  * Builds a SELECT statement with optional sort, limit, offset, projection.
373
392
  */
374
- function buildSelect$1(table, where, controls) {
375
- return buildSelect(mysqlDialect, table, where, controls);
393
+ function buildSelect$1(table, where, controls, lock) {
394
+ return buildSelect(mysqlDialect, table, where, controls, lock);
376
395
  }
377
396
  /**
378
- * Builds an UPDATE ... SET ... WHERE statement with optional LIMIT.
397
+ * Builds an UPDATE ... SET ... WHERE statement with optional [ORDER BY …] LIMIT.
379
398
  */
380
399
  function buildUpdate$1(table, data, where, limit, ops, versionColumn, expectedVersion) {
381
400
  return buildUpdate(mysqlDialect, table, data, where, limit, ops, versionColumn, expectedVersion);
382
401
  }
383
402
  /**
384
- * Builds a DELETE ... WHERE statement with optional LIMIT.
403
+ * Builds a DELETE ... WHERE statement with optional [ORDER BY …] LIMIT.
385
404
  */
386
405
  function buildDelete$1(table, where, limit) {
387
406
  return buildDelete(mysqlDialect, table, where, limit);
388
407
  }
408
+ /** `ORDER BY … LIMIT n` of a bounded write (since 0.1.162), `undefined` when unbounded. */
409
+ function writeLimitClause(bound) {
410
+ return bound && {
411
+ count: writeLimitCount(bound),
412
+ orderBy: orderByList(mysqlDialect, bound.sort, bound.nulls)
413
+ };
414
+ }
389
415
  /**
390
416
  * Builds a CREATE OR REPLACE VIEW statement from a view plan and column mappings.
391
417
  */
@@ -679,11 +705,24 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
679
705
  _txConnection() {
680
706
  return this._getTransactionState();
681
707
  }
682
- async _beginTransaction() {
708
+ async _beginTransaction(opts) {
683
709
  const conn = await this.driver.getConnection();
684
- await conn.exec("START TRANSACTION");
685
- this._log("START TRANSACTION");
686
- return conn;
710
+ let levelPending = false;
711
+ try {
712
+ if (opts?.isolation) {
713
+ const sql = `SET TRANSACTION ISOLATION LEVEL ${opts.isolation.toUpperCase()}`;
714
+ this._log(sql);
715
+ await conn.exec(sql);
716
+ levelPending = true;
717
+ }
718
+ await conn.exec("START TRANSACTION");
719
+ this._log("START TRANSACTION");
720
+ return conn;
721
+ } catch (err) {
722
+ if (levelPending && conn.destroy) conn.destroy();
723
+ else conn.release();
724
+ throw err;
725
+ }
687
726
  }
688
727
  async _commitTransaction(state) {
689
728
  const conn = state;
@@ -775,6 +814,18 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
775
814
  supportsNullsPlacement() {
776
815
  return true;
777
816
  }
817
+ /**
818
+ * Locking reads: `SELECT … FOR UPDATE` / `FOR SHARE` [`NOWAIT` /
819
+ * `SKIP LOCKED`] (MySQL 8). A locking read is a current read — it sees the
820
+ * latest committed row even under REPEATABLE READ. @since 0.1.162
821
+ */
822
+ supportsReadLocks() {
823
+ return true;
824
+ }
825
+ /** Every level, set by `SET TRANSACTION ISOLATION LEVEL` before `START TRANSACTION`. @since 0.1.162 */
826
+ isolationLevels() {
827
+ return ALL_ISOLATION_LEVELS;
828
+ }
778
829
  /** Computed view columns and first-row joins. */
779
830
  viewCapabilities() {
780
831
  return ALL_VIEW_CAPABILITIES;
@@ -912,7 +963,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
912
963
  message: err.sqlMessage ?? err.message
913
964
  }]);
914
965
  if (err.errno === 1451 || err.errno === 1452) throw new DbError("FK_VIOLATION", this._mapFkError(err.message));
915
- if (err.errno === 1213 || err.errno === 1205) throw new DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
966
+ if (err.errno === 1213 || err.errno === 1205 || err.errno === 3572) throw new DbError(err.errno === 1213 ? "DEADLOCK" : "LOCK_TIMEOUT", [{
916
967
  path: "",
917
968
  message: err.sqlMessage ?? err.message
918
969
  }]);
@@ -1257,7 +1308,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1257
1308
  const target = fields.length === 1 ? cols : `(${cols})`;
1258
1309
  const sql = `SELECT ${cols} FROM ${quoteTableName(tableName)} WHERE ${target} IN (${slice.map(() => tuple).join(", ")}) ORDER BY ${cols} FOR UPDATE`;
1259
1310
  this._log(sql, params);
1260
- await this._wrapConstraintError(() => this._exec().all(sql, params));
1311
+ await this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
1261
1312
  }
1262
1313
  }
1263
1314
  }
@@ -1347,15 +1398,15 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1347
1398
  ...query.controls,
1348
1399
  $limit: 1
1349
1400
  };
1350
- const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, controls);
1401
+ const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, controls, query.lock);
1351
1402
  this._log(sql, params);
1352
- return this._exec().get(sql, params);
1403
+ return this._exec().get(sql, params).catch((error) => this._mapConstraintError(error));
1353
1404
  }
1354
1405
  async findMany(query) {
1355
1406
  const where = buildWhere(query.filter);
1356
- const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, query.controls);
1407
+ const { sql, params } = buildSelect$1(this._readSource(query, "rows"), where, query.controls, query.lock);
1357
1408
  this._log(sql, params);
1358
- return this._exec().all(sql, params);
1409
+ return this._exec().all(sql, params).catch((error) => this._mapConstraintError(error));
1359
1410
  }
1360
1411
  /**
1361
1412
  * `$skip` / `$limit` per partition in one statement: a `ROW_NUMBER()`
@@ -1465,10 +1516,18 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1465
1516
  modifiedCount: result.changedRows
1466
1517
  };
1467
1518
  }
1519
+ /** Native `UPDATE/DELETE … ORDER BY … LIMIT n` (since 0.1.162). */
1520
+ supportsWriteLimit() {
1521
+ return true;
1522
+ }
1523
+ /** The `ORDER BY … LIMIT` scan locks as it goes and skips rows gone meanwhile. */
1524
+ writeLimitIsAtomic() {
1525
+ return true;
1526
+ }
1468
1527
  async updateMany(filter, data, ops, opts) {
1469
1528
  const where = this._mutationWhere(filter);
1470
1529
  const versionColumn = this._versionColumnFor(opts);
1471
- const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, ops, versionColumn);
1530
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, writeLimitClause(opts?.limit), ops, versionColumn);
1472
1531
  this._log(sql, params);
1473
1532
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1474
1533
  return {
@@ -1479,8 +1538,8 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1479
1538
  async replaceOne(filter, data, expectedVersion) {
1480
1539
  const where = this._mutationWhere(filter);
1481
1540
  const versionColumn = this._table.versionColumnPhysical;
1482
- const full = fillReplacePayload(data, this._replaceColumns(), versionColumn);
1483
- const { sql, params } = buildUpdate$1(this.resolveTableName(), full, where, 1, void 0, versionColumn, expectedVersion);
1541
+ const full = fillReplacePayload(data, this._replaceColumns(), versionColumn, this._table.onUpdateIncPhysical);
1542
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), full, where, 1, this._onUpdateIncOps(), versionColumn, expectedVersion);
1484
1543
  this._log(sql, params);
1485
1544
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1486
1545
  return {
@@ -1501,7 +1560,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1501
1560
  async replaceMany(filter, data) {
1502
1561
  const where = this._mutationWhere(filter);
1503
1562
  const versionColumn = this._table.versionColumnPhysical;
1504
- const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, void 0, versionColumn);
1563
+ const { sql, params } = buildUpdate$1(this.resolveTableName(), data, where, void 0, this._onUpdateIncOps(), versionColumn);
1505
1564
  this._log(sql, params);
1506
1565
  const result = await this._wrapConstraintError(() => this._exec().run(sql, params));
1507
1566
  return {
@@ -1515,9 +1574,9 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1515
1574
  this._log(sql, params);
1516
1575
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
1517
1576
  }
1518
- async deleteMany(filter) {
1577
+ async deleteMany(filter, opts) {
1519
1578
  const where = this._mutationWhere(filter);
1520
- const { sql, params } = buildDelete$1(this.resolveTableName(), where);
1579
+ const { sql, params } = buildDelete$1(this.resolveTableName(), where, writeLimitClause(opts?.limit));
1521
1580
  this._log(sql, params);
1522
1581
  return { deletedCount: (await this._wrapConstraintError(() => this._exec().run(sql, params))).affectedRows };
1523
1582
  }
@@ -1659,6 +1718,13 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1659
1718
  this._log(sql);
1660
1719
  await this._exec().exec(sql);
1661
1720
  }
1721
+ /**
1722
+ * INFORMATION_SCHEMA reports a 4-byte character (an emoji) of a literal
1723
+ * default as `?` — the column stores it intact.
1724
+ */
1725
+ reportedDefault(desired) {
1726
+ return desired.replace(/[\u{10000}-\u{10FFFF}]/gu, "?");
1727
+ }
1662
1728
  async getExistingColumns() {
1663
1729
  return this.getExistingColumnsForTable(this._table.tableName);
1664
1730
  }
@@ -1680,7 +1746,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1680
1746
  type: r.SRS_ID == null ? r.COLUMN_TYPE.toUpperCase() : `${r.COLUMN_TYPE.toUpperCase()} SRID ${r.SRS_ID}`,
1681
1747
  notnull: r.IS_NULLABLE === "NO",
1682
1748
  pk: Boolean(Number(r.IS_PK ?? 0)),
1683
- dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT)
1749
+ dflt_value: normalizeMysqlDefault(r.COLUMN_DEFAULT, r.EXTRA)
1684
1750
  };
1685
1751
  if (/\b(VIRTUAL|STORED) GENERATED\b/i.test(r.EXTRA ?? "")) column.generated = true;
1686
1752
  return column;
@@ -1788,7 +1854,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1788
1854
  const selectExprs = commonCols.map((c) => {
1789
1855
  const field = fieldsByName.get(c);
1790
1856
  if (field && !field.optional && !field.isPrimaryKey) {
1791
- const fallback = field.defaultValue?.kind === "value" ? defaultValueToSqlLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
1857
+ const fallback = field.defaultValue?.kind === "value" ? mysqlValueLiteral(field.designType, field.defaultValue.value) : defaultValueForType(field.designType);
1792
1858
  return `COALESCE(${qi(c)}, ${fallback}) AS ${qi(c)}`;
1793
1859
  }
1794
1860
  return qi(c);
@@ -1879,7 +1945,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1879
1945
  }
1880
1946
  return mysqlTypeFromField(field);
1881
1947
  }
1882
- async syncIndexes() {
1948
+ async _syncIndexes(dryRun) {
1883
1949
  const tableName = this._table.tableName;
1884
1950
  const schema = this._schema;
1885
1951
  const bytesPerChar = mysqlBytesPerChar(this._charset);
@@ -1893,7 +1959,13 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1893
1959
  return type === void 0 ? void 0 : mysqlIndexPrefix(type, bytesPerChar);
1894
1960
  };
1895
1961
  const renderPart = (column, prefix) => prefix === void 0 ? column : `${column}(${prefix})`;
1896
- await this.syncIndexesWithDiff({
1962
+ return this.syncIndexesWithDiff({
1963
+ dryRun,
1964
+ skipIndex: (index) => {
1965
+ if (index.type !== "geo") return void 0;
1966
+ const fieldMeta = this._table.fieldDescriptors.find((f) => f.physicalName === index.fields[0]?.name);
1967
+ return fieldMeta?.optional ? `[mysql] geo index "${index.name}" skipped — SPATIAL indexes require a NOT NULL column; make "${fieldMeta.path}" required to index it` : void 0;
1968
+ },
1897
1969
  listExisting: async () => {
1898
1970
  const rows = await this._exec().all(`SELECT INDEX_NAME, COLUMN_NAME, SUB_PART, SEQ_IN_INDEX
1899
1971
  FROM INFORMATION_SCHEMA.STATISTICS
@@ -1916,11 +1988,6 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1916
1988
  renderDesiredColumn: (_index, f) => renderPart(f.name, desiredPrefix(f.name)),
1917
1989
  createIndex: async (index) => {
1918
1990
  if (index.type === "geo") {
1919
- const fieldMeta = this._table.fieldDescriptors.find((f) => f.physicalName === index.fields[0]?.name);
1920
- if (fieldMeta?.optional) {
1921
- this.logger.warn(`[mysql] geo index "${index.name}" skipped — SPATIAL indexes require a NOT NULL column; make "${fieldMeta.path}" required to index it`);
1922
- return;
1923
- }
1924
1991
  const sql = `CREATE SPATIAL INDEX ${qi(index.key)} ON ${quoteTableName(this.resolveTableName())} (${qi(index.fields[0].name)})`;
1925
1992
  this._log(sql);
1926
1993
  await this._exec().exec(sql);
@@ -1949,39 +2016,33 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
1949
2016
  });
1950
2017
  }
1951
2018
  async syncForeignKeys() {
1952
- const existingByName = await this._getExistingFkConstraints();
1953
- const desiredFkKeys = /* @__PURE__ */ new Set();
1954
- for (const fk of this._table.foreignKeys.values()) desiredFkKeys.add([...fkColumns(fk).fields].toSorted().join(","));
1955
- for (const [constraintName, columns] of existingByName) {
1956
- const key = columns.toSorted().join(",");
1957
- if (!desiredFkKeys.has(key)) {
1958
- const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
1959
- this._log(ddl);
1960
- await this._exec().exec(ddl);
1961
- }
1962
- }
1963
- const existingKeys = new Set([...existingByName.values()].map((cols) => cols.toSorted().join(",")));
1964
- for (const fk of this._table.foreignKeys.values()) {
1965
- const key = [...fkColumns(fk).fields].toSorted().join(",");
1966
- if (!existingKeys.has(key)) {
2019
+ return this._syncForeignKeys(false);
2020
+ }
2021
+ /** The FK changes {@link syncForeignKeys} would make — no DDL. */
2022
+ async planForeignKeys() {
2023
+ return this._syncForeignKeys(true);
2024
+ }
2025
+ _syncForeignKeys(dryRun) {
2026
+ return this.syncForeignKeysWithDiff({
2027
+ dryRun,
2028
+ listExisting: () => this._getExistingFkConstraints(),
2029
+ dropConstraint: (name) => this._dropFkConstraint(name),
2030
+ addConstraint: async (fk) => {
1967
2031
  const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} ADD ${foreignKeySql(qi, fk)}`;
1968
2032
  this._log(ddl);
1969
2033
  await this._exec().exec(ddl);
1970
2034
  }
1971
- }
2035
+ });
1972
2036
  }
1973
2037
  async dropForeignKeys(fkFieldKeys) {
1974
2038
  if (fkFieldKeys.length === 0) return;
1975
2039
  const keySet = new Set(fkFieldKeys);
1976
- const existingByName = await this._getExistingFkConstraints();
1977
- for (const [constraintName, cols] of existingByName) {
1978
- const key = cols.toSorted().join(",");
1979
- if (keySet.has(key)) {
1980
- const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(constraintName)}`;
1981
- this._log(ddl);
1982
- await this._exec().exec(ddl);
1983
- }
1984
- }
2040
+ for (const [constraintName, cols] of await this._getExistingFkConstraints()) if (keySet.has(fkKey(cols))) await this._dropFkConstraint(constraintName);
2041
+ }
2042
+ async _dropFkConstraint(name) {
2043
+ const ddl = `ALTER TABLE ${quoteTableName(this.resolveTableName())} DROP FOREIGN KEY ${qi(name)}`;
2044
+ this._log(ddl);
2045
+ await this._exec().exec(ddl);
1985
2046
  }
1986
2047
  /** Queries INFORMATION_SCHEMA for existing FK constraints, grouped by constraint name → column names. */
1987
2048
  async _getExistingFkConstraints() {
@@ -2319,14 +2380,46 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
2319
2380
  * `uuid()`), but the diff engine compares against serialized form (`fn:now`,
2320
2381
  * `fn:uuid`). Without normalization, every table with function defaults
2321
2382
  * produces phantom ALTER diffs on re-plan.
2383
+ *
2384
+ * An expression default (`EXTRA` has `DEFAULT_GENERATED` — the form TEXT /
2385
+ * BLOB / JSON / GEOMETRY columns require) is reported as the printed
2386
+ * expression: possibly parenthesised, a string literal with a charset
2387
+ * introducer, escaped once more as a whole (`_utf8mb4\'it\\\'s\'` for
2388
+ * `'it''s'`). It is unwrapped to the literal's value. The value is compared type-aware by
2389
+ * `sameDefaultValue` (core), so `1` / `0.00` need no rewriting here.
2322
2390
  */
2323
- function normalizeMysqlDefault(value) {
2391
+ function normalizeMysqlDefault(value, extra) {
2324
2392
  if (value === null) return;
2325
- const lower = value.toLowerCase();
2393
+ let v = value;
2394
+ const expression = /\bDEFAULT_GENERATED\b/i.test(extra ?? "");
2395
+ if (expression) while (v.startsWith("(") && v.endsWith(")")) v = v.slice(1, -1).trim();
2396
+ const lower = v.toLowerCase();
2326
2397
  if (/^current_timestamp(?:\(\d?\))?$/.test(lower)) return "fn:now";
2327
2398
  if (lower === "uuid()") return "fn:uuid";
2328
- if (value.startsWith("'") && value.endsWith("'")) return value.slice(1, -1).replace(/''/g, "'");
2329
- return value;
2399
+ if (expression) {
2400
+ v = decodeLatin1Utf8(v);
2401
+ const printed = /^(?:_\w+)?\\'/.test(v) ? v.replace(/\\([\s\S])/g, "$1") : v;
2402
+ const literal = /^(?:_\w+)?'([\s\S]*)'$/.exec(printed);
2403
+ return literal ? unescapeMysqlString(literal[1]) : printed;
2404
+ }
2405
+ return unquoteSqlStringLiteral(v) ?? v;
2406
+ }
2407
+ /** `value`'s code units (all ≤ U+00FF) as bytes, decoded as UTF-8 — `value` unchanged otherwise. */
2408
+ function decodeLatin1Utf8(value) {
2409
+ if (!/[\u0080-\u00ff]/.test(value) || /[^\u0000-\u00ff]/.test(value)) return value;
2410
+ const decoded = Buffer.from(value, "latin1").toString("utf8");
2411
+ return decoded.includes("�") ? value : decoded;
2412
+ }
2413
+ const MYSQL_ESCAPES = {
2414
+ n: "\n",
2415
+ r: "\r",
2416
+ t: " ",
2417
+ "0": "\0",
2418
+ Z: ""
2419
+ };
2420
+ /** The body of a printed MySQL string literal: `\x` escapes and doubled `''` resolved. */
2421
+ function unescapeMysqlString(body) {
2422
+ return body.replace(/\\([\s\S])|''/g, (match, ch) => ch === void 0 ? "'" : MYSQL_ESCAPES[ch] ?? ch);
2330
2423
  }
2331
2424
  /** Maps generic similarity metric to MySQL 9+ distance function name. */
2332
2425
  function similarityToMysqlFn(similarity) {
@@ -2510,6 +2603,9 @@ var Mysql2Driver = class {
2510
2603
  },
2511
2604
  release() {
2512
2605
  conn.release();
2606
+ },
2607
+ destroy() {
2608
+ conn.destroy();
2513
2609
  }
2514
2610
  };
2515
2611
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atscript/db-mysql",
3
- "version": "0.1.161",
3
+ "version": "0.1.163",
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.108",
45
- "@atscript/typescript": "^0.1.108",
44
+ "@atscript/core": "^0.1.109",
45
+ "@atscript/typescript": "^0.1.109",
46
46
  "@uniqu/core": "^0.1.15",
47
47
  "mysql2": "^3.11.0",
48
- "unplugin-atscript": "^0.1.108"
48
+ "unplugin-atscript": "^0.1.109"
49
49
  },
50
50
  "peerDependencies": {
51
- "@atscript/core": "^0.1.108",
52
- "@atscript/typescript": "^0.1.108",
51
+ "@atscript/core": "^0.1.109",
52
+ "@atscript/typescript": "^0.1.109",
53
53
  "@uniqu/core": "^0.1.15",
54
54
  "mysql2": ">=3.0.0",
55
- "@atscript/db": "^0.1.161",
56
- "@atscript/db-sql-tools": "^0.1.161"
55
+ "@atscript/db": "^0.1.163",
56
+ "@atscript/db-sql-tools": "^0.1.163"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "mysql2": {