@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 +19 -1
- package/dist/index.d.ts +33 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +144 -248
- package/dist/index.js.map +6 -8
- package/dist/params.d.ts +19 -2
- package/dist/params.d.ts.map +1 -1
- package/dist/pool-config.d.ts +58 -0
- package/dist/pool-config.d.ts.map +1 -0
- package/package.json +2 -2
- package/dist/savepoint-gate.d.ts +0 -86
- package/dist/savepoint-gate.d.ts.map +0 -1
- package/dist/savepoints.d.ts +0 -22
- package/dist/savepoints.d.ts.map +0 -1
- package/dist/transaction-budget.d.ts +0 -144
- package/dist/transaction-budget.d.ts.map +0 -1
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.
|
|
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
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
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 "
|
|
107
|
-
export type { SavepointCounter } from "
|
|
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 "
|
|
113
|
-
export type { BudgetStage, DeadlineClock, TransactionBudget, TransactionOutcome } from "
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAEV,eAAe,EAIhB,MAAM,kBAAkB,CAAC;
|
|
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" ||
|
|
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/
|
|
98
|
-
import {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
123
|
-
const {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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 (
|
|
438
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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=
|
|
582
|
+
//# debugId=79BF9E77B467A34F64756E2164756E21
|