@vibeorm/adapter-bun 1.3.0 → 2.0.0-alpha.10

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