@vibeorm/adapter-pg 2.0.0-alpha.9 → 2.0.1
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 +21 -3
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +48 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +451 -49
- package/dist/index.js.map +11 -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/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 +3 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/adapter-pg/src/index.ts
|
|
3
|
+
import { VibeError as VibeError5 } from "@vibeorm/schema";
|
|
3
4
|
import pg from "pg";
|
|
4
5
|
|
|
5
6
|
// packages/adapter-pg/src/errors.ts
|
|
@@ -10,6 +11,7 @@ var PG_SQLSTATE_ERROR_CODES = Object.freeze({
|
|
|
10
11
|
"23502": "VIBE_VALIDATION",
|
|
11
12
|
"23514": "VIBE_VALIDATION",
|
|
12
13
|
"22P02": "VIBE_VALIDATION",
|
|
14
|
+
"42501": "VIBE_PERMISSION_DENIED",
|
|
13
15
|
"40001": "VIBE_TRANSACTION",
|
|
14
16
|
"40P01": "VIBE_TRANSACTION",
|
|
15
17
|
"57014": "VIBE_TRANSACTION"
|
|
@@ -69,30 +71,103 @@ class PgArrayParam {
|
|
|
69
71
|
this.values = values;
|
|
70
72
|
}
|
|
71
73
|
}
|
|
74
|
+
function serializeDate(params) {
|
|
75
|
+
const { value } = params;
|
|
76
|
+
const time = value.getTime();
|
|
77
|
+
if (Number.isNaN(time))
|
|
78
|
+
return value;
|
|
79
|
+
const year = value.getUTCFullYear();
|
|
80
|
+
if (year < 1 || year > 9999)
|
|
81
|
+
return value;
|
|
82
|
+
return value.toISOString();
|
|
83
|
+
}
|
|
72
84
|
function isDriverNativeValue(value) {
|
|
73
|
-
return value === null || value === undefined || typeof value !== "object" ||
|
|
85
|
+
return value === null || value === undefined || typeof value !== "object" || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
|
|
74
86
|
}
|
|
75
87
|
function serializeParams(params) {
|
|
76
88
|
return params.values.map((value) => {
|
|
77
89
|
if (value instanceof PgArrayParam)
|
|
78
|
-
return value.values;
|
|
90
|
+
return serializeList({ values: value.values });
|
|
91
|
+
if (value instanceof Date)
|
|
92
|
+
return serializeDate({ value });
|
|
79
93
|
if (isDriverNativeValue(value))
|
|
80
94
|
return value;
|
|
81
95
|
return JSON.stringify(value);
|
|
82
96
|
});
|
|
83
97
|
}
|
|
98
|
+
function serializeList(params) {
|
|
99
|
+
const { values } = params;
|
|
100
|
+
return values.some((value) => value instanceof Date) ? values.map((value) => value instanceof Date ? serializeDate({ value }) : value) : values;
|
|
101
|
+
}
|
|
84
102
|
function serializeParamsRaw(params) {
|
|
85
103
|
return params.values.map((value) => {
|
|
86
104
|
if (value instanceof PgArrayParam)
|
|
87
|
-
return value.values;
|
|
105
|
+
return serializeList({ values: value.values });
|
|
88
106
|
if (Array.isArray(value))
|
|
89
|
-
return value;
|
|
107
|
+
return serializeList({ values: value });
|
|
108
|
+
if (value instanceof Date)
|
|
109
|
+
return serializeDate({ value });
|
|
90
110
|
if (isDriverNativeValue(value))
|
|
91
111
|
return value;
|
|
92
112
|
return JSON.stringify(value);
|
|
93
113
|
});
|
|
94
114
|
}
|
|
95
115
|
|
|
116
|
+
// packages/adapter-pg/src/pool-config.ts
|
|
117
|
+
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
118
|
+
var PLAN_CACHE_MODES = ["auto", "force_custom_plan", "force_generic_plan"];
|
|
119
|
+
function isPlanCacheMode(value) {
|
|
120
|
+
return typeof value === "string" && PLAN_CACHE_MODES.includes(value);
|
|
121
|
+
}
|
|
122
|
+
function assertPlanCacheMode(params) {
|
|
123
|
+
const { value } = params;
|
|
124
|
+
if (isPlanCacheMode(value))
|
|
125
|
+
return value;
|
|
126
|
+
throw new VibeError2({
|
|
127
|
+
code: "VIBE_CONFIG",
|
|
128
|
+
message: `planCacheMode must be one of ${PLAN_CACHE_MODES.map((mode) => `"${mode}"`).join(", ")}, received ${JSON.stringify(value)}`,
|
|
129
|
+
meta: { planCacheMode: value, allowed: PLAN_CACHE_MODES }
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function placePlanCacheMode(params) {
|
|
133
|
+
const { connectionString, planCacheMode } = params;
|
|
134
|
+
if (planCacheMode === "auto")
|
|
135
|
+
return { connectionString, options: undefined };
|
|
136
|
+
const setting = `-c plan_cache_mode=${planCacheMode}`;
|
|
137
|
+
if (connectionString === undefined || !URL.canParse(connectionString)) {
|
|
138
|
+
return { connectionString, options: setting };
|
|
139
|
+
}
|
|
140
|
+
const url = new URL(connectionString);
|
|
141
|
+
const existing = url.searchParams.get("options");
|
|
142
|
+
if (existing === null) {
|
|
143
|
+
return { connectionString, options: setting };
|
|
144
|
+
}
|
|
145
|
+
if (/(^|\s)-c\s*plan_cache_mode=/.test(existing)) {
|
|
146
|
+
return { connectionString, options: undefined };
|
|
147
|
+
}
|
|
148
|
+
const merged = existing.trim().length === 0 ? setting : `${existing.trim()} ${setting}`;
|
|
149
|
+
url.searchParams.set("options", merged);
|
|
150
|
+
return { connectionString: url.toString(), options: merged };
|
|
151
|
+
}
|
|
152
|
+
function buildPoolConfig(params) {
|
|
153
|
+
const { max, statementTimeout, connectionTimeout } = params;
|
|
154
|
+
const placement = placePlanCacheMode({
|
|
155
|
+
connectionString: params.connectionString,
|
|
156
|
+
planCacheMode: params.planCacheMode ?? "auto"
|
|
157
|
+
});
|
|
158
|
+
const config = { connectionString: placement.connectionString, max };
|
|
159
|
+
if (statementTimeout !== undefined)
|
|
160
|
+
config.statement_timeout = statementTimeout;
|
|
161
|
+
if (connectionTimeout !== undefined)
|
|
162
|
+
config.connectionTimeoutMillis = connectionTimeout;
|
|
163
|
+
if (placement.options !== undefined)
|
|
164
|
+
config.options = placement.options;
|
|
165
|
+
return config;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// packages/adapter-pg/src/savepoint-gate.ts
|
|
169
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
170
|
+
|
|
96
171
|
// packages/adapter-pg/src/savepoints.ts
|
|
97
172
|
function createSavepointCounter() {
|
|
98
173
|
return { n: 0 };
|
|
@@ -101,26 +176,123 @@ function nextSavepointName(params) {
|
|
|
101
176
|
return `vibeorm_sp_${params.counter.n++}`;
|
|
102
177
|
}
|
|
103
178
|
|
|
179
|
+
// packages/adapter-pg/src/savepoint-gate.ts
|
|
180
|
+
var CURRENT = new AsyncLocalStorage;
|
|
181
|
+
function createSavepointScope() {
|
|
182
|
+
return { depth: 0, state: { counter: createSavepointCounter(), open: null } };
|
|
183
|
+
}
|
|
184
|
+
function runRootScope(params) {
|
|
185
|
+
return CURRENT.run(params.scope, params.body);
|
|
186
|
+
}
|
|
187
|
+
function scopeTurn(params) {
|
|
188
|
+
const open = params.scope.state.open;
|
|
189
|
+
if (mayProceed({ scope: params.scope, open }))
|
|
190
|
+
return;
|
|
191
|
+
return waitForTurn({ scope: params.scope });
|
|
192
|
+
}
|
|
193
|
+
async function runNestedSavepoint(params) {
|
|
194
|
+
const { scope, execute, body } = params;
|
|
195
|
+
const node = await acquire({ scope });
|
|
196
|
+
try {
|
|
197
|
+
if (execute !== undefined)
|
|
198
|
+
await execute(`SAVEPOINT ${node.name}`);
|
|
199
|
+
let result;
|
|
200
|
+
try {
|
|
201
|
+
result = await CURRENT.run(node.scope, () => body(node.scope));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
await drainDescendants({ node });
|
|
204
|
+
if (execute !== undefined) {
|
|
205
|
+
try {
|
|
206
|
+
await execute(`ROLLBACK TO SAVEPOINT ${node.name}`);
|
|
207
|
+
} catch {}
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
await drainDescendants({ node });
|
|
212
|
+
if (execute !== undefined)
|
|
213
|
+
await execute(`RELEASE SAVEPOINT ${node.name}`);
|
|
214
|
+
return result;
|
|
215
|
+
} finally {
|
|
216
|
+
closeNode({ node });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function mayProceed(params) {
|
|
220
|
+
const { scope, open } = params;
|
|
221
|
+
if (open === null)
|
|
222
|
+
return true;
|
|
223
|
+
if (open.scope === scope)
|
|
224
|
+
return true;
|
|
225
|
+
if (open.scope.depth < scope.depth)
|
|
226
|
+
return true;
|
|
227
|
+
return CURRENT.getStore() === open.scope;
|
|
228
|
+
}
|
|
229
|
+
async function waitForTurn(params) {
|
|
230
|
+
for (;; ) {
|
|
231
|
+
const open = params.scope.state.open;
|
|
232
|
+
if (open === null || mayProceed({ scope: params.scope, open }))
|
|
233
|
+
return;
|
|
234
|
+
await open.settled;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async function acquire(params) {
|
|
238
|
+
const { scope } = params;
|
|
239
|
+
for (;; ) {
|
|
240
|
+
const open = scope.state.open;
|
|
241
|
+
if (mayProceed({ scope, open }))
|
|
242
|
+
return pushNode({ scope, parent: open });
|
|
243
|
+
await open.settled;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function pushNode(params) {
|
|
247
|
+
const { scope, parent } = params;
|
|
248
|
+
const state = scope.state;
|
|
249
|
+
let settle = () => {
|
|
250
|
+
return;
|
|
251
|
+
};
|
|
252
|
+
const settled = new Promise((resolve) => {
|
|
253
|
+
settle = () => resolve();
|
|
254
|
+
});
|
|
255
|
+
const node = {
|
|
256
|
+
scope: { depth: (parent === null ? 0 : parent.scope.depth) + 1, state },
|
|
257
|
+
name: nextSavepointName({ counter: state.counter }),
|
|
258
|
+
parent,
|
|
259
|
+
settled,
|
|
260
|
+
settle
|
|
261
|
+
};
|
|
262
|
+
state.open = node;
|
|
263
|
+
return node;
|
|
264
|
+
}
|
|
265
|
+
function closeNode(params) {
|
|
266
|
+
const { node } = params;
|
|
267
|
+
const state = node.scope.state;
|
|
268
|
+
if (state.open === node)
|
|
269
|
+
state.open = node.parent;
|
|
270
|
+
node.settle();
|
|
271
|
+
}
|
|
272
|
+
async function drainDescendants(params) {
|
|
273
|
+
const state = params.node.scope.state;
|
|
274
|
+
for (;; ) {
|
|
275
|
+
const open = state.open;
|
|
276
|
+
if (open === null || open === params.node || open.scope.depth <= params.node.scope.depth)
|
|
277
|
+
return;
|
|
278
|
+
await open.settled;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
104
282
|
// packages/adapter-pg/src/statement-cache.ts
|
|
105
283
|
function createStatementNameCache(params) {
|
|
106
|
-
const
|
|
284
|
+
const max = Math.max(0, params.max);
|
|
107
285
|
const names = new Map;
|
|
108
286
|
let counter = 0;
|
|
109
287
|
return {
|
|
110
288
|
getName(nameParams) {
|
|
111
289
|
const { text } = nameParams;
|
|
112
290
|
const existing = names.get(text);
|
|
113
|
-
if (existing !== undefined)
|
|
114
|
-
names.delete(text);
|
|
115
|
-
names.set(text, existing);
|
|
291
|
+
if (existing !== undefined)
|
|
116
292
|
return existing;
|
|
117
|
-
|
|
293
|
+
if (names.size >= max)
|
|
294
|
+
return;
|
|
118
295
|
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
296
|
names.set(text, name);
|
|
125
297
|
return name;
|
|
126
298
|
},
|
|
@@ -130,8 +302,127 @@ function createStatementNameCache(params) {
|
|
|
130
302
|
};
|
|
131
303
|
}
|
|
132
304
|
|
|
305
|
+
// packages/adapter-pg/src/transaction-budget.ts
|
|
306
|
+
import { VibeError as VibeError3 } from "@vibeorm/schema";
|
|
307
|
+
var DEFAULT_DEADLINE_CLOCK = () => Date.now();
|
|
308
|
+
function validateTransactionDeadline(params) {
|
|
309
|
+
const { options, support, provider } = params;
|
|
310
|
+
const deadline = options?.deadline;
|
|
311
|
+
if (deadline === undefined)
|
|
312
|
+
return;
|
|
313
|
+
if (support === "unsupported") {
|
|
314
|
+
throw new VibeError3({
|
|
315
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
316
|
+
message: `${provider} cannot enforce a transaction deadline \u2014 remove the \`deadline\` option`,
|
|
317
|
+
meta: { provider, feature: "transactionDeadline", supported: support }
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
if (!Number.isInteger(deadline.totalMs) || deadline.totalMs <= 0) {
|
|
321
|
+
throw new VibeError3({
|
|
322
|
+
code: "VIBE_VALIDATION",
|
|
323
|
+
message: `transaction deadline \`totalMs\` must be a positive integer of milliseconds, received ${String(deadline.totalMs)}`,
|
|
324
|
+
meta: { provider, feature: "transactionDeadline", totalMs: deadline.totalMs }
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
const requested = deadline.enforcement ?? "between-statements";
|
|
328
|
+
if (requested === "cancel-running-statements" && support !== "cancel-running-statements") {
|
|
329
|
+
throw new VibeError3({
|
|
330
|
+
code: "VIBE_UNSUPPORTED_CAPABILITY",
|
|
331
|
+
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',
|
|
332
|
+
meta: { provider, feature: "transactionDeadline", requested, supported: support }
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
function startTransactionBudget(params) {
|
|
337
|
+
const { options, provider } = params;
|
|
338
|
+
const clock = params.clock ?? DEFAULT_DEADLINE_CLOCK;
|
|
339
|
+
const deadline = options?.deadline;
|
|
340
|
+
const totalMs = deadline === undefined ? null : deadline.totalMs;
|
|
341
|
+
const enforcement = deadline === undefined ? null : deadline.enforcement ?? "between-statements";
|
|
342
|
+
const expiresAt = totalMs === null ? null : clock() + totalMs;
|
|
343
|
+
let outcome = null;
|
|
344
|
+
const budget = {
|
|
345
|
+
enforcement,
|
|
346
|
+
totalMs,
|
|
347
|
+
statementTimeoutMs: options?.timeout ?? null,
|
|
348
|
+
remainingMs() {
|
|
349
|
+
return expiresAt === null ? null : Math.max(0, expiresAt - clock());
|
|
350
|
+
},
|
|
351
|
+
expired() {
|
|
352
|
+
return expiresAt !== null && clock() >= expiresAt;
|
|
353
|
+
},
|
|
354
|
+
assertUsable(assertParams) {
|
|
355
|
+
if (outcome !== null) {
|
|
356
|
+
throw transactionClosedError({ provider, outcome, stage: assertParams.stage });
|
|
357
|
+
}
|
|
358
|
+
if (expiresAt !== null && clock() >= expiresAt) {
|
|
359
|
+
throw transactionDeadlineError({
|
|
360
|
+
provider,
|
|
361
|
+
stage: assertParams.stage,
|
|
362
|
+
totalMs,
|
|
363
|
+
overdueMs: clock() - expiresAt
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
close(closeParams) {
|
|
368
|
+
if (outcome === null)
|
|
369
|
+
outcome = closeParams.outcome;
|
|
370
|
+
},
|
|
371
|
+
get outcome() {
|
|
372
|
+
return outcome;
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
return budget;
|
|
376
|
+
}
|
|
377
|
+
function engineStatementBudgetMs(params) {
|
|
378
|
+
const { budget } = params;
|
|
379
|
+
if (budget.enforcement !== "cancel-running-statements")
|
|
380
|
+
return null;
|
|
381
|
+
const remaining = budget.remainingMs();
|
|
382
|
+
if (remaining === null)
|
|
383
|
+
return null;
|
|
384
|
+
const totalBound = Math.max(1, Math.ceil(remaining));
|
|
385
|
+
return budget.statementTimeoutMs !== null && budget.statementTimeoutMs > 0 ? Math.min(totalBound, budget.statementTimeoutMs) : totalBound;
|
|
386
|
+
}
|
|
387
|
+
function transactionDeadlineError(params) {
|
|
388
|
+
const { provider, stage, totalMs, overdueMs } = params;
|
|
389
|
+
const what = stage === "commit" ? "the transaction was rolled back instead of committed" : "the statement was never sent and the transaction is being rolled back";
|
|
390
|
+
return new VibeError3({
|
|
391
|
+
code: "VIBE_TRANSACTION",
|
|
392
|
+
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.",
|
|
393
|
+
meta: { provider, reason: "transaction-deadline", stage, totalMs, overdueMs: Math.max(0, Math.round(overdueMs)) }
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function transactionClosedError(params) {
|
|
397
|
+
const { provider, outcome, stage } = params;
|
|
398
|
+
return new VibeError3({
|
|
399
|
+
code: "VIBE_TRANSACTION",
|
|
400
|
+
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",
|
|
401
|
+
meta: { provider, reason: "transaction-closed", outcome, stage }
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function transactionOutcomeUnknownError(params) {
|
|
405
|
+
const { provider, totalMs, cause } = params;
|
|
406
|
+
return new VibeError3({
|
|
407
|
+
code: "VIBE_TRANSACTION",
|
|
408
|
+
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.",
|
|
409
|
+
meta: {
|
|
410
|
+
provider,
|
|
411
|
+
reason: "transaction-outcome-unknown",
|
|
412
|
+
outcome: "unknown",
|
|
413
|
+
totalMs,
|
|
414
|
+
rollbackFailed: true
|
|
415
|
+
},
|
|
416
|
+
cause
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
function isTransactionDeadlineError(error) {
|
|
420
|
+
const reason = error?.meta?.reason;
|
|
421
|
+
return reason === "transaction-deadline";
|
|
422
|
+
}
|
|
423
|
+
|
|
133
424
|
// packages/adapter-pg/src/transaction-sql.ts
|
|
134
|
-
import { VibeError as
|
|
425
|
+
import { VibeError as VibeError4 } from "@vibeorm/schema";
|
|
135
426
|
var ISOLATION_LEVEL_SQL = Object.freeze({
|
|
136
427
|
ReadCommitted: "READ COMMITTED",
|
|
137
428
|
RepeatableRead: "REPEATABLE READ",
|
|
@@ -144,7 +435,7 @@ function beginStatement(params) {
|
|
|
144
435
|
function statementTimeoutStatement(params) {
|
|
145
436
|
const { timeout } = params;
|
|
146
437
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
147
|
-
throw new
|
|
438
|
+
throw new VibeError4({
|
|
148
439
|
code: "VIBE_VALIDATION",
|
|
149
440
|
message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
|
|
150
441
|
meta: { timeout }
|
|
@@ -170,6 +461,8 @@ function refuseNestedTransactionOptions(params) {
|
|
|
170
461
|
passed.push("isolationLevel");
|
|
171
462
|
if (options.timeout !== undefined)
|
|
172
463
|
passed.push("timeout");
|
|
464
|
+
if (options.deadline !== undefined)
|
|
465
|
+
passed.push("deadline");
|
|
173
466
|
if (passed.length === 0)
|
|
174
467
|
return;
|
|
175
468
|
const meta = { provider, nested: true };
|
|
@@ -177,14 +470,17 @@ function refuseNestedTransactionOptions(params) {
|
|
|
177
470
|
meta.isolationLevel = options.isolationLevel;
|
|
178
471
|
if (options.timeout !== undefined)
|
|
179
472
|
meta.timeout = options.timeout;
|
|
180
|
-
|
|
473
|
+
if (options.deadline !== undefined)
|
|
474
|
+
meta.deadline = options.deadline;
|
|
475
|
+
throw new VibeError4({
|
|
181
476
|
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,
|
|
477
|
+
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
478
|
meta
|
|
184
479
|
});
|
|
185
480
|
}
|
|
186
481
|
|
|
187
482
|
// packages/adapter-pg/src/index.ts
|
|
483
|
+
var POOL_STATEMENT_NAMES = new WeakMap;
|
|
188
484
|
function guardBackgroundErrors(params) {
|
|
189
485
|
const { client } = params;
|
|
190
486
|
if (typeof client.on !== "function")
|
|
@@ -200,9 +496,62 @@ function guardBackgroundErrors(params) {
|
|
|
200
496
|
var DIALECT = "postgres";
|
|
201
497
|
var PROVIDER = "postgresql";
|
|
202
498
|
var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
|
|
499
|
+
var BUDGETS = Object.freeze({
|
|
500
|
+
acquireTimeout: true,
|
|
501
|
+
statementTimeout: "all-statements",
|
|
502
|
+
transactionDeadline: "cancel-running-statements"
|
|
503
|
+
});
|
|
504
|
+
var NUMERIC_ARRAY_OID = 1231;
|
|
505
|
+
var TEXT_ARRAY_OID = 1009;
|
|
506
|
+
var TIMESTAMP_OID = 1114;
|
|
507
|
+
var TIMESTAMP_ARRAY_OID = 1115;
|
|
508
|
+
var DATE_OID = 1082;
|
|
509
|
+
var DATE_ARRAY_OID = 1182;
|
|
510
|
+
var driverTypeParser = pg.types.getTypeParser;
|
|
511
|
+
var ZONE_LESS_TEXT = /^\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)?$/;
|
|
512
|
+
function parseZoneLessAsUtc(value) {
|
|
513
|
+
if (ZONE_LESS_TEXT.test(value)) {
|
|
514
|
+
const iso = value.includes(" ") ? `${value.replace(" ", "T").replace(/(\.\d{3})\d+$/, "$1")}Z` : value;
|
|
515
|
+
const parsed = new Date(iso);
|
|
516
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
517
|
+
return parsed;
|
|
518
|
+
}
|
|
519
|
+
return driverTypeParser(TIMESTAMP_OID, "text")(value);
|
|
520
|
+
}
|
|
521
|
+
function parseZoneLessArrayAsUtc(value) {
|
|
522
|
+
const mapElements = (element) => {
|
|
523
|
+
if (element === null)
|
|
524
|
+
return null;
|
|
525
|
+
if (Array.isArray(element))
|
|
526
|
+
return element.map(mapElements);
|
|
527
|
+
return parseZoneLessAsUtc(String(element));
|
|
528
|
+
};
|
|
529
|
+
return mapElements(driverTypeParser(TEXT_ARRAY_OID, "text")(value));
|
|
530
|
+
}
|
|
531
|
+
var EXACT_TYPES = {
|
|
532
|
+
getTypeParser(oid, format) {
|
|
533
|
+
if (format === undefined || format === "text") {
|
|
534
|
+
if (oid === NUMERIC_ARRAY_OID)
|
|
535
|
+
return driverTypeParser(TEXT_ARRAY_OID, "text");
|
|
536
|
+
if (oid === TIMESTAMP_OID || oid === DATE_OID)
|
|
537
|
+
return parseZoneLessAsUtc;
|
|
538
|
+
if (oid === TIMESTAMP_ARRAY_OID || oid === DATE_ARRAY_OID)
|
|
539
|
+
return parseZoneLessArrayAsUtc;
|
|
540
|
+
}
|
|
541
|
+
return driverTypeParser(oid, format);
|
|
542
|
+
}
|
|
543
|
+
};
|
|
203
544
|
function pgAdapter(options) {
|
|
204
545
|
const usePrepared = options?.preparedStatements ?? false;
|
|
205
|
-
const
|
|
546
|
+
const stmtCacheMax = options?.stmtCacheMax ?? 1000;
|
|
547
|
+
const planCacheMode = options?.planCacheMode === undefined ? "auto" : assertPlanCacheMode({ value: options.planCacheMode });
|
|
548
|
+
if (planCacheMode !== "auto" && options?.pool !== undefined) {
|
|
549
|
+
throw new VibeError5({
|
|
550
|
+
code: "VIBE_CONFIG",
|
|
551
|
+
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',
|
|
552
|
+
meta: { planCacheMode }
|
|
553
|
+
});
|
|
554
|
+
}
|
|
206
555
|
let pool = null;
|
|
207
556
|
let ownsPool = false;
|
|
208
557
|
let rawSession = null;
|
|
@@ -215,25 +564,33 @@ function pgAdapter(options) {
|
|
|
215
564
|
ownsPool = false;
|
|
216
565
|
return pool;
|
|
217
566
|
}
|
|
218
|
-
const config = {
|
|
567
|
+
const config = buildPoolConfig({
|
|
219
568
|
connectionString: options?.connectionString ?? process.env.DATABASE_URL,
|
|
220
|
-
max: options?.max ?? 10
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
if (options?.connectionTimeout !== undefined) {
|
|
226
|
-
config.connectionTimeoutMillis = options.connectionTimeout;
|
|
227
|
-
}
|
|
569
|
+
max: options?.max ?? 10,
|
|
570
|
+
...options?.statementTimeout === undefined ? {} : { statementTimeout: options.statementTimeout },
|
|
571
|
+
...options?.connectionTimeout === undefined ? {} : { connectionTimeout: options.connectionTimeout },
|
|
572
|
+
planCacheMode
|
|
573
|
+
});
|
|
228
574
|
const PoolCtor = pg.Pool;
|
|
229
575
|
pool = new PoolCtor(config);
|
|
230
576
|
pool.on?.("error", () => {});
|
|
231
577
|
ownsPool = true;
|
|
232
578
|
return pool;
|
|
233
579
|
}
|
|
580
|
+
function statementNamesFor(params) {
|
|
581
|
+
const existing = POOL_STATEMENT_NAMES.get(params.pool);
|
|
582
|
+
if (existing !== undefined)
|
|
583
|
+
return existing;
|
|
584
|
+
const created = createStatementNameCache({ max: stmtCacheMax });
|
|
585
|
+
POOL_STATEMENT_NAMES.set(params.pool, created);
|
|
586
|
+
return created;
|
|
587
|
+
}
|
|
234
588
|
function buildQueryConfig(params) {
|
|
235
589
|
const values = serializeParams({ values: params.values });
|
|
236
|
-
|
|
590
|
+
if (!usePrepared)
|
|
591
|
+
return { text: params.text, values, types: EXACT_TYPES };
|
|
592
|
+
const name = statementNamesFor({ pool: getPool() }).getName({ text: params.text });
|
|
593
|
+
return name === undefined ? { text: params.text, values, types: EXACT_TYPES } : { name, text: params.text, values, types: EXACT_TYPES };
|
|
237
594
|
}
|
|
238
595
|
async function runExecute(params) {
|
|
239
596
|
const config = buildQueryConfig({ text: params.text, values: params.values });
|
|
@@ -242,35 +599,46 @@ function pgAdapter(options) {
|
|
|
242
599
|
}
|
|
243
600
|
async function runExecuteUnsafe(params) {
|
|
244
601
|
const values = params.values === undefined ? undefined : serializeParamsRaw({ values: params.values });
|
|
245
|
-
const result = await runMapped(() => params.queryable.query({ text: params.text, values }));
|
|
602
|
+
const result = await runMapped(() => params.queryable.query({ text: params.text, values, types: EXACT_TYPES }));
|
|
246
603
|
return { rows: result.rows, affectedRows: result.rowCount ?? 0 };
|
|
247
604
|
}
|
|
248
605
|
function createClientAdapter(params) {
|
|
249
|
-
const { client,
|
|
606
|
+
const { client, scope, budget } = params;
|
|
607
|
+
async function applyEngineBudget() {
|
|
608
|
+
const ms = engineStatementBudgetMs({ budget });
|
|
609
|
+
if (ms === null)
|
|
610
|
+
return;
|
|
611
|
+
await runMapped(() => client.query({ text: statementTimeoutStatement({ timeout: ms }) }));
|
|
612
|
+
}
|
|
250
613
|
return {
|
|
251
614
|
dialect: DIALECT,
|
|
252
615
|
provider: PROVIDER,
|
|
253
616
|
wire: WIRE,
|
|
617
|
+
budgets: BUDGETS,
|
|
254
618
|
async execute(execParams) {
|
|
619
|
+
const turn = scopeTurn({ scope });
|
|
620
|
+
if (turn !== undefined)
|
|
621
|
+
await turn;
|
|
622
|
+
budget.assertUsable({ stage: "statement" });
|
|
623
|
+
await applyEngineBudget();
|
|
255
624
|
return runExecute({ queryable: client, ...execParams });
|
|
256
625
|
},
|
|
257
626
|
async executeUnsafe(execParams) {
|
|
627
|
+
const turn = scopeTurn({ scope });
|
|
628
|
+
if (turn !== undefined)
|
|
629
|
+
await turn;
|
|
630
|
+
budget.assertUsable({ stage: "statement" });
|
|
631
|
+
await applyEngineBudget();
|
|
258
632
|
return runExecuteUnsafe({ queryable: client, ...execParams });
|
|
259
633
|
},
|
|
260
634
|
async transaction(fn, nestedOptions) {
|
|
261
635
|
refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
} catch (error) {
|
|
269
|
-
try {
|
|
270
|
-
await client.query({ text: `ROLLBACK TO SAVEPOINT ${savepoint}` });
|
|
271
|
-
} catch {}
|
|
272
|
-
throw error;
|
|
273
|
-
}
|
|
636
|
+
budget.assertUsable({ stage: "nested" });
|
|
637
|
+
return runNestedSavepoint({
|
|
638
|
+
scope,
|
|
639
|
+
execute: (statement) => runMapped(() => client.query({ text: statement })),
|
|
640
|
+
body: (child) => fn(createClientAdapter({ client, scope: child, budget }))
|
|
641
|
+
});
|
|
274
642
|
},
|
|
275
643
|
async connect() {},
|
|
276
644
|
async disconnect() {},
|
|
@@ -283,6 +651,7 @@ function pgAdapter(options) {
|
|
|
283
651
|
dialect: DIALECT,
|
|
284
652
|
provider: PROVIDER,
|
|
285
653
|
wire: WIRE,
|
|
654
|
+
budgets: BUDGETS,
|
|
286
655
|
async execute(params) {
|
|
287
656
|
return runExecute({ queryable: getPool(), ...params });
|
|
288
657
|
},
|
|
@@ -320,26 +689,49 @@ function pgAdapter(options) {
|
|
|
320
689
|
return runExecuteUnsafe({ queryable: getPool(), ...params });
|
|
321
690
|
},
|
|
322
691
|
async transaction(fn, transactionOptions) {
|
|
692
|
+
validateTransactionDeadline({
|
|
693
|
+
options: transactionOptions,
|
|
694
|
+
support: BUDGETS.transactionDeadline,
|
|
695
|
+
provider: PROVIDER
|
|
696
|
+
});
|
|
323
697
|
const client = await runMapped(() => getPool().connect());
|
|
324
698
|
const unguard = guardBackgroundErrors({ client });
|
|
325
|
-
const
|
|
699
|
+
const scope = createSavepointScope();
|
|
700
|
+
const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
|
|
701
|
+
let discard = false;
|
|
326
702
|
try {
|
|
327
703
|
await runMapped(() => client.query({ text: beginStatement({ isolationLevel: transactionOptions?.isolationLevel }) }));
|
|
328
704
|
if (transactionOptions?.timeout !== undefined) {
|
|
329
705
|
const text = statementTimeoutStatement({ timeout: transactionOptions.timeout });
|
|
330
706
|
await runMapped(() => client.query({ text }));
|
|
331
707
|
}
|
|
332
|
-
const result = await
|
|
708
|
+
const result = await runRootScope({
|
|
709
|
+
scope,
|
|
710
|
+
body: () => fn(createClientAdapter({ client, scope, budget }))
|
|
711
|
+
});
|
|
712
|
+
budget.assertUsable({ stage: "commit" });
|
|
333
713
|
await runMapped(() => client.query({ text: "COMMIT" }));
|
|
714
|
+
budget.close({ outcome: "committed" });
|
|
334
715
|
return result;
|
|
335
716
|
} catch (error) {
|
|
336
717
|
try {
|
|
337
718
|
await client.query({ text: "ROLLBACK" });
|
|
338
|
-
|
|
719
|
+
budget.close({ outcome: "rolled-back" });
|
|
720
|
+
} catch {
|
|
721
|
+
discard = true;
|
|
722
|
+
budget.close({ outcome: "unknown" });
|
|
723
|
+
if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
|
|
724
|
+
throw transactionOutcomeUnknownError({
|
|
725
|
+
provider: PROVIDER,
|
|
726
|
+
totalMs: budget.totalMs,
|
|
727
|
+
cause: error
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
}
|
|
339
731
|
throw error;
|
|
340
732
|
} finally {
|
|
341
733
|
unguard();
|
|
342
|
-
client.release();
|
|
734
|
+
client.release(discard);
|
|
343
735
|
}
|
|
344
736
|
},
|
|
345
737
|
async connect() {
|
|
@@ -370,22 +762,32 @@ function pgAdapter(options) {
|
|
|
370
762
|
};
|
|
371
763
|
}
|
|
372
764
|
export {
|
|
765
|
+
validateTransactionDeadline,
|
|
766
|
+
transactionOutcomeUnknownError,
|
|
767
|
+
transactionDeadlineError,
|
|
768
|
+
transactionClosedError,
|
|
373
769
|
statementTimeoutStatement,
|
|
770
|
+
startTransactionBudget,
|
|
374
771
|
serializeParamsRaw,
|
|
375
772
|
serializeParams,
|
|
376
773
|
refuseNestedTransactionOptions,
|
|
774
|
+
placePlanCacheMode,
|
|
377
775
|
pgErrorSqlstate,
|
|
378
776
|
pgAdapter,
|
|
379
777
|
nextSavepointName,
|
|
380
778
|
mapPgDriverError,
|
|
779
|
+
isTransactionDeadlineError,
|
|
780
|
+
engineStatementBudgetMs,
|
|
381
781
|
createStatementNameCache,
|
|
382
782
|
createSavepointCounter,
|
|
383
783
|
classifyRawTransactionControl,
|
|
784
|
+
buildPoolConfig,
|
|
384
785
|
beginStatement,
|
|
385
786
|
PgArrayParam,
|
|
386
787
|
PG_SQLSTATE_REASONS,
|
|
387
788
|
PG_SQLSTATE_ERROR_CODES,
|
|
388
|
-
ISOLATION_LEVEL_SQL
|
|
789
|
+
ISOLATION_LEVEL_SQL,
|
|
790
|
+
DEFAULT_DEADLINE_CLOCK
|
|
389
791
|
};
|
|
390
792
|
|
|
391
|
-
//# debugId=
|
|
793
|
+
//# debugId=ABBE5E12E130DBA764756E2164756E21
|