@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/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" || value instanceof Date || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
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 { max } = params;
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 VibeError2 } from "@vibeorm/schema";
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 VibeError2({
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
- throw new VibeError2({
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, and a nested timeout is not enforceable; pass options on the top-level $transaction`,
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 statementNames = createStatementNameCache({ max: options?.stmtCacheMax ?? 1000 });
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
- if (options?.statementTimeout !== undefined) {
223
- config.statement_timeout = options.statementTimeout;
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
- return usePrepared ? { name: statementNames.getName({ text: params.text }), text: params.text, values } : { text: params.text, values };
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, savepointCounter } = params;
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
- const savepoint = nextSavepointName({ counter: savepointCounter });
263
- await runMapped(() => client.query({ text: `SAVEPOINT ${savepoint}` }));
264
- try {
265
- const result = await fn(createClientAdapter({ client, savepointCounter }));
266
- await runMapped(() => client.query({ text: `RELEASE SAVEPOINT ${savepoint}` }));
267
- return result;
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 savepointCounter = createSavepointCounter();
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 fn(createClientAdapter({ client, savepointCounter }));
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
- } catch {}
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=E27D9260C6E4F42764756E2164756E21
793
+ //# debugId=ABBE5E12E130DBA764756E2164756E21