@vibeorm/adapter-pg 2.0.0-alpha.1 → 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/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +27 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +356 -36
- package/dist/index.js.map +9 -7
- package/dist/params.d.ts.map +1 -1
- package/dist/savepoint-gate.d.ts +86 -0
- package/dist/savepoint-gate.d.ts.map +1 -0
- package/dist/statement-cache.d.ts +35 -9
- package/dist/statement-cache.d.ts.map +1 -1
- package/dist/transaction-budget.d.ts +144 -0
- package/dist/transaction-budget.d.ts.map +1 -0
- package/dist/transaction-sql.d.ts.map +1 -1
- package/package.json +4 -3
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CA2B1E,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE/D,CAAC;AAYH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAK9E;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CA0BtE;AAED,wEAAwE;AACxE,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMnE"}
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export type PgQueryConfig = {
|
|
|
11
11
|
readonly text: string;
|
|
12
12
|
readonly values?: unknown[];
|
|
13
13
|
readonly name?: string;
|
|
14
|
+
/**
|
|
15
|
+
* QUERY-LOCAL result parsers ({@link EXACT_TYPES}). node-postgres reads this
|
|
16
|
+
* off the query config, so nothing outside VibeORM's own statements is
|
|
17
|
+
* affected — a caller's pool keeps the driver's documented behaviour.
|
|
18
|
+
*/
|
|
19
|
+
readonly types?: PgTypeParsers;
|
|
20
|
+
};
|
|
21
|
+
/** The `pg-types` surface a query config may carry. */
|
|
22
|
+
export type PgTypeParsers = {
|
|
23
|
+
getTypeParser(oid: number, format?: "text" | "binary"): (value: string) => unknown;
|
|
14
24
|
};
|
|
15
25
|
/** The parts of a `pg` result this adapter reads. */
|
|
16
26
|
export type PgQueryResultLike = {
|
|
@@ -20,13 +30,27 @@ export type PgQueryResultLike = {
|
|
|
20
30
|
/** The parts of a `pg` PoolClient this adapter uses. */
|
|
21
31
|
export type PgPoolClientLike = {
|
|
22
32
|
query(config: PgQueryConfig): Promise<PgQueryResultLike>;
|
|
23
|
-
|
|
33
|
+
/**
|
|
34
|
+
* Hand the client back. A TRUTHY argument tells node-postgres to DESTROY it
|
|
35
|
+
* instead of pooling it — used when a rollback failed and the session state
|
|
36
|
+
* can no longer be asserted (EPIC 6). Optional so existing test doubles that
|
|
37
|
+
* declare `release(): void` still satisfy this type.
|
|
38
|
+
*/
|
|
39
|
+
release(destroy?: boolean): void;
|
|
40
|
+
/**
|
|
41
|
+
* EventEmitter surface. A real `pg.PoolClient` has it; test doubles need not,
|
|
42
|
+
* so both members are optional and the adapter feature-detects them.
|
|
43
|
+
*/
|
|
44
|
+
on?(event: "error", listener: (error: unknown) => void): void;
|
|
45
|
+
removeListener?(event: "error", listener: (error: unknown) => void): void;
|
|
24
46
|
};
|
|
25
47
|
/** The parts of a `pg` Pool this adapter uses — satisfied by a real `pg.Pool`. */
|
|
26
48
|
export type PgPoolLike = {
|
|
27
49
|
connect(): Promise<PgPoolClientLike>;
|
|
28
50
|
query(config: PgQueryConfig): Promise<PgQueryResultLike>;
|
|
29
51
|
end(): Promise<void>;
|
|
52
|
+
/** EventEmitter surface; optional for the same reason as on {@link PgPoolClientLike}. */
|
|
53
|
+
on?(event: "error", listener: (error: unknown, client: PgPoolClientLike) => void): void;
|
|
30
54
|
};
|
|
31
55
|
/**
|
|
32
56
|
* Options for {@link pgAdapter}. Either point it at a connection string (the
|
|
@@ -85,4 +109,6 @@ export { createStatementNameCache } from "./statement-cache.ts";
|
|
|
85
109
|
export type { StatementNameCache } from "./statement-cache.ts";
|
|
86
110
|
export { ISOLATION_LEVEL_SQL, beginStatement, classifyRawTransactionControl, refuseNestedTransactionOptions, statementTimeoutStatement, } from "./transaction-sql.ts";
|
|
87
111
|
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";
|
|
88
114
|
//# 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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ var PG_SQLSTATE_ERROR_CODES = Object.freeze({
|
|
|
10
10
|
"23502": "VIBE_VALIDATION",
|
|
11
11
|
"23514": "VIBE_VALIDATION",
|
|
12
12
|
"22P02": "VIBE_VALIDATION",
|
|
13
|
+
"42501": "VIBE_PERMISSION_DENIED",
|
|
13
14
|
"40001": "VIBE_TRANSACTION",
|
|
14
15
|
"40P01": "VIBE_TRANSACTION",
|
|
15
16
|
"57014": "VIBE_TRANSACTION"
|
|
@@ -93,6 +94,9 @@ function serializeParamsRaw(params) {
|
|
|
93
94
|
});
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
// packages/adapter-pg/src/savepoint-gate.ts
|
|
98
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
99
|
+
|
|
96
100
|
// packages/adapter-pg/src/savepoints.ts
|
|
97
101
|
function createSavepointCounter() {
|
|
98
102
|
return { n: 0 };
|
|
@@ -101,26 +105,123 @@ function nextSavepointName(params) {
|
|
|
101
105
|
return `vibeorm_sp_${params.counter.n++}`;
|
|
102
106
|
}
|
|
103
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 });
|
|
121
|
+
}
|
|
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 });
|
|
146
|
+
}
|
|
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;
|
|
164
|
+
}
|
|
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;
|
|
173
|
+
}
|
|
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();
|
|
183
|
+
});
|
|
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
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
104
211
|
// packages/adapter-pg/src/statement-cache.ts
|
|
105
212
|
function createStatementNameCache(params) {
|
|
106
|
-
const
|
|
213
|
+
const max = Math.max(0, params.max);
|
|
107
214
|
const names = new Map;
|
|
108
215
|
let counter = 0;
|
|
109
216
|
return {
|
|
110
217
|
getName(nameParams) {
|
|
111
218
|
const { text } = nameParams;
|
|
112
219
|
const existing = names.get(text);
|
|
113
|
-
if (existing !== undefined)
|
|
114
|
-
names.delete(text);
|
|
115
|
-
names.set(text, existing);
|
|
220
|
+
if (existing !== undefined)
|
|
116
221
|
return existing;
|
|
117
|
-
|
|
222
|
+
if (names.size >= max)
|
|
223
|
+
return;
|
|
118
224
|
const name = `vibeorm_${counter++}`;
|
|
119
|
-
if (names.size >= max) {
|
|
120
|
-
const oldest = names.keys().next().value;
|
|
121
|
-
if (oldest !== undefined)
|
|
122
|
-
names.delete(oldest);
|
|
123
|
-
}
|
|
124
225
|
names.set(text, name);
|
|
125
226
|
return name;
|
|
126
227
|
},
|
|
@@ -130,8 +231,127 @@ function createStatementNameCache(params) {
|
|
|
130
231
|
};
|
|
131
232
|
}
|
|
132
233
|
|
|
133
|
-
// packages/adapter-pg/src/transaction-
|
|
234
|
+
// packages/adapter-pg/src/transaction-budget.ts
|
|
134
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
|
+
// packages/adapter-pg/src/transaction-sql.ts
|
|
354
|
+
import { VibeError as VibeError3 } from "@vibeorm/schema";
|
|
135
355
|
var ISOLATION_LEVEL_SQL = Object.freeze({
|
|
136
356
|
ReadCommitted: "READ COMMITTED",
|
|
137
357
|
RepeatableRead: "REPEATABLE READ",
|
|
@@ -144,7 +364,7 @@ function beginStatement(params) {
|
|
|
144
364
|
function statementTimeoutStatement(params) {
|
|
145
365
|
const { timeout } = params;
|
|
146
366
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
147
|
-
throw new
|
|
367
|
+
throw new VibeError3({
|
|
148
368
|
code: "VIBE_VALIDATION",
|
|
149
369
|
message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
|
|
150
370
|
meta: { timeout }
|
|
@@ -170,6 +390,8 @@ function refuseNestedTransactionOptions(params) {
|
|
|
170
390
|
passed.push("isolationLevel");
|
|
171
391
|
if (options.timeout !== undefined)
|
|
172
392
|
passed.push("timeout");
|
|
393
|
+
if (options.deadline !== undefined)
|
|
394
|
+
passed.push("deadline");
|
|
173
395
|
if (passed.length === 0)
|
|
174
396
|
return;
|
|
175
397
|
const meta = { provider, nested: true };
|
|
@@ -177,23 +399,55 @@ function refuseNestedTransactionOptions(params) {
|
|
|
177
399
|
meta.isolationLevel = options.isolationLevel;
|
|
178
400
|
if (options.timeout !== undefined)
|
|
179
401
|
meta.timeout = options.timeout;
|
|
180
|
-
|
|
402
|
+
if (options.deadline !== undefined)
|
|
403
|
+
meta.deadline = options.deadline;
|
|
404
|
+
throw new VibeError3({
|
|
181
405
|
code: "VIBE_VALIDATION",
|
|
182
|
-
message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins,
|
|
406
|
+
message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins, a nested timeout is not enforceable, and a nested deadline would need its own session setting on a connection it does not own (it inherits the top-level one instead); pass options on the top-level $transaction`,
|
|
183
407
|
meta
|
|
184
408
|
});
|
|
185
409
|
}
|
|
186
410
|
|
|
187
411
|
// packages/adapter-pg/src/index.ts
|
|
412
|
+
var POOL_STATEMENT_NAMES = new WeakMap;
|
|
413
|
+
function guardBackgroundErrors(params) {
|
|
414
|
+
const { client } = params;
|
|
415
|
+
if (typeof client.on !== "function")
|
|
416
|
+
return () => {
|
|
417
|
+
return;
|
|
418
|
+
};
|
|
419
|
+
const swallow = () => {};
|
|
420
|
+
client.on("error", swallow);
|
|
421
|
+
return () => {
|
|
422
|
+
client.removeListener?.("error", swallow);
|
|
423
|
+
};
|
|
424
|
+
}
|
|
188
425
|
var DIALECT = "postgres";
|
|
189
426
|
var PROVIDER = "postgresql";
|
|
190
427
|
var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
|
|
428
|
+
var BUDGETS = Object.freeze({
|
|
429
|
+
acquireTimeout: true,
|
|
430
|
+
statementTimeout: "all-statements",
|
|
431
|
+
transactionDeadline: "cancel-running-statements"
|
|
432
|
+
});
|
|
433
|
+
var NUMERIC_ARRAY_OID = 1231;
|
|
434
|
+
var TEXT_ARRAY_OID = 1009;
|
|
435
|
+
var EXACT_TYPES = {
|
|
436
|
+
getTypeParser(oid, format) {
|
|
437
|
+
if (oid === NUMERIC_ARRAY_OID && (format === undefined || format === "text")) {
|
|
438
|
+
return driverTypeParser(TEXT_ARRAY_OID, "text");
|
|
439
|
+
}
|
|
440
|
+
return driverTypeParser(oid, format);
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
var driverTypeParser = pg.types.getTypeParser;
|
|
191
444
|
function pgAdapter(options) {
|
|
192
445
|
const usePrepared = options?.preparedStatements ?? false;
|
|
193
|
-
const
|
|
446
|
+
const stmtCacheMax = options?.stmtCacheMax ?? 1000;
|
|
194
447
|
let pool = null;
|
|
195
448
|
let ownsPool = false;
|
|
196
449
|
let rawSession = null;
|
|
450
|
+
let rawSessionUnguard = null;
|
|
197
451
|
function getPool() {
|
|
198
452
|
if (pool !== null)
|
|
199
453
|
return pool;
|
|
@@ -214,12 +468,24 @@ function pgAdapter(options) {
|
|
|
214
468
|
}
|
|
215
469
|
const PoolCtor = pg.Pool;
|
|
216
470
|
pool = new PoolCtor(config);
|
|
471
|
+
pool.on?.("error", () => {});
|
|
217
472
|
ownsPool = true;
|
|
218
473
|
return pool;
|
|
219
474
|
}
|
|
475
|
+
function statementNamesFor(params) {
|
|
476
|
+
const existing = POOL_STATEMENT_NAMES.get(params.pool);
|
|
477
|
+
if (existing !== undefined)
|
|
478
|
+
return existing;
|
|
479
|
+
const created = createStatementNameCache({ max: stmtCacheMax });
|
|
480
|
+
POOL_STATEMENT_NAMES.set(params.pool, created);
|
|
481
|
+
return created;
|
|
482
|
+
}
|
|
220
483
|
function buildQueryConfig(params) {
|
|
221
484
|
const values = serializeParams({ values: params.values });
|
|
222
|
-
|
|
485
|
+
if (!usePrepared)
|
|
486
|
+
return { text: params.text, values, types: EXACT_TYPES };
|
|
487
|
+
const name = statementNamesFor({ pool: getPool() }).getName({ text: params.text });
|
|
488
|
+
return name === undefined ? { text: params.text, values, types: EXACT_TYPES } : { name, text: params.text, values, types: EXACT_TYPES };
|
|
223
489
|
}
|
|
224
490
|
async function runExecute(params) {
|
|
225
491
|
const config = buildQueryConfig({ text: params.text, values: params.values });
|
|
@@ -228,35 +494,46 @@ function pgAdapter(options) {
|
|
|
228
494
|
}
|
|
229
495
|
async function runExecuteUnsafe(params) {
|
|
230
496
|
const values = params.values === undefined ? undefined : serializeParamsRaw({ values: params.values });
|
|
231
|
-
const result = await runMapped(() => params.queryable.query({ text: params.text, values }));
|
|
497
|
+
const result = await runMapped(() => params.queryable.query({ text: params.text, values, types: EXACT_TYPES }));
|
|
232
498
|
return { rows: result.rows, affectedRows: result.rowCount ?? 0 };
|
|
233
499
|
}
|
|
234
500
|
function createClientAdapter(params) {
|
|
235
|
-
const { client,
|
|
501
|
+
const { client, scope, budget } = params;
|
|
502
|
+
async function applyEngineBudget() {
|
|
503
|
+
const ms = engineStatementBudgetMs({ budget });
|
|
504
|
+
if (ms === null)
|
|
505
|
+
return;
|
|
506
|
+
await runMapped(() => client.query({ text: statementTimeoutStatement({ timeout: ms }) }));
|
|
507
|
+
}
|
|
236
508
|
return {
|
|
237
509
|
dialect: DIALECT,
|
|
238
510
|
provider: PROVIDER,
|
|
239
511
|
wire: WIRE,
|
|
512
|
+
budgets: BUDGETS,
|
|
240
513
|
async execute(execParams) {
|
|
514
|
+
const turn = scopeTurn({ scope });
|
|
515
|
+
if (turn !== undefined)
|
|
516
|
+
await turn;
|
|
517
|
+
budget.assertUsable({ stage: "statement" });
|
|
518
|
+
await applyEngineBudget();
|
|
241
519
|
return runExecute({ queryable: client, ...execParams });
|
|
242
520
|
},
|
|
243
521
|
async executeUnsafe(execParams) {
|
|
522
|
+
const turn = scopeTurn({ scope });
|
|
523
|
+
if (turn !== undefined)
|
|
524
|
+
await turn;
|
|
525
|
+
budget.assertUsable({ stage: "statement" });
|
|
526
|
+
await applyEngineBudget();
|
|
244
527
|
return runExecuteUnsafe({ queryable: client, ...execParams });
|
|
245
528
|
},
|
|
246
529
|
async transaction(fn, nestedOptions) {
|
|
247
530
|
refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
} catch (error) {
|
|
255
|
-
try {
|
|
256
|
-
await client.query({ text: `ROLLBACK TO SAVEPOINT ${savepoint}` });
|
|
257
|
-
} catch {}
|
|
258
|
-
throw error;
|
|
259
|
-
}
|
|
531
|
+
budget.assertUsable({ stage: "nested" });
|
|
532
|
+
return runNestedSavepoint({
|
|
533
|
+
scope,
|
|
534
|
+
execute: (statement) => runMapped(() => client.query({ text: statement })),
|
|
535
|
+
body: (child) => fn(createClientAdapter({ client, scope: child, budget }))
|
|
536
|
+
});
|
|
260
537
|
},
|
|
261
538
|
async connect() {},
|
|
262
539
|
async disconnect() {},
|
|
@@ -269,6 +546,7 @@ function pgAdapter(options) {
|
|
|
269
546
|
dialect: DIALECT,
|
|
270
547
|
provider: PROVIDER,
|
|
271
548
|
wire: WIRE,
|
|
549
|
+
budgets: BUDGETS,
|
|
272
550
|
async execute(params) {
|
|
273
551
|
return runExecute({ queryable: getPool(), ...params });
|
|
274
552
|
},
|
|
@@ -277,10 +555,13 @@ function pgAdapter(options) {
|
|
|
277
555
|
if (rawSession !== null) {
|
|
278
556
|
if (control === "close") {
|
|
279
557
|
const session = rawSession;
|
|
558
|
+
const unguard = rawSessionUnguard;
|
|
280
559
|
rawSession = null;
|
|
560
|
+
rawSessionUnguard = null;
|
|
281
561
|
try {
|
|
282
562
|
return await runExecuteUnsafe({ queryable: session, ...params });
|
|
283
563
|
} finally {
|
|
564
|
+
unguard?.();
|
|
284
565
|
session.release();
|
|
285
566
|
}
|
|
286
567
|
}
|
|
@@ -288,11 +569,14 @@ function pgAdapter(options) {
|
|
|
288
569
|
}
|
|
289
570
|
if (control === "open") {
|
|
290
571
|
const client = await runMapped(() => getPool().connect());
|
|
572
|
+
const unguard = guardBackgroundErrors({ client });
|
|
291
573
|
try {
|
|
292
574
|
const result = await runExecuteUnsafe({ queryable: client, ...params });
|
|
293
575
|
rawSession = client;
|
|
576
|
+
rawSessionUnguard = unguard;
|
|
294
577
|
return result;
|
|
295
578
|
} catch (error) {
|
|
579
|
+
unguard();
|
|
296
580
|
client.release();
|
|
297
581
|
throw error;
|
|
298
582
|
}
|
|
@@ -300,24 +584,49 @@ function pgAdapter(options) {
|
|
|
300
584
|
return runExecuteUnsafe({ queryable: getPool(), ...params });
|
|
301
585
|
},
|
|
302
586
|
async transaction(fn, transactionOptions) {
|
|
587
|
+
validateTransactionDeadline({
|
|
588
|
+
options: transactionOptions,
|
|
589
|
+
support: BUDGETS.transactionDeadline,
|
|
590
|
+
provider: PROVIDER
|
|
591
|
+
});
|
|
303
592
|
const client = await runMapped(() => getPool().connect());
|
|
304
|
-
const
|
|
593
|
+
const unguard = guardBackgroundErrors({ client });
|
|
594
|
+
const scope = createSavepointScope();
|
|
595
|
+
const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
|
|
596
|
+
let discard = false;
|
|
305
597
|
try {
|
|
306
598
|
await runMapped(() => client.query({ text: beginStatement({ isolationLevel: transactionOptions?.isolationLevel }) }));
|
|
307
599
|
if (transactionOptions?.timeout !== undefined) {
|
|
308
600
|
const text = statementTimeoutStatement({ timeout: transactionOptions.timeout });
|
|
309
601
|
await runMapped(() => client.query({ text }));
|
|
310
602
|
}
|
|
311
|
-
const result = await
|
|
603
|
+
const result = await runRootScope({
|
|
604
|
+
scope,
|
|
605
|
+
body: () => fn(createClientAdapter({ client, scope, budget }))
|
|
606
|
+
});
|
|
607
|
+
budget.assertUsable({ stage: "commit" });
|
|
312
608
|
await runMapped(() => client.query({ text: "COMMIT" }));
|
|
609
|
+
budget.close({ outcome: "committed" });
|
|
313
610
|
return result;
|
|
314
611
|
} catch (error) {
|
|
315
612
|
try {
|
|
316
613
|
await client.query({ text: "ROLLBACK" });
|
|
317
|
-
|
|
614
|
+
budget.close({ outcome: "rolled-back" });
|
|
615
|
+
} catch {
|
|
616
|
+
discard = true;
|
|
617
|
+
budget.close({ outcome: "unknown" });
|
|
618
|
+
if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
|
|
619
|
+
throw transactionOutcomeUnknownError({
|
|
620
|
+
provider: PROVIDER,
|
|
621
|
+
totalMs: budget.totalMs,
|
|
622
|
+
cause: error
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
}
|
|
318
626
|
throw error;
|
|
319
627
|
} finally {
|
|
320
|
-
|
|
628
|
+
unguard();
|
|
629
|
+
client.release(discard);
|
|
321
630
|
}
|
|
322
631
|
},
|
|
323
632
|
async connect() {
|
|
@@ -327,10 +636,13 @@ function pgAdapter(options) {
|
|
|
327
636
|
async disconnect() {
|
|
328
637
|
if (rawSession !== null) {
|
|
329
638
|
const session = rawSession;
|
|
639
|
+
const unguard = rawSessionUnguard;
|
|
330
640
|
rawSession = null;
|
|
641
|
+
rawSessionUnguard = null;
|
|
331
642
|
try {
|
|
332
643
|
await session.query({ text: "ROLLBACK" });
|
|
333
644
|
} catch {}
|
|
645
|
+
unguard?.();
|
|
334
646
|
session.release();
|
|
335
647
|
}
|
|
336
648
|
if (pool !== null && ownsPool) {
|
|
@@ -345,7 +657,12 @@ function pgAdapter(options) {
|
|
|
345
657
|
};
|
|
346
658
|
}
|
|
347
659
|
export {
|
|
660
|
+
validateTransactionDeadline,
|
|
661
|
+
transactionOutcomeUnknownError,
|
|
662
|
+
transactionDeadlineError,
|
|
663
|
+
transactionClosedError,
|
|
348
664
|
statementTimeoutStatement,
|
|
665
|
+
startTransactionBudget,
|
|
349
666
|
serializeParamsRaw,
|
|
350
667
|
serializeParams,
|
|
351
668
|
refuseNestedTransactionOptions,
|
|
@@ -353,6 +670,8 @@ export {
|
|
|
353
670
|
pgAdapter,
|
|
354
671
|
nextSavepointName,
|
|
355
672
|
mapPgDriverError,
|
|
673
|
+
isTransactionDeadlineError,
|
|
674
|
+
engineStatementBudgetMs,
|
|
356
675
|
createStatementNameCache,
|
|
357
676
|
createSavepointCounter,
|
|
358
677
|
classifyRawTransactionControl,
|
|
@@ -360,7 +679,8 @@ export {
|
|
|
360
679
|
PgArrayParam,
|
|
361
680
|
PG_SQLSTATE_REASONS,
|
|
362
681
|
PG_SQLSTATE_ERROR_CODES,
|
|
363
|
-
ISOLATION_LEVEL_SQL
|
|
682
|
+
ISOLATION_LEVEL_SQL,
|
|
683
|
+
DEFAULT_DEADLINE_CLOCK
|
|
364
684
|
};
|
|
365
685
|
|
|
366
|
-
//# debugId=
|
|
686
|
+
//# debugId=17E44C1AF7E3C0D164756E2164756E21
|