@vibeorm/adapter-mysql 2.0.0-alpha.9 → 2.0.0
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 +2 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +440 -22
- package/dist/index.js.map +8 -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 +3 -3
package/README.md
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
MySQL adapter built on [mysql2](https://github.com/sidorares/node-mysql2) (`mysql2/promise`), which ships as a direct dependency. It implements the frozen `DatabaseAdapter` contract from `@vibeorm/runtime` and runs on both Bun and Node.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add @vibeorm/adapter-mysql
|
|
8
|
+
bun add @vibeorm/adapter-mysql
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
> **
|
|
11
|
+
> **Stable.** `2.0.0`, published under the `latest` dist-tag. The v1 line stays installable as `vibeorm@1`.
|
|
12
12
|
|
|
13
13
|
## Usage
|
|
14
14
|
|
package/dist/index.d.ts
CHANGED
|
@@ -178,4 +178,6 @@ export { createSavepointCounter, nextSavepointName } from "./savepoints.ts";
|
|
|
178
178
|
export type { SavepointCounter } from "./savepoints.ts";
|
|
179
179
|
export { ISOLATION_LEVEL_SQL, RESET_MAX_EXECUTION_TIME_SQL, classifyRawTransactionControl, maxExecutionTimeStatement, refuseNestedTransactionOptions, setIsolationLevelStatement, } from "./transaction-sql.ts";
|
|
180
180
|
export type { RawTransactionControl } from "./transaction-sql.ts";
|
|
181
|
+
export { DEFAULT_DEADLINE_CLOCK, engineStatementBudgetMs, isTransactionDeadlineError, startTransactionBudget, transactionClosedError, transactionDeadlineError, transactionOutcomeUnknownError, validateTransactionDeadline, } from "./transaction-budget.ts";
|
|
182
|
+
export type { BudgetStage, DeadlineClock, TransactionBudget, TransactionOutcome } from "./transaction-budget.ts";
|
|
181
183
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -123,6 +123,131 @@ async function runMapped(fn) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
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
|
+
|
|
126
251
|
// packages/adapter-mysql/src/results.ts
|
|
127
252
|
function isResultSetHeader(params) {
|
|
128
253
|
const { value } = params;
|
|
@@ -152,6 +277,128 @@ function normalizeDriverResult(params) {
|
|
|
152
277
|
return normalizeSingle(result);
|
|
153
278
|
}
|
|
154
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
|
+
|
|
155
402
|
// packages/adapter-mysql/src/savepoints.ts
|
|
156
403
|
function createSavepointCounter() {
|
|
157
404
|
return { n: 0 };
|
|
@@ -160,8 +407,111 @@ function nextSavepointName(params) {
|
|
|
160
407
|
return `vibeorm_sp_${params.counter.n++}`;
|
|
161
408
|
}
|
|
162
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
|
+
|
|
163
513
|
// packages/adapter-mysql/src/transaction-sql.ts
|
|
164
|
-
import { VibeError as
|
|
514
|
+
import { VibeError as VibeError5 } from "@vibeorm/schema";
|
|
165
515
|
var ISOLATION_LEVEL_SQL = Object.freeze({
|
|
166
516
|
ReadCommitted: "READ COMMITTED",
|
|
167
517
|
RepeatableRead: "REPEATABLE READ",
|
|
@@ -173,7 +523,7 @@ function setIsolationLevelStatement(params) {
|
|
|
173
523
|
function maxExecutionTimeStatement(params) {
|
|
174
524
|
const { timeout } = params;
|
|
175
525
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
176
|
-
throw new
|
|
526
|
+
throw new VibeError5({
|
|
177
527
|
code: "VIBE_VALIDATION",
|
|
178
528
|
message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
|
|
179
529
|
meta: { timeout }
|
|
@@ -200,6 +550,8 @@ function refuseNestedTransactionOptions(params) {
|
|
|
200
550
|
passed.push("isolationLevel");
|
|
201
551
|
if (options.timeout !== undefined)
|
|
202
552
|
passed.push("timeout");
|
|
553
|
+
if (options.deadline !== undefined)
|
|
554
|
+
passed.push("deadline");
|
|
203
555
|
if (passed.length === 0)
|
|
204
556
|
return;
|
|
205
557
|
const meta = { provider, nested: true };
|
|
@@ -207,9 +559,11 @@ function refuseNestedTransactionOptions(params) {
|
|
|
207
559
|
meta.isolationLevel = options.isolationLevel;
|
|
208
560
|
if (options.timeout !== undefined)
|
|
209
561
|
meta.timeout = options.timeout;
|
|
210
|
-
|
|
562
|
+
if (options.deadline !== undefined)
|
|
563
|
+
meta.deadline = options.deadline;
|
|
564
|
+
throw new VibeError5({
|
|
211
565
|
code: "VIBE_VALIDATION",
|
|
212
|
-
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`,
|
|
213
567
|
meta
|
|
214
568
|
});
|
|
215
569
|
}
|
|
@@ -218,6 +572,11 @@ function refuseNestedTransactionOptions(params) {
|
|
|
218
572
|
var DIALECT = "mysql";
|
|
219
573
|
var PROVIDER = "mysql";
|
|
220
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
|
+
});
|
|
221
580
|
function defaultCreatePool(config) {
|
|
222
581
|
return mysql2CreatePool(config);
|
|
223
582
|
}
|
|
@@ -274,36 +633,51 @@ function mysqlAdapter(options) {
|
|
|
274
633
|
const [result] = await runMapped(() => params.queryable.execute(params.text, params.values));
|
|
275
634
|
return normalizeDriverResult({ result }).rows;
|
|
276
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
|
+
}
|
|
277
641
|
async function runExecuteUnsafe(params) {
|
|
278
642
|
const [result] = await runMapped(() => params.queryable.query(params.text, params.values));
|
|
279
643
|
return normalizeDriverResult({ result });
|
|
280
644
|
}
|
|
281
645
|
function createConnectionAdapter(params) {
|
|
282
|
-
const { conn,
|
|
646
|
+
const { conn, scope, budget } = params;
|
|
283
647
|
return {
|
|
284
648
|
dialect: DIALECT,
|
|
285
649
|
provider: PROVIDER,
|
|
286
650
|
wire: WIRE,
|
|
651
|
+
budgets: BUDGETS,
|
|
287
652
|
async execute(execParams) {
|
|
653
|
+
const turn = scopeTurn({ scope });
|
|
654
|
+
if (turn !== undefined)
|
|
655
|
+
await turn;
|
|
656
|
+
budget.assertUsable({ stage: "statement" });
|
|
288
657
|
return runExecute({ queryable: conn, ...execParams });
|
|
289
658
|
},
|
|
290
659
|
async executeUnsafe(execParams) {
|
|
660
|
+
const turn = scopeTurn({ scope });
|
|
661
|
+
if (turn !== undefined)
|
|
662
|
+
await turn;
|
|
663
|
+
budget.assertUsable({ stage: "statement" });
|
|
291
664
|
return runExecuteUnsafe({ queryable: conn, ...execParams });
|
|
292
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
|
+
},
|
|
293
673
|
async transaction(fn, nestedOptions) {
|
|
294
674
|
refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
} catch (error) {
|
|
302
|
-
try {
|
|
303
|
-
await conn.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);
|
|
304
|
-
} catch {}
|
|
305
|
-
throw error;
|
|
306
|
-
}
|
|
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
|
+
});
|
|
307
681
|
},
|
|
308
682
|
async connect() {},
|
|
309
683
|
async disconnect() {},
|
|
@@ -316,6 +690,7 @@ function mysqlAdapter(options) {
|
|
|
316
690
|
dialect: DIALECT,
|
|
317
691
|
provider: PROVIDER,
|
|
318
692
|
wire: WIRE,
|
|
693
|
+
budgets: BUDGETS,
|
|
319
694
|
async execute(params) {
|
|
320
695
|
if (acquireTimeoutMs === undefined) {
|
|
321
696
|
return runExecute({ queryable: getPool(), ...params });
|
|
@@ -327,6 +702,17 @@ function mysqlAdapter(options) {
|
|
|
327
702
|
conn.release();
|
|
328
703
|
}
|
|
329
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
|
+
}
|
|
715
|
+
},
|
|
330
716
|
async executeUnsafe(params) {
|
|
331
717
|
const control = classifyRawTransactionControl({ text: params.text });
|
|
332
718
|
if (rawSession !== null) {
|
|
@@ -364,7 +750,14 @@ function mysqlAdapter(options) {
|
|
|
364
750
|
},
|
|
365
751
|
async transaction(fn, transactionOptions) {
|
|
366
752
|
const timeoutText = transactionOptions?.timeout === undefined ? undefined : maxExecutionTimeStatement({ timeout: transactionOptions.timeout });
|
|
753
|
+
validateTransactionDeadline({
|
|
754
|
+
options: transactionOptions,
|
|
755
|
+
support: BUDGETS.transactionDeadline,
|
|
756
|
+
provider: PROVIDER
|
|
757
|
+
});
|
|
367
758
|
const conn = await acquireConnection();
|
|
759
|
+
let rollbackFailed = false;
|
|
760
|
+
const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
|
|
368
761
|
try {
|
|
369
762
|
if (timeoutText !== undefined) {
|
|
370
763
|
await runMapped(() => conn.query(timeoutText));
|
|
@@ -375,19 +768,36 @@ function mysqlAdapter(options) {
|
|
|
375
768
|
await runMapped(() => conn.query(isolationText));
|
|
376
769
|
}
|
|
377
770
|
await runMapped(() => conn.beginTransaction());
|
|
771
|
+
const scope = createSavepointScope();
|
|
378
772
|
try {
|
|
379
|
-
const result = await
|
|
773
|
+
const result = await runRootScope({
|
|
774
|
+
scope,
|
|
775
|
+
body: () => fn(createConnectionAdapter({ conn, scope, budget }))
|
|
776
|
+
});
|
|
777
|
+
budget.assertUsable({ stage: "commit" });
|
|
380
778
|
await runMapped(() => conn.commit());
|
|
779
|
+
budget.close({ outcome: "committed" });
|
|
381
780
|
return result;
|
|
382
781
|
} catch (error) {
|
|
383
782
|
try {
|
|
384
783
|
await conn.rollback();
|
|
385
|
-
|
|
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
|
+
}
|
|
386
796
|
throw error;
|
|
387
797
|
}
|
|
388
798
|
} finally {
|
|
389
|
-
let poisoned =
|
|
390
|
-
if (timeoutText !== undefined) {
|
|
799
|
+
let poisoned = rollbackFailed;
|
|
800
|
+
if (timeoutText !== undefined && !poisoned) {
|
|
391
801
|
try {
|
|
392
802
|
await conn.query(RESET_MAX_EXECUTION_TIME_SQL);
|
|
393
803
|
} catch {
|
|
@@ -430,6 +840,11 @@ function mysqlAdapter(options) {
|
|
|
430
840
|
};
|
|
431
841
|
}
|
|
432
842
|
export {
|
|
843
|
+
validateTransactionDeadline,
|
|
844
|
+
transactionOutcomeUnknownError,
|
|
845
|
+
transactionDeadlineError,
|
|
846
|
+
transactionClosedError,
|
|
847
|
+
startTransactionBudget,
|
|
433
848
|
setIsolationLevelStatement,
|
|
434
849
|
runMapped,
|
|
435
850
|
refuseNestedTransactionOptions,
|
|
@@ -439,7 +854,9 @@ export {
|
|
|
439
854
|
mysqlAdapter,
|
|
440
855
|
maxExecutionTimeStatement,
|
|
441
856
|
mapMysqlDriverError,
|
|
857
|
+
isTransactionDeadlineError,
|
|
442
858
|
isResultSetHeader,
|
|
859
|
+
engineStatementBudgetMs,
|
|
443
860
|
createSavepointCounter,
|
|
444
861
|
classifyRawTransactionControl,
|
|
445
862
|
acquireWithTimeout,
|
|
@@ -449,7 +866,8 @@ export {
|
|
|
449
866
|
MYSQL_ERRNO_ERROR_CODES,
|
|
450
867
|
MYSQL_DEFAULT_CONNECTION_LIMIT,
|
|
451
868
|
ISOLATION_LEVEL_SQL,
|
|
869
|
+
DEFAULT_DEADLINE_CLOCK,
|
|
452
870
|
DEFAULT_ACQUIRE_TIMERS
|
|
453
871
|
};
|
|
454
872
|
|
|
455
|
-
//# debugId=
|
|
873
|
+
//# debugId=C68F0F22780DCB5D64756E2164756E21
|