@atscript/db-mysql 0.1.153 → 0.1.155
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 +150 -12
- package/dist/index.d.cts +34 -2
- package/dist/index.d.mts +34 -2
- package/dist/index.mjs +151 -13
- package/package.json +8 -8
package/dist/index.cjs
CHANGED
|
@@ -183,6 +183,15 @@ const mysqlDialect = {
|
|
|
183
183
|
calendarBucket: mysqlCalendarBucket,
|
|
184
184
|
bucketAliasInHaving: true,
|
|
185
185
|
jsonExtract: mysqlJsonExtract,
|
|
186
|
+
jsonExtractText: (quotedCol, path) => {
|
|
187
|
+
const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path)})`;
|
|
188
|
+
return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE JSON_UNQUOTE(${e}) END`;
|
|
189
|
+
},
|
|
190
|
+
jsonExtractValue: (quotedCol, path) => {
|
|
191
|
+
const e = `JSON_EXTRACT(${quotedCol}, ${(0, _atscript_db_sql_tools.jsonDollarPath)(path)})`;
|
|
192
|
+
return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE ${e} END`;
|
|
193
|
+
},
|
|
194
|
+
jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
|
|
186
195
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
|
|
187
196
|
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
188
197
|
};
|
|
@@ -569,6 +578,20 @@ function epochMsToUtcDatetime(ms, fsp = 0) {
|
|
|
569
578
|
*/
|
|
570
579
|
const TZ_PROBE_SQL = "SELECT CONVERT_TZ('2040-06-01 12:00:00', '+00:00', ?) IS NULL AS missing, TIMESTAMPDIFF(MINUTE, '2040-06-01 12:00:00', CONVERT_TZ('2040-06-01 12:00:00', '+00:00', '+01:00')) AS shift";
|
|
571
580
|
const isZero = (v) => v === 0 || v === "0";
|
|
581
|
+
/** A live numeric column type (`INFORMATION_SCHEMA.COLUMNS.COLUMN_TYPE`). */
|
|
582
|
+
const NUMERIC_SQL_TYPE = /^(double|float|real|decimal|numeric|bigint|int|integer|mediumint|smallint|tinyint)\b/i;
|
|
583
|
+
/** A live text column type — e.g. the column atscript-db <= 0.1.154 created for a `T | null` union. */
|
|
584
|
+
const TEXT_SQL_TYPE = /^(tinytext|text|mediumtext|longtext|varchar|char)\b/i;
|
|
585
|
+
/**
|
|
586
|
+
* Whether the type change of `field` from the live `existingType` converts
|
|
587
|
+
* epoch ms to a native TIMESTAMP / DATETIME (through a temp column, see
|
|
588
|
+
* `_migrateEpochColumnToTemporal`); `fromText`: the live column is text.
|
|
589
|
+
*/
|
|
590
|
+
function epochSource(field, existingType) {
|
|
591
|
+
if (!isMysqlTimestampColumn(field)) return void 0;
|
|
592
|
+
const fromText = TEXT_SQL_TYPE.test(existingType);
|
|
593
|
+
return fromText || NUMERIC_SQL_TYPE.test(existingType) ? { fromText } : void 0;
|
|
594
|
+
}
|
|
572
595
|
/** Drivers whose session `sql_mode` was already checked for strictness (see `_warnNonStrictMode`). */
|
|
573
596
|
const nonStrictChecked = /* @__PURE__ */ new WeakSet();
|
|
574
597
|
/**
|
|
@@ -602,6 +625,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
602
625
|
_collation = "utf8mb4_unicode_ci";
|
|
603
626
|
_autoIncrementStart;
|
|
604
627
|
_incrementFields = /* @__PURE__ */ new Set();
|
|
628
|
+
/**
|
|
629
|
+
* Changes of columns leaving a pending primary key that `syncColumns` left
|
|
630
|
+
* to the next `rebuildPrimaryKey` (re-declared in its swap statement).
|
|
631
|
+
*/
|
|
632
|
+
_leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
633
|
+
/**
|
|
634
|
+
* Columns leaving a pending primary key that also convert from epoch ms to
|
|
635
|
+
* TIMESTAMP / DATETIME: the temp-column conversion drops the column, which
|
|
636
|
+
* a key column cannot be — the next `rebuildPrimaryKey` converts them
|
|
637
|
+
* after its swap.
|
|
638
|
+
*/
|
|
639
|
+
_epochAfterRekey = /* @__PURE__ */ new Map();
|
|
605
640
|
_onUpdateFields = /* @__PURE__ */ new Map();
|
|
606
641
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
607
642
|
_supportsVector;
|
|
@@ -1520,12 +1555,22 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1520
1555
|
purpose
|
|
1521
1556
|
};
|
|
1522
1557
|
}
|
|
1558
|
+
/** The quoted table `tableName` of the adapter's schema, or the adapter's own. */
|
|
1559
|
+
_quotedTable(tableName) {
|
|
1560
|
+
return quoteTableName(tableName ? this._schema ? `${this._schema}.${tableName}` : tableName : this.resolveTableName());
|
|
1561
|
+
}
|
|
1523
1562
|
async hasRows(tableName) {
|
|
1524
|
-
const sql = `SELECT EXISTS(SELECT 1 FROM ${
|
|
1563
|
+
const sql = `SELECT EXISTS(SELECT 1 FROM ${this._quotedTable(tableName)}) AS present`;
|
|
1525
1564
|
this._log(sql);
|
|
1526
1565
|
const row = await this._exec().get(sql, []);
|
|
1527
1566
|
return Boolean(Number(row?.present ?? 0));
|
|
1528
1567
|
}
|
|
1568
|
+
async countKeyViolations(columns, tableName) {
|
|
1569
|
+
const sql = (0, _atscript_db_sql_tools.buildKeyViolationCount)(mysqlDialect, this._quotedTable(tableName), columns);
|
|
1570
|
+
this._log(sql);
|
|
1571
|
+
const row = await this._exec().get(sql, []);
|
|
1572
|
+
return Number(row?.violations ?? 0);
|
|
1573
|
+
}
|
|
1529
1574
|
/** Live foreign keys referencing `tableName` (any table of the schema). */
|
|
1530
1575
|
async getReferencingForeignKeys(tableName) {
|
|
1531
1576
|
const rows = await this._exec().all(`SELECT TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_COLUMN_NAME
|
|
@@ -1564,7 +1609,8 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1564
1609
|
* NOT NULL) together with the key swap — so an AUTO_INCREMENT column never
|
|
1565
1610
|
* exists without a key (ER 1075), in safe mode or otherwise. A demoted key
|
|
1566
1611
|
* column loses AUTO_INCREMENT in the same statement (pre-flight guarantees
|
|
1567
|
-
* its model no longer declares increment). Called on an empty table
|
|
1612
|
+
* its model no longer declares increment). Called on an empty table or one
|
|
1613
|
+
* whose rows satisfy the new key (`countKeyViolations`).
|
|
1568
1614
|
*/
|
|
1569
1615
|
async rebuildPrimaryKey(change) {
|
|
1570
1616
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
@@ -1575,6 +1621,9 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1575
1621
|
if (field) clauses.push(`MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
1576
1622
|
}
|
|
1577
1623
|
if (change.from.length > 0) {
|
|
1624
|
+
const leaving = this._leavingKeyColumns;
|
|
1625
|
+
this._leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
1626
|
+
for (const field of leaving.values()) if (!to.has(field.physicalName) && !this._epochAfterRekey.has(field.physicalName)) clauses.push(`MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
1578
1627
|
const placeholders = change.from.map(() => "?").join(", ");
|
|
1579
1628
|
const live = await this._exec().all(`SELECT COLUMN_NAME, COLUMN_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS
|
|
1580
1629
|
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
@@ -1584,7 +1633,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1584
1633
|
...change.from
|
|
1585
1634
|
]);
|
|
1586
1635
|
for (const col of live) {
|
|
1587
|
-
if (to.has(col.COLUMN_NAME) || !/auto_increment/i.test(col.EXTRA ?? "")) continue;
|
|
1636
|
+
if (to.has(col.COLUMN_NAME) || leaving.has(col.COLUMN_NAME) || !/auto_increment/i.test(col.EXTRA ?? "")) continue;
|
|
1588
1637
|
const field = fields.get(col.COLUMN_NAME);
|
|
1589
1638
|
const def = field ? buildColumnDefinition(field, this._columnCtx("modify")).def : `${qi(col.COLUMN_NAME)} ${col.COLUMN_TYPE.toUpperCase()} NOT NULL`;
|
|
1590
1639
|
clauses.push(`MODIFY COLUMN ${def}`);
|
|
@@ -1592,10 +1641,18 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1592
1641
|
clauses.push("DROP PRIMARY KEY");
|
|
1593
1642
|
}
|
|
1594
1643
|
if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1644
|
+
const epochAfter = this._epochAfterRekey;
|
|
1645
|
+
this._epochAfterRekey = /* @__PURE__ */ new Map();
|
|
1646
|
+
if (clauses.length === 0 && epochAfter.size === 0) return;
|
|
1647
|
+
const tableName = this.resolveTableName();
|
|
1648
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ${clauses.join(", ")}`;
|
|
1649
|
+
await this._withStrictSession(async (conn) => {
|
|
1650
|
+
if (clauses.length > 0) {
|
|
1651
|
+
this._log(ddl);
|
|
1652
|
+
await conn.exec(ddl);
|
|
1653
|
+
}
|
|
1654
|
+
for (const { field, fromText } of epochAfter.values()) await this._migrateEpochColumnToTemporal(conn, tableName, field, this.typeMapper(field), fromText);
|
|
1655
|
+
});
|
|
1599
1656
|
}
|
|
1600
1657
|
async _ensureView() {
|
|
1601
1658
|
const view = this._table;
|
|
@@ -1635,6 +1692,13 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1635
1692
|
const tableName = this.resolveTableName();
|
|
1636
1693
|
const added = [];
|
|
1637
1694
|
const renamed = [];
|
|
1695
|
+
const keyChange = diff.primaryKeyChanged;
|
|
1696
|
+
const entering = new Set(keyChange?.to ?? []);
|
|
1697
|
+
const leaving = new Set(keyChange?.from.filter((c) => !entering.has(c)) ?? []);
|
|
1698
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1699
|
+
const name = field.physicalName;
|
|
1700
|
+
if ((keyChange ? keyChange.from.includes(name) && entering.has(name) : field.isPrimaryKey) && epochSource(field, existingType) !== void 0) throw new Error(`Cannot convert primary-key column "${name}" from epoch milliseconds to ${this.typeMapper(field)} — migrate it manually.`);
|
|
1701
|
+
}
|
|
1638
1702
|
for (const { field, oldName } of diff.renamed ?? []) {
|
|
1639
1703
|
const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
|
|
1640
1704
|
this._log(ddl);
|
|
@@ -1654,10 +1718,27 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1654
1718
|
}
|
|
1655
1719
|
added.push(field.physicalName);
|
|
1656
1720
|
}
|
|
1657
|
-
|
|
1721
|
+
this._leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
1722
|
+
this._epochAfterRekey = /* @__PURE__ */ new Map();
|
|
1723
|
+
const rekeyed = (field) => {
|
|
1724
|
+
if (leaving.has(field.physicalName)) {
|
|
1725
|
+
this._leavingKeyColumns.set(field.physicalName, field);
|
|
1726
|
+
return true;
|
|
1727
|
+
}
|
|
1728
|
+
return entering.has(field.physicalName);
|
|
1729
|
+
};
|
|
1658
1730
|
const modified = /* @__PURE__ */ new Map();
|
|
1659
|
-
for (const { field } of diff.typeChanged ?? []) {
|
|
1660
|
-
|
|
1731
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1732
|
+
const epoch = epochSource(field, existingType);
|
|
1733
|
+
if (epoch) {
|
|
1734
|
+
if (leaving.has(field.physicalName)) this._epochAfterRekey.set(field.physicalName, {
|
|
1735
|
+
field,
|
|
1736
|
+
fromText: epoch.fromText
|
|
1737
|
+
});
|
|
1738
|
+
else await this._migrateEpochColumnToTemporal(conn, tableName, field, this.typeMapper(field), epoch.fromText);
|
|
1739
|
+
continue;
|
|
1740
|
+
}
|
|
1741
|
+
if (rekeyed(field)) continue;
|
|
1661
1742
|
const sqlType = this.typeMapper(field);
|
|
1662
1743
|
if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
|
|
1663
1744
|
await this._migrateJsonColumnToPoint(conn, tableName, field);
|
|
@@ -1666,7 +1747,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1666
1747
|
modified.set(field.physicalName, field);
|
|
1667
1748
|
}
|
|
1668
1749
|
for (const { field } of diff.nullableChanged ?? []) {
|
|
1669
|
-
if (
|
|
1750
|
+
if (rekeyed(field)) continue;
|
|
1670
1751
|
if (!field.optional) {
|
|
1671
1752
|
const sqlType = this.typeMapper(field);
|
|
1672
1753
|
const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
|
|
@@ -1676,7 +1757,7 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1676
1757
|
}
|
|
1677
1758
|
modified.set(field.physicalName, field);
|
|
1678
1759
|
}
|
|
1679
|
-
for (const { field } of diff.defaultChanged ?? []) if (!
|
|
1760
|
+
for (const { field } of diff.defaultChanged ?? []) if (!rekeyed(field)) modified.set(field.physicalName, field);
|
|
1680
1761
|
for (const field of modified.values()) {
|
|
1681
1762
|
const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
|
|
1682
1763
|
this._log(ddl);
|
|
@@ -1732,6 +1813,20 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
1732
1813
|
async dropTable() {
|
|
1733
1814
|
return this.dropTableByName(this.resolveTableName());
|
|
1734
1815
|
}
|
|
1816
|
+
async copyFromJsonColumn(source, targets) {
|
|
1817
|
+
const temporal = targets.find((t) => mysqlTemporalFsp(t.field) !== void 0);
|
|
1818
|
+
if (temporal) throw new Error(`"${temporal.column}" is a TIMESTAMP / DATETIME column — its epoch-ms JSON values cannot be copied into it`);
|
|
1819
|
+
const sql = (0, _atscript_db_sql_tools.buildJsonColumnCopy)(mysqlDialect, this.resolveTableName(), source, targets);
|
|
1820
|
+
await this._withStrictSession(async (conn) => {
|
|
1821
|
+
this._log(sql);
|
|
1822
|
+
await conn.exec(sql);
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
async jsonifyTextColumn(column) {
|
|
1826
|
+
const sql = (0, _atscript_db_sql_tools.buildJsonifyText)(mysqlDialect, this.resolveTableName(), column);
|
|
1827
|
+
this._log(sql);
|
|
1828
|
+
await this._exec().exec(sql);
|
|
1829
|
+
}
|
|
1735
1830
|
async dropColumns(columns) {
|
|
1736
1831
|
const tableName = this.resolveTableName();
|
|
1737
1832
|
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
@@ -2162,6 +2257,41 @@ var MysqlAdapter = class MysqlAdapter extends _atscript_db.BaseDbAdapter {
|
|
|
2162
2257
|
return (0, _atscript_db_sql_tools.buildGeoSearchSelect)(mysqlDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, ctx.controls);
|
|
2163
2258
|
}
|
|
2164
2259
|
/**
|
|
2260
|
+
* Migrates an epoch-ms number column to a native `TIMESTAMP` / `DATETIME`
|
|
2261
|
+
* via a temp column: each value becomes the UTC wall time the adapter would
|
|
2262
|
+
* write for it (`formatValue`), truncated to the column's precision. A value
|
|
2263
|
+
* the type cannot hold (before 1970 / after 2038 for `TIMESTAMP`) — or a
|
|
2264
|
+
* column the drop is refused for (an index it would leave with duplicates,
|
|
2265
|
+
* a foreign key) — fails the sync with the original column untouched (the
|
|
2266
|
+
* temp column is dropped again). The column keeps its position. It must
|
|
2267
|
+
* not be keyed (dropping it would shrink the key): `syncColumns` refuses a
|
|
2268
|
+
* column keyed before and after, and defers one leaving the key to
|
|
2269
|
+
* `rebuildPrimaryKey`. In a text column (`fromText`) only a numeric value is
|
|
2270
|
+
* epoch ms; other text is read as a datetime, as a plain `MODIFY` would.
|
|
2271
|
+
*/
|
|
2272
|
+
async _migrateEpochColumnToTemporal(conn, tableName, field, sqlType, fromText = false) {
|
|
2273
|
+
const col = qi(field.physicalName);
|
|
2274
|
+
const tmp = qi(`${field.physicalName}__ts_mig`);
|
|
2275
|
+
const quotedTable = quoteTableName(tableName);
|
|
2276
|
+
const digits = Math.min(mysqlTemporalFsp(field) ?? 0, 3);
|
|
2277
|
+
const epoch = `DATE_ADD(CAST('1970-01-01 00:00:00' AS DATETIME(6)), INTERVAL ${`FLOOR(${col} / ${10 ** (3 - digits)}) * ${10 ** (6 - digits)}`} MICROSECOND)`;
|
|
2278
|
+
const value = fromText ? `CASE WHEN ${col} REGEXP '^[0-9]+([.][0-9]+)?$' THEN ${epoch} ELSE CAST(${col} AS DATETIME(6)) END` : epoch;
|
|
2279
|
+
const run = async (ddl) => {
|
|
2280
|
+
this._log(ddl);
|
|
2281
|
+
await conn.exec(ddl);
|
|
2282
|
+
};
|
|
2283
|
+
await run(`ALTER TABLE ${quotedTable} ADD COLUMN ${tmp} ${sqlType} NULL AFTER ${col}`);
|
|
2284
|
+
try {
|
|
2285
|
+
await run(`UPDATE ${quotedTable} SET ${tmp} = ${value} WHERE ${col} IS NOT NULL`);
|
|
2286
|
+
await run(`ALTER TABLE ${quotedTable} DROP COLUMN ${col}`);
|
|
2287
|
+
} catch (error) {
|
|
2288
|
+
await run(`ALTER TABLE ${quotedTable} DROP COLUMN ${tmp}`).catch(() => void 0);
|
|
2289
|
+
throw error;
|
|
2290
|
+
}
|
|
2291
|
+
await run(`ALTER TABLE ${quotedTable} RENAME COLUMN ${tmp} TO ${col}`);
|
|
2292
|
+
await run(`ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
2293
|
+
}
|
|
2294
|
+
/**
|
|
2165
2295
|
* Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
|
|
2166
2296
|
* temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
|
|
2167
2297
|
*/
|
|
@@ -2228,8 +2358,14 @@ function sanitizeParams(params) {
|
|
|
2228
2358
|
*
|
|
2229
2359
|
* - TIMESTAMP/DATETIME → epoch milliseconds (number) instead of Date objects
|
|
2230
2360
|
* - DECIMAL/NEWDECIMAL → number instead of string
|
|
2361
|
+
* - JSON → its text (since 0.1.155), as MariaDB's `LONGTEXT` JSON and SQLite
|
|
2362
|
+
* return it: the relational mapper parses JSON text, so a JSON string value
|
|
2363
|
+
* is not mistaken for JSON text. Done here as well as with `jsonStrings`:
|
|
2364
|
+
* mysql2 caches compiled row parsers per process without keying them by
|
|
2365
|
+
* `jsonStrings`, so another pool's parser could otherwise be reused.
|
|
2231
2366
|
*/
|
|
2232
2367
|
function atscriptTypeCast(field, next) {
|
|
2368
|
+
if (field.type === "JSON") return field.string("utf8");
|
|
2233
2369
|
if (field.type === "TIMESTAMP" || field.type === "DATETIME") {
|
|
2234
2370
|
const str = field.string();
|
|
2235
2371
|
if (str === null) return null;
|
|
@@ -2312,6 +2448,7 @@ var Mysql2Driver = class {
|
|
|
2312
2448
|
timezone: "+00:00",
|
|
2313
2449
|
supportBigNumbers: true,
|
|
2314
2450
|
bigNumberStrings: false,
|
|
2451
|
+
jsonStrings: true,
|
|
2315
2452
|
typeCast: atscriptTypeCast
|
|
2316
2453
|
});
|
|
2317
2454
|
else this.pool = mysql.createPool({
|
|
@@ -2319,6 +2456,7 @@ var Mysql2Driver = class {
|
|
|
2319
2456
|
timezone: "+00:00",
|
|
2320
2457
|
supportBigNumbers: true,
|
|
2321
2458
|
bigNumberStrings: false,
|
|
2459
|
+
jsonStrings: true,
|
|
2322
2460
|
typeCast: atscriptTypeCast
|
|
2323
2461
|
});
|
|
2324
2462
|
if (strict) ensureStrict(this.pool);
|
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, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
3
3
|
import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
|
|
4
4
|
import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
|
|
@@ -112,6 +112,18 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
112
112
|
private _collation;
|
|
113
113
|
private _autoIncrementStart?;
|
|
114
114
|
private _incrementFields;
|
|
115
|
+
/**
|
|
116
|
+
* Changes of columns leaving a pending primary key that `syncColumns` left
|
|
117
|
+
* to the next `rebuildPrimaryKey` (re-declared in its swap statement).
|
|
118
|
+
*/
|
|
119
|
+
private _leavingKeyColumns;
|
|
120
|
+
/**
|
|
121
|
+
* Columns leaving a pending primary key that also convert from epoch ms to
|
|
122
|
+
* TIMESTAMP / DATETIME: the temp-column conversion drops the column, which
|
|
123
|
+
* a key column cannot be — the next `rebuildPrimaryKey` converts them
|
|
124
|
+
* after its swap.
|
|
125
|
+
*/
|
|
126
|
+
private _epochAfterRekey;
|
|
115
127
|
private _onUpdateFields;
|
|
116
128
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
117
129
|
private _supportsVector;
|
|
@@ -386,7 +398,10 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
386
398
|
private _tableOptions;
|
|
387
399
|
/** The shared column-definition context for ADD/MODIFY statements. */
|
|
388
400
|
private _columnCtx;
|
|
401
|
+
/** The quoted table `tableName` of the adapter's schema, or the adapter's own. */
|
|
402
|
+
private _quotedTable;
|
|
389
403
|
hasRows(tableName?: string): Promise<boolean>;
|
|
404
|
+
countKeyViolations(columns: readonly string[], tableName?: string): Promise<number>;
|
|
390
405
|
/** Live foreign keys referencing `tableName` (any table of the schema). */
|
|
391
406
|
getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
|
|
392
407
|
getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
|
|
@@ -399,7 +414,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
399
414
|
* NOT NULL) together with the key swap — so an AUTO_INCREMENT column never
|
|
400
415
|
* exists without a key (ER 1075), in safe mode or otherwise. A demoted key
|
|
401
416
|
* column loses AUTO_INCREMENT in the same statement (pre-flight guarantees
|
|
402
|
-
* its model no longer declares increment). Called on an empty table
|
|
417
|
+
* its model no longer declares increment). Called on an empty table or one
|
|
418
|
+
* whose rows satisfy the new key (`countKeyViolations`).
|
|
403
419
|
*/
|
|
404
420
|
rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
|
|
405
421
|
private _ensureView;
|
|
@@ -410,6 +426,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
410
426
|
syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
|
|
411
427
|
recreateTable(): Promise<void>;
|
|
412
428
|
dropTable(): Promise<void>;
|
|
429
|
+
copyFromJsonColumn(source: string, targets: readonly TJsonCopyTarget[]): Promise<void>;
|
|
430
|
+
jsonifyTextColumn(column: string): Promise<void>;
|
|
413
431
|
dropColumns(columns: string[]): Promise<void>;
|
|
414
432
|
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
415
433
|
dropTableByName(tableName: string): Promise<void>;
|
|
@@ -467,6 +485,20 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
467
485
|
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
468
486
|
private _prepareGeoSearch;
|
|
469
487
|
private _buildGeoSearchSelect;
|
|
488
|
+
/**
|
|
489
|
+
* Migrates an epoch-ms number column to a native `TIMESTAMP` / `DATETIME`
|
|
490
|
+
* via a temp column: each value becomes the UTC wall time the adapter would
|
|
491
|
+
* write for it (`formatValue`), truncated to the column's precision. A value
|
|
492
|
+
* the type cannot hold (before 1970 / after 2038 for `TIMESTAMP`) — or a
|
|
493
|
+
* column the drop is refused for (an index it would leave with duplicates,
|
|
494
|
+
* a foreign key) — fails the sync with the original column untouched (the
|
|
495
|
+
* temp column is dropped again). The column keeps its position. It must
|
|
496
|
+
* not be keyed (dropping it would shrink the key): `syncColumns` refuses a
|
|
497
|
+
* column keyed before and after, and defers one leaving the key to
|
|
498
|
+
* `rebuildPrimaryKey`. In a text column (`fromText`) only a numeric value is
|
|
499
|
+
* epoch ms; other text is read as a datetime, as a plain `MODIFY` would.
|
|
500
|
+
*/
|
|
501
|
+
private _migrateEpochColumnToTemporal;
|
|
470
502
|
/**
|
|
471
503
|
* Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
|
|
472
504
|
* temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
|
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, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
1
|
+
import { AggregateFn, BaseDbAdapter, BucketUnit, DbQuery, DbSpace, FilterExpr, TColumnDiff, TDbDefaultFn, TDbDeleteResult, TDbFieldMeta, TDbInsertIgnoreOptions, TDbInsertIgnoreSlot, TDbInsertManyResult, TDbInsertResult, TDbObjectKind, TDbUpdateOptions, TDbUpdateResult, TEnsureTableOptions, TExistingColumn, TExistingTableOption, TFieldOps, TJsonCopyTarget, TPrimaryKeyChange, TReferencingForeignKey, TSearchIndexInfo, TSyncColumnResult, TTableOptionDiff, TValueFormatterPair, TViewCapability } from "@atscript/db";
|
|
2
2
|
import { TFilterVisitorOptions, TSqlFragment, TSqlFragment as TSqlFragment$1 } from "@atscript/db-sql-tools";
|
|
3
3
|
import { FilterExpr as FilterExpr$1 } from "@uniqu/core";
|
|
4
4
|
import { TMetadataMap } from "@atscript/typescript/utils";
|
|
@@ -112,6 +112,18 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
112
112
|
private _collation;
|
|
113
113
|
private _autoIncrementStart?;
|
|
114
114
|
private _incrementFields;
|
|
115
|
+
/**
|
|
116
|
+
* Changes of columns leaving a pending primary key that `syncColumns` left
|
|
117
|
+
* to the next `rebuildPrimaryKey` (re-declared in its swap statement).
|
|
118
|
+
*/
|
|
119
|
+
private _leavingKeyColumns;
|
|
120
|
+
/**
|
|
121
|
+
* Columns leaving a pending primary key that also convert from epoch ms to
|
|
122
|
+
* TIMESTAMP / DATETIME: the temp-column conversion drops the column, which
|
|
123
|
+
* a key column cannot be — the next `rebuildPrimaryKey` converts them
|
|
124
|
+
* after its swap.
|
|
125
|
+
*/
|
|
126
|
+
private _epochAfterRekey;
|
|
115
127
|
private _onUpdateFields;
|
|
116
128
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
117
129
|
private _supportsVector;
|
|
@@ -386,7 +398,10 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
386
398
|
private _tableOptions;
|
|
387
399
|
/** The shared column-definition context for ADD/MODIFY statements. */
|
|
388
400
|
private _columnCtx;
|
|
401
|
+
/** The quoted table `tableName` of the adapter's schema, or the adapter's own. */
|
|
402
|
+
private _quotedTable;
|
|
389
403
|
hasRows(tableName?: string): Promise<boolean>;
|
|
404
|
+
countKeyViolations(columns: readonly string[], tableName?: string): Promise<number>;
|
|
390
405
|
/** Live foreign keys referencing `tableName` (any table of the schema). */
|
|
391
406
|
getReferencingForeignKeys(tableName: string): Promise<TReferencingForeignKey[]>;
|
|
392
407
|
getObjectKind(name: string): Promise<TDbObjectKind | undefined>;
|
|
@@ -399,7 +414,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
399
414
|
* NOT NULL) together with the key swap — so an AUTO_INCREMENT column never
|
|
400
415
|
* exists without a key (ER 1075), in safe mode or otherwise. A demoted key
|
|
401
416
|
* column loses AUTO_INCREMENT in the same statement (pre-flight guarantees
|
|
402
|
-
* its model no longer declares increment). Called on an empty table
|
|
417
|
+
* its model no longer declares increment). Called on an empty table or one
|
|
418
|
+
* whose rows satisfy the new key (`countKeyViolations`).
|
|
403
419
|
*/
|
|
404
420
|
rebuildPrimaryKey(change: TPrimaryKeyChange): Promise<void>;
|
|
405
421
|
private _ensureView;
|
|
@@ -410,6 +426,8 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
410
426
|
syncColumns(diff: TColumnDiff): Promise<TSyncColumnResult>;
|
|
411
427
|
recreateTable(): Promise<void>;
|
|
412
428
|
dropTable(): Promise<void>;
|
|
429
|
+
copyFromJsonColumn(source: string, targets: readonly TJsonCopyTarget[]): Promise<void>;
|
|
430
|
+
jsonifyTextColumn(column: string): Promise<void>;
|
|
413
431
|
dropColumns(columns: string[]): Promise<void>;
|
|
414
432
|
dropIndexesForColumns(columns: string[]): Promise<void>;
|
|
415
433
|
dropTableByName(tableName: string): Promise<void>;
|
|
@@ -467,6 +485,20 @@ declare class MysqlAdapter extends BaseDbAdapter {
|
|
|
467
485
|
/** Resolves the shared parts of a geo search once (column, filter, distance, window). */
|
|
468
486
|
private _prepareGeoSearch;
|
|
469
487
|
private _buildGeoSearchSelect;
|
|
488
|
+
/**
|
|
489
|
+
* Migrates an epoch-ms number column to a native `TIMESTAMP` / `DATETIME`
|
|
490
|
+
* via a temp column: each value becomes the UTC wall time the adapter would
|
|
491
|
+
* write for it (`formatValue`), truncated to the column's precision. A value
|
|
492
|
+
* the type cannot hold (before 1970 / after 2038 for `TIMESTAMP`) — or a
|
|
493
|
+
* column the drop is refused for (an index it would leave with duplicates,
|
|
494
|
+
* a foreign key) — fails the sync with the original column untouched (the
|
|
495
|
+
* temp column is dropped again). The column keeps its position. It must
|
|
496
|
+
* not be keyed (dropping it would shrink the key): `syncColumns` refuses a
|
|
497
|
+
* column keyed before and after, and defers one leaving the key to
|
|
498
|
+
* `rebuildPrimaryKey`. In a text column (`fromText`) only a numeric value is
|
|
499
|
+
* epoch ms; other text is read as a datetime, as a plain `MODIFY` would.
|
|
500
|
+
*/
|
|
501
|
+
private _migrateEpochColumnToTemporal;
|
|
470
502
|
/**
|
|
471
503
|
* Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
|
|
472
504
|
* temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ALL_AGGREGATE_FNS, ALL_BUCKET_UNITS, ALL_VIEW_CAPABILITIES, BaseDbAdapter, DbError, DbSpace, NoopLogger, bucketTimeZoneUnavailable, containsRelationFilter, defaultFulltextIndex, describeFulltext, fkColumns, forEachResolvedRelation, isConflict, searchTermInteger, splitFulltextFields, uniqueKeyTuple, vectorIndexNotFoundMessage } from "@atscript/db";
|
|
2
2
|
import { resolveAggregateSearch } from "@atscript/db/agg";
|
|
3
|
-
import { EMPTY_OR, SEARCH_SOURCE_ALIAS, arithOverflowError, buildAggregateCount, buildAggregateSelect, buildCreateView, buildDelete, buildGeoSearchCount, buildGeoSearchSelect, buildInsert, buildInsertMany, buildPartitionedSelect, buildSelect, buildUpdate, buildVectorSearchCount, buildVectorSearchSelect, buildWhere as buildWhere$1, chunkInsertRows, defaultValueForType, defaultValueToSqlLiteral, derivedColumnExpr, fillReplacePayload, foreignKeySql, 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, parseRegexString, renameGeoDistance, replaceColumnsFor, sqlTimeZoneLiteral, stripPartitionRowNumber, toSqlValue, vectorDistanceSource, viewReadSource } from "@atscript/db-sql-tools";
|
|
4
4
|
import { BUCKET_MAX_INSTANT, BUCKET_MIN_INSTANT } from "@uniqu/core";
|
|
5
5
|
//#region src/sql-builder.ts
|
|
6
6
|
/** TEXT / BLOB families (`TINYTEXT` … `LONGBLOB`). */
|
|
@@ -182,6 +182,15 @@ const mysqlDialect = {
|
|
|
182
182
|
calendarBucket: mysqlCalendarBucket,
|
|
183
183
|
bucketAliasInHaving: true,
|
|
184
184
|
jsonExtract: mysqlJsonExtract,
|
|
185
|
+
jsonExtractText: (quotedCol, path) => {
|
|
186
|
+
const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path)})`;
|
|
187
|
+
return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE JSON_UNQUOTE(${e}) END`;
|
|
188
|
+
},
|
|
189
|
+
jsonExtractValue: (quotedCol, path) => {
|
|
190
|
+
const e = `JSON_EXTRACT(${quotedCol}, ${jsonDollarPath(path)})`;
|
|
191
|
+
return `CASE WHEN JSON_TYPE(${e}) = 'NULL' THEN NULL ELSE ${e} END`;
|
|
192
|
+
},
|
|
193
|
+
jsonFromText: (quotedCol) => `IF(JSON_VALID(${quotedCol}), ${quotedCol}, JSON_QUOTE(${quotedCol}))`,
|
|
185
194
|
castDouble: (expr) => `CAST(${expr} AS DOUBLE)`,
|
|
186
195
|
createViewPrefix: "CREATE OR REPLACE VIEW"
|
|
187
196
|
};
|
|
@@ -568,6 +577,20 @@ function epochMsToUtcDatetime(ms, fsp = 0) {
|
|
|
568
577
|
*/
|
|
569
578
|
const TZ_PROBE_SQL = "SELECT CONVERT_TZ('2040-06-01 12:00:00', '+00:00', ?) IS NULL AS missing, TIMESTAMPDIFF(MINUTE, '2040-06-01 12:00:00', CONVERT_TZ('2040-06-01 12:00:00', '+00:00', '+01:00')) AS shift";
|
|
570
579
|
const isZero = (v) => v === 0 || v === "0";
|
|
580
|
+
/** A live numeric column type (`INFORMATION_SCHEMA.COLUMNS.COLUMN_TYPE`). */
|
|
581
|
+
const NUMERIC_SQL_TYPE = /^(double|float|real|decimal|numeric|bigint|int|integer|mediumint|smallint|tinyint)\b/i;
|
|
582
|
+
/** A live text column type — e.g. the column atscript-db <= 0.1.154 created for a `T | null` union. */
|
|
583
|
+
const TEXT_SQL_TYPE = /^(tinytext|text|mediumtext|longtext|varchar|char)\b/i;
|
|
584
|
+
/**
|
|
585
|
+
* Whether the type change of `field` from the live `existingType` converts
|
|
586
|
+
* epoch ms to a native TIMESTAMP / DATETIME (through a temp column, see
|
|
587
|
+
* `_migrateEpochColumnToTemporal`); `fromText`: the live column is text.
|
|
588
|
+
*/
|
|
589
|
+
function epochSource(field, existingType) {
|
|
590
|
+
if (!isMysqlTimestampColumn(field)) return void 0;
|
|
591
|
+
const fromText = TEXT_SQL_TYPE.test(existingType);
|
|
592
|
+
return fromText || NUMERIC_SQL_TYPE.test(existingType) ? { fromText } : void 0;
|
|
593
|
+
}
|
|
571
594
|
/** Drivers whose session `sql_mode` was already checked for strictness (see `_warnNonStrictMode`). */
|
|
572
595
|
const nonStrictChecked = /* @__PURE__ */ new WeakSet();
|
|
573
596
|
/**
|
|
@@ -601,6 +624,18 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
601
624
|
_collation = "utf8mb4_unicode_ci";
|
|
602
625
|
_autoIncrementStart;
|
|
603
626
|
_incrementFields = /* @__PURE__ */ new Set();
|
|
627
|
+
/**
|
|
628
|
+
* Changes of columns leaving a pending primary key that `syncColumns` left
|
|
629
|
+
* to the next `rebuildPrimaryKey` (re-declared in its swap statement).
|
|
630
|
+
*/
|
|
631
|
+
_leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
632
|
+
/**
|
|
633
|
+
* Columns leaving a pending primary key that also convert from epoch ms to
|
|
634
|
+
* TIMESTAMP / DATETIME: the temp-column conversion drops the column, which
|
|
635
|
+
* a key column cannot be — the next `rebuildPrimaryKey` converts them
|
|
636
|
+
* after its swap.
|
|
637
|
+
*/
|
|
638
|
+
_epochAfterRekey = /* @__PURE__ */ new Map();
|
|
604
639
|
_onUpdateFields = /* @__PURE__ */ new Map();
|
|
605
640
|
/** Whether the connected MySQL instance supports native VECTOR type (MySQL 9.0+). */
|
|
606
641
|
_supportsVector;
|
|
@@ -1519,12 +1554,22 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1519
1554
|
purpose
|
|
1520
1555
|
};
|
|
1521
1556
|
}
|
|
1557
|
+
/** The quoted table `tableName` of the adapter's schema, or the adapter's own. */
|
|
1558
|
+
_quotedTable(tableName) {
|
|
1559
|
+
return quoteTableName(tableName ? this._schema ? `${this._schema}.${tableName}` : tableName : this.resolveTableName());
|
|
1560
|
+
}
|
|
1522
1561
|
async hasRows(tableName) {
|
|
1523
|
-
const sql = `SELECT EXISTS(SELECT 1 FROM ${
|
|
1562
|
+
const sql = `SELECT EXISTS(SELECT 1 FROM ${this._quotedTable(tableName)}) AS present`;
|
|
1524
1563
|
this._log(sql);
|
|
1525
1564
|
const row = await this._exec().get(sql, []);
|
|
1526
1565
|
return Boolean(Number(row?.present ?? 0));
|
|
1527
1566
|
}
|
|
1567
|
+
async countKeyViolations(columns, tableName) {
|
|
1568
|
+
const sql = buildKeyViolationCount(mysqlDialect, this._quotedTable(tableName), columns);
|
|
1569
|
+
this._log(sql);
|
|
1570
|
+
const row = await this._exec().get(sql, []);
|
|
1571
|
+
return Number(row?.violations ?? 0);
|
|
1572
|
+
}
|
|
1528
1573
|
/** Live foreign keys referencing `tableName` (any table of the schema). */
|
|
1529
1574
|
async getReferencingForeignKeys(tableName) {
|
|
1530
1575
|
const rows = await this._exec().all(`SELECT TABLE_NAME, CONSTRAINT_NAME, COLUMN_NAME, REFERENCED_COLUMN_NAME
|
|
@@ -1563,7 +1608,8 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1563
1608
|
* NOT NULL) together with the key swap — so an AUTO_INCREMENT column never
|
|
1564
1609
|
* exists without a key (ER 1075), in safe mode or otherwise. A demoted key
|
|
1565
1610
|
* column loses AUTO_INCREMENT in the same statement (pre-flight guarantees
|
|
1566
|
-
* its model no longer declares increment). Called on an empty table
|
|
1611
|
+
* its model no longer declares increment). Called on an empty table or one
|
|
1612
|
+
* whose rows satisfy the new key (`countKeyViolations`).
|
|
1567
1613
|
*/
|
|
1568
1614
|
async rebuildPrimaryKey(change) {
|
|
1569
1615
|
const fields = new Map(this._table.fieldDescriptors.map((f) => [f.physicalName, f]));
|
|
@@ -1574,6 +1620,9 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1574
1620
|
if (field) clauses.push(`MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
1575
1621
|
}
|
|
1576
1622
|
if (change.from.length > 0) {
|
|
1623
|
+
const leaving = this._leavingKeyColumns;
|
|
1624
|
+
this._leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
1625
|
+
for (const field of leaving.values()) if (!to.has(field.physicalName) && !this._epochAfterRekey.has(field.physicalName)) clauses.push(`MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
1577
1626
|
const placeholders = change.from.map(() => "?").join(", ");
|
|
1578
1627
|
const live = await this._exec().all(`SELECT COLUMN_NAME, COLUMN_TYPE, EXTRA FROM INFORMATION_SCHEMA.COLUMNS
|
|
1579
1628
|
WHERE TABLE_NAME = ? AND TABLE_SCHEMA = COALESCE(?, DATABASE())
|
|
@@ -1583,7 +1632,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1583
1632
|
...change.from
|
|
1584
1633
|
]);
|
|
1585
1634
|
for (const col of live) {
|
|
1586
|
-
if (to.has(col.COLUMN_NAME) || !/auto_increment/i.test(col.EXTRA ?? "")) continue;
|
|
1635
|
+
if (to.has(col.COLUMN_NAME) || leaving.has(col.COLUMN_NAME) || !/auto_increment/i.test(col.EXTRA ?? "")) continue;
|
|
1587
1636
|
const field = fields.get(col.COLUMN_NAME);
|
|
1588
1637
|
const def = field ? buildColumnDefinition(field, this._columnCtx("modify")).def : `${qi(col.COLUMN_NAME)} ${col.COLUMN_TYPE.toUpperCase()} NOT NULL`;
|
|
1589
1638
|
clauses.push(`MODIFY COLUMN ${def}`);
|
|
@@ -1591,10 +1640,18 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1591
1640
|
clauses.push("DROP PRIMARY KEY");
|
|
1592
1641
|
}
|
|
1593
1642
|
if (change.to.length > 0) clauses.push(`ADD PRIMARY KEY (${change.to.map((c) => qi(c)).join(", ")})`);
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1643
|
+
const epochAfter = this._epochAfterRekey;
|
|
1644
|
+
this._epochAfterRekey = /* @__PURE__ */ new Map();
|
|
1645
|
+
if (clauses.length === 0 && epochAfter.size === 0) return;
|
|
1646
|
+
const tableName = this.resolveTableName();
|
|
1647
|
+
const ddl = `ALTER TABLE ${quoteTableName(tableName)} ${clauses.join(", ")}`;
|
|
1648
|
+
await this._withStrictSession(async (conn) => {
|
|
1649
|
+
if (clauses.length > 0) {
|
|
1650
|
+
this._log(ddl);
|
|
1651
|
+
await conn.exec(ddl);
|
|
1652
|
+
}
|
|
1653
|
+
for (const { field, fromText } of epochAfter.values()) await this._migrateEpochColumnToTemporal(conn, tableName, field, this.typeMapper(field), fromText);
|
|
1654
|
+
});
|
|
1598
1655
|
}
|
|
1599
1656
|
async _ensureView() {
|
|
1600
1657
|
const view = this._table;
|
|
@@ -1634,6 +1691,13 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1634
1691
|
const tableName = this.resolveTableName();
|
|
1635
1692
|
const added = [];
|
|
1636
1693
|
const renamed = [];
|
|
1694
|
+
const keyChange = diff.primaryKeyChanged;
|
|
1695
|
+
const entering = new Set(keyChange?.to ?? []);
|
|
1696
|
+
const leaving = new Set(keyChange?.from.filter((c) => !entering.has(c)) ?? []);
|
|
1697
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1698
|
+
const name = field.physicalName;
|
|
1699
|
+
if ((keyChange ? keyChange.from.includes(name) && entering.has(name) : field.isPrimaryKey) && epochSource(field, existingType) !== void 0) throw new Error(`Cannot convert primary-key column "${name}" from epoch milliseconds to ${this.typeMapper(field)} — migrate it manually.`);
|
|
1700
|
+
}
|
|
1637
1701
|
for (const { field, oldName } of diff.renamed ?? []) {
|
|
1638
1702
|
const ddl = `ALTER TABLE ${quoteTableName(tableName)} RENAME COLUMN ${qi(oldName)} TO ${qi(field.physicalName)}`;
|
|
1639
1703
|
this._log(ddl);
|
|
@@ -1653,10 +1717,27 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1653
1717
|
}
|
|
1654
1718
|
added.push(field.physicalName);
|
|
1655
1719
|
}
|
|
1656
|
-
|
|
1720
|
+
this._leavingKeyColumns = /* @__PURE__ */ new Map();
|
|
1721
|
+
this._epochAfterRekey = /* @__PURE__ */ new Map();
|
|
1722
|
+
const rekeyed = (field) => {
|
|
1723
|
+
if (leaving.has(field.physicalName)) {
|
|
1724
|
+
this._leavingKeyColumns.set(field.physicalName, field);
|
|
1725
|
+
return true;
|
|
1726
|
+
}
|
|
1727
|
+
return entering.has(field.physicalName);
|
|
1728
|
+
};
|
|
1657
1729
|
const modified = /* @__PURE__ */ new Map();
|
|
1658
|
-
for (const { field } of diff.typeChanged ?? []) {
|
|
1659
|
-
|
|
1730
|
+
for (const { field, existingType } of diff.typeChanged ?? []) {
|
|
1731
|
+
const epoch = epochSource(field, existingType);
|
|
1732
|
+
if (epoch) {
|
|
1733
|
+
if (leaving.has(field.physicalName)) this._epochAfterRekey.set(field.physicalName, {
|
|
1734
|
+
field,
|
|
1735
|
+
fromText: epoch.fromText
|
|
1736
|
+
});
|
|
1737
|
+
else await this._migrateEpochColumnToTemporal(conn, tableName, field, this.typeMapper(field), epoch.fromText);
|
|
1738
|
+
continue;
|
|
1739
|
+
}
|
|
1740
|
+
if (rekeyed(field)) continue;
|
|
1660
1741
|
const sqlType = this.typeMapper(field);
|
|
1661
1742
|
if (field.isGeoPoint && !field.encrypted && sqlType.startsWith("POINT")) {
|
|
1662
1743
|
await this._migrateJsonColumnToPoint(conn, tableName, field);
|
|
@@ -1665,7 +1746,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1665
1746
|
modified.set(field.physicalName, field);
|
|
1666
1747
|
}
|
|
1667
1748
|
for (const { field } of diff.nullableChanged ?? []) {
|
|
1668
|
-
if (
|
|
1749
|
+
if (rekeyed(field)) continue;
|
|
1669
1750
|
if (!field.optional) {
|
|
1670
1751
|
const sqlType = this.typeMapper(field);
|
|
1671
1752
|
const fallback = field.defaultValue?.kind === "value" ? mysqlDefaultLiteral(sqlType, field.designType, field.defaultValue.value) : mysqlTypeDefault(sqlType, field);
|
|
@@ -1675,7 +1756,7 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1675
1756
|
}
|
|
1676
1757
|
modified.set(field.physicalName, field);
|
|
1677
1758
|
}
|
|
1678
|
-
for (const { field } of diff.defaultChanged ?? []) if (!
|
|
1759
|
+
for (const { field } of diff.defaultChanged ?? []) if (!rekeyed(field)) modified.set(field.physicalName, field);
|
|
1679
1760
|
for (const field of modified.values()) {
|
|
1680
1761
|
const ddl = `ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`;
|
|
1681
1762
|
this._log(ddl);
|
|
@@ -1731,6 +1812,20 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
1731
1812
|
async dropTable() {
|
|
1732
1813
|
return this.dropTableByName(this.resolveTableName());
|
|
1733
1814
|
}
|
|
1815
|
+
async copyFromJsonColumn(source, targets) {
|
|
1816
|
+
const temporal = targets.find((t) => mysqlTemporalFsp(t.field) !== void 0);
|
|
1817
|
+
if (temporal) throw new Error(`"${temporal.column}" is a TIMESTAMP / DATETIME column — its epoch-ms JSON values cannot be copied into it`);
|
|
1818
|
+
const sql = buildJsonColumnCopy(mysqlDialect, this.resolveTableName(), source, targets);
|
|
1819
|
+
await this._withStrictSession(async (conn) => {
|
|
1820
|
+
this._log(sql);
|
|
1821
|
+
await conn.exec(sql);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
async jsonifyTextColumn(column) {
|
|
1825
|
+
const sql = buildJsonifyText(mysqlDialect, this.resolveTableName(), column);
|
|
1826
|
+
this._log(sql);
|
|
1827
|
+
await this._exec().exec(sql);
|
|
1828
|
+
}
|
|
1734
1829
|
async dropColumns(columns) {
|
|
1735
1830
|
const tableName = this.resolveTableName();
|
|
1736
1831
|
const drops = columns.map((col) => `DROP COLUMN ${qi(col)}`).join(", ");
|
|
@@ -2161,6 +2256,41 @@ var MysqlAdapter = class MysqlAdapter extends BaseDbAdapter {
|
|
|
2161
2256
|
return buildGeoSearchSelect(mysqlDialect, ctx.tableName, ctx.where, ctx.dist, ctx.window, ctx.controls);
|
|
2162
2257
|
}
|
|
2163
2258
|
/**
|
|
2259
|
+
* Migrates an epoch-ms number column to a native `TIMESTAMP` / `DATETIME`
|
|
2260
|
+
* via a temp column: each value becomes the UTC wall time the adapter would
|
|
2261
|
+
* write for it (`formatValue`), truncated to the column's precision. A value
|
|
2262
|
+
* the type cannot hold (before 1970 / after 2038 for `TIMESTAMP`) — or a
|
|
2263
|
+
* column the drop is refused for (an index it would leave with duplicates,
|
|
2264
|
+
* a foreign key) — fails the sync with the original column untouched (the
|
|
2265
|
+
* temp column is dropped again). The column keeps its position. It must
|
|
2266
|
+
* not be keyed (dropping it would shrink the key): `syncColumns` refuses a
|
|
2267
|
+
* column keyed before and after, and defers one leaving the key to
|
|
2268
|
+
* `rebuildPrimaryKey`. In a text column (`fromText`) only a numeric value is
|
|
2269
|
+
* epoch ms; other text is read as a datetime, as a plain `MODIFY` would.
|
|
2270
|
+
*/
|
|
2271
|
+
async _migrateEpochColumnToTemporal(conn, tableName, field, sqlType, fromText = false) {
|
|
2272
|
+
const col = qi(field.physicalName);
|
|
2273
|
+
const tmp = qi(`${field.physicalName}__ts_mig`);
|
|
2274
|
+
const quotedTable = quoteTableName(tableName);
|
|
2275
|
+
const digits = Math.min(mysqlTemporalFsp(field) ?? 0, 3);
|
|
2276
|
+
const epoch = `DATE_ADD(CAST('1970-01-01 00:00:00' AS DATETIME(6)), INTERVAL ${`FLOOR(${col} / ${10 ** (3 - digits)}) * ${10 ** (6 - digits)}`} MICROSECOND)`;
|
|
2277
|
+
const value = fromText ? `CASE WHEN ${col} REGEXP '^[0-9]+([.][0-9]+)?$' THEN ${epoch} ELSE CAST(${col} AS DATETIME(6)) END` : epoch;
|
|
2278
|
+
const run = async (ddl) => {
|
|
2279
|
+
this._log(ddl);
|
|
2280
|
+
await conn.exec(ddl);
|
|
2281
|
+
};
|
|
2282
|
+
await run(`ALTER TABLE ${quotedTable} ADD COLUMN ${tmp} ${sqlType} NULL AFTER ${col}`);
|
|
2283
|
+
try {
|
|
2284
|
+
await run(`UPDATE ${quotedTable} SET ${tmp} = ${value} WHERE ${col} IS NOT NULL`);
|
|
2285
|
+
await run(`ALTER TABLE ${quotedTable} DROP COLUMN ${col}`);
|
|
2286
|
+
} catch (error) {
|
|
2287
|
+
await run(`ALTER TABLE ${quotedTable} DROP COLUMN ${tmp}`).catch(() => void 0);
|
|
2288
|
+
throw error;
|
|
2289
|
+
}
|
|
2290
|
+
await run(`ALTER TABLE ${quotedTable} RENAME COLUMN ${tmp} TO ${col}`);
|
|
2291
|
+
await run(`ALTER TABLE ${quotedTable} MODIFY COLUMN ${buildColumnDefinition(field, this._columnCtx("modify")).def}`);
|
|
2292
|
+
}
|
|
2293
|
+
/**
|
|
2164
2294
|
* Migrates a v1 JSON `[lng, lat]` column to native `POINT SRID 4326` via a
|
|
2165
2295
|
* temp column (MySQL has no JSON→geometry cast for MODIFY COLUMN).
|
|
2166
2296
|
*/
|
|
@@ -2227,8 +2357,14 @@ function sanitizeParams(params) {
|
|
|
2227
2357
|
*
|
|
2228
2358
|
* - TIMESTAMP/DATETIME → epoch milliseconds (number) instead of Date objects
|
|
2229
2359
|
* - DECIMAL/NEWDECIMAL → number instead of string
|
|
2360
|
+
* - JSON → its text (since 0.1.155), as MariaDB's `LONGTEXT` JSON and SQLite
|
|
2361
|
+
* return it: the relational mapper parses JSON text, so a JSON string value
|
|
2362
|
+
* is not mistaken for JSON text. Done here as well as with `jsonStrings`:
|
|
2363
|
+
* mysql2 caches compiled row parsers per process without keying them by
|
|
2364
|
+
* `jsonStrings`, so another pool's parser could otherwise be reused.
|
|
2230
2365
|
*/
|
|
2231
2366
|
function atscriptTypeCast(field, next) {
|
|
2367
|
+
if (field.type === "JSON") return field.string("utf8");
|
|
2232
2368
|
if (field.type === "TIMESTAMP" || field.type === "DATETIME") {
|
|
2233
2369
|
const str = field.string();
|
|
2234
2370
|
if (str === null) return null;
|
|
@@ -2311,6 +2447,7 @@ var Mysql2Driver = class {
|
|
|
2311
2447
|
timezone: "+00:00",
|
|
2312
2448
|
supportBigNumbers: true,
|
|
2313
2449
|
bigNumberStrings: false,
|
|
2450
|
+
jsonStrings: true,
|
|
2314
2451
|
typeCast: atscriptTypeCast
|
|
2315
2452
|
});
|
|
2316
2453
|
else this.pool = mysql.createPool({
|
|
@@ -2318,6 +2455,7 @@ var Mysql2Driver = class {
|
|
|
2318
2455
|
timezone: "+00:00",
|
|
2319
2456
|
supportBigNumbers: true,
|
|
2320
2457
|
bigNumberStrings: false,
|
|
2458
|
+
jsonStrings: true,
|
|
2321
2459
|
typeCast: atscriptTypeCast
|
|
2322
2460
|
});
|
|
2323
2461
|
if (strict) ensureStrict(this.pool);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atscript/db-mysql",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.155",
|
|
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.105",
|
|
45
|
+
"@atscript/typescript": "^0.1.105",
|
|
46
46
|
"@uniqu/core": "^0.1.15",
|
|
47
47
|
"mysql2": "^3.11.0",
|
|
48
|
-
"unplugin-atscript": "^0.1.
|
|
48
|
+
"unplugin-atscript": "^0.1.105"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
|
-
"@atscript/core": "^0.1.
|
|
52
|
-
"@atscript/typescript": "^0.1.
|
|
51
|
+
"@atscript/core": "^0.1.105",
|
|
52
|
+
"@atscript/typescript": "^0.1.105",
|
|
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.155",
|
|
56
|
+
"@atscript/db-sql-tools": "^0.1.155"
|
|
57
57
|
},
|
|
58
58
|
"peerDependenciesMeta": {
|
|
59
59
|
"mysql2": {
|