@vibeorm/adapter-bun 1.2.0 → 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/src/index.ts DELETED
@@ -1,607 +0,0 @@
1
- /**
2
- * @vibeorm/adapter-bun — Bun's native SQL adapter for VibeORM.
3
- *
4
- * Uses Bun's built-in `bun:sql` driver for high-performance PostgreSQL
5
- * connections. Supports optional prepared statement caching via synthetic
6
- * tagged templates.
7
- */
8
-
9
- import type { DatabaseAdapter, QueryResult, TransactionOptions } from "@vibeorm/runtime";
10
-
11
- export type BunAdapterOptions = {
12
- /** PostgreSQL connection URL. Falls back to DATABASE_URL env var if not provided. */
13
- url?: string;
14
- /** Maximum number of connections in the pool (default: 10). */
15
- max?: number;
16
- /**
17
- * Default statement timeout in milliseconds for all queries (default: none).
18
- *
19
- * Sets PostgreSQL's `statement_timeout` as a connection-level startup parameter,
20
- * so every query on every connection inherits it automatically with zero per-query
21
- * overhead. Queries exceeding this duration are cancelled by PostgreSQL with
22
- * SQLSTATE 57014, which VibeORM surfaces as a `VibeTransientError` with code
23
- * `STATEMENT_TIMEOUT`.
24
- *
25
- * Transaction-level `timeout` (via `$transaction` options) overrides this default
26
- * for the duration of that transaction using `SET LOCAL statement_timeout`.
27
- *
28
- * Recommended: Set to a value appropriate for your workload (e.g. 30000 for
29
- * general use, 250–1000 for latency-sensitive OLTP services).
30
- */
31
- statementTimeout?: number;
32
- /**
33
- * Maximum time in milliseconds to wait when establishing a new connection (default: none).
34
- *
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`.
38
- *
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.
48
- */
49
- connectionTimeout?: number;
50
- /**
51
- * Whether to use named prepared statements for read queries (default: false).
52
- *
53
- * When `true`, the adapter caches SQL texts as synthetic tagged templates,
54
- * creating named prepared statements that PostgreSQL caches execution plans for.
55
- * This saves ~0.1ms of planning time per query but can cause PostgreSQL to
56
- * switch to a "generic plan" after 5 executions, which may choose a worse
57
- * index for queries with range filters or skewed data distributions.
58
- *
59
- * When `false` (default), the adapter uses `sql.unsafe()` which bypasses
60
- * bun:sql's tagged template / named statement path. PostgreSQL replans each
61
- * execution using the actual parameter values, always choosing the optimal index.
62
- *
63
- * Note: The bun:sql `prepare` constructor option is always left at its default
64
- * (`true`). Setting `prepare: false` on the constructor triggers a bun:sql
65
- * performance regression (~28ms per query on remote databases). Instead, we
66
- * only control the execution path: `sql.unsafe()` vs tagged templates.
67
- *
68
- * Recommendation: Leave as `false` unless profiling shows planning overhead
69
- * is a bottleneck (rare — typically only for sub-millisecond queries at
70
- * very high throughput).
71
- */
72
- preparedStatements?: boolean;
73
- /**
74
- * Maximum number of entries in the prepared statement cache (default: 1000).
75
- * Only used when `preparedStatements: true`.
76
- * Prevents unbounded memory growth in long-running processes.
77
- * Each unique SQL text consumes one slot. LRU eviction when full.
78
- */
79
- stmtCacheMax?: number;
80
- /**
81
- * Controls PostgreSQL's plan caching behavior for prepared statements
82
- * (default: `"force_custom_plan"`).
83
- *
84
- * bun:sql uses the extended query protocol, which creates implicit prepared
85
- * statements internally. After ~5 executions of the same query text,
86
- * PostgreSQL may switch to a "generic plan" that ignores actual parameter
87
- * values. This causes significant performance regressions for queries with
88
- * range filters (`>=`, `<=`, `BETWEEN`), pattern matching (`LIKE`), or
89
- * skewed data distributions — the planner picks sequential scans instead
90
- * of index scans because it can't estimate selectivity without real values.
91
- *
92
- * - `"force_custom_plan"` (default): Always generates plans using actual
93
- * parameter values. Costs ~0.1 ms extra planning per query but always
94
- * picks the optimal index. Recommended for most workloads.
95
- * - `"auto"`: PostgreSQL's default — switches to generic plans after ~5
96
- * executions if the estimated cost is similar. Can cause 2-4× regressions
97
- * on filtered COUNT / aggregate queries.
98
- * - `"force_generic_plan"`: Always uses generic plans (not recommended).
99
- *
100
- * Injected via the PostgreSQL `options` startup parameter so it applies to
101
- * every connection in the pool automatically. Requires PostgreSQL 12+.
102
- */
103
- planCacheMode?: "auto" | "force_custom_plan" | "force_generic_plan";
104
- };
105
-
106
- // ─── Internal bun:sql types ───────────────────────────────────────
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
- */
115
- type SqlReserved = {
116
- (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
117
- unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
118
- release(): void;
119
- };
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
-
137
- type SqlInstance = {
138
- (strings: TemplateStringsArray, ...values: unknown[]): Promise<unknown[]>;
139
- unsafe(query: string, values?: unknown[]): Promise<unknown[]>;
140
- begin<T>(fn: (tx: SqlTransaction) => Promise<T>): Promise<T>;
141
- reserve(): Promise<SqlReserved>;
142
- close(): Promise<void>;
143
- };
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
-
162
- /**
163
- * Create a VibeORM database adapter using Bun's built-in SQL driver.
164
- *
165
- * @example
166
- * ```ts
167
- * import { bunAdapter } from "@vibeorm/adapter-bun";
168
- * import { VibeClient } from "./generated/vibeorm";
169
- *
170
- * const db = VibeClient({
171
- * adapter: bunAdapter({ url: "postgres://...", max: 10 }),
172
- * });
173
- *
174
- * // With prepared statements enabled (for high-throughput local scenarios)
175
- * const db2 = VibeClient({
176
- * adapter: bunAdapter({ url: "postgres://...", preparedStatements: true }),
177
- * });
178
- * ```
179
- */
180
- export function bunAdapter(options?: BunAdapterOptions): DatabaseAdapter {
181
- const USE_PREPARED = options?.preparedStatements ?? false;
182
- const STMT_CACHE_MAX = options?.stmtCacheMax ?? 1000;
183
- const PLAN_CACHE_MODE = options?.planCacheMode ?? "force_custom_plan";
184
- const stmtCache = new Map<string, TemplateStringsArray>();
185
-
186
- let sqlInstance: SqlInstance | null = null;
187
-
188
- /**
189
- * Append a PostgreSQL `options` startup parameter to a connection URL.
190
- * The `options` parameter is sent during connection establishment, so it
191
- * applies to every connection created by bun:sql's internal pool.
192
- */
193
- function appendStartupOption(params: { url: string; option: string }): string {
194
- const { url, option } = params;
195
- const separator = url.includes("?") ? "&" : "?";
196
- return `${url}${separator}options=${encodeURIComponent(option)}`;
197
- }
198
-
199
- /**
200
- * Resolve the connection URL, injecting startup parameters as needed.
201
- * Falls back to DATABASE_URL env var when no explicit URL is provided.
202
- *
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):
206
- * - plan_cache_mode (unless "auto")
207
- * - statement_timeout (if configured)
208
- *
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()).
213
- */
214
- function resolveConnectionUrl(): string | undefined {
215
- const STATEMENT_TIMEOUT = options?.statementTimeout;
216
-
217
- // Build startup options string (for -c parameters)
218
- const startupParts: string[] = [];
219
- if (PLAN_CACHE_MODE !== "auto") {
220
- startupParts.push(`-c plan_cache_mode=${PLAN_CACHE_MODE}`);
221
- }
222
- if (STATEMENT_TIMEOUT !== undefined) {
223
- startupParts.push(`-c statement_timeout=${Number(STATEMENT_TIMEOUT)}`);
224
- }
225
-
226
- const needsUrlMutation = startupParts.length > 0;
227
- if (!needsUrlMutation) return options?.url;
228
-
229
- const baseUrl = options?.url ?? process.env.DATABASE_URL;
230
- if (!baseUrl) return undefined;
231
-
232
- return appendStartupOption({
233
- url: baseUrl,
234
- option: startupParts.join(" "),
235
- });
236
- }
237
-
238
- function getSql(): SqlInstance {
239
- if (sqlInstance) return sqlInstance;
240
-
241
- // Import Bun's SQL
242
- // eslint-disable-next-line @typescript-eslint/no-require-imports
243
- const { SQL } = require("bun");
244
-
245
- const sqlOptions: Record<string, unknown> = {
246
- max: options?.max ?? 10,
247
- // Note: We intentionally do NOT set `prepare: false` here.
248
- // Setting prepare=false on the bun:sql constructor triggers a performance
249
- // regression (~28ms per query on remote/SSL databases) due to a bun:sql
250
- // internal behavior change. Instead, we control prepared statement usage
251
- // at the execution level: sql.unsafe() for the default path (no named stmts)
252
- // vs tagged templates for the preparedStatements=true path.
253
- // See: https://github.com/oven-sh/bun/issues/20294
254
- };
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
-
263
- const connectionUrl = resolveConnectionUrl();
264
- if (connectionUrl) {
265
- sqlInstance = new SQL(connectionUrl, sqlOptions) as unknown as SqlInstance;
266
- } else {
267
- // No URL resolved — bun:sql will use PG* env vars.
268
- // Startup parameters (plan_cache_mode, statement_timeout) cannot be
269
- // injected via URL options in this case (would need SET on connect).
270
- sqlInstance = new SQL(sqlOptions) as unknown as SqlInstance;
271
- }
272
-
273
- return sqlInstance;
274
- }
275
-
276
- /**
277
- * LRU-bounded synthetic tagged template cache for prepared statement reuse.
278
- * bun:sql tagged templates create named prepared statements that PostgreSQL
279
- * caches execution plans for. By converting dynamic SQL into synthetic tagged
280
- * templates with stable references, we get the same caching behavior.
281
- *
282
- * Only used when `preparedStatements: true`.
283
- */
284
- function getOrCreateTemplate(params: { text: string }): TemplateStringsArray {
285
- const { text } = params;
286
- let strings = stmtCache.get(text);
287
- if (strings) {
288
- // Move to end (most recently used) by re-inserting
289
- stmtCache.delete(text);
290
- stmtCache.set(text, strings);
291
- return strings;
292
- }
293
- const parts = text.split(/\$\d+/);
294
- strings = Object.assign(parts, { raw: parts }) as unknown as TemplateStringsArray;
295
- // Evict oldest entry if at capacity
296
- if (stmtCache.size >= STMT_CACHE_MAX) {
297
- const oldestKey = stmtCache.keys().next().value;
298
- if (oldestKey !== undefined) stmtCache.delete(oldestKey);
299
- }
300
- stmtCache.set(text, strings);
301
- return strings;
302
- }
303
-
304
- /**
305
- * Convert a JS array to a PostgreSQL array literal string `{val1,val2,...}`.
306
- * bun:sql's extended query protocol sends values as strings, so PostgreSQL
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).
320
- */
321
- function toPgArrayLiteral(arr: unknown[]): string {
322
- const escape = (str: string): string =>
323
- `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
324
-
325
- const escaped = arr.map((v) => {
326
- if (v === null || v === undefined) return "NULL";
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));
345
- });
346
- return `{${escaped.join(",")}}`;
347
- }
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
-
378
- /**
379
- * Execute a query using the configured strategy.
380
- * - preparedStatements=true: uses synthetic tagged templates (named prepared stmts)
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).
388
- */
389
- async function executeQuery(sql: SqlInstance, params: { text: string; values: unknown[] }): Promise<Record<string, unknown>[]> {
390
- if (USE_PREPARED) {
391
- const strings = getOrCreateTemplate({ text: params.text });
392
- const result = await sql(strings, ...params.values);
393
- return result as Record<string, unknown>[];
394
- }
395
- const result = await sql.unsafe(params.text, params.values);
396
- return result as Record<string, unknown>[];
397
- }
398
-
399
- function isolationLevelToSql(params: { level: NonNullable<TransactionOptions["isolationLevel"]> }): string {
400
- switch (params.level) {
401
- case "ReadCommitted": return "READ COMMITTED";
402
- case "RepeatableRead": return "REPEATABLE READ";
403
- case "Serializable": return "SERIALIZABLE";
404
- }
405
- }
406
-
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
-
511
- return {
512
- async execute(execParams) {
513
- return executeQuery(sql as SqlInstance, execParams);
514
- },
515
-
516
- async executeUnsafe(execParams) {
517
- const values = normalizeRawValues(execParams.values);
518
- const result = await sql.unsafe(execParams.text, values);
519
- const resultAny = result as unknown as Record<string, unknown>;
520
- let affectedRows: number;
521
- if (typeof resultAny.count === "number") {
522
- affectedRows = resultAny.count;
523
- } else if (typeof resultAny.affectedRows === "number") {
524
- affectedRows = resultAny.affectedRows;
525
- } else {
526
- affectedRows = (result as unknown[]).length;
527
- }
528
- return {
529
- rows: result as Record<string, unknown>[],
530
- affectedRows,
531
- };
532
- },
533
-
534
- async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
535
- return parentMode === "pool"
536
- ? txViaPool(fn, options)
537
- : txViaSavepoint(fn);
538
- },
539
-
540
- async connect() {
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
- }
547
- },
548
-
549
- async disconnect() {
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.
556
- },
557
-
558
- formatArrayParam(values: unknown[]): unknown {
559
- return toPgArrayLiteral(values);
560
- },
561
- };
562
- }
563
-
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
-
577
- const adapter: DatabaseAdapter = {
578
- async execute(params) {
579
- return poolAdapter().execute(params);
580
- },
581
-
582
- async executeUnsafe(params) {
583
- return poolAdapter().executeUnsafe(params);
584
- },
585
-
586
- async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
587
- return poolAdapter().transaction(fn, options);
588
- },
589
-
590
- async connect() {
591
- return poolAdapter().connect();
592
- },
593
-
594
- async disconnect() {
595
- if (sqlInstance) {
596
- await sqlInstance.close();
597
- sqlInstance = null;
598
- }
599
- },
600
-
601
- formatArrayParam(values: unknown[]): unknown {
602
- return toPgArrayLiteral(values);
603
- },
604
- };
605
-
606
- return adapter;
607
- }