@vibeorm/adapter-bun 1.1.7 → 1.2.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/package.json +2 -2
- package/src/index.ts +282 -75
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibeorm/adapter-bun",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Bun-native database adapter for VibeORM using bun:sql",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"bun": ">=1.1.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@vibeorm/runtime": "1.
|
|
36
|
+
"@vibeorm/runtime": "1.2.0"
|
|
37
37
|
},
|
|
38
38
|
"publishConfig": {
|
|
39
39
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -30,13 +30,21 @@ export type BunAdapterOptions = {
|
|
|
30
30
|
*/
|
|
31
31
|
statementTimeout?: number;
|
|
32
32
|
/**
|
|
33
|
-
* Maximum time in milliseconds to wait
|
|
33
|
+
* Maximum time in milliseconds to wait when establishing a new connection (default: none).
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
35
|
+
* Maps directly to bun:sql's `connectionTimeout` constructor option (which expects
|
|
36
|
+
* seconds — we convert ms → seconds for you). If a connection cannot be established
|
|
37
|
+
* within this duration, bun:sql throws `ERR_POSTGRES_CONNECTION_TIMEOUT`.
|
|
37
38
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
39
|
+
* Note: this is for the initial TCP/handshake establishment, NOT for waiting on
|
|
40
|
+
* pool checkout once the pool is saturated.
|
|
41
|
+
*
|
|
42
|
+
* Historically this was injected as a `connect_timeout=…` URL parameter, but
|
|
43
|
+
* bun:sql forwards unrecognised URL parameters to PostgreSQL as runtime
|
|
44
|
+
* configuration settings, and `connect_timeout` is a libpq *client-side* option
|
|
45
|
+
* (not a server GUC), so PostgreSQL would error with
|
|
46
|
+
* `unrecognized configuration parameter "connect_timeout"`. The constructor
|
|
47
|
+
* option is the canonical way to set it.
|
|
40
48
|
*/
|
|
41
49
|
connectionTimeout?: number;
|
|
42
50
|
/**
|
|
@@ -97,20 +105,60 @@ export type BunAdapterOptions = {
|
|
|
97
105
|
|
|
98
106
|
// ─── Internal bun:sql types ───────────────────────────────────────
|
|
99
107
|
|
|
108
|
+
/**
|
|
109
|
+
* A bun:sql "reserved" connection (single physical connection checked out
|
|
110
|
+
* from the pool). Used by the manual `BEGIN … COMMIT` transaction path.
|
|
111
|
+
*
|
|
112
|
+
* Note: a reserved connection has NO `.begin()` or `.reserve()` — any nested
|
|
113
|
+
* transaction must be implemented via SAVEPOINTs on the same connection.
|
|
114
|
+
*/
|
|
100
115
|
type SqlReserved = {
|
|
101
116
|
(strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
|
|
102
117
|
unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
|
|
103
118
|
release(): void;
|
|
104
119
|
};
|
|
105
120
|
|
|
121
|
+
/**
|
|
122
|
+
* The transactional handle passed to the `sql.begin(callback)` callback.
|
|
123
|
+
* Modern bun:sql exposes `.savepoint(fn)` which creates a savepoint-scoped
|
|
124
|
+
* sub-transaction (the correct API for nested transactions). Calling
|
|
125
|
+
* `.begin()` here throws "cannot call begin inside a transaction use
|
|
126
|
+
* savepoint() instead" — so we always prefer `savepoint` when present.
|
|
127
|
+
*
|
|
128
|
+
* Older / minimal bun:sql versions may lack `.savepoint`, in which case we
|
|
129
|
+
* fall back to explicit `SAVEPOINT <name>` SQL via `.unsafe()`.
|
|
130
|
+
*/
|
|
131
|
+
type SqlTransaction = {
|
|
132
|
+
(strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
|
|
133
|
+
unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
|
|
134
|
+
savepoint?: <T>(fn: (tx: SqlTransaction) => Promise<T>) => Promise<T>;
|
|
135
|
+
};
|
|
136
|
+
|
|
106
137
|
type SqlInstance = {
|
|
107
138
|
(strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
|
|
108
139
|
unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
|
|
109
|
-
begin<T>(fn: (tx:
|
|
140
|
+
begin<T>(fn: (tx: SqlTransaction) => Promise<T>): Promise<T>;
|
|
110
141
|
reserve(): Promise<SqlReserved>;
|
|
111
142
|
close(): Promise<void>;
|
|
112
143
|
};
|
|
113
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Anything that can execute SQL through bun:sql — pool, reserved connection,
|
|
147
|
+
* or in-transaction handle. Used by the unified adapter created at every
|
|
148
|
+
* nesting level.
|
|
149
|
+
*/
|
|
150
|
+
type SqlSource = SqlInstance | SqlReserved | SqlTransaction;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether the SQL source is the outermost pool (can issue real BEGIN/COMMIT
|
|
154
|
+
* or use `sql.begin()`) or is already inside a transaction (must use
|
|
155
|
+
* SAVEPOINTs for nesting).
|
|
156
|
+
*/
|
|
157
|
+
type ParentMode = "pool" | "tx";
|
|
158
|
+
|
|
159
|
+
/** Shared monotonic counter for savepoint naming across one top-level tx. */
|
|
160
|
+
type SavepointCounter = { n: number };
|
|
161
|
+
|
|
114
162
|
/**
|
|
115
163
|
* Create a VibeORM database adapter using Bun's built-in SQL driver.
|
|
116
164
|
*
|
|
@@ -152,16 +200,19 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
152
200
|
* Resolve the connection URL, injecting startup parameters as needed.
|
|
153
201
|
* Falls back to DATABASE_URL env var when no explicit URL is provided.
|
|
154
202
|
*
|
|
155
|
-
* Injects via the PostgreSQL `options` startup parameter
|
|
203
|
+
* Injects via the PostgreSQL `options` startup parameter (server-side runtime
|
|
204
|
+
* configuration; valid because `options` is a real Postgres startup-protocol
|
|
205
|
+
* parameter that the server parses for `-c key=value` settings):
|
|
156
206
|
* - plan_cache_mode (unless "auto")
|
|
157
207
|
* - statement_timeout (if configured)
|
|
158
208
|
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
209
|
+
* `connect_timeout` is NOT injected here — it's a libpq client-side option,
|
|
210
|
+
* not a server GUC, and bun:sql would forward it to the server as an
|
|
211
|
+
* unrecognised configuration parameter. We pass it via the bun:sql
|
|
212
|
+
* `connectionTimeout` constructor option instead (see getSql()).
|
|
161
213
|
*/
|
|
162
214
|
function resolveConnectionUrl(): string | undefined {
|
|
163
215
|
const STATEMENT_TIMEOUT = options?.statementTimeout;
|
|
164
|
-
const CONNECTION_TIMEOUT = options?.connectionTimeout;
|
|
165
216
|
|
|
166
217
|
// Build startup options string (for -c parameters)
|
|
167
218
|
const startupParts: string[] = [];
|
|
@@ -172,29 +223,16 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
172
223
|
startupParts.push(`-c statement_timeout=${Number(STATEMENT_TIMEOUT)}`);
|
|
173
224
|
}
|
|
174
225
|
|
|
175
|
-
const needsUrlMutation = startupParts.length > 0
|
|
226
|
+
const needsUrlMutation = startupParts.length > 0;
|
|
176
227
|
if (!needsUrlMutation) return options?.url;
|
|
177
228
|
|
|
178
229
|
const baseUrl = options?.url ?? process.env.DATABASE_URL;
|
|
179
230
|
if (!baseUrl) return undefined;
|
|
180
231
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
url = appendStartupOption({
|
|
186
|
-
url,
|
|
187
|
-
option: startupParts.join(" "),
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Inject connect_timeout as a URL parameter (PG uses seconds)
|
|
192
|
-
if (CONNECTION_TIMEOUT !== undefined) {
|
|
193
|
-
const separator = url.includes("?") ? "&" : "?";
|
|
194
|
-
url = `${url}${separator}connect_timeout=${Math.ceil(CONNECTION_TIMEOUT / 1000)}`;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return url;
|
|
232
|
+
return appendStartupOption({
|
|
233
|
+
url: baseUrl,
|
|
234
|
+
option: startupParts.join(" "),
|
|
235
|
+
});
|
|
198
236
|
}
|
|
199
237
|
|
|
200
238
|
function getSql(): SqlInstance {
|
|
@@ -215,6 +253,13 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
215
253
|
// See: https://github.com/oven-sh/bun/issues/20294
|
|
216
254
|
};
|
|
217
255
|
|
|
256
|
+
// Connection establishment timeout — bun:sql expects seconds.
|
|
257
|
+
// We accept milliseconds in our public API to stay consistent with pgAdapter
|
|
258
|
+
// and Node convention, then convert here.
|
|
259
|
+
if (options?.connectionTimeout !== undefined) {
|
|
260
|
+
sqlOptions.connectionTimeout = Math.max(1, Math.ceil(options.connectionTimeout / 1000));
|
|
261
|
+
}
|
|
262
|
+
|
|
218
263
|
const connectionUrl = resolveConnectionUrl();
|
|
219
264
|
if (connectionUrl) {
|
|
220
265
|
sqlInstance = new SQL(connectionUrl, sqlOptions) as unknown as SqlInstance;
|
|
@@ -260,21 +305,86 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
260
305
|
* Convert a JS array to a PostgreSQL array literal string `{val1,val2,...}`.
|
|
261
306
|
* bun:sql's extended query protocol sends values as strings, so PostgreSQL
|
|
262
307
|
* needs array parameters in its native array literal format.
|
|
308
|
+
*
|
|
309
|
+
* Element handling:
|
|
310
|
+
* - `null` / `undefined` → `NULL`
|
|
311
|
+
* - `number` / `bigint` / `boolean` → unquoted primitive
|
|
312
|
+
* - `Date` → ISO-8601 string (quoted+escaped) so PG can parse it as
|
|
313
|
+
* `timestamp[]` / `timestamptz[]`. Using the default `String(d)` would
|
|
314
|
+
* yield a non-ISO format like `Mon May 24 2026 …` that PG cannot parse.
|
|
315
|
+
* - `Buffer` / `Uint8Array` → not supported inside array literals; throw
|
|
316
|
+
* a clear error instead of silently producing `[object Object]`.
|
|
317
|
+
* - `string` → quoted with `"`/`\` escaping
|
|
318
|
+
* - other `object` → `JSON.stringify` then quoted+escaped (covers users
|
|
319
|
+
* putting plain objects into a `Json[]` / `Jsonb[]` scalar list).
|
|
263
320
|
*/
|
|
264
321
|
function toPgArrayLiteral(arr: unknown[]): string {
|
|
322
|
+
const escape = (str: string): string =>
|
|
323
|
+
`"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
324
|
+
|
|
265
325
|
const escaped = arr.map((v) => {
|
|
266
326
|
if (v === null || v === undefined) return "NULL";
|
|
267
|
-
if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean")
|
|
268
|
-
|
|
269
|
-
|
|
327
|
+
if (typeof v === "number" || typeof v === "bigint" || typeof v === "boolean") {
|
|
328
|
+
return String(v);
|
|
329
|
+
}
|
|
330
|
+
if (v instanceof Date) {
|
|
331
|
+
return escape(v.toISOString());
|
|
332
|
+
}
|
|
333
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
"toPgArrayLiteral: Buffer/Uint8Array array elements are not supported"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (typeof v === "string") {
|
|
339
|
+
return escape(v);
|
|
340
|
+
}
|
|
341
|
+
if (typeof v === "object") {
|
|
342
|
+
return escape(JSON.stringify(v));
|
|
343
|
+
}
|
|
344
|
+
return escape(String(v));
|
|
270
345
|
});
|
|
271
346
|
return `{${escaped.join(",")}}`;
|
|
272
347
|
}
|
|
273
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Normalise raw-path parameter values for bun:sql.
|
|
351
|
+
*
|
|
352
|
+
* bun:sql's extended query protocol sends params as strings, so a plain JS
|
|
353
|
+
* array binding cannot be auto-converted to a PG array literal by the driver.
|
|
354
|
+
* The ORM hot path wraps array params in `PgArray` and converts them via
|
|
355
|
+
* `formatArrayParam` before they reach the adapter — but raw queries
|
|
356
|
+
* (`$queryRawUnsafe`, `$queryRaw` template-literal, `$executeRaw*`) hand the
|
|
357
|
+
* user's array straight through.
|
|
358
|
+
*
|
|
359
|
+
* To make `db.$queryRawUnsafe('… WHERE "id" = ANY($1)', [1,2,3])` actually
|
|
360
|
+
* match rows, we convert any plain JS array element in the param list to a
|
|
361
|
+
* PG array literal here. Non-array values pass through unchanged so that
|
|
362
|
+
* bun:sql's native handling (numbers, strings, Dates, Buffers, plain objects
|
|
363
|
+
* for json columns when bun supports it) remains in effect.
|
|
364
|
+
*/
|
|
365
|
+
function normalizeRawValues(values: unknown[] | undefined): unknown[] | undefined {
|
|
366
|
+
if (!values || values.length === 0) return values;
|
|
367
|
+
let mutated: unknown[] | null = null;
|
|
368
|
+
for (let i = 0; i < values.length; i++) {
|
|
369
|
+
const v = values[i];
|
|
370
|
+
if (Array.isArray(v)) {
|
|
371
|
+
if (!mutated) mutated = values.slice();
|
|
372
|
+
mutated[i] = toPgArrayLiteral(v);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return mutated ?? values;
|
|
376
|
+
}
|
|
377
|
+
|
|
274
378
|
/**
|
|
275
379
|
* Execute a query using the configured strategy.
|
|
276
380
|
* - preparedStatements=true: uses synthetic tagged templates (named prepared stmts)
|
|
277
381
|
* - preparedStatements=false: uses sql.unsafe() (replanned each time)
|
|
382
|
+
*
|
|
383
|
+
* Note: this is the ORM hot path. Scalar-list / `= ANY()` array values are
|
|
384
|
+
* already wrapped in `PgArray` by the query builder and converted to a PG
|
|
385
|
+
* array literal string via `formatArrayParam` before they reach here, so we
|
|
386
|
+
* intentionally do NOT touch raw JS arrays here (that would corrupt JSON
|
|
387
|
+
* column writes that genuinely pass a JS array as a JSON value).
|
|
278
388
|
*/
|
|
279
389
|
async function executeQuery(sql: SqlInstance, params: { text: string; values: unknown[] }): Promise<Record<string, unknown>[]> {
|
|
280
390
|
if (USE_PREPARED) {
|
|
@@ -294,14 +404,118 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
294
404
|
}
|
|
295
405
|
}
|
|
296
406
|
|
|
297
|
-
|
|
407
|
+
/**
|
|
408
|
+
* Build the typed adapter facade over any bun:sql source — the pool, a
|
|
409
|
+
* reserved connection, or an in-transaction handle. The `parentMode`
|
|
410
|
+
* discriminator tells `.transaction()` whether a nested call should issue
|
|
411
|
+
* a real BEGIN/COMMIT (or `sql.begin()`) at the pool level, or open a
|
|
412
|
+
* SAVEPOINT on the current connection.
|
|
413
|
+
*
|
|
414
|
+
* `savepointCounter` is shared by reference across every adapter created
|
|
415
|
+
* for one top-level transaction, so sibling and deeply-nested transactions
|
|
416
|
+
* always get distinct savepoint names like `vibeorm_sp_0`, `vibeorm_sp_1`,
|
|
417
|
+
* `vibeorm_sp_2`, …
|
|
418
|
+
*/
|
|
419
|
+
function createAdapter(params: {
|
|
420
|
+
sql: SqlSource;
|
|
421
|
+
parentMode: ParentMode;
|
|
422
|
+
savepointCounter: SavepointCounter;
|
|
423
|
+
}): DatabaseAdapter {
|
|
424
|
+
const { sql, parentMode, savepointCounter } = params;
|
|
425
|
+
|
|
426
|
+
async function txViaSavepoint<T>(innerFn: (txAdapter: DatabaseAdapter) => Promise<T>): Promise<T> {
|
|
427
|
+
// We're inside a transaction already — open a SAVEPOINT on this
|
|
428
|
+
// connection. Prefer the driver's native `tx.savepoint()` if exposed
|
|
429
|
+
// (modern bun:sql); else fall back to explicit `SAVEPOINT` SQL on
|
|
430
|
+
// either the in-tx handle (which lacks `.savepoint`) or a reserved
|
|
431
|
+
// connection holding a manual `BEGIN`.
|
|
432
|
+
const txSql = sql as SqlTransaction;
|
|
433
|
+
if (typeof txSql.savepoint === "function") {
|
|
434
|
+
return txSql.savepoint(async (sp) => {
|
|
435
|
+
const nestedAdapter = createAdapter({
|
|
436
|
+
sql: sp,
|
|
437
|
+
parentMode: "tx",
|
|
438
|
+
savepointCounter,
|
|
439
|
+
});
|
|
440
|
+
return innerFn(nestedAdapter);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Fallback: explicit SAVEPOINT via unsafe(). The counter is shared by
|
|
445
|
+
// reference, so sibling/nested SAVEPOINTs never collide.
|
|
446
|
+
const spName = `vibeorm_sp_${savepointCounter.n++}`;
|
|
447
|
+
await txSql.unsafe(`SAVEPOINT ${spName}`);
|
|
448
|
+
try {
|
|
449
|
+
const nestedAdapter = createAdapter({
|
|
450
|
+
sql: txSql,
|
|
451
|
+
parentMode: "tx",
|
|
452
|
+
savepointCounter,
|
|
453
|
+
});
|
|
454
|
+
const result = await innerFn(nestedAdapter);
|
|
455
|
+
await txSql.unsafe(`RELEASE SAVEPOINT ${spName}`);
|
|
456
|
+
return result;
|
|
457
|
+
} catch (err) {
|
|
458
|
+
// Best-effort rollback — mirror pg adapter behaviour so we never
|
|
459
|
+
// mask the original error if the ROLLBACK itself fails.
|
|
460
|
+
try { await txSql.unsafe(`ROLLBACK TO SAVEPOINT ${spName}`); } catch { /* rollback best-effort */ }
|
|
461
|
+
throw err;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function txViaPool<T>(
|
|
466
|
+
innerFn: (txAdapter: DatabaseAdapter) => Promise<T>,
|
|
467
|
+
options?: TransactionOptions
|
|
468
|
+
): Promise<T> {
|
|
469
|
+
const pool = sql as SqlInstance;
|
|
470
|
+
|
|
471
|
+
// Fast path: no custom options — use native sql.begin() for best perf.
|
|
472
|
+
if (!options?.isolationLevel && !options?.timeout) {
|
|
473
|
+
return pool.begin(async (txSql: SqlTransaction) => {
|
|
474
|
+
const txAdapter = createAdapter({
|
|
475
|
+
sql: txSql,
|
|
476
|
+
parentMode: "tx",
|
|
477
|
+
savepointCounter,
|
|
478
|
+
});
|
|
479
|
+
return innerFn(txAdapter);
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Manual path: reserve a single connection for custom BEGIN options.
|
|
484
|
+
const reserved = await pool.reserve();
|
|
485
|
+
try {
|
|
486
|
+
const isolation = options.isolationLevel
|
|
487
|
+
? ` ISOLATION LEVEL ${isolationLevelToSql({ level: options.isolationLevel })}`
|
|
488
|
+
: "";
|
|
489
|
+
await reserved.unsafe(`BEGIN${isolation}`);
|
|
490
|
+
|
|
491
|
+
if (options.timeout) {
|
|
492
|
+
await reserved.unsafe(`SET LOCAL statement_timeout = ${Number(options.timeout)}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const txAdapter = createAdapter({
|
|
496
|
+
sql: reserved,
|
|
497
|
+
parentMode: "tx",
|
|
498
|
+
savepointCounter,
|
|
499
|
+
});
|
|
500
|
+
const result = await innerFn(txAdapter);
|
|
501
|
+
await reserved.unsafe("COMMIT");
|
|
502
|
+
return result;
|
|
503
|
+
} catch (err) {
|
|
504
|
+
try { await reserved.unsafe("ROLLBACK"); } catch { /* rollback best-effort */ }
|
|
505
|
+
throw err;
|
|
506
|
+
} finally {
|
|
507
|
+
reserved.release();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
298
511
|
return {
|
|
299
|
-
async execute(
|
|
300
|
-
return executeQuery(sql,
|
|
512
|
+
async execute(execParams) {
|
|
513
|
+
return executeQuery(sql as SqlInstance, execParams);
|
|
301
514
|
},
|
|
302
515
|
|
|
303
|
-
async executeUnsafe(
|
|
304
|
-
const
|
|
516
|
+
async executeUnsafe(execParams) {
|
|
517
|
+
const values = normalizeRawValues(execParams.values);
|
|
518
|
+
const result = await sql.unsafe(execParams.text, values);
|
|
305
519
|
const resultAny = result as unknown as Record<string, unknown>;
|
|
306
520
|
let affectedRows: number;
|
|
307
521
|
if (typeof resultAny.count === "number") {
|
|
@@ -318,46 +532,27 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
318
532
|
},
|
|
319
533
|
|
|
320
534
|
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const txAdapter = createAdapter(txSql);
|
|
325
|
-
return fn(txAdapter);
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
// Manual path: reserve a connection for custom BEGIN options
|
|
330
|
-
const reserved = await sql.reserve();
|
|
331
|
-
try {
|
|
332
|
-
const isolation = options?.isolationLevel
|
|
333
|
-
? ` ISOLATION LEVEL ${isolationLevelToSql({ level: options.isolationLevel })}`
|
|
334
|
-
: "";
|
|
335
|
-
await reserved.unsafe(`BEGIN${isolation}`);
|
|
336
|
-
|
|
337
|
-
if (options?.timeout) {
|
|
338
|
-
await reserved.unsafe(`SET LOCAL statement_timeout = ${Number(options.timeout)}`);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const txAdapter = createAdapter(reserved as unknown as SqlInstance);
|
|
342
|
-
const result = await fn(txAdapter);
|
|
343
|
-
await reserved.unsafe("COMMIT");
|
|
344
|
-
return result;
|
|
345
|
-
} catch (err) {
|
|
346
|
-
try { await reserved.unsafe("ROLLBACK"); } catch { /* rollback best-effort */ }
|
|
347
|
-
throw err;
|
|
348
|
-
} finally {
|
|
349
|
-
reserved.release();
|
|
350
|
-
}
|
|
535
|
+
return parentMode === "pool"
|
|
536
|
+
? txViaPool(fn, options)
|
|
537
|
+
: txViaSavepoint(fn);
|
|
351
538
|
},
|
|
352
539
|
|
|
353
540
|
async connect() {
|
|
354
|
-
|
|
355
|
-
reserved.
|
|
541
|
+
// Only the pool-level adapter can reserve a fresh connection.
|
|
542
|
+
// Adapters scoped to a reserved/tx connection are already connected.
|
|
543
|
+
if (parentMode === "pool") {
|
|
544
|
+
const reserved = await (sql as SqlInstance).reserve();
|
|
545
|
+
reserved.release();
|
|
546
|
+
}
|
|
356
547
|
},
|
|
357
548
|
|
|
358
549
|
async disconnect() {
|
|
359
|
-
|
|
360
|
-
|
|
550
|
+
if (parentMode === "pool") {
|
|
551
|
+
await (sql as SqlInstance).close();
|
|
552
|
+
sqlInstance = null;
|
|
553
|
+
}
|
|
554
|
+
// Inside a transaction `.disconnect()` is a no-op — the pool owns
|
|
555
|
+
// the connection lifecycle.
|
|
361
556
|
},
|
|
362
557
|
|
|
363
558
|
formatArrayParam(values: unknown[]): unknown {
|
|
@@ -366,22 +561,34 @@ export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
|
|
|
366
561
|
};
|
|
367
562
|
}
|
|
368
563
|
|
|
369
|
-
// Create a lazy adapter that initializes the SQL connection on first use
|
|
564
|
+
// Create a lazy adapter that initializes the SQL connection on first use.
|
|
565
|
+
// Each top-level call lazily resolves the underlying `SqlInstance`. The
|
|
566
|
+
// `savepointCounter` for every top-level transaction is freshly created
|
|
567
|
+
// inside that transaction's own `txViaPool` / `txViaSavepoint` — at the
|
|
568
|
+
// pool level there's no shared counter to manage.
|
|
569
|
+
function poolAdapter(): DatabaseAdapter {
|
|
570
|
+
return createAdapter({
|
|
571
|
+
sql: getSql(),
|
|
572
|
+
parentMode: "pool",
|
|
573
|
+
savepointCounter: { n: 0 },
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
|
|
370
577
|
const adapter: DatabaseAdapter = {
|
|
371
578
|
async execute(params) {
|
|
372
|
-
return
|
|
579
|
+
return poolAdapter().execute(params);
|
|
373
580
|
},
|
|
374
581
|
|
|
375
582
|
async executeUnsafe(params) {
|
|
376
|
-
return
|
|
583
|
+
return poolAdapter().executeUnsafe(params);
|
|
377
584
|
},
|
|
378
585
|
|
|
379
586
|
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
|
|
380
|
-
return
|
|
587
|
+
return poolAdapter().transaction(fn, options);
|
|
381
588
|
},
|
|
382
589
|
|
|
383
590
|
async connect() {
|
|
384
|
-
return
|
|
591
|
+
return poolAdapter().connect();
|
|
385
592
|
},
|
|
386
593
|
|
|
387
594
|
async disconnect() {
|