@vibeorm/adapter-pg 2.0.0 → 2.0.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 CHANGED
@@ -44,8 +44,24 @@ const db = VibeClient({ adapter: pgAdapter({ pool }) });
44
44
  | `max` | `number` | `10` | Maximum pooled connections; ignored when `pool` is given |
45
45
  | `statementTimeout` | `number` (ms) | — | Pool-wide `statement_timeout`; a per-transaction `timeout` overrides it |
46
46
  | `connectionTimeout` | `number` (ms) | — | Wait for a pool checkout before failing (pg's `connectionTimeoutMillis`) |
47
- | `preparedStatements` | `boolean` | `false` | Send ORM-path queries as named prepared statements. Saves planning time, but PostgreSQL may fall back to a generic plan after ~5 executions — leave off unless profiling says otherwise |
47
+ | `preparedStatements` | `boolean` | `false` | Send ORM-path queries as named prepared statements. Measured −17…−25 % on reads and −6 % on a by-key update (see below). Off by default because of transaction-mode poolers — turn it on for direct connections and session pooling |
48
48
  | `stmtCacheMax` | `number` | `1000` | Maximum entries in the prepared-statement name cache (LRU) |
49
+ | `planCacheMode` | `"auto" \| "force_custom_plan" \| "force_generic_plan"` | `"auto"` | PostgreSQL `plan_cache_mode` for every connection of the pool the adapter builds; sent in the `options` startup parameter, merged with an `options` the URL already carries (a URL that sets `plan_cache_mode` itself wins). Refused with `VIBE_CONFIG` next to a caller-supplied `pool` |
50
+
51
+ ### Prepared statements: the measured trade
52
+
53
+ Interleaved rounds on this repository's benchmark database (postgres 16, `apps/benchmark/scratch/probe-join-agg.ts`, p50 of 7 rounds):
54
+
55
+ | shape | unnamed (default) | `preparedStatements: true` | + `planCacheMode: "force_custom_plan"` |
56
+ |---|---|---|---|
57
+ | findUnique × 10 | 1.28 ms | 1.01 ms (−21 %) | 1.06 ms (−17 %) |
58
+ | findMany where + orderBy + take | 0.175 ms | 0.143 ms (−18 %) | 0.144 ms (−18 %) |
59
+ | update by primary key | 0.82 ms | 0.77 ms (−6 %) | 0.76 ms (−8 %) |
60
+ | findMany + include (take 50) | 0.52 ms | 0.42 ms (−20 %) | 0.39 ms (−25 %) |
61
+
62
+ The saving is the server's parse/analyze and the protocol Parse/Describe round on every repeat, not the plan — which is why nearly all of it survives `force_custom_plan`, the setting that removes the classic downside of prepared statements (a generic plan after ~5 executions picking worse indexes for range, `LIKE` or skewed filters).
63
+
64
+ Why it stays opt-in: named statements live on the **backend** connection. Behind a transaction-mode pooler (PgBouncer, Supavisor, Neon's pooler) that is not configured for protocol-level prepared statements, a client that remembers a statement name from an earlier transaction is handed a different backend later. The outcomes are SQLSTATE `26000` (statement missing), `42P05` (name collision on Parse) or — the one no error reveals — another client's statement executed under the same `vibeorm_N` name when its parameter types happen to be compatible. Enable it only where every connection maps to one backend for the process lifetime.
49
65
 
50
66
  ## Runtime support
51
67
 
@@ -69,6 +85,8 @@ Transactions run on one checked-out client; nested `transaction()` calls become
69
85
  - `serializeParams` (ORM path): plain objects **and** plain arrays are JSON-stringified, since at that point they can only be `json`/`jsonb` values. Scalar-list values are tagged by `formatArrayParam` with `PgArrayParam` and unwrapped, reaching the driver as the untouched array.
70
86
  - `serializeParamsRaw` (raw path, `$queryRaw*` / `$executeRaw*` / DDL): plain JS arrays pass straight through, so `$queryRawUnsafe('… WHERE "id" = ANY($1)', [1, 2, 3])` matches rows. Binding an array *into* a `jsonb` column from a raw query means calling `JSON.stringify` yourself.
71
87
 
88
+ On both paths a `Date` is spelled as UTC ISO text (`2026-03-08T19:38:08.538Z`) rather than node-postgres' host-local form, and the result side attaches per-query parsers (never `pg.types` globally) that read `timestamp without time zone`, `date` and their arrays as **UTC** — pg-types reads them as host-local time. Together they give zone-less columns (Prisma's `@db.Timestamp(n)` / `@db.Date`) the UTC wall-clock contract shared with bun:sql, PGlite and Prisma, whatever the process `TZ` is; `timestamptz` is unaffected. See `docs/dialect-notes.md` "DateTime without time zone on postgres".
89
+
72
90
  ## Error mapping
73
91
 
74
92
  Every driver failure becomes a `VibeError` with the driver error as `cause` and `meta` carrying `sqlstate` plus `constraint` / `table` / `column` when the server reported them. Table (`PG_SQLSTATE_ERROR_CODES`, frozen and mirrored by tests):
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * mapping. Only `VibeError` ever escapes.
7
7
  */
8
8
  import type { DatabaseAdapter } from "@vibeorm/runtime";
9
+ import { type PlanCacheMode } from "./pool-config.ts";
9
10
  /** The single `pg` query form this adapter uses (config object, never overloads). */
10
11
  export type PgQueryConfig = {
11
12
  readonly text: string;
@@ -75,13 +76,36 @@ export type PgAdapterOptions = {
75
76
  readonly connectionTimeout?: number;
76
77
  /**
77
78
  * Send ORM-path queries as named prepared statements (default false).
78
- * Named statements save planning time but let PostgreSQL fall back to a
79
- * generic plan after ~5 executions, which can pick worse indexes for range
80
- * filters — leave off unless profiling says otherwise.
79
+ *
80
+ * Measured on this repo's benchmark (postgres 16, interleaved rounds):
81
+ * −17…−25 % on reads, −6 % on a by-key update — the server skips the
82
+ * parse/analyze and the protocol Parse/Describe round on every repeat. The
83
+ * gain survives {@link PgAdapterOptions.planCacheMode} `force_custom_plan`,
84
+ * which removes the classic downside (a generic plan after ~5 executions
85
+ * picking worse indexes for range/LIKE/skewed filters).
86
+ *
87
+ * Off by default because named statements live on the BACKEND connection:
88
+ * behind a transaction-mode pooler (PgBouncer, Supavisor, Neon's pooler)
89
+ * without prepared-statement support a client that remembers a name from an
90
+ * earlier transaction is handed another backend later — SQLSTATE 26000,
91
+ * 42P05, or, worst, another client's statement under the same name executed
92
+ * without any error. Turn it on for direct connections and session pooling.
81
93
  */
82
94
  readonly preparedStatements?: boolean;
83
95
  /** Maximum entries in the prepared-statement name cache (default 1000, LRU). */
84
96
  readonly stmtCacheMax?: number;
97
+ /**
98
+ * PostgreSQL `plan_cache_mode` for every connection of the pool this adapter
99
+ * builds (default `"auto"`, the server default — nothing is sent). Pair
100
+ * `"force_custom_plan"` with {@link PgAdapterOptions.preparedStatements}: the
101
+ * statement is still prepared once, but planned with the real parameter
102
+ * values on every execution. Rides the `options` startup parameter, merged
103
+ * with whatever the connection string already carries (a URL that sets
104
+ * `plan_cache_mode` itself wins). Refused with `VIBE_CONFIG` together with
105
+ * a caller-supplied `pool` — the adapter never reconfigures a pool it does
106
+ * not own; set it on that pool's own `options` instead.
107
+ */
108
+ readonly planCacheMode?: PlanCacheMode;
85
109
  };
86
110
  /**
87
111
  * Create a VibeORM adapter backed by node-postgres.
@@ -103,12 +127,14 @@ export type PgAdapterOptions = {
103
127
  export declare function pgAdapter(options?: PgAdapterOptions): DatabaseAdapter;
104
128
  export { PG_SQLSTATE_ERROR_CODES, PG_SQLSTATE_REASONS, mapPgDriverError, pgErrorSqlstate } from "./errors.ts";
105
129
  export { PgArrayParam, serializeParams, serializeParamsRaw } from "./params.ts";
106
- export { createSavepointCounter, nextSavepointName } from "./savepoints.ts";
107
- export type { SavepointCounter } from "./savepoints.ts";
130
+ export { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adapter-kit";
131
+ export type { SavepointCounter } from "@vibeorm/runtime/adapter-kit";
108
132
  export { createStatementNameCache } from "./statement-cache.ts";
109
133
  export type { StatementNameCache } from "./statement-cache.ts";
134
+ export { buildPoolConfig, placePlanCacheMode } from "./pool-config.ts";
135
+ export type { PlanCacheMode, StartupOptionsPlacement } from "./pool-config.ts";
110
136
  export { ISOLATION_LEVEL_SQL, beginStatement, classifyRawTransactionControl, refuseNestedTransactionOptions, statementTimeoutStatement, } from "./transaction-sql.ts";
111
137
  export type { RawTransactionControl } from "./transaction-sql.ts";
112
- export { DEFAULT_DEADLINE_CLOCK, engineStatementBudgetMs, isTransactionDeadlineError, startTransactionBudget, transactionClosedError, transactionDeadlineError, transactionOutcomeUnknownError, validateTransactionDeadline, } from "./transaction-budget.ts";
113
- export type { BudgetStage, DeadlineClock, TransactionBudget, TransactionOutcome } from "./transaction-budget.ts";
138
+ export { DEFAULT_DEADLINE_CLOCK, engineStatementBudgetMs, isTransactionDeadlineError, startTransactionBudget, transactionClosedError, transactionDeadlineError, transactionOutcomeUnknownError, validateTransactionDeadline, } from "@vibeorm/runtime/adapter-kit";
139
+ export type { BudgetStage, DeadlineClock, TransactionBudget, TransactionOutcome } from "@vibeorm/runtime/adapter-kit";
114
140
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,5 +1,17 @@
1
1
  // @bun
2
2
  // packages/adapter-pg/src/index.ts
3
+ import {
4
+ createSavepointScope,
5
+ engineStatementBudgetMs,
6
+ isTransactionDeadlineError,
7
+ runNestedSavepoint,
8
+ runRootScope,
9
+ scopeTurn,
10
+ startTransactionBudget,
11
+ transactionOutcomeUnknownError,
12
+ validateTransactionDeadline
13
+ } from "@vibeorm/runtime/adapter-kit";
14
+ import { VibeError as VibeError4 } from "@vibeorm/schema";
3
15
  import pg from "pg";
4
16
 
5
17
  // packages/adapter-pg/src/errors.ts
@@ -70,142 +82,98 @@ class PgArrayParam {
70
82
  this.values = values;
71
83
  }
72
84
  }
85
+ function serializeDate(params) {
86
+ const { value } = params;
87
+ const time = value.getTime();
88
+ if (Number.isNaN(time))
89
+ return value;
90
+ const year = value.getUTCFullYear();
91
+ if (year < 1 || year > 9999)
92
+ return value;
93
+ return value.toISOString();
94
+ }
73
95
  function isDriverNativeValue(value) {
74
- return value === null || value === undefined || typeof value !== "object" || value instanceof Date || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
96
+ return value === null || value === undefined || typeof value !== "object" || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
75
97
  }
76
98
  function serializeParams(params) {
77
99
  return params.values.map((value) => {
78
100
  if (value instanceof PgArrayParam)
79
- return value.values;
101
+ return serializeList({ values: value.values });
102
+ if (value instanceof Date)
103
+ return serializeDate({ value });
80
104
  if (isDriverNativeValue(value))
81
105
  return value;
82
106
  return JSON.stringify(value);
83
107
  });
84
108
  }
109
+ function serializeList(params) {
110
+ const { values } = params;
111
+ return values.some((value) => value instanceof Date) ? values.map((value) => value instanceof Date ? serializeDate({ value }) : value) : values;
112
+ }
85
113
  function serializeParamsRaw(params) {
86
114
  return params.values.map((value) => {
87
115
  if (value instanceof PgArrayParam)
88
- return value.values;
116
+ return serializeList({ values: value.values });
89
117
  if (Array.isArray(value))
90
- return value;
118
+ return serializeList({ values: value });
119
+ if (value instanceof Date)
120
+ return serializeDate({ value });
91
121
  if (isDriverNativeValue(value))
92
122
  return value;
93
123
  return JSON.stringify(value);
94
124
  });
95
125
  }
96
126
 
97
- // packages/adapter-pg/src/savepoint-gate.ts
98
- import { AsyncLocalStorage } from "async_hooks";
99
-
100
- // packages/adapter-pg/src/savepoints.ts
101
- function createSavepointCounter() {
102
- return { n: 0 };
103
- }
104
- function nextSavepointName(params) {
105
- return `vibeorm_sp_${params.counter.n++}`;
106
- }
107
-
108
- // packages/adapter-pg/src/savepoint-gate.ts
109
- var CURRENT = new AsyncLocalStorage;
110
- function createSavepointScope() {
111
- return { depth: 0, state: { counter: createSavepointCounter(), open: null } };
112
- }
113
- function runRootScope(params) {
114
- return CURRENT.run(params.scope, params.body);
115
- }
116
- function scopeTurn(params) {
117
- const open = params.scope.state.open;
118
- if (mayProceed({ scope: params.scope, open }))
119
- return;
120
- return waitForTurn({ scope: params.scope });
127
+ // packages/adapter-pg/src/pool-config.ts
128
+ import { VibeError as VibeError2 } from "@vibeorm/schema";
129
+ var PLAN_CACHE_MODES = ["auto", "force_custom_plan", "force_generic_plan"];
130
+ function isPlanCacheMode(value) {
131
+ return typeof value === "string" && PLAN_CACHE_MODES.includes(value);
132
+ }
133
+ function assertPlanCacheMode(params) {
134
+ const { value } = params;
135
+ if (isPlanCacheMode(value))
136
+ return value;
137
+ throw new VibeError2({
138
+ code: "VIBE_CONFIG",
139
+ message: `planCacheMode must be one of ${PLAN_CACHE_MODES.map((mode) => `"${mode}"`).join(", ")}, received ${JSON.stringify(value)}`,
140
+ meta: { planCacheMode: value, allowed: PLAN_CACHE_MODES }
141
+ });
121
142
  }
122
- async function runNestedSavepoint(params) {
123
- const { scope, execute, body } = params;
124
- const node = await acquire({ scope });
125
- try {
126
- if (execute !== undefined)
127
- await execute(`SAVEPOINT ${node.name}`);
128
- let result;
129
- try {
130
- result = await CURRENT.run(node.scope, () => body(node.scope));
131
- } catch (error) {
132
- await drainDescendants({ node });
133
- if (execute !== undefined) {
134
- try {
135
- await execute(`ROLLBACK TO SAVEPOINT ${node.name}`);
136
- } catch {}
137
- }
138
- throw error;
139
- }
140
- await drainDescendants({ node });
141
- if (execute !== undefined)
142
- await execute(`RELEASE SAVEPOINT ${node.name}`);
143
- return result;
144
- } finally {
145
- closeNode({ node });
143
+ function placePlanCacheMode(params) {
144
+ const { connectionString, planCacheMode } = params;
145
+ if (planCacheMode === "auto")
146
+ return { connectionString, options: undefined };
147
+ const setting = `-c plan_cache_mode=${planCacheMode}`;
148
+ if (connectionString === undefined || !URL.canParse(connectionString)) {
149
+ return { connectionString, options: setting };
146
150
  }
147
- }
148
- function mayProceed(params) {
149
- const { scope, open } = params;
150
- if (open === null)
151
- return true;
152
- if (open.scope === scope)
153
- return true;
154
- if (open.scope.depth < scope.depth)
155
- return true;
156
- return CURRENT.getStore() === open.scope;
157
- }
158
- async function waitForTurn(params) {
159
- for (;; ) {
160
- const open = params.scope.state.open;
161
- if (open === null || mayProceed({ scope: params.scope, open }))
162
- return;
163
- await open.settled;
151
+ const url = new URL(connectionString);
152
+ const existing = url.searchParams.get("options");
153
+ if (existing === null) {
154
+ return { connectionString, options: setting };
164
155
  }
165
- }
166
- async function acquire(params) {
167
- const { scope } = params;
168
- for (;; ) {
169
- const open = scope.state.open;
170
- if (mayProceed({ scope, open }))
171
- return pushNode({ scope, parent: open });
172
- await open.settled;
156
+ if (/(^|\s)-c\s*plan_cache_mode=/.test(existing)) {
157
+ return { connectionString, options: undefined };
173
158
  }
174
- }
175
- function pushNode(params) {
176
- const { scope, parent } = params;
177
- const state = scope.state;
178
- let settle = () => {
179
- return;
180
- };
181
- const settled = new Promise((resolve) => {
182
- settle = () => resolve();
159
+ const merged = existing.trim().length === 0 ? setting : `${existing.trim()} ${setting}`;
160
+ url.searchParams.set("options", merged);
161
+ return { connectionString: url.toString(), options: merged };
162
+ }
163
+ function buildPoolConfig(params) {
164
+ const { max, statementTimeout, connectionTimeout } = params;
165
+ const placement = placePlanCacheMode({
166
+ connectionString: params.connectionString,
167
+ planCacheMode: params.planCacheMode ?? "auto"
183
168
  });
184
- const node = {
185
- scope: { depth: (parent === null ? 0 : parent.scope.depth) + 1, state },
186
- name: nextSavepointName({ counter: state.counter }),
187
- parent,
188
- settled,
189
- settle
190
- };
191
- state.open = node;
192
- return node;
193
- }
194
- function closeNode(params) {
195
- const { node } = params;
196
- const state = node.scope.state;
197
- if (state.open === node)
198
- state.open = node.parent;
199
- node.settle();
200
- }
201
- async function drainDescendants(params) {
202
- const state = params.node.scope.state;
203
- for (;; ) {
204
- const open = state.open;
205
- if (open === null || open === params.node || open.scope.depth <= params.node.scope.depth)
206
- return;
207
- await open.settled;
208
- }
169
+ const config = { connectionString: placement.connectionString, max };
170
+ if (statementTimeout !== undefined)
171
+ config.statement_timeout = statementTimeout;
172
+ if (connectionTimeout !== undefined)
173
+ config.connectionTimeoutMillis = connectionTimeout;
174
+ if (placement.options !== undefined)
175
+ config.options = placement.options;
176
+ return config;
209
177
  }
210
178
 
211
179
  // packages/adapter-pg/src/statement-cache.ts
@@ -231,125 +199,6 @@ function createStatementNameCache(params) {
231
199
  };
232
200
  }
233
201
 
234
- // packages/adapter-pg/src/transaction-budget.ts
235
- import { VibeError as VibeError2 } from "@vibeorm/schema";
236
- var DEFAULT_DEADLINE_CLOCK = () => Date.now();
237
- function validateTransactionDeadline(params) {
238
- const { options, support, provider } = params;
239
- const deadline = options?.deadline;
240
- if (deadline === undefined)
241
- return;
242
- if (support === "unsupported") {
243
- throw new VibeError2({
244
- code: "VIBE_UNSUPPORTED_CAPABILITY",
245
- message: `${provider} cannot enforce a transaction deadline \u2014 remove the \`deadline\` option`,
246
- meta: { provider, feature: "transactionDeadline", supported: support }
247
- });
248
- }
249
- if (!Number.isInteger(deadline.totalMs) || deadline.totalMs <= 0) {
250
- throw new VibeError2({
251
- code: "VIBE_VALIDATION",
252
- message: `transaction deadline \`totalMs\` must be a positive integer of milliseconds, received ${String(deadline.totalMs)}`,
253
- meta: { provider, feature: "transactionDeadline", totalMs: deadline.totalMs }
254
- });
255
- }
256
- const requested = deadline.enforcement ?? "between-statements";
257
- if (requested === "cancel-running-statements" && support !== "cancel-running-statements") {
258
- throw new VibeError2({
259
- code: "VIBE_UNSUPPORTED_CAPABILITY",
260
- message: `${provider} cannot cancel a statement that is already running, so the requested ` + "`cancel-running-statements` deadline guarantee is refused rather than silently downgraded \u2014 " + 'ask for "between-statements" if a budget checked between statements is enough',
261
- meta: { provider, feature: "transactionDeadline", requested, supported: support }
262
- });
263
- }
264
- }
265
- function startTransactionBudget(params) {
266
- const { options, provider } = params;
267
- const clock = params.clock ?? DEFAULT_DEADLINE_CLOCK;
268
- const deadline = options?.deadline;
269
- const totalMs = deadline === undefined ? null : deadline.totalMs;
270
- const enforcement = deadline === undefined ? null : deadline.enforcement ?? "between-statements";
271
- const expiresAt = totalMs === null ? null : clock() + totalMs;
272
- let outcome = null;
273
- const budget = {
274
- enforcement,
275
- totalMs,
276
- statementTimeoutMs: options?.timeout ?? null,
277
- remainingMs() {
278
- return expiresAt === null ? null : Math.max(0, expiresAt - clock());
279
- },
280
- expired() {
281
- return expiresAt !== null && clock() >= expiresAt;
282
- },
283
- assertUsable(assertParams) {
284
- if (outcome !== null) {
285
- throw transactionClosedError({ provider, outcome, stage: assertParams.stage });
286
- }
287
- if (expiresAt !== null && clock() >= expiresAt) {
288
- throw transactionDeadlineError({
289
- provider,
290
- stage: assertParams.stage,
291
- totalMs,
292
- overdueMs: clock() - expiresAt
293
- });
294
- }
295
- },
296
- close(closeParams) {
297
- if (outcome === null)
298
- outcome = closeParams.outcome;
299
- },
300
- get outcome() {
301
- return outcome;
302
- }
303
- };
304
- return budget;
305
- }
306
- function engineStatementBudgetMs(params) {
307
- const { budget } = params;
308
- if (budget.enforcement !== "cancel-running-statements")
309
- return null;
310
- const remaining = budget.remainingMs();
311
- if (remaining === null)
312
- return null;
313
- const totalBound = Math.max(1, Math.ceil(remaining));
314
- return budget.statementTimeoutMs !== null && budget.statementTimeoutMs > 0 ? Math.min(totalBound, budget.statementTimeoutMs) : totalBound;
315
- }
316
- function transactionDeadlineError(params) {
317
- const { provider, stage, totalMs, overdueMs } = params;
318
- const what = stage === "commit" ? "the transaction was rolled back instead of committed" : "the statement was never sent and the transaction is being rolled back";
319
- return new VibeError2({
320
- code: "VIBE_TRANSACTION",
321
- message: `transaction deadline of ${totalMs} ms expired ${Math.max(0, Math.round(overdueMs))} ms ago \u2014 ${what}. ` + "Nothing is retried automatically: work the callback did outside the database may already have happened.",
322
- meta: { provider, reason: "transaction-deadline", stage, totalMs, overdueMs: Math.max(0, Math.round(overdueMs)) }
323
- });
324
- }
325
- function transactionClosedError(params) {
326
- const { provider, outcome, stage } = params;
327
- return new VibeError2({
328
- code: "VIBE_TRANSACTION",
329
- message: `this transaction has already finished (${outcome}), so its client can no longer be used \u2014 ` + "a handle kept past the end of `$transaction` would run outside the transaction, on a connection " + "that now belongs to someone else",
330
- meta: { provider, reason: "transaction-closed", outcome, stage }
331
- });
332
- }
333
- function transactionOutcomeUnknownError(params) {
334
- const { provider, totalMs, cause } = params;
335
- return new VibeError2({
336
- code: "VIBE_TRANSACTION",
337
- message: `transaction deadline of ${totalMs} ms expired and the rollback that should have cleaned up failed \u2014 ` + "the commit state of this transaction is UNKNOWN. It is not retried automatically; inspect the data " + "before deciding.",
338
- meta: {
339
- provider,
340
- reason: "transaction-outcome-unknown",
341
- outcome: "unknown",
342
- totalMs,
343
- rollbackFailed: true
344
- },
345
- cause
346
- });
347
- }
348
- function isTransactionDeadlineError(error) {
349
- const reason = error?.meta?.reason;
350
- return reason === "transaction-deadline";
351
- }
352
-
353
202
  // packages/adapter-pg/src/transaction-sql.ts
354
203
  import { VibeError as VibeError3 } from "@vibeorm/schema";
355
204
  var ISOLATION_LEVEL_SQL = Object.freeze({
@@ -409,6 +258,17 @@ function refuseNestedTransactionOptions(params) {
409
258
  }
410
259
 
411
260
  // packages/adapter-pg/src/index.ts
261
+ import { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adapter-kit";
262
+ import {
263
+ DEFAULT_DEADLINE_CLOCK,
264
+ engineStatementBudgetMs as engineStatementBudgetMs2,
265
+ isTransactionDeadlineError as isTransactionDeadlineError2,
266
+ startTransactionBudget as startTransactionBudget2,
267
+ transactionClosedError,
268
+ transactionDeadlineError,
269
+ transactionOutcomeUnknownError as transactionOutcomeUnknownError2,
270
+ validateTransactionDeadline as validateTransactionDeadline2
271
+ } from "@vibeorm/runtime/adapter-kit";
412
272
  var POOL_STATEMENT_NAMES = new WeakMap;
413
273
  function guardBackgroundErrors(params) {
414
274
  const { client } = params;
@@ -432,18 +292,55 @@ var BUDGETS = Object.freeze({
432
292
  });
433
293
  var NUMERIC_ARRAY_OID = 1231;
434
294
  var TEXT_ARRAY_OID = 1009;
295
+ var TIMESTAMP_OID = 1114;
296
+ var TIMESTAMP_ARRAY_OID = 1115;
297
+ var DATE_OID = 1082;
298
+ var DATE_ARRAY_OID = 1182;
299
+ var driverTypeParser = pg.types.getTypeParser;
300
+ var ZONE_LESS_TEXT = /^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)?$/;
301
+ function parseZoneLessAsUtc(value) {
302
+ if (ZONE_LESS_TEXT.test(value)) {
303
+ const iso = value.includes(" ") ? `${value.replace(" ", "T").replace(/(\.\d{3})\d+$/, "$1")}Z` : value;
304
+ const parsed = new Date(iso);
305
+ if (!Number.isNaN(parsed.getTime()))
306
+ return parsed;
307
+ }
308
+ return driverTypeParser(TIMESTAMP_OID, "text")(value);
309
+ }
310
+ function parseZoneLessArrayAsUtc(value) {
311
+ const mapElements = (element) => {
312
+ if (element === null)
313
+ return null;
314
+ if (Array.isArray(element))
315
+ return element.map(mapElements);
316
+ return parseZoneLessAsUtc(String(element));
317
+ };
318
+ return mapElements(driverTypeParser(TEXT_ARRAY_OID, "text")(value));
319
+ }
435
320
  var EXACT_TYPES = {
436
321
  getTypeParser(oid, format) {
437
- if (oid === NUMERIC_ARRAY_OID && (format === undefined || format === "text")) {
438
- return driverTypeParser(TEXT_ARRAY_OID, "text");
322
+ if (format === undefined || format === "text") {
323
+ if (oid === NUMERIC_ARRAY_OID)
324
+ return driverTypeParser(TEXT_ARRAY_OID, "text");
325
+ if (oid === TIMESTAMP_OID || oid === DATE_OID)
326
+ return parseZoneLessAsUtc;
327
+ if (oid === TIMESTAMP_ARRAY_OID || oid === DATE_ARRAY_OID)
328
+ return parseZoneLessArrayAsUtc;
439
329
  }
440
330
  return driverTypeParser(oid, format);
441
331
  }
442
332
  };
443
- var driverTypeParser = pg.types.getTypeParser;
444
333
  function pgAdapter(options) {
445
334
  const usePrepared = options?.preparedStatements ?? false;
446
335
  const stmtCacheMax = options?.stmtCacheMax ?? 1000;
336
+ const planCacheMode = options?.planCacheMode === undefined ? "auto" : assertPlanCacheMode({ value: options.planCacheMode });
337
+ if (planCacheMode !== "auto" && options?.pool !== undefined) {
338
+ throw new VibeError4({
339
+ code: "VIBE_CONFIG",
340
+ message: "planCacheMode configures the pool the adapter builds \u2014 it cannot be applied to a caller-supplied `pool`; " + 'set `options: "-c plan_cache_mode=\u2026"` on that pool instead',
341
+ meta: { planCacheMode }
342
+ });
343
+ }
447
344
  let pool = null;
448
345
  let ownsPool = false;
449
346
  let rawSession = null;
@@ -456,16 +353,13 @@ function pgAdapter(options) {
456
353
  ownsPool = false;
457
354
  return pool;
458
355
  }
459
- const config = {
356
+ const config = buildPoolConfig({
460
357
  connectionString: options?.connectionString ?? process.env.DATABASE_URL,
461
- max: options?.max ?? 10
462
- };
463
- if (options?.statementTimeout !== undefined) {
464
- config.statement_timeout = options.statementTimeout;
465
- }
466
- if (options?.connectionTimeout !== undefined) {
467
- config.connectionTimeoutMillis = options.connectionTimeout;
468
- }
358
+ max: options?.max ?? 10,
359
+ ...options?.statementTimeout === undefined ? {} : { statementTimeout: options.statementTimeout },
360
+ ...options?.connectionTimeout === undefined ? {} : { connectionTimeout: options.connectionTimeout },
361
+ planCacheMode
362
+ });
469
363
  const PoolCtor = pg.Pool;
470
364
  pool = new PoolCtor(config);
471
365
  pool.on?.("error", () => {});
@@ -657,24 +551,26 @@ function pgAdapter(options) {
657
551
  };
658
552
  }
659
553
  export {
660
- validateTransactionDeadline,
661
- transactionOutcomeUnknownError,
554
+ validateTransactionDeadline2 as validateTransactionDeadline,
555
+ transactionOutcomeUnknownError2 as transactionOutcomeUnknownError,
662
556
  transactionDeadlineError,
663
557
  transactionClosedError,
664
558
  statementTimeoutStatement,
665
- startTransactionBudget,
559
+ startTransactionBudget2 as startTransactionBudget,
666
560
  serializeParamsRaw,
667
561
  serializeParams,
668
562
  refuseNestedTransactionOptions,
563
+ placePlanCacheMode,
669
564
  pgErrorSqlstate,
670
565
  pgAdapter,
671
566
  nextSavepointName,
672
567
  mapPgDriverError,
673
- isTransactionDeadlineError,
674
- engineStatementBudgetMs,
568
+ isTransactionDeadlineError2 as isTransactionDeadlineError,
569
+ engineStatementBudgetMs2 as engineStatementBudgetMs,
675
570
  createStatementNameCache,
676
571
  createSavepointCounter,
677
572
  classifyRawTransactionControl,
573
+ buildPoolConfig,
678
574
  beginStatement,
679
575
  PgArrayParam,
680
576
  PG_SQLSTATE_REASONS,
@@ -683,4 +579,4 @@ export {
683
579
  DEFAULT_DEADLINE_CLOCK
684
580
  };
685
581
 
686
- //# debugId=17E44C1AF7E3C0D164756E2164756E21
582
+ //# debugId=79BF9E77B467A34F64756E2164756E21