@nextlyhq/adapter-mysql 0.0.2-alpha.67 → 0.0.2-alpha.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1,966 +1,1006 @@
1
- 'use strict';
2
-
3
- var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
4
- var types = require('@nextlyhq/adapter-drizzle/types');
5
- var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
6
- var drizzleOrm = require('drizzle-orm');
7
- var mysqlCore = require('drizzle-orm/mysql-core');
8
- var mysql2 = require('drizzle-orm/mysql2');
9
- var mysql = require('mysql2/promise');
10
-
11
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
12
-
13
- var mysql__default = /*#__PURE__*/_interopDefault(mysql);
14
-
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ // \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ let _nextlyhq_adapter_drizzle = require("@nextlyhq/adapter-drizzle");
24
+ let _nextlyhq_adapter_drizzle_types = require("@nextlyhq/adapter-drizzle/types");
25
+ let _nextlyhq_adapter_drizzle_version_check = require("@nextlyhq/adapter-drizzle/version-check");
26
+ let drizzle_orm = require("drizzle-orm");
27
+ let drizzle_orm_mysql_core = require("drizzle-orm/mysql-core");
28
+ let drizzle_orm_mysql2 = require("drizzle-orm/mysql2");
29
+ let mysql2_promise = require("mysql2/promise");
30
+ mysql2_promise = __toESM(mysql2_promise, 1);
31
+ // src/pool-wait.ts
32
+ /**
33
+ * The bounded wait for a pooled connection.
34
+ *
35
+ * mysql2 has no such bound. A request that finds every connection in use is
36
+ * queued until one is released, however long that takes, so a database that
37
+ * stays busy holds every request behind it indefinitely. This bounds the wait
38
+ * the way pg-pool's `connectionTimeoutMillis` does: past the bound, the
39
+ * request fails with the adapters' own pool-wait timeout error, which core
40
+ * answers with "database busy".
41
+ *
42
+ * Only the wait in the queue: a request that opens a new connection is bounded
43
+ * by mysql2's `connectTimeout` instead, set from the same setting, and a
44
+ * connection that cannot be opened is a database that cannot be reached
45
+ * rather than one that is busy.
46
+ */
47
+ /**
48
+ * Takes a request that gave up out of the pool's queue, so a pool that stays
49
+ * saturated does not collect every request that has already failed, and a
50
+ * released connection goes to one still waiting. Returns whether it was
51
+ * there: with diagnostics tracing on, mysql2 queues a wrapper of the
52
+ * callback rather than the callback itself, and then it stays queued until a
53
+ * connection reaches it.
54
+ */
55
+ function leaveQueue(pool, waiting) {
56
+ const queue = pool._connectionQueue;
57
+ if (!queue) return false;
58
+ for (let at = 0; at < queue.length; at++) if (queue.peekAt(at) === waiting) {
59
+ queue.removeOne(at);
60
+ return true;
61
+ }
62
+ return false;
63
+ }
64
+ /**
65
+ * Makes `pool` fail a request that has waited `boundMs` in its queue for a
66
+ * connection, and returns it. A bound of 0 leaves the wait unbounded, as
67
+ * pg-pool reads a `connectionTimeoutMillis` of 0.
68
+ *
69
+ * Replaces the pool's own `getConnection`, which its `query` and `execute`
70
+ * go through, so every path to a connection is bounded: the adapter's
71
+ * statements, its transactions, and the Drizzle handle core services query
72
+ * through.
73
+ *
74
+ * A request that gives up leaves the queue. Where mysql2 cannot be asked to
75
+ * drop it, the connection it is eventually handed is released straight back
76
+ * rather than held by a request that has already failed.
77
+ */
78
+ function boundPoolWait(pool, boundMs) {
79
+ if (boundMs <= 0) return pool;
80
+ const getConnection = pool.getConnection.bind(pool);
81
+ const bounded = (callback) => {
82
+ let answered = false;
83
+ const wait = {};
84
+ let queued = false;
85
+ const onEnqueue = () => queued = true;
86
+ pool.once("enqueue", onEnqueue);
87
+ const waiting = (err, connection) => {
88
+ if (answered) {
89
+ connection?.release();
90
+ return;
91
+ }
92
+ answered = true;
93
+ clearTimeout(wait.timer);
94
+ callback(err, connection);
95
+ };
96
+ getConnection(waiting);
97
+ pool.removeListener("enqueue", onEnqueue);
98
+ if (!queued || answered) return;
99
+ wait.timer = setTimeout(() => {
100
+ answered = true;
101
+ leaveQueue(pool, waiting);
102
+ callback((0, _nextlyhq_adapter_drizzle_types.poolWaitTimeoutError)(boundMs));
103
+ }, boundMs);
104
+ };
105
+ pool.getConnection = bounded;
106
+ return pool;
107
+ }
15
108
  // src/index.ts
109
+ /**
110
+ * @nextlyhq/adapter-mysql
111
+ *
112
+ * MySQL database adapter for Nextly.
113
+ * Extends DrizzleAdapter from @nextlyhq/adapter-drizzle to provide MySQL-specific functionality.
114
+ *
115
+ * @remarks
116
+ * This adapter uses the mysql2 package for database connectivity and provides:
117
+ * - Connection pooling via mysql2 Pool
118
+ * - Full transaction support with isolation levels
119
+ * - CRUD operations with workarounds for missing RETURNING clause
120
+ * - MySQL-specific error classification
121
+ * - Automatic retry for deadlocks (error 1213)
122
+ *
123
+ * @example
124
+ * ```typescript
125
+ * import { createMySqlAdapter } from '@nextlyhq/adapter-mysql';
126
+ *
127
+ * const adapter = createMySqlAdapter({
128
+ * url: process.env.DATABASE_URL!,
129
+ * });
130
+ *
131
+ * await adapter.connect();
132
+ *
133
+ * // Query data
134
+ * const users = await adapter.select('users', {
135
+ * where: { and: [{ column: 'status', op: '=', value: 'active' }] },
136
+ * limit: 10,
137
+ * });
138
+ *
139
+ * await adapter.disconnect();
140
+ * ```
141
+ *
142
+ * @packageDocumentation
143
+ */
144
+ /**
145
+ * Puts back what a transaction changed on its connection's session. False
146
+ * when that could not be done, and the connection must not be pooled.
147
+ */
16
148
  async function restoreSession(connection, restore) {
17
- if (restore === null) return true;
18
- if (restore.lockWaitTimeout === null) return false;
19
- try {
20
- await connection.query(
21
- `SET SESSION innodb_lock_wait_timeout = ${restore.lockWaitTimeout}`
22
- );
23
- return true;
24
- } catch {
25
- return false;
26
- }
149
+ if (restore === null) return true;
150
+ if (restore.lockWaitTimeout === null || restore.sqlMode === null) return false;
151
+ try {
152
+ if (restore.lockWaitTimeout !== void 0) await connection.query(`SET SESSION innodb_lock_wait_timeout = ${restore.lockWaitTimeout}`);
153
+ if (restore.sqlMode !== void 0) await connection.query("SET SESSION sql_mode = ?", [restore.sqlMode]);
154
+ return true;
155
+ } catch {
156
+ return false;
157
+ }
27
158
  }
159
+ /**
160
+ * Ends a transaction's hold on its connection: back to the pool once its
161
+ * session is as the transaction found it, or closed when it is not. A
162
+ * connection whose ROLLBACK failed (`rollbackFailed`) may still be inside
163
+ * the transaction, and the next request handed it would write into a
164
+ * transaction nobody commits.
165
+ */
28
166
  async function returnTransactionConnection(connection, rollbackFailed, restore) {
29
- if (!rollbackFailed && await restoreSession(connection, restore)) {
30
- connection.release();
31
- } else {
32
- connection.destroy();
33
- }
167
+ if (!rollbackFailed && await restoreSession(connection, restore)) connection.release();
168
+ else connection.destroy();
34
169
  }
35
- var VERSION = "0.1.0";
36
- var DEFAULT_POOL_CONFIG = {
37
- max: 10,
38
- idleTimeoutMs: 3e4,
39
- connectionTimeoutMs: 1e4
170
+ /**
171
+ * Package version
172
+ */
173
+ const VERSION = "0.1.0";
174
+ /**
175
+ * Default pool configuration values.
176
+ */
177
+ const DEFAULT_POOL_CONFIG = {
178
+ min: 2,
179
+ max: 10,
180
+ idleTimeoutMs: 3e4,
181
+ connectionTimeoutMs: 15e3
40
182
  };
41
- var MYSQL_ERROR_CODES = {
42
- // Unique/Duplicate key violations
43
- 1022: "unique_violation",
44
- // ER_DUP_KEY
45
- 1062: "unique_violation",
46
- // ER_DUP_ENTRY
47
- 1169: "unique_violation",
48
- // ER_DUP_UNIQUE
49
- 1586: "unique_violation",
50
- // ER_DUP_ENTRY_WITH_KEY_NAME
51
- // Foreign key violations
52
- 1216: "foreign_key_violation",
53
- // ER_NO_REFERENCED_ROW
54
- 1217: "foreign_key_violation",
55
- // ER_ROW_IS_REFERENCED
56
- 1451: "foreign_key_violation",
57
- // ER_ROW_IS_REFERENCED_2
58
- 1452: "foreign_key_violation",
59
- // ER_NO_REFERENCED_ROW_2
60
- // Not null violations
61
- 1048: "not_null_violation",
62
- // ER_BAD_NULL_ERROR
63
- 1364: "not_null_violation",
64
- // ER_NO_DEFAULT_FOR_FIELD
65
- // Check constraint violations (MySQL 8.0.16+)
66
- 3819: "check_violation",
67
- // ER_CHECK_CONSTRAINT_VIOLATED
68
- // Deadlock
69
- 1213: "deadlock",
70
- // ER_LOCK_DEADLOCK
71
- // Timeout
72
- 1205: "timeout",
73
- // ER_LOCK_WAIT_TIMEOUT
74
- // Connection errors
75
- 1040: "connection",
76
- // ER_CON_COUNT_ERROR - Too many connections
77
- 1042: "connection",
78
- // ER_BAD_HOST_ERROR
79
- 1043: "connection",
80
- // ER_HANDSHAKE_ERROR
81
- 1044: "connection",
82
- // ER_DBACCESS_DENIED_ERROR
83
- 1045: "connection",
84
- // ER_ACCESS_DENIED_ERROR
85
- 1129: "connection",
86
- // ER_HOST_IS_BLOCKED
87
- 1130: "connection",
88
- // ER_HOST_NOT_PRIVILEGED
89
- 2002: "connection",
90
- // CR_CONNECTION_ERROR
91
- 2003: "connection",
92
- // CR_CONN_HOST_ERROR
93
- 2006: "connection",
94
- // CR_SERVER_GONE_ERROR
95
- 2013: "connection",
96
- // CR_SERVER_LOST
97
- // Query errors
98
- 1064: "query",
99
- // ER_PARSE_ERROR
100
- 1146: "query",
101
- // ER_NO_SUCH_TABLE
102
- 1054: "query"
103
- // ER_BAD_FIELD_ERROR
183
+ /**
184
+ * MySQL error codes mapping to DatabaseErrorKind.
185
+ *
186
+ * @see https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html
187
+ */
188
+ const MYSQL_ERROR_CODES = {
189
+ 1022: "unique_violation",
190
+ 1062: "unique_violation",
191
+ 1169: "unique_violation",
192
+ 1586: "unique_violation",
193
+ 1216: "foreign_key_violation",
194
+ 1217: "foreign_key_violation",
195
+ 1451: "foreign_key_violation",
196
+ 1452: "foreign_key_violation",
197
+ 1048: "not_null_violation",
198
+ 1364: "not_null_violation",
199
+ 3819: "check_violation",
200
+ 1213: "deadlock",
201
+ 1205: "timeout",
202
+ 1040: "connection",
203
+ 1042: "connection",
204
+ 1043: "connection",
205
+ 1044: "connection",
206
+ 1045: "connection",
207
+ 1129: "connection",
208
+ 1130: "connection",
209
+ 2002: "connection",
210
+ 2003: "connection",
211
+ 2006: "connection",
212
+ 2013: "connection",
213
+ 1064: "query",
214
+ 1146: "query",
215
+ 1054: "query"
104
216
  };
217
+ /**
218
+ * Delay helper for retry logic.
219
+ */
105
220
  function delay(ms) {
106
- return new Promise((resolve) => setTimeout(resolve, ms));
221
+ return new Promise((resolve) => setTimeout(resolve, ms));
107
222
  }
108
- var MySqlAdapter = class extends adapterDrizzle.DrizzleAdapter {
109
- // getDrizzle memoization: drizzle v1's constructor builds a relational
110
- // query builder per table in the relations config (~40 tables), and the
111
- // service layer resolves an instance on every db access — construct once
112
- // per relations object (identity-stable: the schema registry caches it
113
- // and hands out a NEW object on invalidation, which naturally misses
114
- // this cache and produces a fresh instance).
115
- drizzleByRelations = /* @__PURE__ */ new WeakMap();
116
- drizzleBare;
117
- /**
118
- * The database dialect - always 'mysql' for this adapter.
119
- */
120
- dialect = "mysql";
121
- /**
122
- * Adapter configuration.
123
- */
124
- config;
125
- /**
126
- * Connection pool instance.
127
- */
128
- pool = null;
129
- /**
130
- * Connection state flag.
131
- */
132
- connected = false;
133
- /**
134
- * Whether the server accepts `SELECT ... FOR SHARE`, read from its version
135
- * string at connect. Undefined until then, which callers treat as not known.
136
- */
137
- sharedRowLock;
138
- /**
139
- * Creates a new MySQL adapter instance.
140
- *
141
- * @param config - Adapter configuration
142
- */
143
- constructor(config) {
144
- super();
145
- this.config = config;
146
- }
147
- /**
148
- * Connect to the MySQL database.
149
- * Creates a connection pool using mysql2.
150
- *
151
- * @remarks
152
- * This method initializes the connection pool and verifies connectivity
153
- * by executing a simple query. It is idempotent - calling it multiple
154
- * times will not create multiple pools.
155
- *
156
- * @throws {DatabaseError} If connection fails
157
- */
158
- async connect() {
159
- if (this.connected && this.pool) {
160
- return;
161
- }
162
- try {
163
- const poolConfig = this.buildPoolConfig();
164
- this.pool = mysql__default.default.createPool(poolConfig);
165
- const connection = await this.pool.getConnection();
166
- try {
167
- await connection.query("SELECT 1");
168
- const { variant } = await versionCheck.checkDialectVersion(connection, "mysql", {
169
- // Why: route variant warnings through the adapter's logger so
170
- // users see a single, consistent log surface.
171
- onWarning: (msg) => this.config.logger?.warn?.(msg)
172
- });
173
- this.sharedRowLock = versionCheck.acceptsForShare(variant);
174
- this.connected = true;
175
- if (this.config.logger?.info) {
176
- this.config.logger.info("MySQL connection established", {
177
- host: this.config.host ?? "from URL",
178
- database: this.config.database ?? "from URL"
179
- });
180
- }
181
- } finally {
182
- connection.release();
183
- }
184
- } catch (error) {
185
- if (this.pool) {
186
- await this.pool.end().catch(() => {
187
- });
188
- this.pool = null;
189
- }
190
- throw this.classifyError(error);
191
- }
192
- }
193
- /**
194
- * Disconnect from the MySQL database.
195
- * Gracefully closes the connection pool.
196
- *
197
- * @remarks
198
- * This method is idempotent - calling it multiple times is safe.
199
- * It waits for all connections to be released before shutting down.
200
- */
201
- async disconnect() {
202
- const pool = this.pool;
203
- this.pool = null;
204
- this.drizzleBare = void 0;
205
- this.drizzleByRelations = /* @__PURE__ */ new WeakMap();
206
- if (!pool) {
207
- return;
208
- }
209
- try {
210
- await pool.end();
211
- if (this.config.logger?.info) {
212
- this.config.logger.info("MySQL connection closed");
213
- }
214
- } finally {
215
- this.connected = false;
216
- }
217
- }
218
- /**
219
- * Check if connected to the database.
220
- */
221
- isConnected() {
222
- return this.connected && this.pool !== null;
223
- }
224
- /**
225
- * Get connection pool statistics.
226
- * Returns null if not connected.
227
- *
228
- * @remarks
229
- * MySQL2 pool exposes different stats than pg:
230
- * - _allConnections: all connections
231
- * - _freeConnections: idle connections
232
- * - _connectionQueue: waiting requests
233
- */
234
- getPoolStats() {
235
- if (!this.pool) {
236
- return null;
237
- }
238
- const poolInternal = this.pool;
239
- const internal = poolInternal.pool;
240
- if (!internal) {
241
- return {
242
- total: 0,
243
- idle: 0,
244
- waiting: 0,
245
- active: 0
246
- };
247
- }
248
- const total = internal._allConnections?.length ?? 0;
249
- const idle = internal._freeConnections?.length ?? 0;
250
- const waiting = internal._connectionQueue?.length ?? 0;
251
- return {
252
- total,
253
- idle,
254
- waiting,
255
- active: total - idle
256
- };
257
- }
258
- /**
259
- * Execute a raw SQL query.
260
- *
261
- * @param sql - SQL query string with ? placeholders
262
- * @param params - Query parameters
263
- * @returns Query results
264
- *
265
- * @throws {DatabaseError} If query execution fails
266
- */
267
- async executeQuery(sql, params = []) {
268
- const pool = this.ensurePool();
269
- const startTime = Date.now();
270
- try {
271
- const [rows] = await pool.query(
272
- sql,
273
- params
274
- );
275
- if (this.config.logger?.query) {
276
- const durationMs = Date.now() - startTime;
277
- this.config.logger.query(sql, params, durationMs);
278
- }
279
- return rows;
280
- } catch (error) {
281
- throw this.classifyError(error, sql);
282
- }
283
- }
284
- /**
285
- * Execute work within a transaction.
286
- *
287
- * @param work - Function containing transactional operations
288
- * @param options - Transaction options (isolation level, timeout, retry)
289
- * @returns Result of the work function
290
- *
291
- * @remarks
292
- * MySQL transactions support isolation levels. Automatic retry is
293
- * implemented for deadlocks (error 1213) when `retryCount` is specified.
294
- *
295
- * Note: Savepoints are disabled in this adapter for safety due to
296
- * MySQL's quirks with nested transactions.
297
- */
298
- async transaction(work, options) {
299
- return this.runInFlowTransaction(
300
- () => this.runTransactionAttempts(work, options)
301
- );
302
- }
303
- /** The attempts `transaction` makes, each on a connection of its own. */
304
- async runTransactionAttempts(work, options) {
305
- const pool = this.ensurePool();
306
- const maxAttempts = (options?.retryCount ?? 0) + 1;
307
- const retryDelayMs = options?.retryDelayMs ?? 100;
308
- let lastError;
309
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
310
- const connection = await pool.getConnection();
311
- const startTime = Date.now();
312
- let rollbackFailed = false;
313
- let restore = null;
314
- try {
315
- restore = await this.beginTransaction(connection, options);
316
- const ctx = this.createTransactionContext(connection);
317
- const result = await work(ctx);
318
- await connection.query("COMMIT");
319
- if (this.config.logger?.debug) {
320
- const durationMs = Date.now() - startTime;
321
- this.config.logger.debug("Transaction committed", {
322
- attempt,
323
- durationMs
324
- });
325
- }
326
- return result;
327
- } catch (error) {
328
- await connection.query("ROLLBACK").catch(() => {
329
- rollbackFailed = true;
330
- });
331
- lastError = error;
332
- if (types.isApplicationError(error)) throw error;
333
- const mysqlError = error;
334
- const isRetryable = mysqlError.errno === 1213;
335
- if (isRetryable && attempt < maxAttempts) {
336
- if (this.config.logger?.warn) {
337
- this.config.logger.warn(
338
- `Transaction failed with deadlock, retrying (${attempt}/${maxAttempts})`,
339
- { errno: mysqlError.errno, attempt }
340
- );
341
- }
342
- await delay(retryDelayMs * attempt);
343
- continue;
344
- }
345
- throw this.classifyError(error);
346
- } finally {
347
- await returnTransactionConnection(connection, rollbackFailed, restore);
348
- }
349
- }
350
- throw this.classifyError(lastError);
351
- }
352
- /**
353
- * Get MySQL database capabilities.
354
- *
355
- * @remarks
356
- * MySQL has some limitations:
357
- * - No JSONB (uses JSON)
358
- * - No arrays
359
- * - No native ILIKE
360
- * - No RETURNING clause
361
- * - Savepoints disabled for safety
362
- */
363
- getCapabilities() {
364
- return {
365
- dialect: "mysql",
366
- supportsJsonb: false,
367
- // MySQL uses JSON, not JSONB
368
- supportsJson: true,
369
- supportsArrays: false,
370
- // MySQL doesn't support array types
371
- supportsGeneratedColumns: true,
372
- // MySQL 5.7.6+
373
- supportsFts: true,
374
- // MySQL has FULLTEXT indexes
375
- supportsIlike: false,
376
- // No native ILIKE, use LOWER() LIKE
377
- supportsReturning: false,
378
- // No RETURNING clause in MySQL
379
- supportsSavepoints: false,
380
- // Disabled for safety per approved approach
381
- supportsOnConflict: true,
382
- // ON DUPLICATE KEY UPDATE
383
- maxParamsPerQuery: 65535,
384
- // MySQL limit
385
- maxIdentifierLength: 64,
386
- // MySQL limit
387
- sharedRowLock: this.sharedRowLock
388
- // Known once connected
389
- };
390
- }
391
- /**
392
- * Build a placeholder for MySQL (uses ? instead of $1, $2, etc.)
393
- *
394
- * @param _index - Parameter index (ignored for MySQL)
395
- * @returns The ? placeholder
396
- */
397
- buildPlaceholder(_index) {
398
- return "?";
399
- }
400
- /**
401
- * Build multiple placeholders for MySQL.
402
- *
403
- * @param count - Number of placeholders needed
404
- * @param _startIndex - Starting index (ignored for MySQL)
405
- * @returns Comma-separated ? placeholders
406
- */
407
- buildPlaceholders(count, _startIndex = 0) {
408
- return Array(count).fill("?").join(", ");
409
- }
410
- /**
411
- * Escape an identifier for MySQL (uses backticks instead of double quotes).
412
- *
413
- * @param identifier - The identifier to escape
414
- * @returns Escaped identifier with backticks
415
- */
416
- escapeIdentifier(identifier) {
417
- return `\`${identifier.replace(/`/g, "``")}\``;
418
- }
419
- // ============================================================
420
- // Protected Helper Methods
421
- // ============================================================
422
- /**
423
- * Ensures pool is connected and returns it.
424
- *
425
- * @throws {DatabaseError} If not connected
426
- */
427
- ensurePool() {
428
- if (!this.pool) {
429
- throw types.createDatabaseError({
430
- kind: "connection",
431
- message: "MySqlAdapter is not connected. Call connect() first."
432
- });
433
- }
434
- return this.pool;
435
- }
436
- /**
437
- * Return the typed Drizzle instance for MySQL.
438
- * Guarded for server-only usage and requires an active connection.
439
- *
440
- * @param schema - Optional schema for relational queries (db.query.*)
441
- * @returns Drizzle ORM instance wrapping the mysql2 pool connection
442
- * @throws {Error} If called in browser or not connected
443
- */
444
- getDrizzle(relations) {
445
- if (typeof window !== "undefined") {
446
- throw new Error("getDrizzle() is server-only");
447
- }
448
- const pool = this.ensurePool();
449
- if (!relations) {
450
- this.drizzleBare ??= this.poolDrizzle(pool);
451
- return this.drizzleBare;
452
- }
453
- let cached = this.drizzleByRelations.get(relations);
454
- if (!cached) {
455
- cached = this.poolDrizzle(pool, relations);
456
- this.drizzleByRelations.set(relations, cached);
457
- }
458
- return cached;
459
- }
460
- /**
461
- * A Drizzle handle on the pool, for `getDrizzle`.
462
- *
463
- * Its `transaction` runs on a connection this adapter checks out and
464
- * returns itself (`pinDrizzleTransactions`): destroyed rather than pooled
465
- * when no ROLLBACK could be confirmed on it. And it counts for
466
- * `transactionOpenInFlow`, as the adapter's does.
467
- */
468
- poolDrizzle(pool, relations) {
469
- const onClient = (client) => relations ? mysql2.drizzle({ client, relations }) : mysql2.drizzle({ client });
470
- const callbackPool = pool.pool;
471
- return this.trackFlowTransactions(
472
- this.pinDrizzleTransactions(onClient(callbackPool), async () => {
473
- const connection = await pool.getConnection();
474
- return {
475
- drizzle: onClient(
476
- connection.connection
477
- ),
478
- rollback: () => connection.query("ROLLBACK"),
479
- release: (dirty) => {
480
- if (dirty) connection.destroy();
481
- else connection.release();
482
- }
483
- };
484
- })
485
- );
486
- }
487
- /**
488
- * Builds mysql2 Pool configuration from adapter config.
489
- */
490
- buildPoolConfig() {
491
- const config = {};
492
- if (this.config.url) {
493
- config.uri = this.config.url;
494
- } else {
495
- if (this.config.host) config.host = this.config.host;
496
- if (this.config.port) config.port = this.config.port;
497
- if (this.config.database) config.database = this.config.database;
498
- if (this.config.user) config.user = this.config.user;
499
- if (this.config.password) config.password = this.config.password;
500
- }
501
- config.connectionLimit = this.config.pool?.max ?? DEFAULT_POOL_CONFIG.max;
502
- config.idleTimeout = this.config.pool?.idleTimeoutMs ?? DEFAULT_POOL_CONFIG.idleTimeoutMs;
503
- config.connectTimeout = this.config.pool?.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
504
- config.waitForConnections = true;
505
- config.queueLimit = 0;
506
- if (this.config.ssl) {
507
- if (typeof this.config.ssl === "boolean") {
508
- config.ssl = this.config.ssl ? {} : void 0;
509
- } else {
510
- config.ssl = {
511
- rejectUnauthorized: this.config.ssl.rejectUnauthorized,
512
- ca: this.config.ssl.ca,
513
- cert: this.config.ssl.cert,
514
- key: this.config.ssl.key
515
- };
516
- }
517
- }
518
- if (this.config.timezone) {
519
- config.timezone = this.config.timezone;
520
- }
521
- if (this.config.charset) {
522
- config.charset = this.config.charset;
523
- }
524
- config.multipleStatements = false;
525
- config.dateStrings = false;
526
- return config;
527
- }
528
- /**
529
- * Begins a transaction with the specified options, and returns what it
530
- * changed on the session for `restoreSession` to put back.
531
- */
532
- async beginTransaction(connection, options) {
533
- if (options?.isolationLevel) {
534
- const isolationMap = {
535
- "read uncommitted": "READ UNCOMMITTED",
536
- "read committed": "READ COMMITTED",
537
- "repeatable read": "REPEATABLE READ",
538
- serializable: "SERIALIZABLE"
539
- };
540
- const level = isolationMap[options.isolationLevel];
541
- if (level) {
542
- await connection.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
543
- }
544
- }
545
- if (options?.readOnly) {
546
- await connection.query("SET TRANSACTION READ ONLY");
547
- }
548
- await connection.query("START TRANSACTION");
549
- if (options?.timeoutMs) {
550
- const [rows] = await connection.query(
551
- "SELECT @@SESSION.innodb_lock_wait_timeout AS timeout"
552
- );
553
- const previous = Number(rows[0]?.timeout);
554
- const timeoutSeconds = Math.ceil(options.timeoutMs / 1e3);
555
- await connection.query(
556
- `SET SESSION innodb_lock_wait_timeout = ${timeoutSeconds}`
557
- );
558
- return { lockWaitTimeout: Number.isInteger(previous) ? previous : null };
559
- }
560
- return null;
561
- }
562
- /**
563
- * Creates a TransactionContext for the given connection.
564
- *
565
- * @remarks
566
- * Note: Savepoint methods are not implemented (set to undefined)
567
- * as savepoints are disabled in this adapter per approved approach.
568
- */
569
- /**
570
- * Upsert with the meaning `INSERT … ON CONFLICT (target)` has on PostgreSQL
571
- * and SQLite, on the tables where MySQL can keep that promise.
572
- *
573
- * MySQL cannot limit a duplicate to one key: `ON DUPLICATE KEY UPDATE`
574
- * fires on a collision with ANY unique key, and a failed insert does not
575
- * say reliably which one. So the conflict target must be the table's only
576
- * unique key apart from its primary key, and any other table is refused
577
- * before anything is written. On such a table the row is inserted first —
578
- * its validation and triggers apply, as they do before `ON CONFLICT`
579
- * elsewhere — and a duplicate is resolved by a locking read of the target
580
- * key, which sees the latest committed row rather than the transaction's
581
- * snapshot. The row found is kept as it is when there is nothing to update
582
- * (a no-op assignment would still run its update triggers), or updated by
583
- * its key. No row there means the collision was on the primary key, and it
584
- * is refused as `ON CONFLICT (target)` refuses it.
585
- *
586
- * The steps run in the caller's transaction, or in one of their own, since a
587
- * locking read locks nothing past its statement under autocommit. Rows are
588
- * read back by their primary key, so a trigger that rewrites the conflict
589
- * value still returns the row stored. The conflict columns must also be
590
- * given values: MySQL cannot look a row up by a key the database would only
591
- * fill in itself. A conflict is resolved on the caller's key value: a
592
- * BEFORE INSERT trigger that moves the key onto another row's value is
593
- * refused as a unique violation rather than resolved onto that row. A failed insert rolls back only itself on MySQL, so a
594
- * caller's transaction stays usable.
595
- */
596
- async upsert(table, data, options, executor) {
597
- const tableObj = this.getTableObject(table);
598
- if (!tableObj) return super.upsert(table, data, options, executor);
599
- let target;
600
- try {
601
- target = this.upsertTarget(tableObj, table, data, options);
602
- } catch (error) {
603
- throw this.handleQueryError(error, "upsert", table);
604
- }
605
- if (executor === void 0) {
606
- return this.transaction((tx) => tx.upsert(table, data, options));
607
- }
608
- try {
609
- const db = executor;
610
- if (target.nullKey) {
611
- return await this.insertRow(db, tableObj, data) ?? data;
612
- }
613
- return await this.insertOrResolve(
614
- db,
615
- tableObj,
616
- data,
617
- options,
618
- target.where
619
- );
620
- } catch (error) {
621
- throw this.handleQueryError(error, "upsert", table);
622
- }
623
- }
624
- /**
625
- * What an upsert is keyed on: the target's condition, and whether its key
626
- * holds a NULL. Refuses a target this dialect cannot keep to, and a conflict
627
- * column left for the database to fill — MySQL cannot look a row up by a key
628
- * value only the database would supply.
629
- */
630
- upsertTarget(tableObj, table, data, options) {
631
- this.assertUpsertTarget(table, data, options, true);
632
- const key = this.conflictKey(tableObj, table, options.conflictColumns);
633
- const values = options.conflictColumns.map((column) => data[column]);
634
- return {
635
- where: drizzleOrm.and(...key.map((column, i) => drizzleOrm.eq(column, values[i]))),
636
- nullKey: values.some((value) => value === null)
637
- };
638
- }
639
- /**
640
- * The table's columns for a conflict target, refusing a target MySQL cannot
641
- * keep to: one that is not a primary or unique key, which an update could
642
- * spread over several rows, and one beside which the table has another
643
- * unique key, a collision on which MySQL would not tell apart.
644
- */
645
- conflictKey(tableObj, table, conflictColumns) {
646
- const record = tableObj;
647
- const columns = conflictColumns.map((name) => record[name]);
648
- const names = new Set(columns.map((column) => column?.name));
649
- const isTarget = (key) => key.length === names.size && key.every((name) => names.has(name));
650
- const { primary, unique } = keysOf(tableObj);
651
- if (primary.length === 0) {
652
- throw types.createDatabaseError({
653
- kind: "query",
654
- message: `upsert on ${table}: MySQL upserts need a table with a primary key, to read back the row they write.`
655
- });
656
- }
657
- const known = columns.every((column) => column !== void 0);
658
- if (!known || !(isTarget(primary) || unique.some((k) => isTarget(k.columns)))) {
659
- throw types.createDatabaseError({
660
- kind: "query",
661
- message: `upsert on ${table}: the conflict columns (${conflictColumns.join(", ")}) are not a primary or unique key of the table.`
662
- });
663
- }
664
- const others = unique.filter((key) => !isTarget(key.columns));
665
- if (others.length > 0) {
666
- throw types.createDatabaseError({
667
- kind: "query",
668
- message: `upsert on ${table}: MySQL cannot limit a duplicate to the conflict columns (${conflictColumns.join(", ")}) while the table has other unique keys (${others.map((key) => key.name).join(", ")}). Upsert only on a table whose one unique key besides the primary key is the conflict target, or insert and update inside a transaction.`
669
- });
670
- }
671
- return columns;
672
- }
673
- /** The update an upsert applies to an existing row: never its key. */
674
- updateValues(data, options) {
675
- const key = new Set(options.conflictColumns);
676
- const update = {};
677
- for (const column of options.updateColumns ?? Object.keys(data)) {
678
- if (!key.has(column) && column in data) update[column] = data[column];
679
- }
680
- return update;
681
- }
682
- /** The row matching `where`, by a locking (current) read. */
683
- async lockedRow(db, tableObj, where) {
684
- const rows = await db.select().from(tableObj).where(where).limit(1).for("update");
685
- return rows[0];
686
- }
687
- /**
688
- * Insert the row, or resolve a duplicate on the target key. The insert is
689
- * read back by the primary key it reports, not by the conflict key: a
690
- * BEFORE INSERT trigger may have rewritten the conflict value, and the row
691
- * stored is the one to return.
692
- *
693
- * A duplicate is resolved when a row holds the TARGET key, whichever unique
694
- * key MySQL names in its error. PostgreSQL checks the target's index before
695
- * any other, so a row colliding on the target and on another key resolves
696
- * on the target there too; refusing it here because MySQL happened to check
697
- * the other index first would answer that case differently. A target row
698
- * committed by another transaction after this insert failed is the order in
699
- * which that transaction ran first, where PostgreSQL also resolves on it.
700
- */
701
- async insertOrResolve(db, tableObj, data, options, where) {
702
- let failure;
703
- try {
704
- const stored = await this.insertRow(db, tableObj, data);
705
- return stored ?? await this.lockedRow(db, tableObj, where);
706
- } catch (error) {
707
- if (this.classifyError(error).kind !== "unique_violation") throw error;
708
- failure = error;
709
- }
710
- const existing = await this.lockedRow(db, tableObj, where);
711
- if (existing === void 0) throw failure;
712
- const update = this.updateValues(data, options);
713
- if (Object.keys(update).length === 0) return existing;
714
- await db.update(tableObj).set(update).where(where);
715
- const identity = this.byIdentity(tableObj, {
716
- ...existing,
717
- ...update
718
- });
719
- const updated = identity === void 0 ? void 0 : await this.lockedRow(db, tableObj, identity);
720
- return updated ?? this.lockedRow(db, tableObj, where);
721
- }
722
- /**
723
- * Insert a row and read it back by its primary key: the value the caller
724
- * gave, or the one MySQL or the model generated, which `$returningId()`
725
- * reports and a given value it does not. Undefined when the table has no
726
- * primary key to read by.
727
- */
728
- async insertRow(db, tableObj, data) {
729
- const [generated] = await db.insert(tableObj).values(data).$returningId();
730
- const given = Object.fromEntries(
731
- Object.entries(data).filter(
732
- ([, value]) => value !== void 0 && value !== null
733
- )
734
- );
735
- const identity = this.byIdentity(tableObj, { ...generated, ...given });
736
- return identity === void 0 ? void 0 : this.lockedRow(db, tableObj, identity);
737
- }
738
- /**
739
- * A row's primary key as a condition, from a record keyed as the model's
740
- * properties are. Undefined when the table has no primary key or the record
741
- * lacks part of it.
742
- */
743
- byIdentity(tableObj, record) {
744
- const primary = new Set(keysOf(tableObj).primary);
745
- const parts = Object.entries(drizzleOrm.getColumns(tableObj)).filter(
746
- ([, column]) => primary.has(column.name)
747
- );
748
- if (parts.length === 0 || parts.some(
749
- ([name]) => record[name] === void 0 || record[name] === null
750
- )) {
751
- return void 0;
752
- }
753
- return drizzleOrm.and(
754
- ...parts.map(([name, column]) => drizzleOrm.eq(column, record[name]))
755
- );
756
- }
757
- /** A table-level `primaryKey({ columns })`, read through the MySQL table config. */
758
- compositePrimaryKey(tableObj) {
759
- return mysqlCore.getTableConfig(
760
- tableObj
761
- ).primaryKeys.flatMap((key) => key.columns);
762
- }
763
- createTransactionContext(connection) {
764
- const txConnection = connection.connection;
765
- const txHandles = this.transactionDrizzleHandles(
766
- (relations) => relations ? mysql2.drizzle({ client: txConnection, relations }) : mysql2.drizzle({ client: txConnection })
767
- );
768
- const txDb = txHandles.bare;
769
- return {
770
- drizzleWithRelations: txHandles.withRelations,
771
- execute: async (sql, params = []) => {
772
- const [rows] = await connection.query(
773
- sql,
774
- params
775
- );
776
- return rows;
777
- },
778
- // Run on the transaction-bound Drizzle instance rather than the pool, so
779
- // the statement is part of this transaction and sees its uncommitted rows.
780
- runStatement: async (statement) => {
781
- await txDb().execute(statement);
782
- },
783
- // mysql2 answers a `[rows, fields]` tuple; the transaction-bound instance
784
- // keeps the read inside this transaction so it sees its uncommitted writes.
785
- queryStatement: async (statement) => {
786
- const result = await txDb().execute(statement);
787
- if (!Array.isArray(result)) {
788
- throw this.createDatabaseError(
789
- "query",
790
- "Drizzle statement returned a result shape this adapter does not recognise; refusing to report it as an empty result.",
791
- void 0
792
- );
793
- }
794
- return result[0];
795
- },
796
- lockRow: async (table, id) => {
797
- const idColumn = this.escapeIdentifier("id");
798
- await connection.query(
799
- `SELECT ${idColumn} FROM ${this.escapeIdentifier(table)} WHERE ${idColumn} = ? FOR UPDATE`,
800
- [id]
801
- );
802
- },
803
- insert: async (table, data, options) => {
804
- const mapped = this.mapRowToRawSql(this.getTableObject(table), data);
805
- const columns = Object.keys(mapped);
806
- const values = Object.values(mapped);
807
- const placeholders = this.buildPlaceholders(values.length, 0);
808
- const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
809
- const [result] = await connection.query(sql, values);
810
- const ret = options?.returning;
811
- if (Array.isArray(ret) && ret.length === 0) {
812
- return void 0;
813
- }
814
- const insertTableObj = this.getTableObject(table);
815
- const aliases = this.dateWallClockAliases(insertTableObj, ret ?? "*");
816
- const spelled = aliases.map(
817
- (a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`
818
- ).join(", ");
819
- const projected = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(insertTableObj, ret).map((c) => this.escapeIdentifier(c)).join(", ");
820
- const selectList = spelled ? `${projected}, ${spelled}` : projected;
821
- const idValue = result.insertId ? result.insertId : mapped.id;
822
- if (idValue !== void 0) {
823
- const [rows2] = await connection.query(
824
- `SELECT ${selectList} FROM ${this.escapeIdentifier(table)} WHERE id = ?`,
825
- [idValue]
826
- );
827
- return this.mapRowFromRawSql(insertTableObj, rows2[0], aliases);
828
- }
829
- const whereClauses = columns.map(
830
- (c) => `${this.escapeIdentifier(c)} = ?`
831
- );
832
- const [rows] = await connection.query(
833
- `SELECT ${selectList} FROM ${this.escapeIdentifier(table)} WHERE ${whereClauses.join(" AND ")} LIMIT 1`,
834
- values
835
- );
836
- return this.mapRowFromRawSql(insertTableObj, rows[0], aliases);
837
- },
838
- insertMany: async (table, data, options) => {
839
- if (data.length === 0) return [];
840
- const retMany = options?.returning;
841
- const skipReread = Array.isArray(retMany) && retMany.length === 0;
842
- const tableObj = this.getTableObject(table);
843
- const mappedRecords = data.map((r) => this.mapRowToRawSql(tableObj, r));
844
- const columns = Object.keys(mappedRecords[0]);
845
- const allValues = [];
846
- const valuesClauses = [];
847
- for (const record of mappedRecords) {
848
- const placeholders = [];
849
- for (const col of columns) {
850
- allValues.push(record[col]);
851
- placeholders.push("?");
852
- }
853
- valuesClauses.push(`(${placeholders.join(", ")})`);
854
- }
855
- const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
856
- const [result] = await connection.query(
857
- sql,
858
- allValues
859
- );
860
- if (!skipReread && result.insertId && result.affectedRows > 0) {
861
- const ids = [];
862
- for (let i = 0; i < result.affectedRows; i++) {
863
- ids.push(result.insertId + i);
864
- }
865
- const placeholders = ids.map(() => "?").join(", ");
866
- const bulkAliases = this.dateWallClockAliases(tableObj, "*");
867
- const bulkSpelled = bulkAliases.map(
868
- (a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`
869
- ).join(", ");
870
- const [rows] = await connection.query(
871
- `SELECT *${bulkSpelled ? `, ${bulkSpelled}` : ""} FROM ${this.escapeIdentifier(table)} WHERE id IN (${placeholders})`,
872
- ids
873
- );
874
- return rows.map(
875
- (r) => this.mapRowFromRawSql(tableObj, r, bulkAliases)
876
- );
877
- }
878
- return [];
879
- },
880
- // Adapter-built, as `insert` above is; `transactionUpdate` says why.
881
- // A column the model does not declare binds natively, as every value
882
- // on this path's insert does.
883
- update: this.transactionUpdate(
884
- txDb,
885
- async (statement) => {
886
- await txDb().execute(statement);
887
- return void 0;
888
- },
889
- // Structured values as JSON text: mysql2's formatter spells an object
890
- // as `key = value` pairs and an array as a value list, neither JSON.
891
- (value) => this.bindUnmodeledStructuredAsJson(value)
892
- ),
893
- ...this.createTransactionForwarders(txDb),
894
- // Savepoints disabled per approved approach
895
- savepoint: void 0,
896
- rollbackToSavepoint: void 0,
897
- releaseSavepoint: void 0
898
- };
899
- }
900
- /**
901
- * Classifies a MySQL error into a DatabaseError.
902
- *
903
- * @param error - Original error from mysql2
904
- * @param sql - SQL statement that caused the error (optional)
905
- * @returns DatabaseError with proper classification
906
- */
907
- classifyError(error, sql) {
908
- if (types.isDatabaseError(error)) return error;
909
- const mysqlError = this.driverErrorIn(error, (candidate) => typeof candidate.errno === "number");
910
- const kind = mysqlError.errno && MYSQL_ERROR_CODES[mysqlError.errno] || "unknown";
911
- let message = mysqlError.message ?? String(error);
912
- if (sql && kind === "query") {
913
- message = `Query failed: ${message}`;
914
- }
915
- return types.createDatabaseError({
916
- kind,
917
- message,
918
- code: mysqlError.code ?? mysqlError.errno?.toString(),
919
- detail: mysqlError.sql,
920
- cause: error instanceof Error ? error : void 0
921
- });
922
- }
223
+ /**
224
+ * MySQL database adapter for Nextly.
225
+ *
226
+ * Extends the base DrizzleAdapter to provide MySQL-specific functionality
227
+ * using the mysql2 package.
228
+ *
229
+ * @remarks
230
+ * MySQL has some limitations compared to PostgreSQL:
231
+ * - No native RETURNING clause (requires INSERT then SELECT)
232
+ * - No native ILIKE (uses LOWER() LIKE workaround)
233
+ * - No native JSONB (uses JSON type)
234
+ * - No array types
235
+ * - Savepoints disabled for safety (MySQL has nested transaction quirks)
236
+ *
237
+ * @example
238
+ * ```typescript
239
+ * const adapter = new MySqlAdapter({
240
+ * url: 'mysql://user:pass@localhost:3306/mydb',
241
+ * pool: { max: 20 },
242
+ * });
243
+ *
244
+ * await adapter.connect();
245
+ * ```
246
+ */
247
+ var MySqlAdapter = class extends _nextlyhq_adapter_drizzle.DrizzleAdapter {
248
+ drizzleByRelations = /* @__PURE__ */ new WeakMap();
249
+ drizzleBare;
250
+ /**
251
+ * The database dialect - always 'mysql' for this adapter.
252
+ */
253
+ dialect = "mysql";
254
+ /**
255
+ * Adapter configuration.
256
+ */
257
+ config;
258
+ /**
259
+ * Connection pool instance.
260
+ */
261
+ pool = null;
262
+ /**
263
+ * Connection state flag.
264
+ */
265
+ connected = false;
266
+ /**
267
+ * Whether the server accepts `SELECT ... FOR SHARE`, read from its version
268
+ * string at connect. Undefined until then, which callers treat as not known.
269
+ */
270
+ sharedRowLock;
271
+ /**
272
+ * Creates a new MySQL adapter instance.
273
+ *
274
+ * @param config - Adapter configuration
275
+ */
276
+ constructor(config) {
277
+ super();
278
+ this.config = config;
279
+ }
280
+ /**
281
+ * Connect to the MySQL database.
282
+ * Creates a connection pool using mysql2.
283
+ *
284
+ * @remarks
285
+ * This method initializes the connection pool and verifies connectivity
286
+ * by executing a simple query. It is idempotent - calling it multiple
287
+ * times will not create multiple pools.
288
+ *
289
+ * @throws {DatabaseError} If connection fails
290
+ */
291
+ async connect() {
292
+ if (this.connected && this.pool) return;
293
+ try {
294
+ const poolConfig = this.buildPoolConfig();
295
+ const pool = mysql2_promise.default.createPool(poolConfig);
296
+ boundPoolWait(pool.pool, poolConfig.connectTimeout ?? 0);
297
+ this.pool = pool;
298
+ const connection = await this.pool.getConnection();
299
+ try {
300
+ await connection.query("SELECT 1");
301
+ const { variant } = await (0, _nextlyhq_adapter_drizzle_version_check.checkDialectVersion)(connection, "mysql", { onWarning: (msg) => this.config.logger?.warn?.(msg) });
302
+ this.sharedRowLock = (0, _nextlyhq_adapter_drizzle_version_check.acceptsForShare)(variant);
303
+ this.connected = true;
304
+ if (this.config.logger?.info) this.config.logger.info("MySQL connection established", {
305
+ host: this.config.host ?? "from URL",
306
+ database: this.config.database ?? "from URL"
307
+ });
308
+ } finally {
309
+ connection.release();
310
+ }
311
+ } catch (error) {
312
+ if (this.pool) {
313
+ await this.pool.end().catch(() => {});
314
+ this.pool = null;
315
+ }
316
+ throw this.classifyError(error);
317
+ }
318
+ }
319
+ /**
320
+ * Disconnect from the MySQL database.
321
+ * Gracefully closes the connection pool.
322
+ *
323
+ * @remarks
324
+ * This method is idempotent - calling it multiple times is safe.
325
+ * It waits for all connections to be released before shutting down.
326
+ */
327
+ async disconnect() {
328
+ const pool = this.pool;
329
+ this.pool = null;
330
+ this.drizzleBare = void 0;
331
+ this.drizzleByRelations = /* @__PURE__ */ new WeakMap();
332
+ if (!pool) return;
333
+ try {
334
+ await pool.end();
335
+ if (this.config.logger?.info) this.config.logger.info("MySQL connection closed");
336
+ } finally {
337
+ this.connected = false;
338
+ }
339
+ }
340
+ /**
341
+ * Check if connected to the database.
342
+ */
343
+ isConnected() {
344
+ return this.connected && this.pool !== null;
345
+ }
346
+ /**
347
+ * Get connection pool statistics.
348
+ * Returns null if not connected.
349
+ *
350
+ * @remarks
351
+ * MySQL2 pool exposes different stats than pg:
352
+ * - _allConnections: all connections
353
+ * - _freeConnections: idle connections
354
+ * - _connectionQueue: waiting requests
355
+ */
356
+ getPoolStats() {
357
+ if (!this.pool) return null;
358
+ const internal = this.pool.pool;
359
+ if (!internal) return {
360
+ total: 0,
361
+ idle: 0,
362
+ waiting: 0,
363
+ active: 0
364
+ };
365
+ const total = internal._allConnections?.length ?? 0;
366
+ const idle = internal._freeConnections?.length ?? 0;
367
+ return {
368
+ total,
369
+ idle,
370
+ waiting: internal._connectionQueue?.length ?? 0,
371
+ active: total - idle
372
+ };
373
+ }
374
+ /**
375
+ * Execute a raw SQL query.
376
+ *
377
+ * @param sql - SQL query string with ? placeholders
378
+ * @param params - Query parameters
379
+ * @returns Query results
380
+ *
381
+ * @throws {DatabaseError} If query execution fails
382
+ */
383
+ async executeQuery(sql, params = []) {
384
+ const pool = this.ensurePool();
385
+ const startTime = Date.now();
386
+ try {
387
+ this.notePoolAccess(sql);
388
+ const [rows] = await pool.query(sql, params);
389
+ if (this.config.logger?.query) {
390
+ const durationMs = Date.now() - startTime;
391
+ this.config.logger.query(sql, params, durationMs);
392
+ }
393
+ return rows;
394
+ } catch (error) {
395
+ throw this.classifyError(error, sql);
396
+ }
397
+ }
398
+ /**
399
+ * Execute work within a transaction.
400
+ *
401
+ * @param work - Function containing transactional operations
402
+ * @param options - Transaction options (isolation level, timeout, retry)
403
+ * @returns Result of the work function
404
+ *
405
+ * @remarks
406
+ * MySQL transactions support isolation levels. Automatic retry is
407
+ * implemented for deadlocks (error 1213) when `retryCount` is specified.
408
+ *
409
+ * Note: Savepoints are disabled in this adapter for safety due to
410
+ * MySQL's quirks with nested transactions.
411
+ */
412
+ async transaction(work, options) {
413
+ return this.runInFlowTransaction(() => this.runTransactionAttempts(work, options));
414
+ }
415
+ /** The attempts `transaction` makes, each on a connection of its own. */
416
+ async runTransactionAttempts(work, options) {
417
+ const pool = this.ensurePool();
418
+ const maxAttempts = (options?.retryCount ?? 0) + 1;
419
+ const retryDelayMs = options?.retryDelayMs ?? 100;
420
+ let lastError;
421
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
422
+ const connection = await this.connectFlowTransaction(() => pool.getConnection());
423
+ const startTime = Date.now();
424
+ let rollbackFailed = false;
425
+ const restore = {};
426
+ try {
427
+ await this.beginTransaction(connection, options, restore);
428
+ const result = await work(this.createTransactionContext(connection));
429
+ await connection.query("COMMIT");
430
+ if (this.config.logger?.debug) {
431
+ const durationMs = Date.now() - startTime;
432
+ this.config.logger.debug("Transaction committed", {
433
+ attempt,
434
+ durationMs
435
+ });
436
+ }
437
+ return result;
438
+ } catch (error) {
439
+ await connection.query("ROLLBACK").catch(() => {
440
+ rollbackFailed = true;
441
+ });
442
+ lastError = error;
443
+ if ((0, _nextlyhq_adapter_drizzle_types.isApplicationError)(error)) throw error;
444
+ const mysqlError = error;
445
+ if (mysqlError.errno === 1213 && attempt < maxAttempts) {
446
+ if (this.config.logger?.warn) this.config.logger.warn(`Transaction failed with deadlock, retrying (${attempt}/${maxAttempts})`, {
447
+ errno: mysqlError.errno,
448
+ attempt
449
+ });
450
+ await delay(retryDelayMs * attempt);
451
+ continue;
452
+ }
453
+ throw this.classifyError(error);
454
+ } finally {
455
+ await returnTransactionConnection(connection, rollbackFailed, restore);
456
+ }
457
+ }
458
+ throw this.classifyError(lastError);
459
+ }
460
+ /**
461
+ * Get MySQL database capabilities.
462
+ *
463
+ * @remarks
464
+ * MySQL has some limitations:
465
+ * - No JSONB (uses JSON)
466
+ * - No arrays
467
+ * - No native ILIKE
468
+ * - No RETURNING clause
469
+ * - Savepoints disabled for safety
470
+ */
471
+ getCapabilities() {
472
+ return {
473
+ dialect: "mysql",
474
+ supportsJsonb: false,
475
+ supportsJson: true,
476
+ supportsArrays: false,
477
+ supportsGeneratedColumns: true,
478
+ supportsFts: true,
479
+ supportsIlike: false,
480
+ supportsReturning: false,
481
+ supportsSavepoints: false,
482
+ supportsOnConflict: true,
483
+ maxParamsPerQuery: 65535,
484
+ maxIdentifierLength: 64,
485
+ sharedRowLock: this.sharedRowLock
486
+ };
487
+ }
488
+ /**
489
+ * Build a placeholder for MySQL (uses ? instead of $1, $2, etc.)
490
+ *
491
+ * @param _index - Parameter index (ignored for MySQL)
492
+ * @returns The ? placeholder
493
+ */
494
+ buildPlaceholder(_index) {
495
+ return "?";
496
+ }
497
+ /**
498
+ * Build multiple placeholders for MySQL.
499
+ *
500
+ * @param count - Number of placeholders needed
501
+ * @param _startIndex - Starting index (ignored for MySQL)
502
+ * @returns Comma-separated ? placeholders
503
+ */
504
+ buildPlaceholders(count, _startIndex = 0) {
505
+ return Array(count).fill("?").join(", ");
506
+ }
507
+ /**
508
+ * Escape an identifier for MySQL (uses backticks instead of double quotes).
509
+ *
510
+ * @param identifier - The identifier to escape
511
+ * @returns Escaped identifier with backticks
512
+ */
513
+ escapeIdentifier(identifier) {
514
+ return `\`${identifier.replace(/`/g, "``")}\``;
515
+ }
516
+ /**
517
+ * Ensures pool is connected and returns it.
518
+ *
519
+ * @throws {DatabaseError} If not connected
520
+ */
521
+ ensurePool() {
522
+ if (!this.pool) throw (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
523
+ kind: "connection",
524
+ message: "MySqlAdapter is not connected. Call connect() first."
525
+ });
526
+ return this.pool;
527
+ }
528
+ /**
529
+ * Return the typed Drizzle instance for MySQL.
530
+ * Guarded for server-only usage and requires an active connection.
531
+ *
532
+ * @param schema - Optional schema for relational queries (db.query.*)
533
+ * @returns Drizzle ORM instance wrapping the mysql2 pool connection
534
+ * @throws {Error} If called in browser or not connected
535
+ */
536
+ getDrizzle(relations) {
537
+ if (typeof window !== "undefined") throw new Error("getDrizzle() is server-only");
538
+ const pool = this.ensurePool();
539
+ if (!relations) {
540
+ this.drizzleBare ??= this.poolDrizzle(pool);
541
+ return this.drizzleBare;
542
+ }
543
+ let cached = this.drizzleByRelations.get(relations);
544
+ if (!cached) {
545
+ cached = this.poolDrizzle(pool, relations);
546
+ this.drizzleByRelations.set(relations, cached);
547
+ }
548
+ return cached;
549
+ }
550
+ /**
551
+ * A Drizzle handle on the pool, for `getDrizzle`.
552
+ *
553
+ * Its `transaction` runs on a connection this adapter checks out and
554
+ * returns itself (`pinDrizzleTransactions`): destroyed rather than pooled
555
+ * when no ROLLBACK could be confirmed on it. And it counts for
556
+ * `transactionOpenInFlow`, as the adapter's does.
557
+ */
558
+ poolDrizzle(pool, relations) {
559
+ const onClient = (client) => relations ? (0, drizzle_orm_mysql2.drizzle)({
560
+ client,
561
+ relations
562
+ }) : (0, drizzle_orm_mysql2.drizzle)({ client });
563
+ const callbackPool = this.observedPool(pool.pool, [
564
+ "query",
565
+ "execute",
566
+ "getConnection"
567
+ ]);
568
+ return this.trackFlowTransactions(this.pinDrizzleTransactions(onClient(callbackPool), async () => {
569
+ const connection = await this.connectFlowTransaction(() => pool.getConnection());
570
+ return {
571
+ drizzle: onClient(connection.connection),
572
+ rollback: () => connection.query("ROLLBACK"),
573
+ release: (dirty) => {
574
+ if (dirty) connection.destroy();
575
+ else connection.release();
576
+ }
577
+ };
578
+ }));
579
+ }
580
+ /**
581
+ * Builds mysql2 Pool configuration from adapter config.
582
+ */
583
+ buildPoolConfig() {
584
+ const config = {};
585
+ if (this.config.url) config.uri = this.config.url;
586
+ else {
587
+ if (this.config.host) config.host = this.config.host;
588
+ if (this.config.port) config.port = this.config.port;
589
+ if (this.config.database) config.database = this.config.database;
590
+ if (this.config.user) config.user = this.config.user;
591
+ if (this.config.password) config.password = this.config.password;
592
+ }
593
+ config.connectionLimit = this.config.pool?.max ?? DEFAULT_POOL_CONFIG.max;
594
+ config.idleTimeout = this.config.pool?.idleTimeoutMs ?? DEFAULT_POOL_CONFIG.idleTimeoutMs;
595
+ config.connectTimeout = this.config.pool?.connectionTimeoutMs ?? DEFAULT_POOL_CONFIG.connectionTimeoutMs;
596
+ config.waitForConnections = true;
597
+ config.queueLimit = 0;
598
+ if (this.config.ssl) {
599
+ if (typeof this.config.ssl === "boolean") config.ssl = this.config.ssl ? {} : void 0;
600
+ else config.ssl = {
601
+ rejectUnauthorized: this.config.ssl.rejectUnauthorized,
602
+ ca: this.config.ssl.ca,
603
+ cert: this.config.ssl.cert,
604
+ key: this.config.ssl.key
605
+ };
606
+ }
607
+ if (this.config.timezone) config.timezone = this.config.timezone;
608
+ if (this.config.charset) config.charset = this.config.charset;
609
+ config.multipleStatements = false;
610
+ config.dateStrings = false;
611
+ return config;
612
+ }
613
+ /**
614
+ * Begins a transaction with the specified options, recording in `changed`
615
+ * each session setting it changes, as it changes it, for `restoreSession`
616
+ * to put back.
617
+ */
618
+ async beginTransaction(connection, options, changed) {
619
+ if (options?.isolationLevel) {
620
+ const level = {
621
+ "read uncommitted": "READ UNCOMMITTED",
622
+ "read committed": "READ COMMITTED",
623
+ "repeatable read": "REPEATABLE READ",
624
+ serializable: "SERIALIZABLE"
625
+ }[options.isolationLevel];
626
+ if (level) await connection.query(`SET TRANSACTION ISOLATION LEVEL ${level}`);
627
+ }
628
+ if (options?.readOnly) await connection.query("SET TRANSACTION READ ONLY");
629
+ await connection.query("START TRANSACTION");
630
+ if (options?.timeoutMs) {
631
+ const [rows] = await connection.query("SELECT @@SESSION.innodb_lock_wait_timeout AS timeout");
632
+ const previous = Number(rows[0]?.timeout);
633
+ const timeoutSeconds = Math.ceil(options.timeoutMs / 1e3);
634
+ await connection.query(`SET SESSION innodb_lock_wait_timeout = ${timeoutSeconds}`);
635
+ changed.lockWaitTimeout = Number.isInteger(previous) ? previous : null;
636
+ }
637
+ if (options?.strictSqlMode) {
638
+ const [rows] = await connection.query("SELECT @@SESSION.sql_mode AS mode");
639
+ const previous = rows[0]?.mode;
640
+ await connection.query("SET SESSION sql_mode = TRIM(BOTH ',' FROM CONCAT(@@SESSION.sql_mode, ',STRICT_ALL_TABLES'))");
641
+ changed.sqlMode = typeof previous === "string" ? previous : null;
642
+ }
643
+ }
644
+ /**
645
+ * Creates a TransactionContext for the given connection.
646
+ *
647
+ * @remarks
648
+ * Note: Savepoint methods are not implemented (set to undefined)
649
+ * as savepoints are disabled in this adapter per approved approach.
650
+ */
651
+ /**
652
+ * Upsert with the meaning `INSERT … ON CONFLICT (target)` has on PostgreSQL
653
+ * and SQLite, on the tables where MySQL can keep that promise.
654
+ *
655
+ * MySQL cannot limit a duplicate to one key: `ON DUPLICATE KEY UPDATE`
656
+ * fires on a collision with ANY unique key, and a failed insert does not
657
+ * say reliably which one. So the conflict target must be the table's only
658
+ * unique key apart from its primary key, and any other table is refused
659
+ * before anything is written. On such a table the row is inserted first —
660
+ * its validation and triggers apply, as they do before `ON CONFLICT`
661
+ * elsewhere — and a duplicate is resolved by a locking read of the target
662
+ * key, which sees the latest committed row rather than the transaction's
663
+ * snapshot. The row found is kept as it is when there is nothing to update
664
+ * (a no-op assignment would still run its update triggers), or updated by
665
+ * its key. No row there means the collision was on the primary key, and it
666
+ * is refused as `ON CONFLICT (target)` refuses it.
667
+ *
668
+ * The steps run in the caller's transaction, or in one of their own, since a
669
+ * locking read locks nothing past its statement under autocommit. Rows are
670
+ * read back by their primary key, so a trigger that rewrites the conflict
671
+ * value still returns the row stored. The conflict columns must also be
672
+ * given values: MySQL cannot look a row up by a key the database would only
673
+ * fill in itself. A conflict is resolved on the caller's key value: a
674
+ * BEFORE INSERT trigger that moves the key onto another row's value is
675
+ * refused as a unique violation rather than resolved onto that row. A failed insert rolls back only itself on MySQL, so a
676
+ * caller's transaction stays usable.
677
+ */
678
+ async upsert(table, data, options, executor) {
679
+ const tableObj = this.getTableObject(table);
680
+ if (!tableObj) return super.upsert(table, data, options, executor);
681
+ let target;
682
+ try {
683
+ target = this.upsertTarget(tableObj, table, data, options);
684
+ } catch (error) {
685
+ throw this.handleQueryError(error, "upsert", table);
686
+ }
687
+ if (executor === void 0) return this.transaction((tx) => tx.upsert(table, data, options));
688
+ try {
689
+ const db = executor;
690
+ if (target.nullKey) return await this.insertRow(db, tableObj, data) ?? data;
691
+ return await this.insertOrResolve(db, tableObj, data, options, target.where);
692
+ } catch (error) {
693
+ throw this.handleQueryError(error, "upsert", table);
694
+ }
695
+ }
696
+ /**
697
+ * What an upsert is keyed on: the target's condition, and whether its key
698
+ * holds a NULL. Refuses a target this dialect cannot keep to, and a conflict
699
+ * column left for the database to fill — MySQL cannot look a row up by a key
700
+ * value only the database would supply.
701
+ */
702
+ upsertTarget(tableObj, table, data, options) {
703
+ this.assertUpsertTarget(table, data, options, true);
704
+ const key = this.conflictKey(tableObj, table, options.conflictColumns);
705
+ const values = options.conflictColumns.map((column) => data[column]);
706
+ return {
707
+ where: (0, drizzle_orm.and)(...key.map((column, i) => (0, drizzle_orm.eq)(column, values[i]))),
708
+ nullKey: values.some((value) => value === null)
709
+ };
710
+ }
711
+ /**
712
+ * The table's columns for a conflict target, refusing a target MySQL cannot
713
+ * keep to: one that is not a primary or unique key, which an update could
714
+ * spread over several rows, and one beside which the table has another
715
+ * unique key, a collision on which MySQL would not tell apart.
716
+ */
717
+ conflictKey(tableObj, table, conflictColumns) {
718
+ const record = tableObj;
719
+ const columns = conflictColumns.map((name) => record[name]);
720
+ const names = new Set(columns.map((column) => column?.name));
721
+ const isTarget = (key) => key.length === names.size && key.every((name) => names.has(name));
722
+ const { primary, unique } = keysOf(tableObj);
723
+ if (primary.length === 0) throw (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
724
+ kind: "query",
725
+ message: `upsert on ${table}: MySQL upserts need a table with a primary key, to read back the row they write.`
726
+ });
727
+ if (!columns.every((column) => column !== void 0) || !(isTarget(primary) || unique.some((k) => isTarget(k.columns)))) throw (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
728
+ kind: "query",
729
+ message: `upsert on ${table}: the conflict columns (${conflictColumns.join(", ")}) are not a primary or unique key of the table.`
730
+ });
731
+ const others = unique.filter((key) => !isTarget(key.columns));
732
+ if (others.length > 0) throw (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
733
+ kind: "query",
734
+ message: `upsert on ${table}: MySQL cannot limit a duplicate to the conflict columns (${conflictColumns.join(", ")}) while the table has other unique keys (${others.map((key) => key.name).join(", ")}). Upsert only on a table whose one unique key besides the primary key is the conflict target, or insert and update inside a transaction.`
735
+ });
736
+ return columns;
737
+ }
738
+ /** The update an upsert applies to an existing row: never its key. */
739
+ updateValues(data, options) {
740
+ const key = new Set(options.conflictColumns);
741
+ const update = {};
742
+ for (const column of options.updateColumns ?? Object.keys(data)) if (!key.has(column) && column in data) update[column] = data[column];
743
+ return update;
744
+ }
745
+ /** The row matching `where`, by a locking (current) read. */
746
+ async lockedRow(db, tableObj, where) {
747
+ return (await db.select().from(tableObj).where(where).limit(1).for("update"))[0];
748
+ }
749
+ /**
750
+ * Insert the row, or resolve a duplicate on the target key. The insert is
751
+ * read back by the primary key it reports, not by the conflict key: a
752
+ * BEFORE INSERT trigger may have rewritten the conflict value, and the row
753
+ * stored is the one to return.
754
+ *
755
+ * A duplicate is resolved when a row holds the TARGET key, whichever unique
756
+ * key MySQL names in its error. PostgreSQL checks the target's index before
757
+ * any other, so a row colliding on the target and on another key resolves
758
+ * on the target there too; refusing it here because MySQL happened to check
759
+ * the other index first would answer that case differently. A target row
760
+ * committed by another transaction after this insert failed is the order in
761
+ * which that transaction ran first, where PostgreSQL also resolves on it.
762
+ */
763
+ async insertOrResolve(db, tableObj, data, options, where) {
764
+ let failure;
765
+ try {
766
+ return await this.insertRow(db, tableObj, data) ?? await this.lockedRow(db, tableObj, where);
767
+ } catch (error) {
768
+ if (this.classifyError(error).kind !== "unique_violation") throw error;
769
+ failure = error;
770
+ }
771
+ const existing = await this.lockedRow(db, tableObj, where);
772
+ if (existing === void 0) throw failure;
773
+ const update = this.updateValues(data, options);
774
+ if (Object.keys(update).length === 0) return existing;
775
+ await db.update(tableObj).set(update).where(where);
776
+ const identity = this.byIdentity(tableObj, {
777
+ ...existing,
778
+ ...update
779
+ });
780
+ return (identity === void 0 ? void 0 : await this.lockedRow(db, tableObj, identity)) ?? this.lockedRow(db, tableObj, where);
781
+ }
782
+ /**
783
+ * Insert a row and read it back by its primary key: the value the caller
784
+ * gave, or the one MySQL or the model generated, which `$returningId()`
785
+ * reports and a given value it does not. Undefined when the table has no
786
+ * primary key to read by.
787
+ */
788
+ async insertRow(db, tableObj, data) {
789
+ const [generated] = await db.insert(tableObj).values(data).$returningId();
790
+ const given = Object.fromEntries(Object.entries(data).filter(([, value]) => value !== void 0 && value !== null));
791
+ const identity = this.byIdentity(tableObj, {
792
+ ...generated,
793
+ ...given
794
+ });
795
+ return identity === void 0 ? void 0 : this.lockedRow(db, tableObj, identity);
796
+ }
797
+ /**
798
+ * A row's primary key as a condition, from a record keyed as the model's
799
+ * properties are. Undefined when the table has no primary key or the record
800
+ * lacks part of it.
801
+ */
802
+ byIdentity(tableObj, record) {
803
+ const primary = new Set(keysOf(tableObj).primary);
804
+ const parts = Object.entries((0, drizzle_orm.getColumns)(tableObj)).filter(([, column]) => primary.has(column.name));
805
+ if (parts.length === 0 || parts.some(([name]) => record[name] === void 0 || record[name] === null)) return;
806
+ return (0, drizzle_orm.and)(...parts.map(([name, column]) => (0, drizzle_orm.eq)(column, record[name])));
807
+ }
808
+ /** A table-level `primaryKey({ columns })`, read through the MySQL table config. */
809
+ compositePrimaryKey(tableObj) {
810
+ return (0, drizzle_orm_mysql_core.getTableConfig)(tableObj).primaryKeys.flatMap((key) => key.columns);
811
+ }
812
+ createTransactionContext(connection) {
813
+ const txConnection = connection.connection;
814
+ const txHandles = this.transactionDrizzleHandles((relations) => relations ? (0, drizzle_orm_mysql2.drizzle)({
815
+ client: txConnection,
816
+ relations
817
+ }) : (0, drizzle_orm_mysql2.drizzle)({ client: txConnection }));
818
+ const txDb = txHandles.bare;
819
+ return {
820
+ drizzleWithRelations: txHandles.withRelations,
821
+ execute: async (sql, params = []) => {
822
+ const [rows] = await connection.query(sql, params);
823
+ return rows;
824
+ },
825
+ runStatement: async (statement) => {
826
+ await txDb().execute(statement);
827
+ },
828
+ queryStatement: async (statement) => {
829
+ const result = await txDb().execute(statement);
830
+ if (!Array.isArray(result)) throw this.createDatabaseError("query", "Drizzle statement returned a result shape this adapter does not recognise; refusing to report it as an empty result.", void 0);
831
+ return result[0];
832
+ },
833
+ lockRow: async (table, id) => {
834
+ const idColumn = this.escapeIdentifier("id");
835
+ await connection.query(`SELECT ${idColumn} FROM ${this.escapeIdentifier(table)} WHERE ${idColumn} = ? FOR UPDATE`, [id]);
836
+ },
837
+ insert: async (table, data, options) => {
838
+ const mapped = this.mapRowToRawSql(this.getTableObject(table), data);
839
+ const columns = Object.keys(mapped);
840
+ const values = Object.values(mapped);
841
+ const placeholders = this.buildPlaceholders(values.length, 0);
842
+ const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES (${placeholders})`;
843
+ const [result] = await connection.query(sql, values);
844
+ const ret = options?.returning;
845
+ if (Array.isArray(ret) && ret.length === 0) return;
846
+ const insertTableObj = this.getTableObject(table);
847
+ const aliases = this.dateWallClockAliases(insertTableObj, ret ?? "*");
848
+ const spelled = aliases.map((a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`).join(", ");
849
+ const projected = !ret || ret === "*" ? "*" : this.mapColumnNamesToSql(insertTableObj, ret).map((c) => this.escapeIdentifier(c)).join(", ");
850
+ const selectList = spelled ? `${projected}, ${spelled}` : projected;
851
+ const idValue = result.insertId ? result.insertId : mapped.id;
852
+ if (idValue !== void 0) {
853
+ const [rows] = await connection.query(`SELECT ${selectList} FROM ${this.escapeIdentifier(table)} WHERE id = ?`, [idValue]);
854
+ return this.mapRowFromRawSql(insertTableObj, rows[0], aliases);
855
+ }
856
+ const whereClauses = columns.map((c) => `${this.escapeIdentifier(c)} = ?`);
857
+ const [rows] = await connection.query(`SELECT ${selectList} FROM ${this.escapeIdentifier(table)} WHERE ${whereClauses.join(" AND ")} LIMIT 1`, values);
858
+ return this.mapRowFromRawSql(insertTableObj, rows[0], aliases);
859
+ },
860
+ insertMany: async (table, data, options) => {
861
+ if (data.length === 0) return [];
862
+ const retMany = options?.returning;
863
+ const skipReread = Array.isArray(retMany) && retMany.length === 0;
864
+ const tableObj = this.getTableObject(table);
865
+ const mappedRecords = data.map((r) => this.mapRowToRawSql(tableObj, r));
866
+ const columns = Object.keys(mappedRecords[0]);
867
+ const allValues = [];
868
+ const valuesClauses = [];
869
+ for (const record of mappedRecords) {
870
+ const placeholders = [];
871
+ for (const col of columns) {
872
+ allValues.push(record[col]);
873
+ placeholders.push("?");
874
+ }
875
+ valuesClauses.push(`(${placeholders.join(", ")})`);
876
+ }
877
+ const sql = `INSERT INTO ${this.escapeIdentifier(table)} (${columns.map((c) => this.escapeIdentifier(c)).join(", ")}) VALUES ${valuesClauses.join(", ")}`;
878
+ const [result] = await connection.query(sql, allValues);
879
+ if (!skipReread && result.insertId && result.affectedRows > 0) {
880
+ const ids = [];
881
+ for (let i = 0; i < result.affectedRows; i++) ids.push(result.insertId + i);
882
+ const placeholders = ids.map(() => "?").join(", ");
883
+ const bulkAliases = this.dateWallClockAliases(tableObj, "*");
884
+ const bulkSpelled = bulkAliases.map((a) => `DATE_FORMAT(${this.escapeIdentifier(a.sqlName)}, '%Y-%m-%dT%H:%i:%s.%f') AS ${this.escapeIdentifier(a.alias)}`).join(", ");
885
+ const [rows] = await connection.query(`SELECT *${bulkSpelled ? `, ${bulkSpelled}` : ""} FROM ${this.escapeIdentifier(table)} WHERE id IN (${placeholders})`, ids);
886
+ return rows.map((r) => this.mapRowFromRawSql(tableObj, r, bulkAliases));
887
+ }
888
+ return [];
889
+ },
890
+ update: this.transactionUpdate(txDb, async (statement) => {
891
+ await txDb().execute(statement);
892
+ }, (value) => this.bindUnmodeledStructuredAsJson(value)),
893
+ ...this.createTransactionForwarders(txDb),
894
+ savepoint: void 0,
895
+ rollbackToSavepoint: void 0,
896
+ releaseSavepoint: void 0
897
+ };
898
+ }
899
+ /**
900
+ * Classifies a MySQL error into a DatabaseError.
901
+ *
902
+ * @param error - Original error from mysql2
903
+ * @param sql - SQL statement that caused the error (optional)
904
+ * @returns DatabaseError with proper classification
905
+ */
906
+ classifyError(error, sql) {
907
+ if ((0, _nextlyhq_adapter_drizzle_types.isDatabaseError)(error)) return error;
908
+ const mysqlError = this.driverErrorIn(error, (candidate) => typeof candidate.errno === "number");
909
+ const kind = mysqlError.errno && MYSQL_ERROR_CODES[mysqlError.errno] || "unknown";
910
+ let message = mysqlError.message ?? String(error);
911
+ if (sql && kind === "query") message = `Query failed: ${message}`;
912
+ return (0, _nextlyhq_adapter_drizzle_types.createDatabaseError)({
913
+ kind,
914
+ message,
915
+ code: mysqlError.code ?? mysqlError.errno?.toString(),
916
+ detail: mysqlError.sql,
917
+ cause: error instanceof Error ? error : void 0
918
+ });
919
+ }
923
920
  };
924
- var TABLE_KEYS = /* @__PURE__ */ new WeakMap();
921
+ const TABLE_KEYS = /* @__PURE__ */ new WeakMap();
922
+ /**
923
+ * A table's primary key and its unique keys other than the primary key, read
924
+ * from its model once and kept for the table's lifetime: a model is not
925
+ * changed in place, a new schema brings new table objects.
926
+ */
925
927
  function keysOf(tableObj) {
926
- const cached = TABLE_KEYS.get(tableObj);
927
- if (cached) return cached;
928
- const config = mysqlCore.getTableConfig(tableObj);
929
- const primary = [
930
- ...config.columns.filter((column) => column.primary),
931
- ...config.primaryKeys.flatMap((key) => key.columns)
932
- ].map((column) => column.name);
933
- const unique = [
934
- ...config.columns.filter((column) => column.isUnique && !column.primary).map((column) => ({
935
- // Drizzle's own name for an unnamed column-level unique key.
936
- name: column.uniqueName ?? `${config.name}_${column.name}_unique`,
937
- columns: [column.name]
938
- })),
939
- ...config.uniqueConstraints.map((constraint) => ({
940
- name: constraint.name,
941
- columns: constraint.columns.map((column) => column.name)
942
- })),
943
- ...config.indexes.filter((index) => index.config.unique === true).map((index) => ({
944
- name: index.config.name,
945
- columns: index.config.columns.map(
946
- (column) => column.name
947
- )
948
- }))
949
- ];
950
- const keys = { primary, unique };
951
- TABLE_KEYS.set(tableObj, keys);
952
- return keys;
928
+ const cached = TABLE_KEYS.get(tableObj);
929
+ if (cached) return cached;
930
+ const config = (0, drizzle_orm_mysql_core.getTableConfig)(tableObj);
931
+ const keys = {
932
+ primary: [...config.columns.filter((column) => column.primary), ...config.primaryKeys.flatMap((key) => key.columns)].map((column) => column.name),
933
+ unique: [
934
+ ...config.columns.filter((column) => column.isUnique && !column.primary).map((column) => ({
935
+ name: column.uniqueName ?? `${config.name}_${column.name}_unique`,
936
+ columns: [column.name]
937
+ })),
938
+ ...config.uniqueConstraints.map((constraint) => ({
939
+ name: constraint.name,
940
+ columns: constraint.columns.map((column) => column.name)
941
+ })),
942
+ ...config.indexes.filter((index) => index.config.unique === true).map((index) => ({
943
+ name: index.config.name,
944
+ columns: index.config.columns.map((column) => column.name)
945
+ }))
946
+ ]
947
+ };
948
+ TABLE_KEYS.set(tableObj, keys);
949
+ return keys;
953
950
  }
951
+ /**
952
+ * Create a MySQL database adapter.
953
+ *
954
+ * @param config - MySQL adapter configuration
955
+ * @returns A new MySqlAdapter instance
956
+ *
957
+ * @example
958
+ * ```typescript
959
+ * // Simple usage with URL
960
+ * const adapter = createMySqlAdapter({
961
+ * url: 'mysql://user:pass@localhost:3306/mydb',
962
+ * });
963
+ *
964
+ * // Full configuration
965
+ * const adapter = createMySqlAdapter({
966
+ * url: process.env.DATABASE_URL!,
967
+ * pool: {
968
+ * min: 2,
969
+ * max: 20,
970
+ * idleTimeoutMs: 30000,
971
+ * connectionTimeoutMs: 10000,
972
+ * },
973
+ * ssl: {
974
+ * rejectUnauthorized: true,
975
+ * },
976
+ * });
977
+ *
978
+ * await adapter.connect();
979
+ * ```
980
+ */
954
981
  function createMySqlAdapter(config) {
955
- return new MySqlAdapter(config);
982
+ return new MySqlAdapter(config);
956
983
  }
984
+ /**
985
+ * Type guard to check if a value is a MySqlAdapter.
986
+ *
987
+ * @param value - Value to check
988
+ * @returns True if value is a MySqlAdapter instance
989
+ *
990
+ * @example
991
+ * ```typescript
992
+ * if (isMySqlAdapter(adapter)) {
993
+ * // TypeScript knows adapter is MySqlAdapter
994
+ * console.log('Using MySQL');
995
+ * }
996
+ * ```
997
+ */
957
998
  function isMySqlAdapter(value) {
958
- return value instanceof MySqlAdapter;
999
+ return value instanceof MySqlAdapter;
959
1000
  }
960
-
961
1001
  exports.MySqlAdapter = MySqlAdapter;
962
1002
  exports.VERSION = VERSION;
963
1003
  exports.createMySqlAdapter = createMySqlAdapter;
964
1004
  exports.isMySqlAdapter = isMySqlAdapter;
965
- //# sourceMappingURL=index.cjs.map
1005
+
966
1006
  //# sourceMappingURL=index.cjs.map