@nextlyhq/adapter-mysql 0.0.2-alpha.73 → 0.0.2-alpha.75
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 +167 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -3
- package/dist/index.d.ts +54 -3
- package/dist/index.mjs +168 -53
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DrizzleAdapter } from "@nextlyhq/adapter-drizzle";
|
|
2
|
-
import { createDatabaseError,
|
|
2
|
+
import { createDatabaseError, isDatabaseError, poolWaitTimeoutError } from "@nextlyhq/adapter-drizzle/types";
|
|
3
3
|
import { acceptsForShare, checkDialectVersion } from "@nextlyhq/adapter-drizzle/version-check";
|
|
4
4
|
import { and, eq, getColumns } from "drizzle-orm";
|
|
5
5
|
import { getTableConfig } from "drizzle-orm/mysql-core";
|
|
@@ -145,6 +145,16 @@ async function returnTransactionConnection(connection, rollbackFailed, restore)
|
|
|
145
145
|
else connection.destroy();
|
|
146
146
|
}
|
|
147
147
|
/**
|
|
148
|
+
* mysql2 accepts only `local`, `Z` and `±HH:MM` spellings for its driver
|
|
149
|
+
* zone, and the ones that resolve to UTC are exactly these. `local` resolves
|
|
150
|
+
* to the Node process's own zone, so it agrees with a UTC session only on a
|
|
151
|
+
* UTC host.
|
|
152
|
+
*/
|
|
153
|
+
function isUtcDriverZone(zone) {
|
|
154
|
+
if (zone === "local") return (/* @__PURE__ */ new Date()).getTimezoneOffset() === 0;
|
|
155
|
+
return zone === "Z" || zone === "+00:00" || zone === "-00:00";
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
148
158
|
* Package version
|
|
149
159
|
*/
|
|
150
160
|
const VERSION = "0.1.0";
|
|
@@ -191,11 +201,19 @@ const MYSQL_ERROR_CODES = {
|
|
|
191
201
|
1146: "query",
|
|
192
202
|
1054: "query"
|
|
193
203
|
};
|
|
204
|
+
/** ER_LOCK_DEADLOCK: the transaction lost a race, and running it again may succeed. */
|
|
205
|
+
const DEADLOCK = 1213;
|
|
194
206
|
/**
|
|
195
|
-
*
|
|
207
|
+
* The MySQL error number an error carries: its own, or that of the driver
|
|
208
|
+
* error it wraps, as a classified or a Drizzle query error does.
|
|
196
209
|
*/
|
|
197
|
-
function
|
|
198
|
-
|
|
210
|
+
function errnoOf(error) {
|
|
211
|
+
for (let at = error, hops = 0; hops < 4; hops++) {
|
|
212
|
+
if (typeof at !== "object" || at === null) return void 0;
|
|
213
|
+
const errno = Reflect.get(at, "errno");
|
|
214
|
+
if (typeof errno === "number") return errno;
|
|
215
|
+
at = Reflect.get(at, "cause");
|
|
216
|
+
}
|
|
199
217
|
}
|
|
200
218
|
/**
|
|
201
219
|
* MySQL database adapter for Nextly.
|
|
@@ -271,8 +289,14 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
271
289
|
async connect() {
|
|
272
290
|
if (this.connected && this.pool) return;
|
|
273
291
|
try {
|
|
292
|
+
this.warnOnDriverZoneDisagreement();
|
|
274
293
|
const poolConfig = this.buildPoolConfig();
|
|
275
294
|
const pool = mysql.createPool(poolConfig);
|
|
295
|
+
pool.pool.on("connection", (connection) => {
|
|
296
|
+
connection.query("SET SESSION time_zone = '+00:00'", (error) => {
|
|
297
|
+
if (error) connection.destroy();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
276
300
|
boundPoolWait(pool.pool, poolConfig.connectTimeout ?? 0);
|
|
277
301
|
this.pool = pool;
|
|
278
302
|
const connection = await this.pool.getConnection();
|
|
@@ -355,6 +379,59 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
355
379
|
};
|
|
356
380
|
}
|
|
357
381
|
/**
|
|
382
|
+
* The connection as a health check reads it, on one pooled connection: the
|
|
383
|
+
* server's version, timed for the round trip, then that session's
|
|
384
|
+
* `Ssl_cipher` status, which names the cipher when the session's connection
|
|
385
|
+
* is encrypted and is empty when it is not.
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* The status and the connection's own socket are weighed by
|
|
389
|
+
* `encryptionOf`: behind a proxy the status can describe the proxy's own
|
|
390
|
+
* connection to the server, while this connection's socket is TLS. The
|
|
391
|
+
* socket is read as it is, never inferred from the `ssl` setting.
|
|
392
|
+
*
|
|
393
|
+
* Whether the database is on this machine is judged from the configured
|
|
394
|
+
* address, which never leaves this method. Any failure answers `"unknown"`.
|
|
395
|
+
*/
|
|
396
|
+
async inspectConnection() {
|
|
397
|
+
const local = this.isLocalHost(this.connectionHost());
|
|
398
|
+
let connection;
|
|
399
|
+
try {
|
|
400
|
+
connection = await this.ensurePool().getConnection();
|
|
401
|
+
const started = performance.now();
|
|
402
|
+
const [versionRows] = await connection.query("SELECT VERSION() AS version");
|
|
403
|
+
const roundTripMs = Math.max(0, Math.round(performance.now() - started));
|
|
404
|
+
const [statusRows] = await connection.query("SHOW SESSION STATUS LIKE 'Ssl_cipher'");
|
|
405
|
+
const cipher = statusRows[0]?.Value;
|
|
406
|
+
const version = versionRows[0]?.version;
|
|
407
|
+
return {
|
|
408
|
+
encryption: this.encryptionOf({
|
|
409
|
+
session: typeof cipher === "string" ? cipher !== "" : void 0,
|
|
410
|
+
ownSocketTls: this.ownSocketIsTls(connection)
|
|
411
|
+
}),
|
|
412
|
+
local,
|
|
413
|
+
roundTripMs,
|
|
414
|
+
...typeof version === "string" && version !== "" ? { serverVersion: version.split("-")[0] } : {}
|
|
415
|
+
};
|
|
416
|
+
} catch {
|
|
417
|
+
return {
|
|
418
|
+
encryption: "unknown",
|
|
419
|
+
local
|
|
420
|
+
};
|
|
421
|
+
} finally {
|
|
422
|
+
connection?.release();
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* The host this adapter connects to, as mysql2 resolves it: the connection
|
|
427
|
+
* string's when one is set, else the `host` option, else `localhost`.
|
|
428
|
+
* `undefined` when the connection string cannot be read.
|
|
429
|
+
*/
|
|
430
|
+
connectionHost() {
|
|
431
|
+
const configured = this.config.url ? this.hostOfUrl(this.config.url) : this.config.host ?? "";
|
|
432
|
+
return configured === "" ? "localhost" : configured;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
358
435
|
* Execute a raw SQL query.
|
|
359
436
|
*
|
|
360
437
|
* @param sql - SQL query string with ? placeholders
|
|
@@ -391,9 +468,29 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
391
468
|
*
|
|
392
469
|
* Note: Savepoints are disabled in this adapter for safety due to
|
|
393
470
|
* MySQL's quirks with nested transactions.
|
|
471
|
+
*
|
|
472
|
+
* A call made inside another transaction's work therefore joins it flat,
|
|
473
|
+
* on its connection: its statements are the outer's. Its failure cannot be
|
|
474
|
+
* undone alone, so it fails the whole transaction, even when the outer
|
|
475
|
+
* catches it: the outer's COMMIT is refused, it rolls back, and it rejects
|
|
476
|
+
* with the error that failed the joined call; any later joined call fails
|
|
477
|
+
* with that error at once. A joined call keeps the outer's options and is
|
|
478
|
+
* never retried itself. `retryWhen` is asked first, of every error that
|
|
479
|
+
* ends an attempt, application ones included.
|
|
394
480
|
*/
|
|
395
|
-
|
|
396
|
-
return this.
|
|
481
|
+
transaction(work, options) {
|
|
482
|
+
return this.openOrJoin(work, options, {
|
|
483
|
+
run: (_attempt, openedAt) => this.runAttempt(work, options, openedAt),
|
|
484
|
+
retryable: (error) => errnoOf(error) === DEADLOCK,
|
|
485
|
+
describe: (error) => {
|
|
486
|
+
const errno = errnoOf(error);
|
|
487
|
+
return {
|
|
488
|
+
label: errno === DEADLOCK ? "deadlock" : "a retryable error",
|
|
489
|
+
detail: { errno }
|
|
490
|
+
};
|
|
491
|
+
},
|
|
492
|
+
logger: this.config.logger
|
|
493
|
+
});
|
|
397
494
|
}
|
|
398
495
|
/**
|
|
399
496
|
* Runs `work` on a connection of its own, opened for it with the pool's
|
|
@@ -435,6 +532,7 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
435
532
|
try {
|
|
436
533
|
try {
|
|
437
534
|
await connection.query("SET SESSION autocommit = 1");
|
|
535
|
+
await connection.query("SET SESSION time_zone = '+00:00'");
|
|
438
536
|
if (options?.strictSqlMode) await this.enterStrictMode(connection, {});
|
|
439
537
|
} catch (error) {
|
|
440
538
|
throw this.classifyError(error);
|
|
@@ -453,50 +551,48 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
453
551
|
connection.destroy();
|
|
454
552
|
}
|
|
455
553
|
}
|
|
456
|
-
/**
|
|
457
|
-
async
|
|
554
|
+
/** One attempt at an outermost transaction, on a connection of its own. */
|
|
555
|
+
async runAttempt(work, options, openedAt) {
|
|
458
556
|
const pool = this.ensurePool();
|
|
459
|
-
|
|
460
|
-
const
|
|
461
|
-
let
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const result = await work(this.createTransactionContext(connection));
|
|
557
|
+
this.notePoolAccess();
|
|
558
|
+
const connection = await pool.getConnection();
|
|
559
|
+
let rollbackFailed = false;
|
|
560
|
+
const restore = {};
|
|
561
|
+
const settled = await this.settleOutermost({
|
|
562
|
+
context: this.createTransactionContext(connection),
|
|
563
|
+
join: this.joinSteps(connection),
|
|
564
|
+
openedAt,
|
|
565
|
+
begin: () => this.beginTransaction(connection, options, restore),
|
|
566
|
+
commit: async () => {
|
|
470
567
|
await connection.query("COMMIT");
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
attempt,
|
|
475
|
-
durationMs
|
|
476
|
-
});
|
|
477
|
-
}
|
|
478
|
-
return result;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
await connection.query("ROLLBACK").catch(() => {
|
|
568
|
+
},
|
|
569
|
+
rollback: async () => {
|
|
570
|
+
await connection.query("ROLLBACK").catch((rollbackError) => {
|
|
481
571
|
rollbackFailed = true;
|
|
572
|
+
throw rollbackError;
|
|
482
573
|
});
|
|
483
|
-
lastError = error;
|
|
484
|
-
if (isApplicationError(error)) throw error;
|
|
485
|
-
const mysqlError = error;
|
|
486
|
-
if (mysqlError.errno === 1213 && attempt < maxAttempts) {
|
|
487
|
-
if (this.config.logger?.warn) this.config.logger.warn(`Transaction failed with deadlock, retrying (${attempt}/${maxAttempts})`, {
|
|
488
|
-
errno: mysqlError.errno,
|
|
489
|
-
attempt
|
|
490
|
-
});
|
|
491
|
-
await delay(retryDelayMs * attempt);
|
|
492
|
-
continue;
|
|
493
|
-
}
|
|
494
|
-
throw this.classifyError(error);
|
|
495
|
-
} finally {
|
|
496
|
-
await returnTransactionConnection(connection, rollbackFailed, restore);
|
|
497
574
|
}
|
|
498
|
-
}
|
|
499
|
-
|
|
575
|
+
}, work, options);
|
|
576
|
+
await returnTransactionConnection(connection, rollbackFailed, restore);
|
|
577
|
+
return settled;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* How a call made inside a transaction on `connection` joins it: flat, on
|
|
581
|
+
* the same connection, with nothing to send. Any failure inside it fails
|
|
582
|
+
* the whole transaction, since no savepoint can undo the part alone.
|
|
583
|
+
*/
|
|
584
|
+
joinSteps(connection) {
|
|
585
|
+
const callbackConnection = connection.connection;
|
|
586
|
+
return this.joinStepsOn(() => this.createTransactionContext(connection), (relations) => relations ? drizzle({
|
|
587
|
+
client: callbackConnection,
|
|
588
|
+
relations
|
|
589
|
+
}) : drizzle({ client: callbackConnection }), (context) => ({
|
|
590
|
+
context,
|
|
591
|
+
begin() {},
|
|
592
|
+
commit() {},
|
|
593
|
+
rollback() {},
|
|
594
|
+
dooms: () => true
|
|
595
|
+
}));
|
|
500
596
|
}
|
|
501
597
|
/**
|
|
502
598
|
* Get MySQL database capabilities.
|
|
@@ -607,17 +703,36 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
607
703
|
"execute",
|
|
608
704
|
"getConnection"
|
|
609
705
|
]);
|
|
610
|
-
return this.
|
|
611
|
-
const connection = await
|
|
706
|
+
return this.joiningHandle(onClient(callbackPool), async () => {
|
|
707
|
+
const connection = await pool.getConnection();
|
|
612
708
|
return {
|
|
613
709
|
drizzle: onClient(connection.connection),
|
|
614
710
|
rollback: () => connection.query("ROLLBACK"),
|
|
615
711
|
release: (dirty) => {
|
|
616
712
|
if (dirty) connection.destroy();
|
|
617
713
|
else connection.release();
|
|
618
|
-
}
|
|
714
|
+
},
|
|
715
|
+
join: this.joinSteps(connection)
|
|
619
716
|
};
|
|
620
|
-
})
|
|
717
|
+
}, relations);
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Warns when a caller's configured driver zone does not agree with the
|
|
721
|
+
* pinned UTC session.
|
|
722
|
+
*
|
|
723
|
+
* mysql2's driver-level `timezone` decides how it parses the date strings
|
|
724
|
+
* the pinned session answers with and how it binds raw Date parameters, so
|
|
725
|
+
* a non-UTC spelling there would shift values a round-trip fails to
|
|
726
|
+
* cancel. The configuration is aligned to the pin (`buildConnectionConfig`
|
|
727
|
+
* sets UTC unconditionally, for pooled and standalone connections alike),
|
|
728
|
+
* never rewritten silently: the caller configured a reading, and this is
|
|
729
|
+
* where that reading is named as lost. Runs from `connect`, once per pool
|
|
730
|
+
* build — the configuration cannot change between calls.
|
|
731
|
+
*/
|
|
732
|
+
warnOnDriverZoneDisagreement() {
|
|
733
|
+
const requestedZone = this.config.timezone ?? "local";
|
|
734
|
+
if (isUtcDriverZone(requestedZone)) return;
|
|
735
|
+
this.config.logger?.warn?.(`Driver timezone "${requestedZone}" is not UTC; it is aligned to the pinned UTC session and no longer shifts reads or writes.`);
|
|
621
736
|
}
|
|
622
737
|
/**
|
|
623
738
|
* Builds mysql2 Pool configuration from adapter config.
|
|
@@ -638,7 +753,7 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
638
753
|
const config = this.buildAddressConfig();
|
|
639
754
|
config.connectTimeout = this.config.pool?.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
|
|
640
755
|
if (this.config.ssl) config.ssl = this.buildSslConfig(this.config.ssl);
|
|
641
|
-
|
|
756
|
+
config.timezone = "Z";
|
|
642
757
|
if (this.config.charset) config.charset = this.config.charset;
|
|
643
758
|
config.multipleStatements = false;
|
|
644
759
|
config.dateStrings = false;
|
|
@@ -901,7 +1016,8 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
901
1016
|
await connection.query(`SELECT ${idColumn} FROM ${this.escapeIdentifier(table)} WHERE ${idColumn} = ? FOR UPDATE`, [id]);
|
|
902
1017
|
},
|
|
903
1018
|
insert: async (table, data, options) => {
|
|
904
|
-
const
|
|
1019
|
+
const insertTableObj = this.resolvedTableObject(table);
|
|
1020
|
+
const mapped = this.mapRowToRawSql(insertTableObj, data);
|
|
905
1021
|
const columns = Object.keys(mapped);
|
|
906
1022
|
const values = Object.values(mapped);
|
|
907
1023
|
const placeholders = this.buildPlaceholders(values.length, 0);
|
|
@@ -909,7 +1025,6 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
909
1025
|
const [result] = await connection.query(sql, values);
|
|
910
1026
|
const ret = options?.returning;
|
|
911
1027
|
if (Array.isArray(ret) && ret.length === 0) return;
|
|
912
|
-
const insertTableObj = this.getTableObject(table);
|
|
913
1028
|
const aliases = this.dateWallClockAliases(insertTableObj, ret ?? "*");
|
|
914
1029
|
const spelled = aliases.map((a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`).join(", ");
|
|
915
1030
|
const projected = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(insertTableObj, ret).map((c) => this.escapeIdentifier(c)).join(", ");
|
|
@@ -927,7 +1042,7 @@ var MySqlAdapter = class extends DrizzleAdapter {
|
|
|
927
1042
|
if (data.length === 0) return [];
|
|
928
1043
|
const retMany = options?.returning;
|
|
929
1044
|
const skipReread = Array.isArray(retMany) && retMany.length === 0;
|
|
930
|
-
const tableObj = this.
|
|
1045
|
+
const tableObj = this.resolvedTableObject(table);
|
|
931
1046
|
const mappedRecords = data.map((r) => this.mapRowToRawSql(tableObj, r));
|
|
932
1047
|
const columns = Object.keys(mappedRecords[0]);
|
|
933
1048
|
const allValues = [];
|