@vibeorm/adapter-mysql 2.0.0-alpha.1 → 2.0.0-alpha.10
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/acquire.d.ts +73 -0
- package/dist/acquire.d.ts.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +529 -34
- package/dist/index.js.map +9 -5
- package/dist/placeholders.d.ts +70 -0
- package/dist/placeholders.d.ts.map +1 -0
- package/dist/savepoint-gate.d.ts +86 -0
- package/dist/savepoint-gate.d.ts.map +1 -0
- package/dist/transaction-budget.d.ts +144 -0
- package/dist/transaction-budget.d.ts.map +1 -0
- package/dist/transaction-sql.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -3,8 +3,56 @@
|
|
|
3
3
|
import { internalError } from "@vibeorm/schema";
|
|
4
4
|
import { createPool as mysql2CreatePool } from "mysql2/promise";
|
|
5
5
|
|
|
6
|
-
// packages/adapter-mysql/src/
|
|
6
|
+
// packages/adapter-mysql/src/acquire.ts
|
|
7
7
|
import { VibeError } from "@vibeorm/schema";
|
|
8
|
+
var DEFAULT_ACQUIRE_TIMERS = Object.freeze({
|
|
9
|
+
set: (fn, ms) => setTimeout(fn, ms),
|
|
10
|
+
clear: (handle) => clearTimeout(handle)
|
|
11
|
+
});
|
|
12
|
+
var MYSQL_DEFAULT_CONNECTION_LIMIT = 10;
|
|
13
|
+
function acquireTimeoutError(params) {
|
|
14
|
+
const { timeoutMs, poolSize } = params;
|
|
15
|
+
const size = poolSize === undefined ? "pool size unknown \u2014 the pool was supplied by the caller" : `connectionLimit ${poolSize}`;
|
|
16
|
+
return new VibeError({
|
|
17
|
+
code: "VIBE_ADAPTER",
|
|
18
|
+
message: `timed out after ${timeoutMs} ms waiting for a mysql pool connection \u2014 every pooled connection ` + `is busy (${size}). Raise \`connectionLimit\`, shorten the work holding a connection, or raise ` + "the adapter's `acquireTimeout` option.",
|
|
19
|
+
meta: {
|
|
20
|
+
reason: "acquire-timeout",
|
|
21
|
+
timeoutMs,
|
|
22
|
+
...poolSize === undefined ? {} : { connectionLimit: poolSize }
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async function acquireWithTimeout(params) {
|
|
27
|
+
const { pool, timeoutMs, poolSize } = params;
|
|
28
|
+
const timers = params.timers ?? DEFAULT_ACQUIRE_TIMERS;
|
|
29
|
+
const pending = pool.getConnection();
|
|
30
|
+
let timedOut = false;
|
|
31
|
+
let handle;
|
|
32
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
33
|
+
handle = timers.set(() => {
|
|
34
|
+
timedOut = true;
|
|
35
|
+
reject(acquireTimeoutError({ timeoutMs, poolSize }));
|
|
36
|
+
}, timeoutMs);
|
|
37
|
+
});
|
|
38
|
+
pending.then((connection) => {
|
|
39
|
+
if (timedOut)
|
|
40
|
+
connection.release();
|
|
41
|
+
}, () => {
|
|
42
|
+
return;
|
|
43
|
+
}).catch(() => {
|
|
44
|
+
return;
|
|
45
|
+
});
|
|
46
|
+
try {
|
|
47
|
+
return await Promise.race([pending, deadline]);
|
|
48
|
+
} finally {
|
|
49
|
+
if (handle !== undefined)
|
|
50
|
+
timers.clear(handle);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// packages/adapter-mysql/src/errors.ts
|
|
55
|
+
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
8
56
|
var MYSQL_ERRNO_ERROR_CODES = Object.freeze({
|
|
9
57
|
1062: "VIBE_UNIQUE_VIOLATION",
|
|
10
58
|
1586: "VIBE_UNIQUE_VIOLATION",
|
|
@@ -44,7 +92,7 @@ function mysqlErrorErrno(params) {
|
|
|
44
92
|
}
|
|
45
93
|
function mapMysqlDriverError(params) {
|
|
46
94
|
const { error } = params;
|
|
47
|
-
if (error instanceof
|
|
95
|
+
if (error instanceof VibeError2)
|
|
48
96
|
return error;
|
|
49
97
|
const source = error !== null && typeof error === "object" ? error : {};
|
|
50
98
|
const errno = mysqlErrorErrno({ error });
|
|
@@ -65,7 +113,7 @@ function mapMysqlDriverError(params) {
|
|
|
65
113
|
if (reason !== undefined)
|
|
66
114
|
meta.reason = reason;
|
|
67
115
|
const message = readStringField({ source, key: "message" }) ?? (sqlMessage !== undefined ? sqlMessage.trim() : undefined) ?? (typeof error === "string" ? error : "unknown mysql driver error");
|
|
68
|
-
return new
|
|
116
|
+
return new VibeError2({ code, message, meta, cause: error });
|
|
69
117
|
}
|
|
70
118
|
async function runMapped(fn) {
|
|
71
119
|
try {
|
|
@@ -75,6 +123,131 @@ async function runMapped(fn) {
|
|
|
75
123
|
}
|
|
76
124
|
}
|
|
77
125
|
|
|
126
|
+
// packages/adapter-mysql/src/placeholders.ts
|
|
127
|
+
import { VibeError as VibeError3 } from "@vibeorm/schema";
|
|
128
|
+
var ROW_COUNT_HINT = /\b(?:limit|offset)\b/i;
|
|
129
|
+
function isIdentChar(char) {
|
|
130
|
+
return char !== undefined && /[A-Za-z0-9_$]/.test(char);
|
|
131
|
+
}
|
|
132
|
+
function skipQuoted(text, start, quote) {
|
|
133
|
+
let index = start + 1;
|
|
134
|
+
while (index < text.length) {
|
|
135
|
+
const char = text[index];
|
|
136
|
+
if (char === "\\") {
|
|
137
|
+
index += 2;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (char === quote) {
|
|
141
|
+
if (text[index + 1] === quote) {
|
|
142
|
+
index += 2;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
return index + 1;
|
|
146
|
+
}
|
|
147
|
+
index += 1;
|
|
148
|
+
}
|
|
149
|
+
return text.length;
|
|
150
|
+
}
|
|
151
|
+
function skipLineComment(text, start) {
|
|
152
|
+
const newline = text.indexOf(`
|
|
153
|
+
`, start);
|
|
154
|
+
return newline === -1 ? text.length : newline + 1;
|
|
155
|
+
}
|
|
156
|
+
function skipBlockComment(text, start) {
|
|
157
|
+
const close = text.indexOf("*/", start + 2);
|
|
158
|
+
return close === -1 ? text.length : close + 2;
|
|
159
|
+
}
|
|
160
|
+
function scanPlaceholders(params) {
|
|
161
|
+
const { text } = params;
|
|
162
|
+
const rowCountPositions = new Set;
|
|
163
|
+
let count = 0;
|
|
164
|
+
let inRowCount = false;
|
|
165
|
+
let index = 0;
|
|
166
|
+
while (index < text.length) {
|
|
167
|
+
const char = text[index];
|
|
168
|
+
if (char === "'" || char === '"' || char === "`") {
|
|
169
|
+
index = skipQuoted(text, index, char);
|
|
170
|
+
inRowCount = false;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (char === "-" && text[index + 1] === "-" && (text[index + 2] === undefined || /\s/.test(text[index + 2]))) {
|
|
174
|
+
index = skipLineComment(text, index);
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (char === "#") {
|
|
178
|
+
index = skipLineComment(text, index);
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (char === "/" && text[index + 1] === "*") {
|
|
182
|
+
index = skipBlockComment(text, index);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (char === "?") {
|
|
186
|
+
if (inRowCount)
|
|
187
|
+
rowCountPositions.add(count);
|
|
188
|
+
count += 1;
|
|
189
|
+
index += 1;
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (/\s/.test(char) || char === ",") {
|
|
193
|
+
index += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (/[0-9]/.test(char)) {
|
|
197
|
+
while (index < text.length && /[0-9]/.test(text[index]))
|
|
198
|
+
index += 1;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (/[A-Za-z_$]/.test(char)) {
|
|
202
|
+
const start = index;
|
|
203
|
+
while (isIdentChar(text[index]))
|
|
204
|
+
index += 1;
|
|
205
|
+
const word = text.slice(start, index).toUpperCase();
|
|
206
|
+
inRowCount = word === "LIMIT" || word === "OFFSET";
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
inRowCount = false;
|
|
210
|
+
index += 1;
|
|
211
|
+
}
|
|
212
|
+
return { count, rowCountPositions };
|
|
213
|
+
}
|
|
214
|
+
function toRowCount(params) {
|
|
215
|
+
const { value, ordinal, text } = params;
|
|
216
|
+
const refuse = (reason) => {
|
|
217
|
+
throw new VibeError3({
|
|
218
|
+
code: "VIBE_VALIDATION",
|
|
219
|
+
message: `parameter $${ordinal} sits in a LIMIT/OFFSET position and ${reason} \u2014 ` + "a row count must be a non-negative integer",
|
|
220
|
+
meta: { placeholder: ordinal, value: String(value), text }
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
if (typeof value === "number") {
|
|
224
|
+
if (!Number.isSafeInteger(value))
|
|
225
|
+
return refuse("is not a safe integer");
|
|
226
|
+
if (value < 0)
|
|
227
|
+
return refuse("is negative");
|
|
228
|
+
return String(value);
|
|
229
|
+
}
|
|
230
|
+
if (typeof value === "bigint") {
|
|
231
|
+
if (value < 0n)
|
|
232
|
+
return refuse("is negative");
|
|
233
|
+
return value.toString();
|
|
234
|
+
}
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
function bindRowCountParams(params) {
|
|
238
|
+
const { text, values } = params;
|
|
239
|
+
if (values.length === 0)
|
|
240
|
+
return values;
|
|
241
|
+
if (!values.some((value) => typeof value === "number" || typeof value === "bigint"))
|
|
242
|
+
return values;
|
|
243
|
+
if (!ROW_COUNT_HINT.test(text))
|
|
244
|
+
return values;
|
|
245
|
+
const scanned = scanPlaceholders({ text });
|
|
246
|
+
if (scanned.rowCountPositions.size === 0 || scanned.count !== values.length)
|
|
247
|
+
return values;
|
|
248
|
+
return values.map((value, index) => scanned.rowCountPositions.has(index) ? toRowCount({ value, ordinal: index + 1, text }) : value);
|
|
249
|
+
}
|
|
250
|
+
|
|
78
251
|
// packages/adapter-mysql/src/results.ts
|
|
79
252
|
function isResultSetHeader(params) {
|
|
80
253
|
const { value } = params;
|
|
@@ -104,6 +277,128 @@ function normalizeDriverResult(params) {
|
|
|
104
277
|
return normalizeSingle(result);
|
|
105
278
|
}
|
|
106
279
|
|
|
280
|
+
// packages/adapter-mysql/src/transaction-budget.ts
|
|
281
|
+
import { VibeError as VibeError4 } from "@vibeorm/schema";
|
|
282
|
+
var DEFAULT_DEADLINE_CLOCK = () => Date.now();
|
|
283
|
+
function validateTransactionDeadline(params) {
|
|
284
|
+
const { options, support, provider } = params;
|
|
285
|
+
const deadline = options?.deadline;
|
|
286
|
+
if (deadline === undefined)
|
|
287
|
+
return;
|
|
288
|
+
if (support === "unsupported") {
|
|
289
|
+
throw new VibeError4({
|
|
290
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
291
|
+
message: `${provider} cannot enforce a transaction deadline \u2014 remove the \`deadline\` option`,
|
|
292
|
+
meta: { provider, feature: "transactionDeadline", supported: support }
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isInteger(deadline.totalMs) || deadline.totalMs <= 0) {
|
|
296
|
+
throw new VibeError4({
|
|
297
|
+
code: "VIBE_VALIDATION",
|
|
298
|
+
message: `transaction deadline \`totalMs\` must be a positive integer of milliseconds, received ${String(deadline.totalMs)}`,
|
|
299
|
+
meta: { provider, feature: "transactionDeadline", totalMs: deadline.totalMs }
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const requested = deadline.enforcement ?? "between-statements";
|
|
303
|
+
if (requested === "cancel-running-statements" && support !== "cancel-running-statements") {
|
|
304
|
+
throw new VibeError4({
|
|
305
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
306
|
+
message: `${provider} cannot cancel a statement that is already running, so the requested ` + "`cancel-running-statements` deadline guarantee is refused rather than silently downgraded \u2014 " + 'ask for "between-statements" if a budget checked between statements is enough',
|
|
307
|
+
meta: { provider, feature: "transactionDeadline", requested, supported: support }
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function startTransactionBudget(params) {
|
|
312
|
+
const { options, provider } = params;
|
|
313
|
+
const clock = params.clock ?? DEFAULT_DEADLINE_CLOCK;
|
|
314
|
+
const deadline = options?.deadline;
|
|
315
|
+
const totalMs = deadline === undefined ? null : deadline.totalMs;
|
|
316
|
+
const enforcement = deadline === undefined ? null : deadline.enforcement ?? "between-statements";
|
|
317
|
+
const expiresAt = totalMs === null ? null : clock() + totalMs;
|
|
318
|
+
let outcome = null;
|
|
319
|
+
const budget = {
|
|
320
|
+
enforcement,
|
|
321
|
+
totalMs,
|
|
322
|
+
statementTimeoutMs: options?.timeout ?? null,
|
|
323
|
+
remainingMs() {
|
|
324
|
+
return expiresAt === null ? null : Math.max(0, expiresAt - clock());
|
|
325
|
+
},
|
|
326
|
+
expired() {
|
|
327
|
+
return expiresAt !== null && clock() >= expiresAt;
|
|
328
|
+
},
|
|
329
|
+
assertUsable(assertParams) {
|
|
330
|
+
if (outcome !== null) {
|
|
331
|
+
throw transactionClosedError({ provider, outcome, stage: assertParams.stage });
|
|
332
|
+
}
|
|
333
|
+
if (expiresAt !== null && clock() >= expiresAt) {
|
|
334
|
+
throw transactionDeadlineError({
|
|
335
|
+
provider,
|
|
336
|
+
stage: assertParams.stage,
|
|
337
|
+
totalMs,
|
|
338
|
+
overdueMs: clock() - expiresAt
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
close(closeParams) {
|
|
343
|
+
if (outcome === null)
|
|
344
|
+
outcome = closeParams.outcome;
|
|
345
|
+
},
|
|
346
|
+
get outcome() {
|
|
347
|
+
return outcome;
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
return budget;
|
|
351
|
+
}
|
|
352
|
+
function engineStatementBudgetMs(params) {
|
|
353
|
+
const { budget } = params;
|
|
354
|
+
if (budget.enforcement !== "cancel-running-statements")
|
|
355
|
+
return null;
|
|
356
|
+
const remaining = budget.remainingMs();
|
|
357
|
+
if (remaining === null)
|
|
358
|
+
return null;
|
|
359
|
+
const totalBound = Math.max(1, Math.ceil(remaining));
|
|
360
|
+
return budget.statementTimeoutMs !== null && budget.statementTimeoutMs > 0 ? Math.min(totalBound, budget.statementTimeoutMs) : totalBound;
|
|
361
|
+
}
|
|
362
|
+
function transactionDeadlineError(params) {
|
|
363
|
+
const { provider, stage, totalMs, overdueMs } = params;
|
|
364
|
+
const what = stage === "commit" ? "the transaction was rolled back instead of committed" : "the statement was never sent and the transaction is being rolled back";
|
|
365
|
+
return new VibeError4({
|
|
366
|
+
code: "VIBE_TRANSACTION",
|
|
367
|
+
message: `transaction deadline of ${totalMs} ms expired ${Math.max(0, Math.round(overdueMs))} ms ago \u2014 ${what}. ` + "Nothing is retried automatically: work the callback did outside the database may already have happened.",
|
|
368
|
+
meta: { provider, reason: "transaction-deadline", stage, totalMs, overdueMs: Math.max(0, Math.round(overdueMs)) }
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function transactionClosedError(params) {
|
|
372
|
+
const { provider, outcome, stage } = params;
|
|
373
|
+
return new VibeError4({
|
|
374
|
+
code: "VIBE_TRANSACTION",
|
|
375
|
+
message: `this transaction has already finished (${outcome}), so its client can no longer be used \u2014 ` + "a handle kept past the end of `$transaction` would run outside the transaction, on a connection " + "that now belongs to someone else",
|
|
376
|
+
meta: { provider, reason: "transaction-closed", outcome, stage }
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function transactionOutcomeUnknownError(params) {
|
|
380
|
+
const { provider, totalMs, cause } = params;
|
|
381
|
+
return new VibeError4({
|
|
382
|
+
code: "VIBE_TRANSACTION",
|
|
383
|
+
message: `transaction deadline of ${totalMs} ms expired and the rollback that should have cleaned up failed \u2014 ` + "the commit state of this transaction is UNKNOWN. It is not retried automatically; inspect the data " + "before deciding.",
|
|
384
|
+
meta: {
|
|
385
|
+
provider,
|
|
386
|
+
reason: "transaction-outcome-unknown",
|
|
387
|
+
outcome: "unknown",
|
|
388
|
+
totalMs,
|
|
389
|
+
rollbackFailed: true
|
|
390
|
+
},
|
|
391
|
+
cause
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function isTransactionDeadlineError(error) {
|
|
395
|
+
const reason = error?.meta?.reason;
|
|
396
|
+
return reason === "transaction-deadline";
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// packages/adapter-mysql/src/savepoint-gate.ts
|
|
400
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
401
|
+
|
|
107
402
|
// packages/adapter-mysql/src/savepoints.ts
|
|
108
403
|
function createSavepointCounter() {
|
|
109
404
|
return { n: 0 };
|
|
@@ -112,8 +407,111 @@ function nextSavepointName(params) {
|
|
|
112
407
|
return `vibeorm_sp_${params.counter.n++}`;
|
|
113
408
|
}
|
|
114
409
|
|
|
410
|
+
// packages/adapter-mysql/src/savepoint-gate.ts
|
|
411
|
+
var CURRENT = new AsyncLocalStorage;
|
|
412
|
+
function createSavepointScope() {
|
|
413
|
+
return { depth: 0, state: { counter: createSavepointCounter(), open: null } };
|
|
414
|
+
}
|
|
415
|
+
function runRootScope(params) {
|
|
416
|
+
return CURRENT.run(params.scope, params.body);
|
|
417
|
+
}
|
|
418
|
+
function scopeTurn(params) {
|
|
419
|
+
const open = params.scope.state.open;
|
|
420
|
+
if (mayProceed({ scope: params.scope, open }))
|
|
421
|
+
return;
|
|
422
|
+
return waitForTurn({ scope: params.scope });
|
|
423
|
+
}
|
|
424
|
+
async function runNestedSavepoint(params) {
|
|
425
|
+
const { scope, execute, body } = params;
|
|
426
|
+
const node = await acquire({ scope });
|
|
427
|
+
try {
|
|
428
|
+
if (execute !== undefined)
|
|
429
|
+
await execute(`SAVEPOINT ${node.name}`);
|
|
430
|
+
let result;
|
|
431
|
+
try {
|
|
432
|
+
result = await CURRENT.run(node.scope, () => body(node.scope));
|
|
433
|
+
} catch (error) {
|
|
434
|
+
await drainDescendants({ node });
|
|
435
|
+
if (execute !== undefined) {
|
|
436
|
+
try {
|
|
437
|
+
await execute(`ROLLBACK TO SAVEPOINT ${node.name}`);
|
|
438
|
+
} catch {}
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
await drainDescendants({ node });
|
|
443
|
+
if (execute !== undefined)
|
|
444
|
+
await execute(`RELEASE SAVEPOINT ${node.name}`);
|
|
445
|
+
return result;
|
|
446
|
+
} finally {
|
|
447
|
+
closeNode({ node });
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
function mayProceed(params) {
|
|
451
|
+
const { scope, open } = params;
|
|
452
|
+
if (open === null)
|
|
453
|
+
return true;
|
|
454
|
+
if (open.scope === scope)
|
|
455
|
+
return true;
|
|
456
|
+
if (open.scope.depth < scope.depth)
|
|
457
|
+
return true;
|
|
458
|
+
return CURRENT.getStore() === open.scope;
|
|
459
|
+
}
|
|
460
|
+
async function waitForTurn(params) {
|
|
461
|
+
for (;; ) {
|
|
462
|
+
const open = params.scope.state.open;
|
|
463
|
+
if (open === null || mayProceed({ scope: params.scope, open }))
|
|
464
|
+
return;
|
|
465
|
+
await open.settled;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function acquire(params) {
|
|
469
|
+
const { scope } = params;
|
|
470
|
+
for (;; ) {
|
|
471
|
+
const open = scope.state.open;
|
|
472
|
+
if (mayProceed({ scope, open }))
|
|
473
|
+
return pushNode({ scope, parent: open });
|
|
474
|
+
await open.settled;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function pushNode(params) {
|
|
478
|
+
const { scope, parent } = params;
|
|
479
|
+
const state = scope.state;
|
|
480
|
+
let settle = () => {
|
|
481
|
+
return;
|
|
482
|
+
};
|
|
483
|
+
const settled = new Promise((resolve) => {
|
|
484
|
+
settle = () => resolve();
|
|
485
|
+
});
|
|
486
|
+
const node = {
|
|
487
|
+
scope: { depth: (parent === null ? 0 : parent.scope.depth) + 1, state },
|
|
488
|
+
name: nextSavepointName({ counter: state.counter }),
|
|
489
|
+
parent,
|
|
490
|
+
settled,
|
|
491
|
+
settle
|
|
492
|
+
};
|
|
493
|
+
state.open = node;
|
|
494
|
+
return node;
|
|
495
|
+
}
|
|
496
|
+
function closeNode(params) {
|
|
497
|
+
const { node } = params;
|
|
498
|
+
const state = node.scope.state;
|
|
499
|
+
if (state.open === node)
|
|
500
|
+
state.open = node.parent;
|
|
501
|
+
node.settle();
|
|
502
|
+
}
|
|
503
|
+
async function drainDescendants(params) {
|
|
504
|
+
const state = params.node.scope.state;
|
|
505
|
+
for (;; ) {
|
|
506
|
+
const open = state.open;
|
|
507
|
+
if (open === null || open === params.node || open.scope.depth <= params.node.scope.depth)
|
|
508
|
+
return;
|
|
509
|
+
await open.settled;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
115
513
|
// packages/adapter-mysql/src/transaction-sql.ts
|
|
116
|
-
import { VibeError as
|
|
514
|
+
import { VibeError as VibeError5 } from "@vibeorm/schema";
|
|
117
515
|
var ISOLATION_LEVEL_SQL = Object.freeze({
|
|
118
516
|
ReadCommitted: "READ COMMITTED",
|
|
119
517
|
RepeatableRead: "REPEATABLE READ",
|
|
@@ -125,7 +523,7 @@ function setIsolationLevelStatement(params) {
|
|
|
125
523
|
function maxExecutionTimeStatement(params) {
|
|
126
524
|
const { timeout } = params;
|
|
127
525
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
128
|
-
throw new
|
|
526
|
+
throw new VibeError5({
|
|
129
527
|
code: "VIBE_VALIDATION",
|
|
130
528
|
message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
|
|
131
529
|
meta: { timeout }
|
|
@@ -152,6 +550,8 @@ function refuseNestedTransactionOptions(params) {
|
|
|
152
550
|
passed.push("isolationLevel");
|
|
153
551
|
if (options.timeout !== undefined)
|
|
154
552
|
passed.push("timeout");
|
|
553
|
+
if (options.deadline !== undefined)
|
|
554
|
+
passed.push("deadline");
|
|
155
555
|
if (passed.length === 0)
|
|
156
556
|
return;
|
|
157
557
|
const meta = { provider, nested: true };
|
|
@@ -159,9 +559,11 @@ function refuseNestedTransactionOptions(params) {
|
|
|
159
559
|
meta.isolationLevel = options.isolationLevel;
|
|
160
560
|
if (options.timeout !== undefined)
|
|
161
561
|
meta.timeout = options.timeout;
|
|
162
|
-
|
|
562
|
+
if (options.deadline !== undefined)
|
|
563
|
+
meta.deadline = options.deadline;
|
|
564
|
+
throw new VibeError5({
|
|
163
565
|
code: "VIBE_VALIDATION",
|
|
164
|
-
message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins,
|
|
566
|
+
message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins, a nested timeout is not enforceable, and a nested deadline would need its own session setting on a connection it does not own (it inherits the top-level one instead); pass options on the top-level $transaction`,
|
|
165
567
|
meta
|
|
166
568
|
});
|
|
167
569
|
}
|
|
@@ -170,6 +572,11 @@ function refuseNestedTransactionOptions(params) {
|
|
|
170
572
|
var DIALECT = "mysql";
|
|
171
573
|
var PROVIDER = "mysql";
|
|
172
574
|
var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
|
|
575
|
+
var BUDGETS = Object.freeze({
|
|
576
|
+
acquireTimeout: true,
|
|
577
|
+
statementTimeout: "select-only",
|
|
578
|
+
transactionDeadline: "between-statements"
|
|
579
|
+
});
|
|
173
580
|
function defaultCreatePool(config) {
|
|
174
581
|
return mysql2CreatePool(config);
|
|
175
582
|
}
|
|
@@ -183,6 +590,8 @@ function mysqlAdapter(options) {
|
|
|
183
590
|
let pool = null;
|
|
184
591
|
let ownsPool = false;
|
|
185
592
|
let rawSession = null;
|
|
593
|
+
const acquireTimeoutMs = options?.acquireTimeout;
|
|
594
|
+
const poolSize = options?.pool === undefined ? options?.connectionLimit ?? MYSQL_DEFAULT_CONNECTION_LIMIT : undefined;
|
|
186
595
|
function getPool() {
|
|
187
596
|
if (pool !== null)
|
|
188
597
|
return pool;
|
|
@@ -213,40 +622,62 @@ function mysqlAdapter(options) {
|
|
|
213
622
|
ownsPool = true;
|
|
214
623
|
return pool;
|
|
215
624
|
}
|
|
625
|
+
async function acquireConnection() {
|
|
626
|
+
const active = getPool();
|
|
627
|
+
if (acquireTimeoutMs === undefined) {
|
|
628
|
+
return runMapped(() => active.getConnection());
|
|
629
|
+
}
|
|
630
|
+
return runMapped(() => acquireWithTimeout({ pool: active, timeoutMs: acquireTimeoutMs, poolSize }));
|
|
631
|
+
}
|
|
216
632
|
async function runExecute(params) {
|
|
217
633
|
const [result] = await runMapped(() => params.queryable.execute(params.text, params.values));
|
|
218
634
|
return normalizeDriverResult({ result }).rows;
|
|
219
635
|
}
|
|
636
|
+
async function runExecuteAffected(params) {
|
|
637
|
+
const values = bindRowCountParams({ text: params.text, values: params.values });
|
|
638
|
+
const [result] = await runMapped(() => params.queryable.execute(params.text, values));
|
|
639
|
+
return normalizeDriverResult({ result });
|
|
640
|
+
}
|
|
220
641
|
async function runExecuteUnsafe(params) {
|
|
221
642
|
const [result] = await runMapped(() => params.queryable.query(params.text, params.values));
|
|
222
643
|
return normalizeDriverResult({ result });
|
|
223
644
|
}
|
|
224
645
|
function createConnectionAdapter(params) {
|
|
225
|
-
const { conn,
|
|
646
|
+
const { conn, scope, budget } = params;
|
|
226
647
|
return {
|
|
227
648
|
dialect: DIALECT,
|
|
228
649
|
provider: PROVIDER,
|
|
229
650
|
wire: WIRE,
|
|
651
|
+
budgets: BUDGETS,
|
|
230
652
|
async execute(execParams) {
|
|
653
|
+
const turn = scopeTurn({ scope });
|
|
654
|
+
if (turn !== undefined)
|
|
655
|
+
await turn;
|
|
656
|
+
budget.assertUsable({ stage: "statement" });
|
|
231
657
|
return runExecute({ queryable: conn, ...execParams });
|
|
232
658
|
},
|
|
233
659
|
async executeUnsafe(execParams) {
|
|
660
|
+
const turn = scopeTurn({ scope });
|
|
661
|
+
if (turn !== undefined)
|
|
662
|
+
await turn;
|
|
663
|
+
budget.assertUsable({ stage: "statement" });
|
|
234
664
|
return runExecuteUnsafe({ queryable: conn, ...execParams });
|
|
235
665
|
},
|
|
666
|
+
async executeAffected(execParams) {
|
|
667
|
+
const turn = scopeTurn({ scope });
|
|
668
|
+
if (turn !== undefined)
|
|
669
|
+
await turn;
|
|
670
|
+
budget.assertUsable({ stage: "statement" });
|
|
671
|
+
return runExecuteAffected({ queryable: conn, ...execParams });
|
|
672
|
+
},
|
|
236
673
|
async transaction(fn, nestedOptions) {
|
|
237
674
|
refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
} catch (error) {
|
|
245
|
-
try {
|
|
246
|
-
await conn.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
247
|
-
} catch {}
|
|
248
|
-
throw error;
|
|
249
|
-
}
|
|
675
|
+
budget.assertUsable({ stage: "nested" });
|
|
676
|
+
return runNestedSavepoint({
|
|
677
|
+
scope,
|
|
678
|
+
execute: (statement) => runMapped(() => conn.query(statement)),
|
|
679
|
+
body: (child) => fn(createConnectionAdapter({ conn, scope: child, budget }))
|
|
680
|
+
});
|
|
250
681
|
},
|
|
251
682
|
async connect() {},
|
|
252
683
|
async disconnect() {},
|
|
@@ -259,8 +690,28 @@ function mysqlAdapter(options) {
|
|
|
259
690
|
dialect: DIALECT,
|
|
260
691
|
provider: PROVIDER,
|
|
261
692
|
wire: WIRE,
|
|
693
|
+
budgets: BUDGETS,
|
|
262
694
|
async execute(params) {
|
|
263
|
-
|
|
695
|
+
if (acquireTimeoutMs === undefined) {
|
|
696
|
+
return runExecute({ queryable: getPool(), ...params });
|
|
697
|
+
}
|
|
698
|
+
const conn = await acquireConnection();
|
|
699
|
+
try {
|
|
700
|
+
return await runExecute({ queryable: conn, ...params });
|
|
701
|
+
} finally {
|
|
702
|
+
conn.release();
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
async executeAffected(params) {
|
|
706
|
+
if (acquireTimeoutMs === undefined) {
|
|
707
|
+
return runExecuteAffected({ queryable: getPool(), ...params });
|
|
708
|
+
}
|
|
709
|
+
const conn = await acquireConnection();
|
|
710
|
+
try {
|
|
711
|
+
return await runExecuteAffected({ queryable: conn, ...params });
|
|
712
|
+
} finally {
|
|
713
|
+
conn.release();
|
|
714
|
+
}
|
|
264
715
|
},
|
|
265
716
|
async executeUnsafe(params) {
|
|
266
717
|
const control = classifyRawTransactionControl({ text: params.text });
|
|
@@ -277,21 +728,36 @@ function mysqlAdapter(options) {
|
|
|
277
728
|
return runExecuteUnsafe({ queryable: rawSession, ...params });
|
|
278
729
|
}
|
|
279
730
|
if (control === "open") {
|
|
280
|
-
const
|
|
731
|
+
const conn2 = await acquireConnection();
|
|
281
732
|
try {
|
|
282
|
-
const result = await runExecuteUnsafe({ queryable:
|
|
283
|
-
rawSession =
|
|
733
|
+
const result = await runExecuteUnsafe({ queryable: conn2, ...params });
|
|
734
|
+
rawSession = conn2;
|
|
284
735
|
return result;
|
|
285
736
|
} catch (error) {
|
|
286
|
-
|
|
737
|
+
conn2.release();
|
|
287
738
|
throw error;
|
|
288
739
|
}
|
|
289
740
|
}
|
|
290
|
-
|
|
741
|
+
if (acquireTimeoutMs === undefined) {
|
|
742
|
+
return runExecuteUnsafe({ queryable: getPool(), ...params });
|
|
743
|
+
}
|
|
744
|
+
const conn = await acquireConnection();
|
|
745
|
+
try {
|
|
746
|
+
return await runExecuteUnsafe({ queryable: conn, ...params });
|
|
747
|
+
} finally {
|
|
748
|
+
conn.release();
|
|
749
|
+
}
|
|
291
750
|
},
|
|
292
751
|
async transaction(fn, transactionOptions) {
|
|
293
752
|
const timeoutText = transactionOptions?.timeout === undefined ? undefined : maxExecutionTimeStatement({ timeout: transactionOptions.timeout });
|
|
294
|
-
|
|
753
|
+
validateTransactionDeadline({
|
|
754
|
+
options: transactionOptions,
|
|
755
|
+
support: BUDGETS.transactionDeadline,
|
|
756
|
+
provider: PROVIDER
|
|
757
|
+
});
|
|
758
|
+
const conn = await acquireConnection();
|
|
759
|
+
let rollbackFailed = false;
|
|
760
|
+
const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
|
|
295
761
|
try {
|
|
296
762
|
if (timeoutText !== undefined) {
|
|
297
763
|
await runMapped(() => conn.query(timeoutText));
|
|
@@ -302,19 +768,36 @@ function mysqlAdapter(options) {
|
|
|
302
768
|
await runMapped(() => conn.query(isolationText));
|
|
303
769
|
}
|
|
304
770
|
await runMapped(() => conn.beginTransaction());
|
|
771
|
+
const scope = createSavepointScope();
|
|
305
772
|
try {
|
|
306
|
-
const result = await
|
|
773
|
+
const result = await runRootScope({
|
|
774
|
+
scope,
|
|
775
|
+
body: () => fn(createConnectionAdapter({ conn, scope, budget }))
|
|
776
|
+
});
|
|
777
|
+
budget.assertUsable({ stage: "commit" });
|
|
307
778
|
await runMapped(() => conn.commit());
|
|
779
|
+
budget.close({ outcome: "committed" });
|
|
308
780
|
return result;
|
|
309
781
|
} catch (error) {
|
|
310
782
|
try {
|
|
311
783
|
await conn.rollback();
|
|
312
|
-
|
|
784
|
+
budget.close({ outcome: "rolled-back" });
|
|
785
|
+
} catch {
|
|
786
|
+
rollbackFailed = true;
|
|
787
|
+
budget.close({ outcome: "unknown" });
|
|
788
|
+
if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
|
|
789
|
+
throw transactionOutcomeUnknownError({
|
|
790
|
+
provider: PROVIDER,
|
|
791
|
+
totalMs: budget.totalMs,
|
|
792
|
+
cause: error
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
313
796
|
throw error;
|
|
314
797
|
}
|
|
315
798
|
} finally {
|
|
316
|
-
let poisoned =
|
|
317
|
-
if (timeoutText !== undefined) {
|
|
799
|
+
let poisoned = rollbackFailed;
|
|
800
|
+
if (timeoutText !== undefined && !poisoned) {
|
|
318
801
|
try {
|
|
319
802
|
await conn.query(RESET_MAX_EXECUTION_TIME_SQL);
|
|
320
803
|
} catch {
|
|
@@ -329,7 +812,7 @@ function mysqlAdapter(options) {
|
|
|
329
812
|
}
|
|
330
813
|
},
|
|
331
814
|
async connect() {
|
|
332
|
-
const conn = await
|
|
815
|
+
const conn = await acquireConnection();
|
|
333
816
|
try {
|
|
334
817
|
await runMapped(() => conn.ping());
|
|
335
818
|
} finally {
|
|
@@ -357,6 +840,11 @@ function mysqlAdapter(options) {
|
|
|
357
840
|
};
|
|
358
841
|
}
|
|
359
842
|
export {
|
|
843
|
+
validateTransactionDeadline,
|
|
844
|
+
transactionOutcomeUnknownError,
|
|
845
|
+
transactionDeadlineError,
|
|
846
|
+
transactionClosedError,
|
|
847
|
+
startTransactionBudget,
|
|
360
848
|
setIsolationLevelStatement,
|
|
361
849
|
runMapped,
|
|
362
850
|
refuseNestedTransactionOptions,
|
|
@@ -366,13 +854,20 @@ export {
|
|
|
366
854
|
mysqlAdapter,
|
|
367
855
|
maxExecutionTimeStatement,
|
|
368
856
|
mapMysqlDriverError,
|
|
857
|
+
isTransactionDeadlineError,
|
|
369
858
|
isResultSetHeader,
|
|
859
|
+
engineStatementBudgetMs,
|
|
370
860
|
createSavepointCounter,
|
|
371
861
|
classifyRawTransactionControl,
|
|
862
|
+
acquireWithTimeout,
|
|
863
|
+
acquireTimeoutError,
|
|
372
864
|
RESET_MAX_EXECUTION_TIME_SQL,
|
|
373
865
|
MYSQL_ERRNO_REASONS,
|
|
374
866
|
MYSQL_ERRNO_ERROR_CODES,
|
|
375
|
-
|
|
867
|
+
MYSQL_DEFAULT_CONNECTION_LIMIT,
|
|
868
|
+
ISOLATION_LEVEL_SQL,
|
|
869
|
+
DEFAULT_DEADLINE_CLOCK,
|
|
870
|
+
DEFAULT_ACQUIRE_TIMERS
|
|
376
871
|
};
|
|
377
872
|
|
|
378
|
-
//# debugId=
|
|
873
|
+
//# debugId=C68F0F22780DCB5D64756E2164756E21
|