@atscript/db-mysql 0.1.160 → 0.1.162
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +158 -62
- package/dist/index.d.cts +33 -5
- package/dist/index.d.mts +33 -5
- package/dist/index.mjs +160 -64
- package/package.json +8 -8
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 = (
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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" ? (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
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
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
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
|
-
|
|
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 (
|
|
2330
|
-
|
|
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
|
-
|
|
438
|
-
syncForeignKeys(): Promise<
|
|
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
|
-
|
|
438
|
-
syncForeignKeys(): Promise<
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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.
|
|
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,
|
|
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,
|
|
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,
|
|
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" ?
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
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
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
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
|
-
|
|
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 (
|
|
2329
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.162",
|
|
4
4
|
"description": "MySQL adapter for @atscript/db with mysql2 driver support.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"atscript",
|
|
@@ -41,19 +41,19 @@
|
|
|
41
41
|
"access": "public"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@atscript/core": "^0.1.
|
|
45
|
-
"@atscript/typescript": "^0.1.
|
|
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.
|
|
48
|
+
"unplugin-atscript": "^0.1.109"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@atscript/core": "^0.1.
|
|
52
|
-
"@atscript/typescript": "^0.1.
|
|
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.
|
|
56
|
-
"@atscript/db-sql-tools": "^0.1.
|
|
55
|
+
"@atscript/db": "^0.1.162",
|
|
56
|
+
"@atscript/db-sql-tools": "^0.1.162"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"mysql2": {
|