@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/README.md +5 -5
- package/dist/index.cjs +985 -945
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +262 -299
- package/dist/index.d.ts +262 -299
- package/dist/index.mjs +961 -938
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -16
package/dist/index.cjs
CHANGED
|
@@ -1,966 +1,1006 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
var
|
|
9
|
-
var
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
} else {
|
|
32
|
-
connection.destroy();
|
|
33
|
-
}
|
|
167
|
+
if (!rollbackFailed && await restoreSession(connection, restore)) connection.release();
|
|
168
|
+
else connection.destroy();
|
|
34
169
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
221
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
107
222
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
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
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
+
|
|
966
1006
|
//# sourceMappingURL=index.cjs.map
|