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