@vibeorm/adapter-pg 1.3.0 → 2.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -54
- package/dist/errors.d.ts +36 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +366 -0
- package/dist/index.js.map +15 -0
- package/dist/params.d.ts +40 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/savepoints.d.ts +22 -0
- package/dist/savepoints.d.ts.map +1 -0
- package/dist/statement-cache.d.ts +24 -0
- package/dist/statement-cache.d.ts.map +1 -0
- package/dist/transaction-sql.d.ts +62 -0
- package/dist/transaction-sql.d.ts.map +1 -0
- package/package.json +35 -19
- package/src/index.ts +0 -469
package/src/index.ts
DELETED
|
@@ -1,469 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vibeorm/adapter-pg — node-postgres (pg) adapter for VibeORM.
|
|
3
|
-
*
|
|
4
|
-
* Uses the `pg` package's Pool for connection pooling with optional
|
|
5
|
-
* named prepared statement caching for PostgreSQL plan reuse.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { DatabaseAdapter, QueryResult, TransactionOptions } from "@vibeorm/runtime";
|
|
9
|
-
import { PgArray } from "@vibeorm/runtime";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Options for creating a pg adapter.
|
|
13
|
-
* Either provide connection parameters or an existing Pool instance.
|
|
14
|
-
*/
|
|
15
|
-
export type PgAdapterOptions = {
|
|
16
|
-
/** PostgreSQL connection URL. Falls back to DATABASE_URL env var if not provided. */
|
|
17
|
-
connectionString?: string;
|
|
18
|
-
/** Maximum number of connections in the pool (default: 10). */
|
|
19
|
-
max?: number;
|
|
20
|
-
/**
|
|
21
|
-
* Default statement timeout in milliseconds for all queries (default: none).
|
|
22
|
-
*
|
|
23
|
-
* Sets PostgreSQL's `statement_timeout` at the pool level via the pg driver's
|
|
24
|
-
* `statement_timeout` option, so every query on every connection inherits it
|
|
25
|
-
* automatically. Queries exceeding this duration are cancelled by PostgreSQL
|
|
26
|
-
* with SQLSTATE 57014, which VibeORM surfaces as a `VibeTransientError` with
|
|
27
|
-
* code `STATEMENT_TIMEOUT`.
|
|
28
|
-
*
|
|
29
|
-
* Transaction-level `timeout` (via `$transaction` options) overrides this
|
|
30
|
-
* default for the duration of that transaction using `SET LOCAL statement_timeout`.
|
|
31
|
-
*
|
|
32
|
-
* Recommended: Set to a value appropriate for your workload (e.g. 30000 for
|
|
33
|
-
* general use, 250–1000 for latency-sensitive OLTP services).
|
|
34
|
-
*/
|
|
35
|
-
statementTimeout?: number;
|
|
36
|
-
/**
|
|
37
|
-
* Maximum time in milliseconds to wait for a connection from the pool (default: none).
|
|
38
|
-
*
|
|
39
|
-
* When set, pool checkout that exceeds this duration throws an error.
|
|
40
|
-
* Prevents indefinite blocking when the pool is exhausted under load.
|
|
41
|
-
*
|
|
42
|
-
* Maps to pg Pool's `connectionTimeoutMillis` option.
|
|
43
|
-
*/
|
|
44
|
-
connectionTimeout?: number;
|
|
45
|
-
/**
|
|
46
|
-
* Whether to use named prepared statements for read queries (default: false).
|
|
47
|
-
*
|
|
48
|
-
* When `true`, the adapter assigns a deterministic name to each unique SQL text,
|
|
49
|
-
* creating named prepared statements that PostgreSQL caches execution plans for.
|
|
50
|
-
* This saves ~0.1ms of planning time per query but can cause PostgreSQL to
|
|
51
|
-
* switch to a "generic plan" after 5 executions, which may choose a worse
|
|
52
|
-
* index for queries with range filters or skewed data distributions.
|
|
53
|
-
*
|
|
54
|
-
* When `false` (default), the adapter sends queries without a statement name.
|
|
55
|
-
* PostgreSQL replans each execution using the actual parameter values, always
|
|
56
|
-
* choosing the optimal index.
|
|
57
|
-
*
|
|
58
|
-
* Recommendation: Leave as `false` unless profiling shows planning overhead
|
|
59
|
-
* is a bottleneck (rare — typically only for sub-millisecond queries at
|
|
60
|
-
* very high throughput).
|
|
61
|
-
*/
|
|
62
|
-
preparedStatements?: boolean;
|
|
63
|
-
/**
|
|
64
|
-
* Maximum number of entries in the prepared statement name cache (default: 1000).
|
|
65
|
-
* Only used when `preparedStatements: true`.
|
|
66
|
-
* Each unique SQL text gets a deterministic name for PostgreSQL plan caching.
|
|
67
|
-
* LRU eviction when full.
|
|
68
|
-
*/
|
|
69
|
-
stmtCacheMax?: number;
|
|
70
|
-
} | {
|
|
71
|
-
/** An existing pg Pool instance to use. */
|
|
72
|
-
pool: PgPool;
|
|
73
|
-
/**
|
|
74
|
-
* Whether to use named prepared statements for read queries (default: false).
|
|
75
|
-
* See the other overload for full documentation.
|
|
76
|
-
*/
|
|
77
|
-
preparedStatements?: boolean;
|
|
78
|
-
/**
|
|
79
|
-
* Maximum number of entries in the prepared statement name cache (default: 1000).
|
|
80
|
-
* Only used when `preparedStatements: true`.
|
|
81
|
-
*/
|
|
82
|
-
stmtCacheMax?: number;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
// ─── Minimal pg types (avoids hard dependency on @types/pg) ───────
|
|
86
|
-
|
|
87
|
-
type PgPoolClient = {
|
|
88
|
-
query(config: { text: string; values?: unknown[]; name?: string }): Promise<PgQueryResult>;
|
|
89
|
-
query(text: string, values?: unknown[]): Promise<PgQueryResult>;
|
|
90
|
-
release(): void;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
type PgQueryResult = {
|
|
94
|
-
rows: Record<string, unknown>[];
|
|
95
|
-
rowCount: number | null;
|
|
96
|
-
command: string;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
type PgPool = {
|
|
100
|
-
connect(): Promise<PgPoolClient>;
|
|
101
|
-
query(config: { text: string; values?: unknown[]; name?: string }): Promise<PgQueryResult>;
|
|
102
|
-
query(text: string, values?: unknown[]): Promise<PgQueryResult>;
|
|
103
|
-
end(): Promise<void>;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
// ─── Internal marker for array params ─────────────────────────────
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Internal wrapper to tag a value as a PostgreSQL array parameter (for = ANY()).
|
|
110
|
-
* This distinguishes array params from JSON array values so the adapter
|
|
111
|
-
* can JSON.stringify JSON values without affecting array params.
|
|
112
|
-
*/
|
|
113
|
-
class PgArrayParam {
|
|
114
|
-
constructor(public readonly values: unknown[]) {}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// ─── Value Serialization ──────────────────────────────────────────
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Pre-process query parameter values for the pg driver — ORM hot path.
|
|
121
|
-
*
|
|
122
|
-
* The pg driver auto-converts JS arrays to PostgreSQL array literals (`{1,2,3}`)
|
|
123
|
-
* and JS objects to `[object Object]`. The ORM doesn't know which column a
|
|
124
|
-
* value lands in (it doesn't carry typed-parameter hints), and for `json` /
|
|
125
|
-
* `jsonb` columns it wants any JS object OR JS array passed through as JSON.
|
|
126
|
-
* Therefore on the ORM path we JSON.stringify both objects and arrays —
|
|
127
|
-
* scalar-list array values (`= ANY()` / `String[]` / `Int[]`) are already
|
|
128
|
-
* tagged with `PgArrayParam` by `formatArrayParam` and unwrapped here without
|
|
129
|
-
* stringification.
|
|
130
|
-
*
|
|
131
|
-
* Raw-query paths use `serializeParamsRaw` instead (see below) which does NOT
|
|
132
|
-
* stringify plain JS arrays so that `db.$queryRawUnsafe('… WHERE "id" = ANY($1)',
|
|
133
|
-
* [1, 2, 3])` actually matches rows.
|
|
134
|
-
*
|
|
135
|
-
* - PgArrayParam wrappers are unwrapped to raw arrays (for = ANY() queries
|
|
136
|
-
* and for scalar-list array operations the ORM emits).
|
|
137
|
-
* - Plain objects and arrays are JSON.stringify'd (for json/jsonb columns).
|
|
138
|
-
* - Dates, Buffers, null, and primitives pass through unchanged.
|
|
139
|
-
*
|
|
140
|
-
* TODO: PgArrayParam nesting is not currently reachable through the public API,
|
|
141
|
-
* so the unwrap is intentionally non-recursive. If PgArrayParam ever becomes
|
|
142
|
-
* user-constructible and nestable inside other arrays/objects, make recursive.
|
|
143
|
-
*/
|
|
144
|
-
function serializeParams(params: { values: unknown[] }): unknown[] {
|
|
145
|
-
return params.values.map((v) => {
|
|
146
|
-
// Unwrap any array-param wrapper — covers BOTH the adapter's internal
|
|
147
|
-
// PgArrayParam (set by this adapter's formatArrayParam) AND the runtime's
|
|
148
|
-
// PgArray marker. The runtime's `client.ts` normally unwraps PgArray into
|
|
149
|
-
// PgArrayParam via `adapter.formatArrayParam(...)`, but recognising both
|
|
150
|
-
// here is defence-in-depth — any future executor path that bypasses that
|
|
151
|
-
// unwrap step would otherwise silently double-serialise the array as JSON.
|
|
152
|
-
// Bug 3 — see .ai/bug-report-2026-05-24.md.
|
|
153
|
-
if (v instanceof PgArrayParam || v instanceof PgArray) {
|
|
154
|
-
return v.values;
|
|
155
|
-
}
|
|
156
|
-
// JSON.stringify plain objects and arrays for json/jsonb columns
|
|
157
|
-
if (
|
|
158
|
-
v !== null &&
|
|
159
|
-
v !== undefined &&
|
|
160
|
-
typeof v === "object" &&
|
|
161
|
-
!(v instanceof Date) &&
|
|
162
|
-
!Buffer.isBuffer(v) &&
|
|
163
|
-
!(v instanceof Uint8Array)
|
|
164
|
-
) {
|
|
165
|
-
return JSON.stringify(v);
|
|
166
|
-
}
|
|
167
|
-
return v;
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* Pre-process query parameter values for the pg driver — RAW QUERY path.
|
|
173
|
-
*
|
|
174
|
-
* Identical to `serializeParams` EXCEPT that plain JS arrays are passed through
|
|
175
|
-
* to `pg` instead of being JSON.stringify'd. node-postgres natively converts a
|
|
176
|
-
* JS array to a PostgreSQL array literal, which is what users expect when they
|
|
177
|
-
* write raw queries like:
|
|
178
|
-
*
|
|
179
|
-
* db.$queryRawUnsafe('SELECT * FROM "User" WHERE "id" = ANY($1)', [1, 2, 3])
|
|
180
|
-
* db.$queryRaw`SELECT * FROM "User" WHERE "id" = ANY(${[1, 2, 3]})`
|
|
181
|
-
*
|
|
182
|
-
* (Previously the adapter JSON.stringify'd the array, producing `"[1,2,3]"` —
|
|
183
|
-
* a JSON string that PG could not match against any row.)
|
|
184
|
-
*
|
|
185
|
-
* Tradeoff: a user who wants to bind a JS array INTO a `json` / `jsonb` column
|
|
186
|
-
* from a raw query must `JSON.stringify(...)` it themselves. Plain objects are
|
|
187
|
-
* still auto-stringified here, so the common `{ foo: "bar" }` case continues
|
|
188
|
-
* to work.
|
|
189
|
-
*/
|
|
190
|
-
function serializeParamsRaw(params: { values: unknown[] }): unknown[] {
|
|
191
|
-
return params.values.map((v) => {
|
|
192
|
-
// Recognise both wrappers — see serializeParams for rationale (Bug 3).
|
|
193
|
-
if (v instanceof PgArrayParam || v instanceof PgArray) {
|
|
194
|
-
return v.values;
|
|
195
|
-
}
|
|
196
|
-
// Plain JS arrays pass through — pg converts to PG array literal natively.
|
|
197
|
-
if (Array.isArray(v)) {
|
|
198
|
-
return v;
|
|
199
|
-
}
|
|
200
|
-
// JSON.stringify plain non-array objects for json/jsonb columns.
|
|
201
|
-
if (
|
|
202
|
-
v !== null &&
|
|
203
|
-
v !== undefined &&
|
|
204
|
-
typeof v === "object" &&
|
|
205
|
-
!(v instanceof Date) &&
|
|
206
|
-
!Buffer.isBuffer(v) &&
|
|
207
|
-
!(v instanceof Uint8Array)
|
|
208
|
-
) {
|
|
209
|
-
return JSON.stringify(v);
|
|
210
|
-
}
|
|
211
|
-
return v;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Create a VibeORM database adapter using node-postgres (pg).
|
|
217
|
-
*
|
|
218
|
-
* @example
|
|
219
|
-
* ```ts
|
|
220
|
-
* import { pgAdapter } from "@vibeorm/adapter-pg";
|
|
221
|
-
* import { VibeClient } from "./generated/vibeorm";
|
|
222
|
-
*
|
|
223
|
-
* // With connection string
|
|
224
|
-
* const db = VibeClient({
|
|
225
|
-
* adapter: pgAdapter({ connectionString: "postgres://...", max: 20 }),
|
|
226
|
-
* });
|
|
227
|
-
*
|
|
228
|
-
* // With existing pg Pool
|
|
229
|
-
* import { Pool } from "pg";
|
|
230
|
-
* const pool = new Pool({ connectionString: "postgres://..." });
|
|
231
|
-
* const db = VibeClient({
|
|
232
|
-
* adapter: pgAdapter({ pool }),
|
|
233
|
-
* });
|
|
234
|
-
*
|
|
235
|
-
* // With prepared statements enabled (for high-throughput local scenarios)
|
|
236
|
-
* const db2 = VibeClient({
|
|
237
|
-
* adapter: pgAdapter({ connectionString: "postgres://...", preparedStatements: true }),
|
|
238
|
-
* });
|
|
239
|
-
* ```
|
|
240
|
-
*/
|
|
241
|
-
export function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter {
|
|
242
|
-
const USE_PREPARED = (options && "preparedStatements" in options ? options.preparedStatements : undefined) ?? false;
|
|
243
|
-
const stmtCacheMax = (options && "stmtCacheMax" in options ? options.stmtCacheMax : undefined) ?? 1000;
|
|
244
|
-
|
|
245
|
-
// LRU cache: SQL text -> deterministic prepared statement name
|
|
246
|
-
const stmtNameCache = new Map<string, string>();
|
|
247
|
-
let stmtCounter = 0;
|
|
248
|
-
|
|
249
|
-
function getOrCreateStmtName(params: { text: string }): string {
|
|
250
|
-
const { text } = params;
|
|
251
|
-
let name = stmtNameCache.get(text);
|
|
252
|
-
if (name) {
|
|
253
|
-
// Move to end (most recently used)
|
|
254
|
-
stmtNameCache.delete(text);
|
|
255
|
-
stmtNameCache.set(text, name);
|
|
256
|
-
return name;
|
|
257
|
-
}
|
|
258
|
-
name = `vibeorm_${stmtCounter++}`;
|
|
259
|
-
if (stmtNameCache.size >= stmtCacheMax) {
|
|
260
|
-
const oldestKey = stmtNameCache.keys().next().value;
|
|
261
|
-
if (oldestKey !== undefined) stmtNameCache.delete(oldestKey);
|
|
262
|
-
}
|
|
263
|
-
stmtNameCache.set(text, name);
|
|
264
|
-
return name;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
let pool: PgPool | null = null;
|
|
268
|
-
let ownsPool = false;
|
|
269
|
-
|
|
270
|
-
function getPool(): PgPool {
|
|
271
|
-
if (pool) return pool;
|
|
272
|
-
|
|
273
|
-
// Dynamic import of pg to avoid bundling issues
|
|
274
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
275
|
-
const pg = require("pg");
|
|
276
|
-
const Pool = pg.Pool ?? pg.default?.Pool;
|
|
277
|
-
|
|
278
|
-
if (options && "pool" in options && options.pool) {
|
|
279
|
-
pool = options.pool;
|
|
280
|
-
ownsPool = false;
|
|
281
|
-
} else {
|
|
282
|
-
const connectionString =
|
|
283
|
-
(options && "connectionString" in options ? options.connectionString : undefined) ??
|
|
284
|
-
process.env.DATABASE_URL;
|
|
285
|
-
const max = (options && "max" in options ? options.max : undefined) ?? 10;
|
|
286
|
-
const statementTimeout =
|
|
287
|
-
options && "statementTimeout" in options ? options.statementTimeout : undefined;
|
|
288
|
-
const connectionTimeout =
|
|
289
|
-
options && "connectionTimeout" in options ? options.connectionTimeout : undefined;
|
|
290
|
-
|
|
291
|
-
const poolConfig: Record<string, unknown> = {
|
|
292
|
-
connectionString,
|
|
293
|
-
max,
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
// Set statement_timeout at the pool level — every connection inherits it
|
|
297
|
-
if (statementTimeout !== undefined) {
|
|
298
|
-
poolConfig.statement_timeout = Number(statementTimeout);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Set connectionTimeoutMillis — pool.connect() throws if exceeded
|
|
302
|
-
if (connectionTimeout !== undefined) {
|
|
303
|
-
poolConfig.connectionTimeoutMillis = Number(connectionTimeout);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
pool = new Pool(poolConfig) as PgPool;
|
|
307
|
-
ownsPool = true;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return pool;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* Execute a parameterized query through a pg client or pool.
|
|
315
|
-
* - preparedStatements=true: uses named statements for plan caching
|
|
316
|
-
* - preparedStatements=false: uses unnamed statements (replanned each time)
|
|
317
|
-
*/
|
|
318
|
-
function buildQueryConfig(params: { text: string; values: unknown[] }): { text: string; values: unknown[]; name?: string } {
|
|
319
|
-
const serialized = serializeParams({ values: params.values });
|
|
320
|
-
if (USE_PREPARED) {
|
|
321
|
-
return {
|
|
322
|
-
name: getOrCreateStmtName({ text: params.text }),
|
|
323
|
-
text: params.text,
|
|
324
|
-
values: serialized,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
text: params.text,
|
|
329
|
-
values: serialized,
|
|
330
|
-
};
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Build a client-scoped adapter (already inside a transaction). Nested
|
|
335
|
-
* `.transaction(fn)` calls open SAVEPOINTs on the same client.
|
|
336
|
-
*
|
|
337
|
-
* `savepointCounter` is shared by reference across every adapter created
|
|
338
|
-
* for one top-level transaction so sibling and deeply-nested savepoints
|
|
339
|
-
* never collide (Bug #11 — counter previously reset at each nesting level,
|
|
340
|
-
* so two sibling sub-transactions both got `vibeorm_sp_0`).
|
|
341
|
-
*/
|
|
342
|
-
function createClientAdapter(params: {
|
|
343
|
-
client: PgPoolClient;
|
|
344
|
-
savepointCounter: { n: number };
|
|
345
|
-
}): DatabaseAdapter {
|
|
346
|
-
const { client, savepointCounter } = params;
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
async execute(execParams) {
|
|
350
|
-
const config = buildQueryConfig(execParams);
|
|
351
|
-
const result = await client.query(config);
|
|
352
|
-
return result.rows;
|
|
353
|
-
},
|
|
354
|
-
|
|
355
|
-
async executeUnsafe(execParams) {
|
|
356
|
-
const values = execParams.values ? serializeParamsRaw({ values: execParams.values }) : undefined;
|
|
357
|
-
const result = await client.query(execParams.text, values);
|
|
358
|
-
const affectedRows = result.rowCount ?? 0;
|
|
359
|
-
return { rows: result.rows, affectedRows };
|
|
360
|
-
},
|
|
361
|
-
|
|
362
|
-
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>): Promise<T> {
|
|
363
|
-
// Nested transactions use unique SAVEPOINTs.
|
|
364
|
-
const spName = `vibeorm_sp_${savepointCounter.n++}`;
|
|
365
|
-
await client.query(`SAVEPOINT ${spName}`);
|
|
366
|
-
try {
|
|
367
|
-
const nestedAdapter = createClientAdapter({ client, savepointCounter });
|
|
368
|
-
const result = await fn(nestedAdapter);
|
|
369
|
-
await client.query(`RELEASE SAVEPOINT ${spName}`);
|
|
370
|
-
return result;
|
|
371
|
-
} catch (err) {
|
|
372
|
-
// Bug #7: best-effort rollback — never mask the original error if
|
|
373
|
-
// `ROLLBACK TO SAVEPOINT` itself fails (e.g. the client died).
|
|
374
|
-
try { await client.query(`ROLLBACK TO SAVEPOINT ${spName}`); } catch { /* rollback best-effort */ }
|
|
375
|
-
throw err;
|
|
376
|
-
}
|
|
377
|
-
},
|
|
378
|
-
|
|
379
|
-
async connect() {
|
|
380
|
-
// No-op for client-level adapter (already connected)
|
|
381
|
-
},
|
|
382
|
-
|
|
383
|
-
async disconnect() {
|
|
384
|
-
// No-op for client-level adapter (pool manages connections)
|
|
385
|
-
},
|
|
386
|
-
|
|
387
|
-
formatArrayParam(values: unknown[]): unknown {
|
|
388
|
-
// Wrap in PgArrayParam so serializeParams knows not to JSON.stringify it
|
|
389
|
-
return new PgArrayParam(values);
|
|
390
|
-
},
|
|
391
|
-
};
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function isolationLevelToSql(params: { level: NonNullable<TransactionOptions["isolationLevel"]> }): string {
|
|
395
|
-
switch (params.level) {
|
|
396
|
-
case "ReadCommitted": return "READ COMMITTED";
|
|
397
|
-
case "RepeatableRead": return "REPEATABLE READ";
|
|
398
|
-
case "Serializable": return "SERIALIZABLE";
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
const adapter: DatabaseAdapter = {
|
|
403
|
-
async execute(params) {
|
|
404
|
-
const p = getPool();
|
|
405
|
-
const config = buildQueryConfig(params);
|
|
406
|
-
const result = await p.query(config);
|
|
407
|
-
return result.rows;
|
|
408
|
-
},
|
|
409
|
-
|
|
410
|
-
async executeUnsafe(params) {
|
|
411
|
-
const p = getPool();
|
|
412
|
-
const values = params.values ? serializeParamsRaw({ values: params.values }) : undefined;
|
|
413
|
-
const result = await p.query(params.text, values);
|
|
414
|
-
const affectedRows = result.rowCount ?? 0;
|
|
415
|
-
return { rows: result.rows, affectedRows };
|
|
416
|
-
},
|
|
417
|
-
|
|
418
|
-
async transaction<T>(fn: (txAdapter: DatabaseAdapter) => Promise<T>, options?: TransactionOptions): Promise<T> {
|
|
419
|
-
const p = getPool();
|
|
420
|
-
const client = await p.connect();
|
|
421
|
-
// Per top-level transaction: one shared savepoint counter for all
|
|
422
|
-
// sibling/nested SAVEPOINTs (Bug #11).
|
|
423
|
-
const savepointCounter = { n: 0 };
|
|
424
|
-
try {
|
|
425
|
-
const isolation = options?.isolationLevel
|
|
426
|
-
? ` ISOLATION LEVEL ${isolationLevelToSql({ level: options.isolationLevel })}`
|
|
427
|
-
: "";
|
|
428
|
-
await client.query(`BEGIN${isolation}`);
|
|
429
|
-
|
|
430
|
-
if (options?.timeout) {
|
|
431
|
-
await client.query(`SET LOCAL statement_timeout = ${Number(options.timeout)}`);
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const txAdapter = createClientAdapter({ client, savepointCounter });
|
|
435
|
-
const result = await fn(txAdapter);
|
|
436
|
-
await client.query("COMMIT");
|
|
437
|
-
return result;
|
|
438
|
-
} catch (err) {
|
|
439
|
-
// Bug #7: best-effort ROLLBACK — never mask the original error if
|
|
440
|
-
// ROLLBACK itself fails (e.g. the connection was killed). pg's pool
|
|
441
|
-
// auto-discards the client on `release()` if its state is broken.
|
|
442
|
-
try { await client.query("ROLLBACK"); } catch { /* rollback best-effort */ }
|
|
443
|
-
throw err;
|
|
444
|
-
} finally {
|
|
445
|
-
client.release();
|
|
446
|
-
}
|
|
447
|
-
},
|
|
448
|
-
|
|
449
|
-
async connect() {
|
|
450
|
-
const p = getPool();
|
|
451
|
-
const client = await p.connect();
|
|
452
|
-
client.release();
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
async disconnect() {
|
|
456
|
-
if (pool && ownsPool) {
|
|
457
|
-
await pool.end();
|
|
458
|
-
pool = null;
|
|
459
|
-
}
|
|
460
|
-
},
|
|
461
|
-
|
|
462
|
-
formatArrayParam(values: unknown[]): unknown {
|
|
463
|
-
// Wrap in PgArrayParam so serializeParams knows not to JSON.stringify it
|
|
464
|
-
return new PgArrayParam(values);
|
|
465
|
-
},
|
|
466
|
-
};
|
|
467
|
-
|
|
468
|
-
return adapter;
|
|
469
|
-
}
|