@vibeorm/adapter-mysql 2.0.0-alpha.1

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 ADDED
@@ -0,0 +1,104 @@
1
+ # @vibeorm/adapter-mysql
2
+
3
+ > Part of **[VibeORM](https://github.com/vibeorm/vibeorm)** — a type-safe TypeScript ORM for Bun and Node. Prisma-schema or TypeScript-DSL input, a canonical schema IR, a generated client, and a dialect-aware SQL layer over PostgreSQL, PGlite, SQLite and MySQL.
4
+
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
+
7
+ ```bash
8
+ bun add @vibeorm/adapter-mysql@alpha
9
+ ```
10
+
11
+ > **Pre-release.** `2.0.0-alpha.x`, published under the `alpha` dist-tag. `npm i vibeorm` still resolves to the stable v1 line.
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { VibeClient } from "./generated/vibeorm/index.js";
17
+ import { mysqlAdapter } from "@vibeorm/adapter-mysql";
18
+
19
+ const db = VibeClient({ adapter: mysqlAdapter() }); // reads MYSQL_URL
20
+
21
+ const users = await db.user.findMany({ where: { email: { contains: "@example.com" } }, take: 10 });
22
+
23
+ await db.$disconnect();
24
+ ```
25
+
26
+ ## Options
27
+
28
+ `mysqlAdapter(options?: MysqlAdapterOptions)`
29
+
30
+ | Option | Type | Default | Meaning |
31
+ |---|---|---|---|
32
+ | `url` | `string` | `process.env.MYSQL_URL` | Connection URL, `mysql://user:pw@host:3306/db`. |
33
+ | `host` / `port` / `user` / `password` / `database` | `string` / `number` | — | Discrete connection fields. When both are given, these win over the parsed `url`. |
34
+ | `pool` | `MysqlPoolLike` | — | An existing mysql2 promise pool. The adapter never ends a pool it did not create, so `disconnect()` is a no-op; configuring it to the codec contract is on you. |
35
+ | `connectionLimit` | `number` | mysql2's `10` | Maximum pooled connections. Ignored when `pool` is given. |
36
+ | `timezone` | `string` | `"Z"` | mysql2 timezone. `DATETIME(3)` carries no zone, so `"Z"` makes outgoing `Date`s serialize as UTC and incoming text parse as UTC. mysql2's own default (`"local"`) would shift values by the process offset. |
37
+ | `charset` | `string` | mysql2's `utf8mb4_general_ci` | Connection charset/collation. |
38
+ | `multipleStatements` | `boolean` | `false` | Allow several statements per `executeUnsafe` call. Leave it off: the ORM never batches, `@vibeorm/migrate` applies DDL per statement, and it widens the injection blast radius of raw queries. |
39
+ | `createPool` | `(config: MysqlPoolConfig) => MysqlPoolLike` | mysql2's `createPool` | Pool factory; a test seam. Ignored when `pool` is given. |
40
+
41
+ The rest of the pool config is pinned to the runtime's `MYSQL_CODECS` contract and is not overridable: `decimalNumbers: false` and `supportBigNumbers` + `bigNumberStrings` (DECIMAL and BIGINT arrive as exact strings), `dateStrings: false` (DATETIME arrives as `Date`), `jsonStrings: false` (JSON arrives parsed), `namedPlaceholders: false` (the dialect renders positional `?`).
42
+
43
+ `execute` (the ORM hot path) uses mysql2's binary protocol with per-connection prepared-statement caching; `executeUnsafe` (raw SQL and DDL) uses the text protocol, which is what makes DDL work.
44
+
45
+ ## Runtime support
46
+
47
+ Node.js and Bun. mysql2 is a pure-JS driver and the dist output is plain ESM; the package declares `engines.bun >= 1.2.0` and is exercised on both runtimes.
48
+
49
+ ## Capabilities and limits
50
+
51
+ The mysql capability table is frozen and mirrored by tests, exported to [`docs/dialects.md`](https://github.com/vibeorm/vibeorm/blob/master/docs/dialects.md). A feature the dialect cannot express throws `VibeError` code `VIBE_UNSUPPORTED_CAPABILITY` at the earliest knowable moment (generate, then build, then execute) — it never silently misbehaves.
52
+
53
+ Refused:
54
+
55
+ - `scalarArrays` — scalar list columns are rejected at generate and migrate time.
56
+ - `lateralJoin` — the `"join"` relation strategy is refused. Use the default `"query"` strategy.
57
+ - `partialIndexes` — partial (predicate) indexes are rejected at migrate time.
58
+ - `createManyAndReturn` — refused; use `createMany` and re-query.
59
+
60
+ Handled by a documented fallback:
61
+
62
+ - No `RETURNING`: mutations read the row back via `SELECT LAST_INSERT_ID()` plus a re-select, inside one transaction when the key is database-assigned. That is sound because `transaction()` pins one pooled connection and `LAST_INSERT_ID()` is per-connection state.
63
+ - Upserts render `ON DUPLICATE KEY UPDATE` (no conflict-target refinement); "do nothing" renders `INSERT IGNORE`, which is broader — it downgrades other row-level errors to warnings too.
64
+ - `mode: "insensitive"` renders `LOWER(x) LIKE LOWER(?)`, redundant on the default `_ci` collations and index-degrading.
65
+ - `distinct` keeps the first row per key client-side, in the query's row order.
66
+ - Null ordering is emulated with an `IS NULL` sort term; `OFFSET` without `LIMIT` uses the documented huge-limit idiom.
67
+ - DDL auto-commits, so the migration runner records per-statement progress and a failed migration resumes instead of re-running.
68
+
69
+ Native: enums and a real JSON column type; `fulltext` is the search engine.
70
+
71
+ Transaction `timeout` is implemented as `SET SESSION MAX_EXECUTION_TIME`, which **bounds SELECT statements only** — INSERT/UPDATE/DELETE/DDL inside the transaction run unbounded. The setting is reset in `finally`; if the reset fails the connection is destroyed rather than returned to the pool. Nested `transaction()` calls become savepoints and refuse `isolationLevel`/`timeout` with `VIBE_VALIDATION`.
72
+
73
+ On the default `_ci` collations both `=`/`IN` and `LIKE` are case-insensitive; `mode: "strict"` casts the operand to `BINARY` for byte-exact comparison. See [`docs/dialect-notes.md`](https://github.com/vibeorm/vibeorm/blob/master/docs/dialect-notes.md).
74
+
75
+ ## Error mapping
76
+
77
+ Every driver failure becomes a `VibeError` with the original error as `cause`; `meta` carries `errno`, the symbolic `code`, `sqlState`, a trimmed `sqlMessage`, and `reason` where applicable.
78
+
79
+ | errno | MySQL name | VibeError code | `meta.reason` |
80
+ |---|---|---|---|
81
+ | 1062 / 1586 / 1761 / 1762 | duplicate key family | `VIBE_UNIQUE_VIOLATION` | |
82
+ | 1216 / 1217 / 1451 / 1452 | foreign key family | `VIBE_FK_VIOLATION` | |
83
+ | 1048 | null violation | `VIBE_VALIDATION` | |
84
+ | 1264 | out of range | `VIBE_VALIDATION` | |
85
+ | 1265 | data truncated (bad ENUM value or truncated numeric string; a hard error in strict mode) | `VIBE_VALIDATION` | |
86
+ | 1292 | truncated wrong value, e.g. a bad datetime literal | `VIBE_VALIDATION` | |
87
+ | 1364 | no default | `VIBE_VALIDATION` | |
88
+ | 1366 | wrong-typed value for column | `VIBE_VALIDATION` | |
89
+ | 1406 | data too long | `VIBE_VALIDATION` | |
90
+ | 3819 | check violation | `VIBE_VALIDATION` | |
91
+ | 1213 | deadlock | `VIBE_TRANSACTION` | `"deadlock"` |
92
+ | 1205 | lock wait timeout | `VIBE_TRANSACTION` | `"timeout"` |
93
+ | 3024 | query interrupted (`max_execution_time`) | `VIBE_TRANSACTION` | `"timeout"` |
94
+ | everything else, including 1044/1045, `ECONNREFUSED`, `PROTOCOL_CONNECTION_LOST`, `ETIMEDOUT` | | `VIBE_ADAPTER` | |
95
+
96
+ `VibeError`s pass through unchanged, and a transaction callback's own error is rethrown identically.
97
+
98
+ ## Links
99
+
100
+ - [Documentation](https://github.com/vibeorm/vibeorm/tree/master/docs)
101
+ - [Getting started](https://github.com/vibeorm/vibeorm/blob/master/docs/getting-started.md)
102
+ - [Dialect capability tables](https://github.com/vibeorm/vibeorm/blob/master/docs/dialects.md)
103
+ - [Repository and issues](https://github.com/vibeorm/vibeorm)
104
+ - MIT licensed
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Driver-error mapping for mysql2.
3
+ *
4
+ * Constitution rule 5: a raw driver error never escapes an adapter. Every
5
+ * failure becomes a `VibeError` with a stable code, the driver error attached
6
+ * as `cause`, and the MySQL errno / symbolic code / SQLSTATE / server message
7
+ * in `meta`.
8
+ *
9
+ * mysql2 errors carry `errno` (the numeric MySQL server error, e.g. 1062),
10
+ * `code` (the symbolic name, `ER_DUP_ENTRY`, or a client-side string like
11
+ * `ECONNREFUSED` / `PROTOCOL_CONNECTION_LOST`), `sqlState` and `sqlMessage`.
12
+ * The table is keyed by `errno` because it is the stable server-side identity;
13
+ * client-side failures have no errno and fall through to `VIBE_ADAPTER`.
14
+ */
15
+ import { VibeError, type VibeErrorCode } from "@vibeorm/schema";
16
+ /**
17
+ * MySQL server errno → stable VibeError code. Frozen and mirrored by tests;
18
+ * anything absent from this table maps to `VIBE_ADAPTER` — including access
19
+ * failures (1044 `ER_DBACCESS_DENIED_ERROR`, 1045 `ER_ACCESS_DENIED_ERROR`)
20
+ * and every client-side connection error (`ECONNREFUSED`,
21
+ * `PROTOCOL_CONNECTION_LOST`, `ETIMEDOUT`), which carry no errno at all.
22
+ */
23
+ export declare const MYSQL_ERRNO_ERROR_CODES: Readonly<Record<number, VibeErrorCode>>;
24
+ /** Errnos that additionally explain themselves through `meta.reason`. */
25
+ export declare const MYSQL_ERRNO_REASONS: Readonly<Record<number, string>>;
26
+ /**
27
+ * Extract the numeric MySQL errno from a mysql2 error. Client-side failures
28
+ * (`ECONNREFUSED`, `PROTOCOL_CONNECTION_LOST`, …) have no numeric errno and
29
+ * yield `undefined`.
30
+ */
31
+ export declare function mysqlErrorErrno(params: {
32
+ error: unknown;
33
+ }): number | undefined;
34
+ /**
35
+ * Map any error thrown by mysql2 onto a `VibeError`.
36
+ *
37
+ * A `VibeError` passes through unchanged, so an error raised deeper inside the
38
+ * ORM (or by a nested adapter call) is never re-wrapped, and a user callback's
39
+ * error is rethrown identically.
40
+ */
41
+ export declare function mapMysqlDriverError(params: {
42
+ error: unknown;
43
+ }): VibeError;
44
+ /** Run a driver call, mapping anything it throws onto a `VibeError`. */
45
+ export declare function runMapped<T>(fn: () => Promise<T>): Promise<T>;
46
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAoD1E,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/D,CAAC;AASH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAK9E;AAID;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CA2BzE;AAED,wEAAwE;AACxE,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMnE"}
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @vibeorm/adapter-mysql — mysql2 adapter for VibeORM v2.
3
+ *
4
+ * Runs on Bun and Node over `mysql2/promise`. Owns pooling, transactions
5
+ * (nested → savepoints on the same pinned connection) and driver-error
6
+ * mapping. Only `VibeError` ever escapes.
7
+ *
8
+ * SAME-CONNECTION GUARANTEE (board #7): `transaction()` checks ONE connection
9
+ * out of the pool and every operation on the callback's adapter — `execute`,
10
+ * `executeUnsafe`, nested `transaction` — runs on that same connection until
11
+ * commit/rollback. That pinning is what makes `LAST_INSERT_ID()` sound: the
12
+ * runtime wraps DB-assigned-key creates in `adapter.transaction` and issues
13
+ * `SELECT LAST_INSERT_ID() AS id` via `executeUnsafe`, and LAST_INSERT_ID() is
14
+ * per-connection state in MySQL — on any other connection it would answer for
15
+ * someone else's INSERT.
16
+ *
17
+ * Driver configuration is pinned to the runtime's MYSQL_CODECS contract
18
+ * (packages/runtime/src/codecs.ts) — see {@link MysqlPoolConfig}.
19
+ */
20
+ import type { DatabaseAdapter } from "@vibeorm/runtime";
21
+ /** The parts of a mysql2 `PoolConnection` this adapter uses. */
22
+ export type MysqlConnectionLike = {
23
+ /** Binary protocol — prepared statements, cached per connection by mysql2. */
24
+ execute(text: string, values?: unknown[]): Promise<[unknown, unknown]>;
25
+ /** Text protocol — raw path and transaction-control statements. */
26
+ query(text: string, values?: unknown[]): Promise<[unknown, unknown]>;
27
+ beginTransaction(): Promise<void>;
28
+ commit(): Promise<void>;
29
+ rollback(): Promise<void>;
30
+ ping(): Promise<void>;
31
+ release(): void;
32
+ destroy(): void;
33
+ };
34
+ /** The parts of a mysql2 promise `Pool` this adapter uses. */
35
+ export type MysqlPoolLike = {
36
+ execute(text: string, values?: unknown[]): Promise<[unknown, unknown]>;
37
+ query(text: string, values?: unknown[]): Promise<[unknown, unknown]>;
38
+ getConnection(): Promise<MysqlConnectionLike>;
39
+ end(): Promise<void>;
40
+ };
41
+ /**
42
+ * The exact configuration this adapter hands to `mysql2.createPool`. The
43
+ * literal-typed fields are pinned to the runtime's MYSQL_CODECS contract and
44
+ * are not user-overridable:
45
+ *
46
+ * - `decimalNumbers: false` — DECIMAL comes back as a string; the Decimal
47
+ * codec keeps it a string (no float rounding) and trims the fixed-scale
48
+ * padding MySQL reports (`DECIMAL(65, 30)` → `"42.420…00"` → `"42.42"`).
49
+ * - `supportBigNumbers: true` + `bigNumberStrings: true` — BIGINT (and
50
+ * DECIMAL) always come back as strings, never a precision-lossy JS number;
51
+ * the BigInt codec decodes strings exactly.
52
+ * - `dateStrings: false` — DATETIME comes back as a JS `Date`; the DateTime
53
+ * codec passes `Date` through in both directions.
54
+ * - `jsonStrings: false` — JSON columns come back already parsed, which is
55
+ * what the Json codec's decode expects from mysql2.
56
+ * - `namedPlaceholders: false` — the mysql dialect renders positional `?`.
57
+ * - `multipleStatements` defaults to false: the ORM never batches, and
58
+ * @vibeorm/migrate's mysql runner applies DDL per-statement anyway (DDL
59
+ * auto-commits, so per-statement progress tracking needs it).
60
+ */
61
+ export type MysqlPoolConfig = {
62
+ readonly uri?: string;
63
+ readonly host?: string;
64
+ readonly port?: number;
65
+ readonly user?: string;
66
+ readonly password?: string;
67
+ readonly database?: string;
68
+ readonly connectionLimit?: number;
69
+ readonly charset?: string;
70
+ readonly timezone: string;
71
+ readonly decimalNumbers: false;
72
+ readonly supportBigNumbers: true;
73
+ readonly bigNumberStrings: true;
74
+ readonly dateStrings: false;
75
+ readonly jsonStrings: false;
76
+ readonly namedPlaceholders: false;
77
+ readonly multipleStatements: boolean;
78
+ };
79
+ /**
80
+ * Options for {@link mysqlAdapter}. Point it at a connection URL **or** at
81
+ * discrete `host`/`port`/`user`/`password`/`database` fields (mysql2 accepts
82
+ * both; when both are given the discrete fields win — mysql2 merges the parsed
83
+ * URL underneath explicit config), or hand it a pool you already own.
84
+ */
85
+ export type MysqlAdapterOptions = {
86
+ /** MySQL connection URL (`mysql://user:pw@host:3306/db`). Defaults to `process.env.MYSQL_URL`. */
87
+ readonly url?: string;
88
+ readonly host?: string;
89
+ readonly port?: number;
90
+ readonly user?: string;
91
+ readonly password?: string;
92
+ readonly database?: string;
93
+ /**
94
+ * An existing mysql2 promise pool to use instead of creating one. The
95
+ * adapter never ends a pool it did not create, so `disconnect()` is a no-op
96
+ * in this mode. The pool MUST be configured to the codec contract
97
+ * ({@link MysqlPoolConfig}) — the adapter cannot verify it.
98
+ */
99
+ readonly pool?: MysqlPoolLike;
100
+ /** Maximum pooled connections (mysql2 default 10). Ignored when `pool` is given. */
101
+ readonly connectionLimit?: number;
102
+ /**
103
+ * mysql2 `timezone`, default `"Z"`: the codec table stores DateTime as UTC
104
+ * in `DATETIME(3)` columns, and DATETIME has no zone of its own — with
105
+ * `"Z"` mysql2 serializes outgoing `Date`s as UTC and parses incoming
106
+ * DATETIME text as UTC, so the instant round-trips regardless of the server
107
+ * or process time zone. mysql2's own default (`"local"`) would silently
108
+ * shift values by the process offset.
109
+ */
110
+ readonly timezone?: string;
111
+ /** Connection charset/collation (mysql2 default `utf8mb4_general_ci`). */
112
+ readonly charset?: string;
113
+ /**
114
+ * Allow multiple statements per `executeUnsafe` call (default false). Leave
115
+ * it off: the ORM never batches, @vibeorm/migrate applies DDL per statement,
116
+ * and enabling it widens the SQL-injection blast radius of raw queries.
117
+ */
118
+ readonly multipleStatements?: boolean;
119
+ /**
120
+ * Pool factory — a test seam. Defaults to `mysql2/promise`'s `createPool`;
121
+ * tests inject a recording fake here to assert the exact config the adapter
122
+ * builds. Ignored when `pool` is given.
123
+ */
124
+ readonly createPool?: (config: MysqlPoolConfig) => MysqlPoolLike;
125
+ };
126
+ /**
127
+ * Create a VibeORM adapter backed by mysql2.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * const adapter = mysqlAdapter({ url: process.env.MYSQL_URL, connectionLimit: 20 });
132
+ * await adapter.connect();
133
+ * const rows = await adapter.execute({ text: "SELECT * FROM `User` WHERE `id` = ?", values: [1] });
134
+ * ```
135
+ *
136
+ * @example Discrete fields, or reusing a pool you already own
137
+ * ```ts
138
+ * const adapter = mysqlAdapter({ host: "127.0.0.1", port: 3306, user: "app", password: "pw", database: "app" });
139
+ * const shared = mysqlAdapter({ pool: existingPool });
140
+ * ```
141
+ *
142
+ * ORM path (`execute`) uses mysql2's binary protocol (`pool.execute`), which
143
+ * prepares statements and caches them per connection keyed by statement text
144
+ * (mysql2's `maxPreparedStatements` LRU, default 16000). Raw path
145
+ * (`executeUnsafe`) uses the text protocol (`pool.query`) — no statement
146
+ * cache, and DDL (which the binary protocol cannot prepare) works.
147
+ *
148
+ * Parameters arrive PRE-ENCODED by the runtime codec table: Json is already a
149
+ * string, DateTime is a `Date` (mysql2 serializes it per `timezone`), BigInt
150
+ * is a string. The adapter forwards values verbatim — no second conversion.
151
+ */
152
+ export declare function mysqlAdapter(options?: MysqlAdapterOptions): DatabaseAdapter;
153
+ export { MYSQL_ERRNO_ERROR_CODES, MYSQL_ERRNO_REASONS, mapMysqlDriverError, mysqlErrorErrno, runMapped, } from "./errors.ts";
154
+ export { isResultSetHeader, normalizeDriverResult } from "./results.ts";
155
+ export { createSavepointCounter, nextSavepointName } from "./savepoints.ts";
156
+ export type { SavepointCounter } from "./savepoints.ts";
157
+ export { ISOLATION_LEVEL_SQL, RESET_MAX_EXECUTION_TIME_SQL, classifyRawTransactionControl, maxExecutionTimeStatement, refuseNestedTransactionOptions, setIsolationLevelStatement, } from "./transaction-sql.ts";
158
+ export type { RawTransactionControl } from "./transaction-sql.ts";
159
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,378 @@
1
+ // @bun
2
+ // packages/adapter-mysql/src/index.ts
3
+ import { internalError } from "@vibeorm/schema";
4
+ import { createPool as mysql2CreatePool } from "mysql2/promise";
5
+
6
+ // packages/adapter-mysql/src/errors.ts
7
+ import { VibeError } from "@vibeorm/schema";
8
+ var MYSQL_ERRNO_ERROR_CODES = Object.freeze({
9
+ 1062: "VIBE_UNIQUE_VIOLATION",
10
+ 1586: "VIBE_UNIQUE_VIOLATION",
11
+ 1761: "VIBE_UNIQUE_VIOLATION",
12
+ 1762: "VIBE_UNIQUE_VIOLATION",
13
+ 1216: "VIBE_FK_VIOLATION",
14
+ 1217: "VIBE_FK_VIOLATION",
15
+ 1451: "VIBE_FK_VIOLATION",
16
+ 1452: "VIBE_FK_VIOLATION",
17
+ 1048: "VIBE_VALIDATION",
18
+ 1264: "VIBE_VALIDATION",
19
+ 1364: "VIBE_VALIDATION",
20
+ 1406: "VIBE_VALIDATION",
21
+ 1265: "VIBE_VALIDATION",
22
+ 1366: "VIBE_VALIDATION",
23
+ 1292: "VIBE_VALIDATION",
24
+ 3819: "VIBE_VALIDATION",
25
+ 1213: "VIBE_TRANSACTION",
26
+ 1205: "VIBE_TRANSACTION",
27
+ 3024: "VIBE_TRANSACTION"
28
+ });
29
+ var MYSQL_ERRNO_REASONS = Object.freeze({
30
+ 1213: "deadlock",
31
+ 1205: "timeout",
32
+ 3024: "timeout"
33
+ });
34
+ function readStringField(params) {
35
+ const value = params.source[params.key];
36
+ return typeof value === "string" && value.length > 0 ? value : undefined;
37
+ }
38
+ function mysqlErrorErrno(params) {
39
+ const { error } = params;
40
+ if (error === null || typeof error !== "object")
41
+ return;
42
+ const errno = error.errno;
43
+ return typeof errno === "number" && Number.isInteger(errno) ? errno : undefined;
44
+ }
45
+ function mapMysqlDriverError(params) {
46
+ const { error } = params;
47
+ if (error instanceof VibeError)
48
+ return error;
49
+ const source = error !== null && typeof error === "object" ? error : {};
50
+ const errno = mysqlErrorErrno({ error });
51
+ const code = (errno === undefined ? undefined : MYSQL_ERRNO_ERROR_CODES[errno]) ?? "VIBE_ADAPTER";
52
+ const meta = {};
53
+ if (errno !== undefined)
54
+ meta.errno = errno;
55
+ const symbolicCode = readStringField({ source, key: "code" });
56
+ if (symbolicCode !== undefined)
57
+ meta.code = symbolicCode;
58
+ const sqlState = readStringField({ source, key: "sqlState" });
59
+ if (sqlState !== undefined)
60
+ meta.sqlState = sqlState;
61
+ const sqlMessage = readStringField({ source, key: "sqlMessage" });
62
+ if (sqlMessage !== undefined)
63
+ meta.sqlMessage = sqlMessage.trim();
64
+ const reason = errno === undefined ? undefined : MYSQL_ERRNO_REASONS[errno];
65
+ if (reason !== undefined)
66
+ meta.reason = reason;
67
+ 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 });
69
+ }
70
+ async function runMapped(fn) {
71
+ try {
72
+ return await fn();
73
+ } catch (error) {
74
+ throw mapMysqlDriverError({ error });
75
+ }
76
+ }
77
+
78
+ // packages/adapter-mysql/src/results.ts
79
+ function isResultSetHeader(params) {
80
+ const { value } = params;
81
+ if (value === null || typeof value !== "object" || Array.isArray(value))
82
+ return false;
83
+ const source = value;
84
+ return typeof source.affectedRows === "number" && typeof source.serverStatus === "number";
85
+ }
86
+ function normalizeSingle(value) {
87
+ if (Array.isArray(value)) {
88
+ return { rows: value, affectedRows: 0 };
89
+ }
90
+ if (isResultSetHeader({ value })) {
91
+ return { rows: [], affectedRows: value.affectedRows };
92
+ }
93
+ return { rows: [], affectedRows: 0 };
94
+ }
95
+ function normalizeDriverResult(params) {
96
+ const { result } = params;
97
+ if (Array.isArray(result)) {
98
+ const last = result.at(-1);
99
+ if (last !== undefined && (Array.isArray(last) || isResultSetHeader({ value: last }))) {
100
+ return normalizeSingle(last);
101
+ }
102
+ return { rows: result, affectedRows: 0 };
103
+ }
104
+ return normalizeSingle(result);
105
+ }
106
+
107
+ // packages/adapter-mysql/src/savepoints.ts
108
+ function createSavepointCounter() {
109
+ return { n: 0 };
110
+ }
111
+ function nextSavepointName(params) {
112
+ return `vibeorm_sp_${params.counter.n++}`;
113
+ }
114
+
115
+ // packages/adapter-mysql/src/transaction-sql.ts
116
+ import { VibeError as VibeError2 } from "@vibeorm/schema";
117
+ var ISOLATION_LEVEL_SQL = Object.freeze({
118
+ ReadCommitted: "READ COMMITTED",
119
+ RepeatableRead: "REPEATABLE READ",
120
+ Serializable: "SERIALIZABLE"
121
+ });
122
+ function setIsolationLevelStatement(params) {
123
+ return `SET TRANSACTION ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[params.isolationLevel]}`;
124
+ }
125
+ function maxExecutionTimeStatement(params) {
126
+ const { timeout } = params;
127
+ if (!Number.isInteger(timeout) || timeout < 0) {
128
+ throw new VibeError2({
129
+ code: "VIBE_VALIDATION",
130
+ message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
131
+ meta: { timeout }
132
+ });
133
+ }
134
+ return `SET SESSION MAX_EXECUTION_TIME = ${timeout}`;
135
+ }
136
+ var RESET_MAX_EXECUTION_TIME_SQL = "SET SESSION MAX_EXECUTION_TIME = DEFAULT";
137
+ var RAW_OPEN_PATTERN = /^\s*(?:begin|start\s+transaction)\b[^;]*;?\s*$/i;
138
+ var RAW_CLOSE_PATTERN = /^\s*(?:commit|rollback)(?:\s+work)?\s*;?\s*$/i;
139
+ function classifyRawTransactionControl(params) {
140
+ if (RAW_OPEN_PATTERN.test(params.text))
141
+ return "open";
142
+ if (RAW_CLOSE_PATTERN.test(params.text))
143
+ return "close";
144
+ return null;
145
+ }
146
+ function refuseNestedTransactionOptions(params) {
147
+ const { options, provider } = params;
148
+ if (options === undefined)
149
+ return;
150
+ const passed = [];
151
+ if (options.isolationLevel !== undefined)
152
+ passed.push("isolationLevel");
153
+ if (options.timeout !== undefined)
154
+ passed.push("timeout");
155
+ if (passed.length === 0)
156
+ return;
157
+ const meta = { provider, nested: true };
158
+ if (options.isolationLevel !== undefined)
159
+ meta.isolationLevel = options.isolationLevel;
160
+ if (options.timeout !== undefined)
161
+ meta.timeout = options.timeout;
162
+ throw new VibeError2({
163
+ 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`,
165
+ meta
166
+ });
167
+ }
168
+
169
+ // packages/adapter-mysql/src/index.ts
170
+ var DIALECT = "mysql";
171
+ var PROVIDER = "mysql";
172
+ var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
173
+ function defaultCreatePool(config) {
174
+ return mysql2CreatePool(config);
175
+ }
176
+ function throwOnArrayParam(values) {
177
+ throw internalError({
178
+ message: "formatArrayParam called on the mysql adapter \u2014 the mysql dialect expands scalar-list parameters into IN (?, ?, \u2026) placeholders, so the runtime should have expanded this array before binding",
179
+ meta: { length: values.length }
180
+ });
181
+ }
182
+ function mysqlAdapter(options) {
183
+ let pool = null;
184
+ let ownsPool = false;
185
+ let rawSession = null;
186
+ function getPool() {
187
+ if (pool !== null)
188
+ return pool;
189
+ if (options?.pool !== undefined) {
190
+ pool = options.pool;
191
+ ownsPool = false;
192
+ return pool;
193
+ }
194
+ const config = {
195
+ uri: options?.url ?? process.env.MYSQL_URL,
196
+ host: options?.host,
197
+ port: options?.port,
198
+ user: options?.user,
199
+ password: options?.password,
200
+ database: options?.database,
201
+ connectionLimit: options?.connectionLimit,
202
+ charset: options?.charset,
203
+ timezone: options?.timezone ?? "Z",
204
+ decimalNumbers: false,
205
+ supportBigNumbers: true,
206
+ bigNumberStrings: true,
207
+ dateStrings: false,
208
+ jsonStrings: false,
209
+ namedPlaceholders: false,
210
+ multipleStatements: options?.multipleStatements ?? false
211
+ };
212
+ pool = (options?.createPool ?? defaultCreatePool)(config);
213
+ ownsPool = true;
214
+ return pool;
215
+ }
216
+ async function runExecute(params) {
217
+ const [result] = await runMapped(() => params.queryable.execute(params.text, params.values));
218
+ return normalizeDriverResult({ result }).rows;
219
+ }
220
+ async function runExecuteUnsafe(params) {
221
+ const [result] = await runMapped(() => params.queryable.query(params.text, params.values));
222
+ return normalizeDriverResult({ result });
223
+ }
224
+ function createConnectionAdapter(params) {
225
+ const { conn, savepointCounter } = params;
226
+ return {
227
+ dialect: DIALECT,
228
+ provider: PROVIDER,
229
+ wire: WIRE,
230
+ async execute(execParams) {
231
+ return runExecute({ queryable: conn, ...execParams });
232
+ },
233
+ async executeUnsafe(execParams) {
234
+ return runExecuteUnsafe({ queryable: conn, ...execParams });
235
+ },
236
+ async transaction(fn, nestedOptions) {
237
+ 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
+ }
250
+ },
251
+ async connect() {},
252
+ async disconnect() {},
253
+ formatArrayParam(values) {
254
+ return throwOnArrayParam(values);
255
+ }
256
+ };
257
+ }
258
+ return {
259
+ dialect: DIALECT,
260
+ provider: PROVIDER,
261
+ wire: WIRE,
262
+ async execute(params) {
263
+ return runExecute({ queryable: getPool(), ...params });
264
+ },
265
+ async executeUnsafe(params) {
266
+ const control = classifyRawTransactionControl({ text: params.text });
267
+ if (rawSession !== null) {
268
+ if (control === "close") {
269
+ const session = rawSession;
270
+ rawSession = null;
271
+ try {
272
+ return await runExecuteUnsafe({ queryable: session, ...params });
273
+ } finally {
274
+ session.release();
275
+ }
276
+ }
277
+ return runExecuteUnsafe({ queryable: rawSession, ...params });
278
+ }
279
+ if (control === "open") {
280
+ const conn = await runMapped(() => getPool().getConnection());
281
+ try {
282
+ const result = await runExecuteUnsafe({ queryable: conn, ...params });
283
+ rawSession = conn;
284
+ return result;
285
+ } catch (error) {
286
+ conn.release();
287
+ throw error;
288
+ }
289
+ }
290
+ return runExecuteUnsafe({ queryable: getPool(), ...params });
291
+ },
292
+ async transaction(fn, transactionOptions) {
293
+ const timeoutText = transactionOptions?.timeout === undefined ? undefined : maxExecutionTimeStatement({ timeout: transactionOptions.timeout });
294
+ const conn = await runMapped(() => getPool().getConnection());
295
+ try {
296
+ if (timeoutText !== undefined) {
297
+ await runMapped(() => conn.query(timeoutText));
298
+ }
299
+ const isolationLevel = transactionOptions?.isolationLevel;
300
+ if (isolationLevel !== undefined) {
301
+ const isolationText = setIsolationLevelStatement({ isolationLevel });
302
+ await runMapped(() => conn.query(isolationText));
303
+ }
304
+ await runMapped(() => conn.beginTransaction());
305
+ try {
306
+ const result = await fn(createConnectionAdapter({ conn, savepointCounter: createSavepointCounter() }));
307
+ await runMapped(() => conn.commit());
308
+ return result;
309
+ } catch (error) {
310
+ try {
311
+ await conn.rollback();
312
+ } catch {}
313
+ throw error;
314
+ }
315
+ } finally {
316
+ let poisoned = false;
317
+ if (timeoutText !== undefined) {
318
+ try {
319
+ await conn.query(RESET_MAX_EXECUTION_TIME_SQL);
320
+ } catch {
321
+ poisoned = true;
322
+ }
323
+ }
324
+ if (poisoned) {
325
+ conn.destroy();
326
+ } else {
327
+ conn.release();
328
+ }
329
+ }
330
+ },
331
+ async connect() {
332
+ const conn = await runMapped(() => getPool().getConnection());
333
+ try {
334
+ await runMapped(() => conn.ping());
335
+ } finally {
336
+ conn.release();
337
+ }
338
+ },
339
+ async disconnect() {
340
+ if (rawSession !== null) {
341
+ const session = rawSession;
342
+ rawSession = null;
343
+ try {
344
+ await session.query("ROLLBACK");
345
+ } catch {}
346
+ session.release();
347
+ }
348
+ if (pool !== null && ownsPool) {
349
+ const closing = pool;
350
+ pool = null;
351
+ await runMapped(() => closing.end());
352
+ }
353
+ },
354
+ formatArrayParam(values) {
355
+ return throwOnArrayParam(values);
356
+ }
357
+ };
358
+ }
359
+ export {
360
+ setIsolationLevelStatement,
361
+ runMapped,
362
+ refuseNestedTransactionOptions,
363
+ normalizeDriverResult,
364
+ nextSavepointName,
365
+ mysqlErrorErrno,
366
+ mysqlAdapter,
367
+ maxExecutionTimeStatement,
368
+ mapMysqlDriverError,
369
+ isResultSetHeader,
370
+ createSavepointCounter,
371
+ classifyRawTransactionControl,
372
+ RESET_MAX_EXECUTION_TIME_SQL,
373
+ MYSQL_ERRNO_REASONS,
374
+ MYSQL_ERRNO_ERROR_CODES,
375
+ ISOLATION_LEVEL_SQL
376
+ };
377
+
378
+ //# debugId=81C757629DAE03DB64756E2164756E21
@@ -0,0 +1,14 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts", "../src/errors.ts", "../src/results.ts", "../src/savepoints.ts", "../src/transaction-sql.ts"],
4
+ "sourcesContent": [
5
+ "/**\n * @vibeorm/adapter-mysql — mysql2 adapter for VibeORM v2.\n *\n * Runs on Bun and Node over `mysql2/promise`. Owns pooling, transactions\n * (nested → savepoints on the same pinned connection) and driver-error\n * mapping. Only `VibeError` ever escapes.\n *\n * SAME-CONNECTION GUARANTEE (board #7): `transaction()` checks ONE connection\n * out of the pool and every operation on the callback's adapter — `execute`,\n * `executeUnsafe`, nested `transaction` — runs on that same connection until\n * commit/rollback. That pinning is what makes `LAST_INSERT_ID()` sound: the\n * runtime wraps DB-assigned-key creates in `adapter.transaction` and issues\n * `SELECT LAST_INSERT_ID() AS id` via `executeUnsafe`, and LAST_INSERT_ID() is\n * per-connection state in MySQL — on any other connection it would answer for\n * someone else's INSERT.\n *\n * Driver configuration is pinned to the runtime's MYSQL_CODECS contract\n * (packages/runtime/src/codecs.ts) — see {@link MysqlPoolConfig}.\n */\n\nimport type { DatabaseAdapter, QueryResult, TransactionOptions, WireFidelity } from \"@vibeorm/runtime\";\nimport type { Dialect, Provider } from \"@vibeorm/schema\";\nimport { internalError } from \"@vibeorm/schema\";\nimport { createPool as mysql2CreatePool } from \"mysql2/promise\";\nimport { mapMysqlDriverError, runMapped } from \"./errors.ts\";\nimport { normalizeDriverResult } from \"./results.ts\";\nimport { createSavepointCounter, nextSavepointName, type SavepointCounter } from \"./savepoints.ts\";\nimport {\n RESET_MAX_EXECUTION_TIME_SQL,\n classifyRawTransactionControl,\n maxExecutionTimeStatement,\n refuseNestedTransactionOptions,\n setIsolationLevelStatement,\n} from \"./transaction-sql.ts\";\n\n// ─── Minimal mysql2 surface ───────────────────────────────────────\n\n/** The parts of a mysql2 `PoolConnection` this adapter uses. */\nexport type MysqlConnectionLike = {\n /** Binary protocol — prepared statements, cached per connection by mysql2. */\n execute(text: string, values?: unknown[]): Promise<[unknown, unknown]>;\n /** Text protocol — raw path and transaction-control statements. */\n query(text: string, values?: unknown[]): Promise<[unknown, unknown]>;\n beginTransaction(): Promise<void>;\n commit(): Promise<void>;\n rollback(): Promise<void>;\n ping(): Promise<void>;\n release(): void;\n destroy(): void;\n};\n\n/** The parts of a mysql2 promise `Pool` this adapter uses. */\nexport type MysqlPoolLike = {\n execute(text: string, values?: unknown[]): Promise<[unknown, unknown]>;\n query(text: string, values?: unknown[]): Promise<[unknown, unknown]>;\n getConnection(): Promise<MysqlConnectionLike>;\n end(): Promise<void>;\n};\n\n/** Anything that can run a statement: the pool itself or a pinned connection. */\ntype Queryable = Pick<MysqlConnectionLike, \"execute\" | \"query\">;\n\n// ─── Pool config ──────────────────────────────────────────────────\n\n/**\n * The exact configuration this adapter hands to `mysql2.createPool`. The\n * literal-typed fields are pinned to the runtime's MYSQL_CODECS contract and\n * are not user-overridable:\n *\n * - `decimalNumbers: false` — DECIMAL comes back as a string; the Decimal\n * codec keeps it a string (no float rounding) and trims the fixed-scale\n * padding MySQL reports (`DECIMAL(65, 30)` → `\"42.420…00\"` → `\"42.42\"`).\n * - `supportBigNumbers: true` + `bigNumberStrings: true` — BIGINT (and\n * DECIMAL) always come back as strings, never a precision-lossy JS number;\n * the BigInt codec decodes strings exactly.\n * - `dateStrings: false` — DATETIME comes back as a JS `Date`; the DateTime\n * codec passes `Date` through in both directions.\n * - `jsonStrings: false` — JSON columns come back already parsed, which is\n * what the Json codec's decode expects from mysql2.\n * - `namedPlaceholders: false` — the mysql dialect renders positional `?`.\n * - `multipleStatements` defaults to false: the ORM never batches, and\n * @vibeorm/migrate's mysql runner applies DDL per-statement anyway (DDL\n * auto-commits, so per-statement progress tracking needs it).\n */\nexport type MysqlPoolConfig = {\n readonly uri?: string;\n readonly host?: string;\n readonly port?: number;\n readonly user?: string;\n readonly password?: string;\n readonly database?: string;\n readonly connectionLimit?: number;\n readonly charset?: string;\n readonly timezone: string;\n readonly decimalNumbers: false;\n readonly supportBigNumbers: true;\n readonly bigNumberStrings: true;\n readonly dateStrings: false;\n readonly jsonStrings: false;\n readonly namedPlaceholders: false;\n readonly multipleStatements: boolean;\n};\n\n// ─── Options ──────────────────────────────────────────────────────\n\n/**\n * Options for {@link mysqlAdapter}. Point it at a connection URL **or** at\n * discrete `host`/`port`/`user`/`password`/`database` fields (mysql2 accepts\n * both; when both are given the discrete fields win — mysql2 merges the parsed\n * URL underneath explicit config), or hand it a pool you already own.\n */\nexport type MysqlAdapterOptions = {\n /** MySQL connection URL (`mysql://user:pw@host:3306/db`). Defaults to `process.env.MYSQL_URL`. */\n readonly url?: string;\n readonly host?: string;\n readonly port?: number;\n readonly user?: string;\n readonly password?: string;\n readonly database?: string;\n /**\n * An existing mysql2 promise pool to use instead of creating one. The\n * adapter never ends a pool it did not create, so `disconnect()` is a no-op\n * in this mode. The pool MUST be configured to the codec contract\n * ({@link MysqlPoolConfig}) — the adapter cannot verify it.\n */\n readonly pool?: MysqlPoolLike;\n /** Maximum pooled connections (mysql2 default 10). Ignored when `pool` is given. */\n readonly connectionLimit?: number;\n /**\n * mysql2 `timezone`, default `\"Z\"`: the codec table stores DateTime as UTC\n * in `DATETIME(3)` columns, and DATETIME has no zone of its own — with\n * `\"Z\"` mysql2 serializes outgoing `Date`s as UTC and parses incoming\n * DATETIME text as UTC, so the instant round-trips regardless of the server\n * or process time zone. mysql2's own default (`\"local\"`) would silently\n * shift values by the process offset.\n */\n readonly timezone?: string;\n /** Connection charset/collation (mysql2 default `utf8mb4_general_ci`). */\n readonly charset?: string;\n /**\n * Allow multiple statements per `executeUnsafe` call (default false). Leave\n * it off: the ORM never batches, @vibeorm/migrate applies DDL per statement,\n * and enabling it widens the SQL-injection blast radius of raw queries.\n */\n readonly multipleStatements?: boolean;\n /**\n * Pool factory — a test seam. Defaults to `mysql2/promise`'s `createPool`;\n * tests inject a recording fake here to assert the exact config the adapter\n * builds. Ignored when `pool` is given.\n */\n readonly createPool?: (config: MysqlPoolConfig) => MysqlPoolLike;\n};\n\n// ─── Adapter ──────────────────────────────────────────────────────\n\nconst DIALECT: Dialect = \"mysql\";\nconst PROVIDER: Provider = \"mysql\";\n\n/**\n * mysql2 wire (board #30, live-verified on both execute paths, 8.4):\n * datetime(3) → `Date` (under the adapter's pinned `timezone: \"Z\"`), JSON →\n * parsed values, int/double → numbers. (BIGINT arrives as a string and\n * TINYINT(1) as 0/1 — BigInt/Boolean decoding stays.)\n */\nconst WIRE: WireFidelity = Object.freeze({ dateTime: \"native\", json: \"parsed\", numbers: \"native\" } as const);\n\nfunction defaultCreatePool(config: MysqlPoolConfig): MysqlPoolLike {\n // Single cast seam: mysql2's Pool satisfies MysqlPoolLike structurally, but\n // its generic overloads don't reduce to our plain signatures.\n return mysql2CreatePool(config as Parameters<typeof mysql2CreatePool>[0]) as unknown as MysqlPoolLike;\n}\n\n/** Scalar-array params must never reach this adapter — the dialect expands IN. */\nfunction throwOnArrayParam(values: unknown[]): never {\n throw internalError({\n message:\n \"formatArrayParam called on the mysql adapter — the mysql dialect expands scalar-list parameters into IN (?, ?, …) placeholders, so the runtime should have expanded this array before binding\",\n meta: { length: values.length },\n });\n}\n\n/**\n * Create a VibeORM adapter backed by mysql2.\n *\n * @example\n * ```ts\n * const adapter = mysqlAdapter({ url: process.env.MYSQL_URL, connectionLimit: 20 });\n * await adapter.connect();\n * const rows = await adapter.execute({ text: \"SELECT * FROM `User` WHERE `id` = ?\", values: [1] });\n * ```\n *\n * @example Discrete fields, or reusing a pool you already own\n * ```ts\n * const adapter = mysqlAdapter({ host: \"127.0.0.1\", port: 3306, user: \"app\", password: \"pw\", database: \"app\" });\n * const shared = mysqlAdapter({ pool: existingPool });\n * ```\n *\n * ORM path (`execute`) uses mysql2's binary protocol (`pool.execute`), which\n * prepares statements and caches them per connection keyed by statement text\n * (mysql2's `maxPreparedStatements` LRU, default 16000). Raw path\n * (`executeUnsafe`) uses the text protocol (`pool.query`) — no statement\n * cache, and DDL (which the binary protocol cannot prepare) works.\n *\n * Parameters arrive PRE-ENCODED by the runtime codec table: Json is already a\n * string, DateTime is a `Date` (mysql2 serializes it per `timezone`), BigInt\n * is a string. The adapter forwards values verbatim — no second conversion.\n */\nexport function mysqlAdapter(options?: MysqlAdapterOptions): DatabaseAdapter {\n let pool: MysqlPoolLike | null = null;\n let ownsPool = false;\n\n /**\n * The pinned connection of an OPEN raw transaction — session affinity for\n * the raw path. A raw `BEGIN` … `COMMIT` sequence over `executeUnsafe` is\n * sound only when every statement in between rides the SAME pooled\n * connection, so a pure `BEGIN` checks one out, raw statements ride it, and\n * the matching `COMMIT`/`ROLLBACK` releases it (same semantics as the pg and\n * bun adapters). The ORM path (`execute`) and `transaction()` stay on the\n * pool. Concurrent raw transactions on one adapter are unsupported.\n */\n let rawSession: MysqlConnectionLike | null = null;\n\n function getPool(): MysqlPoolLike {\n if (pool !== null) return pool;\n\n if (options?.pool !== undefined) {\n pool = options.pool;\n ownsPool = false;\n return pool;\n }\n\n const config: MysqlPoolConfig = {\n uri: options?.url ?? process.env.MYSQL_URL,\n host: options?.host,\n port: options?.port,\n user: options?.user,\n password: options?.password,\n database: options?.database,\n connectionLimit: options?.connectionLimit,\n charset: options?.charset,\n timezone: options?.timezone ?? \"Z\",\n decimalNumbers: false,\n supportBigNumbers: true,\n bigNumberStrings: true,\n dateStrings: false,\n jsonStrings: false,\n namedPlaceholders: false,\n multipleStatements: options?.multipleStatements ?? false,\n };\n pool = (options?.createPool ?? defaultCreatePool)(config);\n ownsPool = true;\n return pool;\n }\n\n async function runExecute(params: {\n queryable: Queryable;\n text: string;\n values: unknown[];\n }): Promise<Record<string, unknown>[]> {\n const [result] = await runMapped(() => params.queryable.execute(params.text, params.values));\n return normalizeDriverResult({ result }).rows;\n }\n\n async function runExecuteUnsafe(params: {\n queryable: Queryable;\n text: string;\n values?: unknown[];\n }): Promise<QueryResult> {\n const [result] = await runMapped(() => params.queryable.query(params.text, params.values));\n return normalizeDriverResult({ result });\n }\n\n /**\n * Adapter bound to the ONE pinned connection of an open transaction — the\n * same-connection guarantee. Nested `transaction()` calls open savepoints on\n * that connection, naming them from the counter shared across the whole\n * top-level transaction so siblings never collide.\n */\n function createConnectionAdapter(params: {\n conn: MysqlConnectionLike;\n savepointCounter: SavepointCounter;\n }): DatabaseAdapter {\n const { conn, savepointCounter } = params;\n\n return {\n dialect: DIALECT,\n provider: PROVIDER,\n wire: WIRE,\n\n async execute(execParams) {\n return runExecute({ queryable: conn, ...execParams });\n },\n\n async executeUnsafe(execParams) {\n return runExecuteUnsafe({ queryable: conn, ...execParams });\n },\n\n async transaction<T>(\n fn: (txAdapter: DatabaseAdapter) => Promise<T>,\n nestedOptions?: TransactionOptions,\n ): Promise<T> {\n refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });\n const savepoint = nextSavepointName({ counter: savepointCounter });\n await runMapped(() => conn.query(`SAVEPOINT ${savepoint}`));\n try {\n const result = await fn(createConnectionAdapter({ conn, savepointCounter }));\n await runMapped(() => conn.query(`RELEASE SAVEPOINT ${savepoint}`));\n return result;\n } catch (error) {\n // Best effort: never mask the original failure with a rollback failure.\n try {\n await conn.query(`ROLLBACK TO SAVEPOINT ${savepoint}`);\n } catch {\n /* rollback is best-effort */\n }\n throw error;\n }\n },\n\n async connect(): Promise<void> {\n // Already connected — this adapter is bound to a pinned connection.\n },\n\n async disconnect(): Promise<void> {\n // The pool owns the connection lifecycle; releasing happens at the\n // top-level transaction boundary.\n },\n\n formatArrayParam(values: unknown[]): unknown {\n return throwOnArrayParam(values);\n },\n };\n }\n\n return {\n dialect: DIALECT,\n provider: PROVIDER,\n wire: WIRE,\n\n async execute(params) {\n return runExecute({ queryable: getPool(), ...params });\n },\n\n async executeUnsafe(params) {\n const control = classifyRawTransactionControl({ text: params.text });\n if (rawSession !== null) {\n if (control === \"close\") {\n const session = rawSession;\n rawSession = null;\n try {\n return await runExecuteUnsafe({ queryable: session, ...params });\n } finally {\n session.release();\n }\n }\n return runExecuteUnsafe({ queryable: rawSession, ...params });\n }\n if (control === \"open\") {\n const conn = await runMapped(() => getPool().getConnection());\n try {\n const result = await runExecuteUnsafe({ queryable: conn, ...params });\n rawSession = conn;\n return result;\n } catch (error) {\n conn.release();\n throw error;\n }\n }\n return runExecuteUnsafe({ queryable: getPool(), ...params });\n },\n\n async transaction<T>(\n fn: (txAdapter: DatabaseAdapter) => Promise<T>,\n transactionOptions?: TransactionOptions,\n ): Promise<T> {\n // Validate the timeout before taking a connection, so a bad option fails\n // fast instead of wasting a checkout.\n const timeoutText =\n transactionOptions?.timeout === undefined\n ? undefined\n : maxExecutionTimeStatement({ timeout: transactionOptions.timeout });\n\n const conn = await runMapped(() => getPool().getConnection());\n try {\n // Session-scoped SELECT bound — see transaction-sql.ts for the loud\n // \"SELECT statements only\" caveat. Reset in finally below.\n if (timeoutText !== undefined) {\n await runMapped(() => conn.query(timeoutText));\n }\n // Applies to the NEXT transaction in the session → must precede begin.\n const isolationLevel = transactionOptions?.isolationLevel;\n if (isolationLevel !== undefined) {\n const isolationText = setIsolationLevelStatement({ isolationLevel });\n await runMapped(() => conn.query(isolationText));\n }\n await runMapped(() => conn.beginTransaction());\n try {\n const result = await fn(createConnectionAdapter({ conn, savepointCounter: createSavepointCounter() }));\n await runMapped(() => conn.commit());\n return result;\n } catch (error) {\n // Best effort: never mask the original failure with a rollback failure.\n try {\n await conn.rollback();\n } catch {\n /* rollback is best-effort */\n }\n throw error;\n }\n } finally {\n let poisoned = false;\n if (timeoutText !== undefined) {\n try {\n await conn.query(RESET_MAX_EXECUTION_TIME_SQL);\n } catch {\n poisoned = true;\n }\n }\n // A connection whose session default could not be restored must never\n // return to the pool with a lowered MAX_EXECUTION_TIME.\n if (poisoned) {\n conn.destroy();\n } else {\n conn.release();\n }\n }\n },\n\n async connect(): Promise<void> {\n const conn = await runMapped(() => getPool().getConnection());\n try {\n await runMapped(() => conn.ping());\n } finally {\n conn.release();\n }\n },\n\n async disconnect(): Promise<void> {\n if (rawSession !== null) {\n // An abandoned raw transaction must not leak an in-transaction\n // connection; roll it back best-effort before the pool ends.\n const session = rawSession;\n rawSession = null;\n try {\n await session.query(\"ROLLBACK\");\n } catch {\n /* the pool is closing; the session is going away regardless */\n }\n session.release();\n }\n if (pool !== null && ownsPool) {\n const closing = pool;\n pool = null;\n await runMapped(() => closing.end());\n }\n },\n\n formatArrayParam(values: unknown[]): unknown {\n return throwOnArrayParam(values);\n },\n };\n}\n\n// ─── Re-exports (driver-error table, helpers) ─────────────────────\n\nexport {\n MYSQL_ERRNO_ERROR_CODES,\n MYSQL_ERRNO_REASONS,\n mapMysqlDriverError,\n mysqlErrorErrno,\n runMapped,\n} from \"./errors.ts\";\nexport { isResultSetHeader, normalizeDriverResult } from \"./results.ts\";\nexport { createSavepointCounter, nextSavepointName } from \"./savepoints.ts\";\nexport type { SavepointCounter } from \"./savepoints.ts\";\nexport {\n ISOLATION_LEVEL_SQL,\n RESET_MAX_EXECUTION_TIME_SQL,\n classifyRawTransactionControl,\n maxExecutionTimeStatement,\n refuseNestedTransactionOptions,\n setIsolationLevelStatement,\n} from \"./transaction-sql.ts\";\nexport type { RawTransactionControl } from \"./transaction-sql.ts\";\n",
6
+ "/**\n * Driver-error mapping for mysql2.\n *\n * Constitution rule 5: a raw driver error never escapes an adapter. Every\n * failure becomes a `VibeError` with a stable code, the driver error attached\n * as `cause`, and the MySQL errno / symbolic code / SQLSTATE / server message\n * in `meta`.\n *\n * mysql2 errors carry `errno` (the numeric MySQL server error, e.g. 1062),\n * `code` (the symbolic name, `ER_DUP_ENTRY`, or a client-side string like\n * `ECONNREFUSED` / `PROTOCOL_CONNECTION_LOST`), `sqlState` and `sqlMessage`.\n * The table is keyed by `errno` because it is the stable server-side identity;\n * client-side failures have no errno and fall through to `VIBE_ADAPTER`.\n */\n\nimport { VibeError, type VibeErrorCode } from \"@vibeorm/schema\";\n\n// ─── Errno table ──────────────────────────────────────────────────\n\n/**\n * MySQL server errno → stable VibeError code. Frozen and mirrored by tests;\n * anything absent from this table maps to `VIBE_ADAPTER` — including access\n * failures (1044 `ER_DBACCESS_DENIED_ERROR`, 1045 `ER_ACCESS_DENIED_ERROR`)\n * and every client-side connection error (`ECONNREFUSED`,\n * `PROTOCOL_CONNECTION_LOST`, `ETIMEDOUT`), which carry no errno at all.\n */\nexport const MYSQL_ERRNO_ERROR_CODES: Readonly<Record<number, VibeErrorCode>> = Object.freeze({\n /** ER_DUP_ENTRY */\n 1062: \"VIBE_UNIQUE_VIOLATION\",\n /** ER_DUP_ENTRY_WITH_KEY_NAME */\n 1586: \"VIBE_UNIQUE_VIOLATION\",\n /** ER_FOREIGN_DUPLICATE_KEY_WITH_CHILD_INFO */\n 1761: \"VIBE_UNIQUE_VIOLATION\",\n /** ER_FOREIGN_DUPLICATE_KEY_WITHOUT_CHILD_INFO */\n 1762: \"VIBE_UNIQUE_VIOLATION\",\n /** ER_NO_REFERENCED_ROW */\n 1216: \"VIBE_FK_VIOLATION\",\n /** ER_ROW_IS_REFERENCED */\n 1217: \"VIBE_FK_VIOLATION\",\n /** ER_ROW_IS_REFERENCED_2 */\n 1451: \"VIBE_FK_VIOLATION\",\n /** ER_NO_REFERENCED_ROW_2 */\n 1452: \"VIBE_FK_VIOLATION\",\n /** ER_BAD_NULL_ERROR — NOT NULL column set to NULL */\n 1048: \"VIBE_VALIDATION\",\n /** ER_WARN_DATA_OUT_OF_RANGE — value out of column range */\n 1264: \"VIBE_VALIDATION\",\n /** ER_NO_DEFAULT_FOR_FIELD — column without default omitted */\n 1364: \"VIBE_VALIDATION\",\n /** ER_DATA_TOO_LONG */\n 1406: \"VIBE_VALIDATION\",\n /**\n * WARN_DATA_TRUNCATED — despite the WARN_ name, strict mode raises it as a\n * hard error: an out-of-range ENUM value or a truncated numeric string\n * (live-verified on 8.4: `role = \"SUPERUSER\"` and `n = \"12abc\"` → 1265).\n * The same bad input is a CHECK violation on sqlite and SQLSTATE 22P02 on\n * postgres — all VIBE_VALIDATION (board #9).\n */\n 1265: \"VIBE_VALIDATION\",\n /**\n * ER_TRUNCATED_WRONG_VALUE_FOR_FIELD — a wrong-TYPED value for the column\n * (live-verified on 8.4: `\"abc\"` into INT, `\"12.x9\"` into DECIMAL → 1366).\n */\n 1366: \"VIBE_VALIDATION\",\n /**\n * ER_TRUNCATED_WRONG_VALUE — an unparseable literal, e.g. a bad datetime\n * (live-verified on 8.4: `\"not-a-date\"` into DATETIME(3) → 1292,\n * SQLSTATE 22007).\n */\n 1292: \"VIBE_VALIDATION\",\n /** ER_CHECK_CONSTRAINT_VIOLATED */\n 3819: \"VIBE_VALIDATION\",\n /** ER_LOCK_DEADLOCK */\n 1213: \"VIBE_TRANSACTION\",\n /** ER_LOCK_WAIT_TIMEOUT */\n 1205: \"VIBE_TRANSACTION\",\n /** ER_QUERY_TIMEOUT — max_execution_time exceeded, statement interrupted */\n 3024: \"VIBE_TRANSACTION\",\n});\n\n/** Errnos that additionally explain themselves through `meta.reason`. */\nexport const MYSQL_ERRNO_REASONS: Readonly<Record<number, string>> = Object.freeze({\n /** ER_LOCK_DEADLOCK */\n 1213: \"deadlock\",\n /** ER_LOCK_WAIT_TIMEOUT */\n 1205: \"timeout\",\n /** ER_QUERY_TIMEOUT */\n 3024: \"timeout\",\n});\n\n// ─── Reading driver errors ────────────────────────────────────────\n\nfunction readStringField(params: { source: Record<string, unknown>; key: string }): string | undefined {\n const value = params.source[params.key];\n return typeof value === \"string\" && value.length > 0 ? value : undefined;\n}\n\n/**\n * Extract the numeric MySQL errno from a mysql2 error. Client-side failures\n * (`ECONNREFUSED`, `PROTOCOL_CONNECTION_LOST`, …) have no numeric errno and\n * yield `undefined`.\n */\nexport function mysqlErrorErrno(params: { error: unknown }): number | undefined {\n const { error } = params;\n if (error === null || typeof error !== \"object\") return undefined;\n const errno = (error as Record<string, unknown>).errno;\n return typeof errno === \"number\" && Number.isInteger(errno) ? errno : undefined;\n}\n\n// ─── Mapping ──────────────────────────────────────────────────────\n\n/**\n * Map any error thrown by mysql2 onto a `VibeError`.\n *\n * A `VibeError` passes through unchanged, so an error raised deeper inside the\n * ORM (or by a nested adapter call) is never re-wrapped, and a user callback's\n * error is rethrown identically.\n */\nexport function mapMysqlDriverError(params: { error: unknown }): VibeError {\n const { error } = params;\n if (error instanceof VibeError) return error;\n\n const source: Record<string, unknown> =\n error !== null && typeof error === \"object\" ? (error as Record<string, unknown>) : {};\n const errno = mysqlErrorErrno({ error });\n const code: VibeErrorCode =\n (errno === undefined ? undefined : MYSQL_ERRNO_ERROR_CODES[errno]) ?? \"VIBE_ADAPTER\";\n\n const meta: Record<string, unknown> = {};\n if (errno !== undefined) meta.errno = errno;\n const symbolicCode = readStringField({ source, key: \"code\" });\n if (symbolicCode !== undefined) meta.code = symbolicCode;\n const sqlState = readStringField({ source, key: \"sqlState\" });\n if (sqlState !== undefined) meta.sqlState = sqlState;\n const sqlMessage = readStringField({ source, key: \"sqlMessage\" });\n if (sqlMessage !== undefined) meta.sqlMessage = sqlMessage.trim();\n const reason = errno === undefined ? undefined : MYSQL_ERRNO_REASONS[errno];\n if (reason !== undefined) meta.reason = reason;\n\n const message =\n readStringField({ source, key: \"message\" }) ??\n (sqlMessage !== undefined ? sqlMessage.trim() : undefined) ??\n (typeof error === \"string\" ? error : \"unknown mysql driver error\");\n\n return new VibeError({ code, message, meta, cause: error });\n}\n\n/** Run a driver call, mapping anything it throws onto a `VibeError`. */\nexport async function runMapped<T>(fn: () => Promise<T>): Promise<T> {\n try {\n return await fn();\n } catch (error) {\n throw mapMysqlDriverError({ error });\n }\n}\n",
7
+ "/**\n * Result normalization for mysql2.\n *\n * mysql2 resolves `execute()` / `query()` with a `[result, fields]` tuple where\n * `result` is one of:\n * - `RowDataPacket[]` — a SELECT's rows (plain objects; `rowsAsArray` is off);\n * - `ResultSetHeader` — an INSERT / UPDATE / DELETE / DDL OK-packet carrying\n * `affectedRows` and `insertId`;\n * - an array of the above — only when `multipleStatements` is enabled, one\n * entry per statement.\n *\n * The adapter's contract: `execute` returns rows (a header normalizes to `[]`);\n * `executeUnsafe` returns `{ rows, affectedRows }` (a SELECT has\n * `affectedRows: 0`, a mutation has `rows: []`). With `multipleStatements`\n * enabled the LAST statement's result wins, mirroring adapter-pglite's `exec`.\n */\n\nimport type { QueryResult } from \"@vibeorm/runtime\";\n\n// ─── Header detection ─────────────────────────────────────────────\n\n/**\n * True when a driver result value is shaped like mysql2's `ResultSetHeader`\n * (an OK-packet). Checks `affectedRows` + `serverStatus` together so a row\n * object that merely aliases a column as `affectedRows` is not misread.\n */\nexport function isResultSetHeader(params: { value: unknown }): boolean {\n const { value } = params;\n if (value === null || typeof value !== \"object\" || Array.isArray(value)) return false;\n const source = value as Record<string, unknown>;\n return typeof source.affectedRows === \"number\" && typeof source.serverStatus === \"number\";\n}\n\n// ─── Normalization ────────────────────────────────────────────────\n\n/** One result set (never a multi-statement array) → the adapter contract. */\nfunction normalizeSingle(value: unknown): QueryResult {\n if (Array.isArray(value)) {\n return { rows: value as Record<string, unknown>[], affectedRows: 0 };\n }\n if (isResultSetHeader({ value })) {\n return { rows: [], affectedRows: (value as { affectedRows: number }).affectedRows };\n }\n return { rows: [], affectedRows: 0 };\n}\n\n/**\n * Normalize the first element of mysql2's `[result, fields]` tuple. A\n * multi-statement result (array whose last entry is itself a result set or an\n * OK-packet) collapses to its LAST statement's result.\n */\nexport function normalizeDriverResult(params: { result: unknown }): QueryResult {\n const { result } = params;\n if (Array.isArray(result)) {\n const last: unknown = result.at(-1);\n if (last !== undefined && (Array.isArray(last) || isResultSetHeader({ value: last }))) {\n return normalizeSingle(last);\n }\n // A plain result set: row objects (or an empty SELECT).\n return { rows: result as Record<string, unknown>[], affectedRows: 0 };\n }\n return normalizeSingle(result);\n}\n",
8
+ "/**\n * Savepoint naming for nested transactions.\n *\n * v1 lesson (LEARNINGS.md): the counter is shared **by reference** across every\n * adapter created for one top-level transaction, so sibling and deeply nested\n * savepoints can never collide on a name.\n */\n\n/** Mutable counter shared by reference across one top-level transaction. */\nexport type SavepointCounter = { n: number };\n\n/** Fresh counter — one per top-level transaction. */\nexport function createSavepointCounter(): SavepointCounter {\n return { n: 0 };\n}\n\n/**\n * Allocate the next savepoint name (`vibeorm_sp_0`, `vibeorm_sp_1`, …) and\n * advance the shared counter. The name is an identifier by construction, so it\n * is safe to interpolate into `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` SQL.\n */\nexport function nextSavepointName(params: { counter: SavepointCounter }): string {\n return `vibeorm_sp_${params.counter.n++}`;\n}\n",
9
+ "/**\n * Transaction-control SQL text (mysql dialect).\n *\n * Kept apart from the adapter so the mapping from `TransactionOptions` to SQL\n * is unit-testable without a server, and so the timeout value is validated\n * before it is ever interpolated into a statement.\n *\n * MySQL specifics:\n * - `SET TRANSACTION ISOLATION LEVEL <level>` applies to the NEXT transaction\n * started in the session, so the adapter issues it BEFORE\n * `conn.beginTransaction()` — never inside the open transaction.\n * - MySQL has no general per-transaction statement timeout.\n * `MAX_EXECUTION_TIME` is the closest tool and it bounds **SELECT statements\n * only** — see {@link maxExecutionTimeStatement}.\n */\n\nimport type { IsolationLevel, TransactionOptions } from \"@vibeorm/runtime\";\nimport { VibeError } from \"@vibeorm/schema\";\n\n/** VibeORM isolation level → MySQL isolation level keyword. */\nexport const ISOLATION_LEVEL_SQL: Readonly<Record<IsolationLevel, string>> = Object.freeze({\n ReadCommitted: \"READ COMMITTED\",\n RepeatableRead: \"REPEATABLE READ\",\n Serializable: \"SERIALIZABLE\",\n});\n\n/**\n * `SET TRANSACTION ISOLATION LEVEL <level>` — scoped to the next transaction\n * in the session, which is why the adapter sends it before\n * `conn.beginTransaction()` on the pinned connection.\n *\n * Observability trap (verified live on MySQL 8.4): the one-shot value IS\n * applied to the next transaction — `performance_schema.\n * events_transactions_current` reports the requested level — but\n * `@@transaction_isolation` NEVER reflects it; that variable keeps answering\n * the session value both before and inside the transaction. Tests must assert\n * the level's behavior (e.g. SERIALIZABLE turning plain SELECTs into locking\n * reads), not the variable — see tests/live.test.ts.\n */\nexport function setIsolationLevelStatement(params: { isolationLevel: IsolationLevel }): string {\n return `SET TRANSACTION ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[params.isolationLevel]}`;\n}\n\n/**\n * `SET SESSION MAX_EXECUTION_TIME = <ms>` — the closest MySQL gets to a\n * transaction timeout, and the semantics differ LOUDLY from postgres:\n *\n * **`MAX_EXECUTION_TIME` bounds SELECT statements only.** INSERT / UPDATE /\n * DELETE / DDL inside the transaction run unbounded; MySQL simply has no\n * per-statement timeout for mutations. An over-long SELECT is interrupted with\n * errno 3024 (`ER_QUERY_TIMEOUT`), mapped to `VIBE_TRANSACTION` with\n * `meta.reason = \"timeout\"`. The setting is session-scoped (not\n * transaction-scoped), so the adapter resets it to DEFAULT in `finally` before\n * the connection returns to the pool.\n *\n * One more caveat (verified live on MySQL 8.4): `SELECT SLEEP(n)` cannot\n * observe the bound — an interrupted `SLEEP()` swallows the kill and returns\n * `1` with NO error, so the statement comes back \"successfully\" at ~timeout.\n * Only a SELECT doing real work is cancelled with errno 3024.\n *\n * @throws VibeError `VIBE_VALIDATION` when the timeout is not a non-negative\n * integer — the value is interpolated into SQL, so it is never trusted blindly.\n */\nexport function maxExecutionTimeStatement(params: { timeout: number }): string {\n const { timeout } = params;\n if (!Number.isInteger(timeout) || timeout < 0) {\n throw new VibeError({\n code: \"VIBE_VALIDATION\",\n message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,\n meta: { timeout },\n });\n }\n return `SET SESSION MAX_EXECUTION_TIME = ${timeout}`;\n}\n\n/**\n * Restores the session default after a transaction that set a timeout. Issued\n * in `finally`; if it fails the adapter destroys the connection instead of\n * releasing it, so a poisoned session never returns to the pool.\n */\nexport const RESET_MAX_EXECUTION_TIME_SQL: string = \"SET SESSION MAX_EXECUTION_TIME = DEFAULT\";\n\n// ─── Raw transaction control (session affinity) ─────────────────\n\n/** How a raw statement steers the adapter's session-affine raw transaction. */\nexport type RawTransactionControl = \"open\" | \"close\" | null;\n\nconst RAW_OPEN_PATTERN: RegExp = /^\\s*(?:begin|start\\s+transaction)\\b[^;]*;?\\s*$/i;\nconst RAW_CLOSE_PATTERN: RegExp = /^\\s*(?:commit|rollback)(?:\\s+work)?\\s*;?\\s*$/i;\n\n/**\n * Classify a raw statement as transaction control. A raw `BEGIN` … `COMMIT`\n * sequence over `executeUnsafe` is sound only when every statement in between\n * rides the SAME pooled connection, so the adapter pins one connection for\n * the whole raw transaction (\"open\" checks out, \"close\" releases). MySQL DDL\n * auto-commits, so @vibeorm/migrate never wraps here — this exists for raw\n * user sequences and keeps the three pool adapters' semantics identical.\n *\n * Deliberately strict: only PURE single-statement control text matches.\n * Compound scripts, `ROLLBACK TO SAVEPOINT` and `COMMIT AND CHAIN` (which\n * keeps a transaction open) never engage the pinning.\n */\nexport function classifyRawTransactionControl(params: { text: string }): RawTransactionControl {\n if (RAW_OPEN_PATTERN.test(params.text)) return \"open\";\n if (RAW_CLOSE_PATTERN.test(params.text)) return \"close\";\n return null;\n}\n\n// ─── Nested transaction options (the cross-adapter contract) ──────\n\n/**\n * Refuse options on a NESTED `transaction()` call — the same contract on every\n * adapter (see the `DatabaseAdapter.transaction` JSDoc in @vibeorm/runtime):\n * a nested transaction is a SAVEPOINT, and a savepoint can neither change the\n * isolation level of the transaction it joins nor enforce its own timeout.\n * Silently dropping the option (the pre-#3 behaviour) hid exactly that.\n *\n * @throws VibeError `VIBE_VALIDATION` when `isolationLevel` or `timeout` is\n * present (an `undefined` or empty options object is accepted).\n */\nexport function refuseNestedTransactionOptions(params: {\n options?: TransactionOptions;\n provider: string;\n}): void {\n const { options, provider } = params;\n if (options === undefined) return;\n\n const passed: string[] = [];\n if (options.isolationLevel !== undefined) passed.push(\"isolationLevel\");\n if (options.timeout !== undefined) passed.push(\"timeout\");\n if (passed.length === 0) return;\n\n const meta: Record<string, unknown> = { provider, nested: true };\n if (options.isolationLevel !== undefined) meta.isolationLevel = options.isolationLevel;\n if (options.timeout !== undefined) meta.timeout = options.timeout;\n\n throw new VibeError({\n code: \"VIBE_VALIDATION\",\n message: `nested transactions run as savepoints and cannot honor ${passed.join(\" or \")} — 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`,\n meta,\n });\n}\n"
10
+ ],
11
+ "mappings": "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",
12
+ "debugId": "81C757629DAE03DB64756E2164756E21",
13
+ "names": []
14
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Result normalization for mysql2.
3
+ *
4
+ * mysql2 resolves `execute()` / `query()` with a `[result, fields]` tuple where
5
+ * `result` is one of:
6
+ * - `RowDataPacket[]` — a SELECT's rows (plain objects; `rowsAsArray` is off);
7
+ * - `ResultSetHeader` — an INSERT / UPDATE / DELETE / DDL OK-packet carrying
8
+ * `affectedRows` and `insertId`;
9
+ * - an array of the above — only when `multipleStatements` is enabled, one
10
+ * entry per statement.
11
+ *
12
+ * The adapter's contract: `execute` returns rows (a header normalizes to `[]`);
13
+ * `executeUnsafe` returns `{ rows, affectedRows }` (a SELECT has
14
+ * `affectedRows: 0`, a mutation has `rows: []`). With `multipleStatements`
15
+ * enabled the LAST statement's result wins, mirroring adapter-pglite's `exec`.
16
+ */
17
+ import type { QueryResult } from "@vibeorm/runtime";
18
+ /**
19
+ * True when a driver result value is shaped like mysql2's `ResultSetHeader`
20
+ * (an OK-packet). Checks `affectedRows` + `serverStatus` together so a row
21
+ * object that merely aliases a column as `affectedRows` is not misread.
22
+ */
23
+ export declare function isResultSetHeader(params: {
24
+ value: unknown;
25
+ }): boolean;
26
+ /**
27
+ * Normalize the first element of mysql2's `[result, fields]` tuple. A
28
+ * multi-statement result (array whose last entry is itself a result set or an
29
+ * OK-packet) collapses to its LAST statement's result.
30
+ */
31
+ export declare function normalizeDriverResult(params: {
32
+ result: unknown;
33
+ }): QueryResult;
34
+ //# sourceMappingURL=results.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"results.d.ts","sourceRoot":"","sources":["../src/results.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAIpD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAKrE;AAeD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,GAAG,WAAW,CAW9E"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Savepoint naming for nested transactions.
3
+ *
4
+ * v1 lesson (LEARNINGS.md): the counter is shared **by reference** across every
5
+ * adapter created for one top-level transaction, so sibling and deeply nested
6
+ * savepoints can never collide on a name.
7
+ */
8
+ /** Mutable counter shared by reference across one top-level transaction. */
9
+ export type SavepointCounter = {
10
+ n: number;
11
+ };
12
+ /** Fresh counter — one per top-level transaction. */
13
+ export declare function createSavepointCounter(): SavepointCounter;
14
+ /**
15
+ * Allocate the next savepoint name (`vibeorm_sp_0`, `vibeorm_sp_1`, …) and
16
+ * advance the shared counter. The name is an identifier by construction, so it
17
+ * is safe to interpolate into `SAVEPOINT` / `RELEASE` / `ROLLBACK TO` SQL.
18
+ */
19
+ export declare function nextSavepointName(params: {
20
+ counter: SavepointCounter;
21
+ }): string;
22
+ //# sourceMappingURL=savepoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"savepoints.d.ts","sourceRoot":"","sources":["../src/savepoints.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,4EAA4E;AAC5E,MAAM,MAAM,gBAAgB,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE7C,qDAAqD;AACrD,wBAAgB,sBAAsB,IAAI,gBAAgB,CAEzD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,gBAAgB,CAAA;CAAE,GAAG,MAAM,CAE/E"}
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Transaction-control SQL text (mysql dialect).
3
+ *
4
+ * Kept apart from the adapter so the mapping from `TransactionOptions` to SQL
5
+ * is unit-testable without a server, and so the timeout value is validated
6
+ * before it is ever interpolated into a statement.
7
+ *
8
+ * MySQL specifics:
9
+ * - `SET TRANSACTION ISOLATION LEVEL <level>` applies to the NEXT transaction
10
+ * started in the session, so the adapter issues it BEFORE
11
+ * `conn.beginTransaction()` — never inside the open transaction.
12
+ * - MySQL has no general per-transaction statement timeout.
13
+ * `MAX_EXECUTION_TIME` is the closest tool and it bounds **SELECT statements
14
+ * only** — see {@link maxExecutionTimeStatement}.
15
+ */
16
+ import type { IsolationLevel, TransactionOptions } from "@vibeorm/runtime";
17
+ /** VibeORM isolation level → MySQL isolation level keyword. */
18
+ export declare const ISOLATION_LEVEL_SQL: Readonly<Record<IsolationLevel, string>>;
19
+ /**
20
+ * `SET TRANSACTION ISOLATION LEVEL <level>` — scoped to the next transaction
21
+ * in the session, which is why the adapter sends it before
22
+ * `conn.beginTransaction()` on the pinned connection.
23
+ *
24
+ * Observability trap (verified live on MySQL 8.4): the one-shot value IS
25
+ * applied to the next transaction — `performance_schema.
26
+ * events_transactions_current` reports the requested level — but
27
+ * `@@transaction_isolation` NEVER reflects it; that variable keeps answering
28
+ * the session value both before and inside the transaction. Tests must assert
29
+ * the level's behavior (e.g. SERIALIZABLE turning plain SELECTs into locking
30
+ * reads), not the variable — see tests/live.test.ts.
31
+ */
32
+ export declare function setIsolationLevelStatement(params: {
33
+ isolationLevel: IsolationLevel;
34
+ }): string;
35
+ /**
36
+ * `SET SESSION MAX_EXECUTION_TIME = <ms>` — the closest MySQL gets to a
37
+ * transaction timeout, and the semantics differ LOUDLY from postgres:
38
+ *
39
+ * **`MAX_EXECUTION_TIME` bounds SELECT statements only.** INSERT / UPDATE /
40
+ * DELETE / DDL inside the transaction run unbounded; MySQL simply has no
41
+ * per-statement timeout for mutations. An over-long SELECT is interrupted with
42
+ * errno 3024 (`ER_QUERY_TIMEOUT`), mapped to `VIBE_TRANSACTION` with
43
+ * `meta.reason = "timeout"`. The setting is session-scoped (not
44
+ * transaction-scoped), so the adapter resets it to DEFAULT in `finally` before
45
+ * the connection returns to the pool.
46
+ *
47
+ * One more caveat (verified live on MySQL 8.4): `SELECT SLEEP(n)` cannot
48
+ * observe the bound — an interrupted `SLEEP()` swallows the kill and returns
49
+ * `1` with NO error, so the statement comes back "successfully" at ~timeout.
50
+ * Only a SELECT doing real work is cancelled with errno 3024.
51
+ *
52
+ * @throws VibeError `VIBE_VALIDATION` when the timeout is not a non-negative
53
+ * integer — the value is interpolated into SQL, so it is never trusted blindly.
54
+ */
55
+ export declare function maxExecutionTimeStatement(params: {
56
+ timeout: number;
57
+ }): string;
58
+ /**
59
+ * Restores the session default after a transaction that set a timeout. Issued
60
+ * in `finally`; if it fails the adapter destroys the connection instead of
61
+ * releasing it, so a poisoned session never returns to the pool.
62
+ */
63
+ export declare const RESET_MAX_EXECUTION_TIME_SQL: string;
64
+ /** How a raw statement steers the adapter's session-affine raw transaction. */
65
+ export type RawTransactionControl = "open" | "close" | null;
66
+ /**
67
+ * Classify a raw statement as transaction control. A raw `BEGIN` … `COMMIT`
68
+ * sequence over `executeUnsafe` is sound only when every statement in between
69
+ * rides the SAME pooled connection, so the adapter pins one connection for
70
+ * the whole raw transaction ("open" checks out, "close" releases). MySQL DDL
71
+ * auto-commits, so @vibeorm/migrate never wraps here — this exists for raw
72
+ * user sequences and keeps the three pool adapters' semantics identical.
73
+ *
74
+ * Deliberately strict: only PURE single-statement control text matches.
75
+ * Compound scripts, `ROLLBACK TO SAVEPOINT` and `COMMIT AND CHAIN` (which
76
+ * keeps a transaction open) never engage the pinning.
77
+ */
78
+ export declare function classifyRawTransactionControl(params: {
79
+ text: string;
80
+ }): RawTransactionControl;
81
+ /**
82
+ * Refuse options on a NESTED `transaction()` call — the same contract on every
83
+ * adapter (see the `DatabaseAdapter.transaction` JSDoc in @vibeorm/runtime):
84
+ * a nested transaction is a SAVEPOINT, and a savepoint can neither change the
85
+ * isolation level of the transaction it joins nor enforce its own timeout.
86
+ * Silently dropping the option (the pre-#3 behaviour) hid exactly that.
87
+ *
88
+ * @throws VibeError `VIBE_VALIDATION` when `isolationLevel` or `timeout` is
89
+ * present (an `undefined` or empty options object is accepted).
90
+ */
91
+ export declare function refuseNestedTransactionOptions(params: {
92
+ options?: TransactionOptions;
93
+ provider: string;
94
+ }): void;
95
+ //# sourceMappingURL=transaction-sql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transaction-sql.d.ts","sourceRoot":"","sources":["../src/transaction-sql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG3E,+DAA+D;AAC/D,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,MAAM,CAAC,CAIvE,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE;IAAE,cAAc,EAAE,cAAc,CAAA;CAAE,GAAG,MAAM,CAE7F;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAU7E;AAED;;;;GAIG;AACH,eAAO,MAAM,4BAA4B,EAAE,MAAmD,CAAC;AAI/F,+EAA+E;AAC/E,MAAM,MAAM,qBAAqB,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AAK5D;;;;;;;;;;;GAWG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,qBAAqB,CAI7F;AAID;;;;;;;;;GASG;AACH,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAAI,CAkBP"}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@vibeorm/adapter-mysql",
3
+ "version": "2.0.0-alpha.1",
4
+ "description": "mysql2 adapter for VibeORM v2",
5
+ "keywords": [
6
+ "orm",
7
+ "typescript",
8
+ "sql",
9
+ "database",
10
+ "bun",
11
+ "type-safe",
12
+ "mysql",
13
+ "mysql2",
14
+ "mariadb",
15
+ "adapter",
16
+ "driver"
17
+ ],
18
+ "homepage": "https://github.com/vibeorm/vibeorm/tree/master/packages/adapter-mysql#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/vibeorm/vibeorm/issues"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/vibeorm/vibeorm.git",
25
+ "directory": "packages/adapter-mysql"
26
+ },
27
+ "license": "MIT",
28
+ "author": "VibeORM contributors",
29
+ "type": "module",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "default": "./dist/index.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md"
39
+ ],
40
+ "engines": {
41
+ "bun": ">=1.2.0"
42
+ },
43
+ "dependencies": {
44
+ "@vibeorm/runtime": "2.0.0-alpha.1",
45
+ "@vibeorm/schema": "2.0.0-alpha.1",
46
+ "mysql2": "^3.15.0"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }