@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/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/errors.ts
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 VibeError)
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 VibeError({ code, message, meta, cause: error });
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 VibeError2 } from "@vibeorm/schema";
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 VibeError2({
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
- throw new VibeError2({
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, and a nested timeout is not enforceable; pass options on the top-level $transaction`,
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, savepointCounter } = params;
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
- const savepoint = nextSavepointName({ counter: savepointCounter });
239
- await runMapped(() => conn.query(`SAVEPOINT ${savepoint}`));
240
- try {
241
- const result = await fn(createConnectionAdapter({ conn, savepointCounter }));
242
- await runMapped(() => conn.query(`RELEASE SAVEPOINT ${savepoint}`));
243
- return result;
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
- return runExecute({ queryable: getPool(), ...params });
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 conn = await runMapped(() => getPool().getConnection());
731
+ const conn2 = await acquireConnection();
281
732
  try {
282
- const result = await runExecuteUnsafe({ queryable: conn, ...params });
283
- rawSession = conn;
733
+ const result = await runExecuteUnsafe({ queryable: conn2, ...params });
734
+ rawSession = conn2;
284
735
  return result;
285
736
  } catch (error) {
286
- conn.release();
737
+ conn2.release();
287
738
  throw error;
288
739
  }
289
740
  }
290
- return runExecuteUnsafe({ queryable: getPool(), ...params });
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
- const conn = await runMapped(() => getPool().getConnection());
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 fn(createConnectionAdapter({ conn, savepointCounter: createSavepointCounter() }));
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
- } catch {}
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 = false;
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 runMapped(() => getPool().getConnection());
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
- ISOLATION_LEVEL_SQL
867
+ MYSQL_DEFAULT_CONNECTION_LIMIT,
868
+ ISOLATION_LEVEL_SQL,
869
+ DEFAULT_DEADLINE_CLOCK,
870
+ DEFAULT_ACQUIRE_TIMERS
376
871
  };
377
872
 
378
- //# debugId=81C757629DAE03DB64756E2164756E21
873
+ //# debugId=C68F0F22780DCB5D64756E2164756E21