@vibeorm/adapter-bun 2.4.1 → 4.0.0

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
@@ -2,39 +2,57 @@
2
2
  // packages/adapter-bun/src/driver.ts
3
3
  var {SQL } = globalThis.Bun;
4
4
  function createBunSql(params) {
5
- return params.url === undefined ? new SQL(params.options) : new SQL(params.url, params.options);
5
+ return new SQL(params.url, params.options);
6
6
  }
7
7
 
8
8
  // packages/adapter-bun/src/config.ts
9
+ var MINIMUM_BUN_VERSION = "1.4.1";
10
+ var MAX_BIND_PARAMETERS = 65535;
9
11
  var SESSION_CONNECTION = Object.freeze({
10
12
  max: 1,
11
13
  closeGraceSeconds: 0.001
12
14
  });
13
15
 
16
+ // packages/adapter-bun/src/bun-version.ts
17
+ import { VibeError } from "@vibeorm/schema";
18
+ function refuseUnsupportedBun(params) {
19
+ if (Bun.semver.satisfies(params.version, `>=${MINIMUM_BUN_VERSION}`))
20
+ return;
21
+ throw new VibeError({
22
+ code: "VIBE_CONFIG",
23
+ message: `@vibeorm/adapter-bun requires Bun ${MINIMUM_BUN_VERSION} or newer (running ${params.version}): older bun:sql ` + "releases hang after COPY, read timestamps without a time zone in host-local time and turn infinity dates into Invalid Date. " + "Upgrade Bun, or use @vibeorm/adapter-pg.",
24
+ meta: { bunVersion: params.version, minimumBunVersion: MINIMUM_BUN_VERSION }
25
+ });
26
+ }
27
+
14
28
  // packages/adapter-bun/src/index.ts
15
29
  import { postgresDialect, renderDiagnostic } from "@vibeorm/sql";
16
30
  import {
17
31
  abortTransactionOnServerFailure,
32
+ createDeferredControl,
18
33
  observeRowChanges,
19
34
  readRowChanges,
20
35
  statementRowChanges,
21
- markSavepointOpened,
36
+ createAdapterLifecycleGate,
22
37
  runPinnedSession,
38
+ runShutdownCleanup,
39
+ transactionLifecycleRefusal,
23
40
  createSavepointScope,
24
- savepointRollbackFailedError,
25
41
  engineStatementBudgetMs,
26
- isTransactionDeadlineError,
42
+ markTransactionAborted,
27
43
  runNestedSavepoint,
28
44
  runRootScope,
29
45
  scopeTurn,
46
+ settleFailedTransaction,
30
47
  startTransactionBudget,
31
- transactionOutcomeUnknownError,
32
- validateTransactionDeadline
48
+ transactionAbortedError,
49
+ validateTransactionDeadline,
50
+ readOnlyRequested
33
51
  } from "@vibeorm/runtime/adapter-kit";
34
- import { VibeError as VibeError6 } from "@vibeorm/schema";
52
+ import { VibeError as VibeError7 } from "@vibeorm/schema";
35
53
 
36
54
  // packages/adapter-bun/src/connection-url.ts
37
- import { VibeError } from "@vibeorm/schema";
55
+ import { VibeError as VibeError2 } from "@vibeorm/schema";
38
56
  function buildStartupOptions(params) {
39
57
  const { planCacheMode, statementTimeout } = params;
40
58
  const parts = [];
@@ -42,7 +60,7 @@ function buildStartupOptions(params) {
42
60
  parts.push(`-c plan_cache_mode=${planCacheMode}`);
43
61
  if (statementTimeout !== undefined) {
44
62
  if (!Number.isInteger(statementTimeout) || statementTimeout < 0) {
45
- throw new VibeError({
63
+ throw new VibeError2({
46
64
  code: "VIBE_VALIDATION",
47
65
  message: `statementTimeout must be a non-negative integer of milliseconds, received ${String(statementTimeout)}`,
48
66
  meta: { statementTimeout }
@@ -52,9 +70,28 @@ function buildStartupOptions(params) {
52
70
  }
53
71
  return parts.length === 0 ? undefined : parts.join(" ");
54
72
  }
73
+ function takeConnectTimeout(params) {
74
+ if (!URL.canParse(params.url))
75
+ return { url: params.url };
76
+ const url = new URL(params.url);
77
+ const raw = url.searchParams.get("connect_timeout");
78
+ if (raw === null)
79
+ return { url: params.url };
80
+ const seconds = Number(raw.trim());
81
+ if (raw.trim() === "" || !Number.isInteger(seconds)) {
82
+ throw new VibeError2({
83
+ code: "VIBE_VALIDATION",
84
+ message: `connect_timeout in the connection URL must be an integer number of seconds, received "${raw}"`,
85
+ meta: { connectTimeout: raw }
86
+ });
87
+ }
88
+ url.searchParams.delete("connect_timeout");
89
+ url.search = url.searchParams.toString().replace(/\+/g, "%20");
90
+ return seconds > 0 ? { url: url.toString(), seconds } : { url: url.toString() };
91
+ }
55
92
  function applyStartupOptions(params) {
56
93
  if (!URL.canParse(params.url)) {
57
- throw new VibeError({ code: "VIBE_VALIDATION", message: "invalid PostgreSQL connection URL" });
94
+ throw new VibeError2({ code: "VIBE_VALIDATION", message: "invalid PostgreSQL connection URL" });
58
95
  }
59
96
  const url = new URL(params.url);
60
97
  const merged = [...url.searchParams.getAll("options"), params.startupOptions].filter(Boolean).join(" ");
@@ -64,7 +101,7 @@ function applyStartupOptions(params) {
64
101
  }
65
102
 
66
103
  // packages/adapter-bun/src/errors.ts
67
- import { VibeError as VibeError2 } from "@vibeorm/schema";
104
+ import { VibeError as VibeError3 } from "@vibeorm/schema";
68
105
  var BUN_SQLSTATE_ERROR_CODES = Object.freeze({
69
106
  "23P01": "VIBE_EXCLUSION_VIOLATION",
70
107
  "23505": "VIBE_UNIQUE_VIOLATION",
@@ -81,16 +118,18 @@ var BUN_SQLSTATE_ERROR_CODES = Object.freeze({
81
118
  "40001": "VIBE_TRANSACTION",
82
119
  "40P01": "VIBE_TRANSACTION",
83
120
  "57014": "VIBE_TRANSACTION",
84
- "55P03": "VIBE_LOCK_NOT_AVAILABLE"
121
+ "55P03": "VIBE_LOCK_NOT_AVAILABLE",
122
+ "25006": "VIBE_TRANSACTION"
85
123
  });
86
124
  var BUN_SQLSTATE_REASONS = Object.freeze({
87
125
  "23P01": "exclusion-violation",
88
126
  "57014": "timeout",
89
127
  "55P03": "lock-not-available",
90
128
  "40P01": "deadlock",
91
- "40001": "serialization-failure"
129
+ "40001": "serialization-failure",
130
+ "25006": "read-only-transaction"
92
131
  });
93
- var SQLSTATE_PATTERN = /^[0-9A-Z]{5}$/;
132
+ var SQLSTATE_PATTERN = /^(?=[A-Z]*\d)[0-9A-Z]{5}$/;
94
133
  function readStringField(params) {
95
134
  const value = params.source[params.key];
96
135
  return typeof value === "string" && value.length > 0 ? value : undefined;
@@ -109,7 +148,7 @@ function bunErrorSqlstate(params) {
109
148
  }
110
149
  function mapBunDriverError(params) {
111
150
  const { error } = params;
112
- if (error instanceof VibeError2)
151
+ if (error instanceof VibeError3)
113
152
  return error;
114
153
  const source = error !== null && typeof error === "object" ? error : {};
115
154
  const sqlstate = bunErrorSqlstate({ error });
@@ -133,7 +172,7 @@ function mapBunDriverError(params) {
133
172
  if (driverCode !== undefined && driverCode !== sqlstate)
134
173
  meta.driverCode = driverCode;
135
174
  const message = readStringField({ source, key: "message" }) ?? (typeof error === "string" ? error : "unknown bun:sql driver error");
136
- return new VibeError2({ code, message, meta, cause: error });
175
+ return new VibeError3({ code, message, meta, cause: error });
137
176
  }
138
177
  async function runMapped(fn) {
139
178
  try {
@@ -144,7 +183,8 @@ async function runMapped(fn) {
144
183
  }
145
184
 
146
185
  // packages/adapter-bun/src/params.ts
147
- import { VibeError as VibeError3 } from "@vibeorm/schema";
186
+ import { Buffer } from "buffer";
187
+ import { VibeError as VibeError4 } from "@vibeorm/schema";
148
188
  function quoteElement(value) {
149
189
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
150
190
  }
@@ -157,8 +197,11 @@ function toPgArrayLiteral(values) {
157
197
  }
158
198
  if (value instanceof Date)
159
199
  return quoteElement(value.toISOString());
200
+ if (value instanceof Uint8Array) {
201
+ return quoteElement(`\\x${Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("hex")}`);
202
+ }
160
203
  if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) {
161
- throw new VibeError3({
204
+ throw new VibeError4({
162
205
  code: "VIBE_VALIDATION",
163
206
  message: "binary values are not supported inside a postgres array literal",
164
207
  meta: { driver: "bun:sql" }
@@ -181,21 +224,31 @@ function serializeDate(params) {
181
224
  return value;
182
225
  return value.toISOString();
183
226
  }
227
+ function serializeScalar(value) {
228
+ if (typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
229
+ return String(value);
230
+ if (value instanceof Date)
231
+ return serializeDate({ value });
232
+ return value;
233
+ }
234
+ function isJsonDocument(value) {
235
+ return typeof value === "object" && value !== null && !(value instanceof Date) && !ArrayBuffer.isView(value) && !(value instanceof ArrayBuffer);
236
+ }
184
237
  function serializeParams(params) {
185
- return params.values.map((value) => value instanceof Date ? serializeDate({ value }) : value);
238
+ const stringifyDocuments = params.namedStatements !== true;
239
+ return params.values.map((value) => stringifyDocuments && isJsonDocument(value) ? JSON.stringify(value) : serializeScalar(value));
186
240
  }
187
241
  function serializeParamsRaw(params) {
242
+ const stringifyDocuments = params.namedStatements !== true;
188
243
  return params.values.map((value) => {
189
244
  if (Array.isArray(value))
190
245
  return toPgArrayLiteral(value);
191
- if (value instanceof Date)
192
- return serializeDate({ value });
193
- return value;
246
+ return stringifyDocuments && isJsonDocument(value) ? JSON.stringify(value) : serializeScalar(value);
194
247
  });
195
248
  }
196
249
 
197
250
  // packages/adapter-bun/src/template-cache.ts
198
- import { VibeError as VibeError4 } from "@vibeorm/schema";
251
+ import { VibeError as VibeError5 } from "@vibeorm/schema";
199
252
  function isIdentChar(char) {
200
253
  if (char === undefined)
201
254
  return false;
@@ -292,7 +345,7 @@ function scanPlaceholders(params) {
292
345
  }
293
346
  chunks.push(text.slice(chunkStart));
294
347
  const template = Object.assign(chunks, { raw: chunks });
295
- return { template, indices };
348
+ return { template, indices, identityOrder: indices.every((placeholder, index) => placeholder === index + 1) };
296
349
  }
297
350
  function templateChunks(params) {
298
351
  return [...scanPlaceholders(params).template];
@@ -301,7 +354,7 @@ function bindTemplateValues(params) {
301
354
  const { text, indices, values } = params;
302
355
  return indices.map((placeholder) => {
303
356
  if (placeholder < 1 || placeholder > values.length) {
304
- throw new VibeError4({
357
+ throw new VibeError5({
305
358
  code: "VIBE_VALIDATION",
306
359
  message: `bun:sql prepared statement references $${placeholder} but only ${values.length} parameter(s) were given`,
307
360
  meta: { text, placeholder, provided: values.length }
@@ -338,7 +391,7 @@ function createTemplateCache(params) {
338
391
  }
339
392
 
340
393
  // packages/adapter-bun/src/transaction-sql.ts
341
- import { VibeError as VibeError5 } from "@vibeorm/schema";
394
+ import { VibeError as VibeError6 } from "@vibeorm/schema";
342
395
  import { refuseNestedTransactionOptions } from "@vibeorm/runtime/adapter-kit";
343
396
  var ISOLATION_LEVEL_SQL = Object.freeze({
344
397
  ReadCommitted: "READ COMMITTED",
@@ -346,13 +399,23 @@ var ISOLATION_LEVEL_SQL = Object.freeze({
346
399
  Serializable: "SERIALIZABLE"
347
400
  });
348
401
  function beginStatement(params) {
349
- const { isolationLevel } = params;
350
- return isolationLevel === undefined ? "BEGIN" : `BEGIN ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[isolationLevel]}`;
402
+ const { isolationLevel, readOnly } = params;
403
+ const modes = [];
404
+ if (isolationLevel !== undefined)
405
+ modes.push(`ISOLATION LEVEL ${ISOLATION_LEVEL_SQL[isolationLevel]}`);
406
+ if (readOnly === true)
407
+ modes.push("READ ONLY");
408
+ return modes.length === 0 ? "BEGIN" : `BEGIN ${modes.join(", ")}`;
409
+ }
410
+ function openTransactionStatement(params) {
411
+ const begin = beginStatement(params);
412
+ return params.timeout === undefined ? begin : `${begin}; ${statementTimeoutStatement({ timeout: params.timeout })}`;
351
413
  }
414
+ var STATEMENT_BOUND_TEXT = "SELECT set_config('statement_timeout', $1, true)";
352
415
  function statementTimeoutStatement(params) {
353
416
  const { timeout } = params;
354
417
  if (!Number.isInteger(timeout) || timeout < 0) {
355
- throw new VibeError5({
418
+ throw new VibeError6({
356
419
  code: "VIBE_VALIDATION",
357
420
  message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
358
421
  meta: { timeout }
@@ -375,78 +438,176 @@ import { createSavepointCounter, nextSavepointName } from "@vibeorm/runtime/adap
375
438
  import {
376
439
  DEFAULT_DEADLINE_CLOCK,
377
440
  engineStatementBudgetMs as engineStatementBudgetMs2,
378
- isTransactionDeadlineError as isTransactionDeadlineError2,
441
+ isTransactionDeadlineError,
379
442
  startTransactionBudget as startTransactionBudget2,
380
443
  transactionClosedError,
381
444
  transactionDeadlineError,
382
- transactionOutcomeUnknownError as transactionOutcomeUnknownError2,
445
+ transactionOutcomeUnknownError,
383
446
  validateTransactionDeadline as validateTransactionDeadline2
384
447
  } from "@vibeorm/runtime/adapter-kit";
385
-
386
- class CallbackError extends Error {
387
- inner;
388
- constructor(inner) {
389
- super("vibeorm: transaction callback failed");
390
- this.name = "CallbackError";
391
- this.inner = inner;
392
- }
393
- }
448
+ var ignore = () => {
449
+ return;
450
+ };
394
451
  var DIALECT = "postgres";
395
452
  var PROVIDER = "postgresql";
396
453
  var BUDGETS = Object.freeze({
397
454
  acquireTimeout: false,
398
455
  statementTimeout: "all-statements",
399
- transactionDeadline: "cancel-running-statements"
456
+ transactionDeadline: "cancel-running-statements",
457
+ accessModes: Object.freeze(["readOnly"])
400
458
  });
401
459
  var WIRE_TEXT = Object.freeze({ dateTime: "native", json: "text", numbers: "native" });
402
460
  var WIRE_PARSED = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
461
+ var ROUND_TRIPS = "network";
462
+ function requireUrl(params) {
463
+ const url = params.options?.url;
464
+ if (typeof url === "string" && url.trim() !== "")
465
+ return url;
466
+ throw new VibeError7({
467
+ code: "VIBE_CONFIG",
468
+ message: "bunAdapter needs an explicit address: pass `url` (a PostgreSQL URL). " + "The adapter does not read DATABASE_URL, POSTGRES_URL or the PG* environment variables.",
469
+ meta: { adapter: "bun", reason: "missing-address" }
470
+ });
471
+ }
403
472
  function bunAdapter(options) {
473
+ refuseUnsupportedBun({ version: Bun.version });
404
474
  const usePrepared = options?.preparedStatements ?? false;
475
+ const address = takeConnectTimeout({ url: requireUrl({ options }) });
405
476
  const templates = createTemplateCache({ max: options?.stmtCacheMax ?? 1000 });
406
477
  let activeWire = WIRE_TEXT;
407
478
  let wireSettled = false;
408
479
  let wireProbe = null;
409
480
  let instance = null;
410
- const deadlineTurns = new WeakMap;
411
- function budgetTurn(params) {
412
- const { source, budget, body, guard } = params;
413
- const run = async () => {
414
- if (params.cleanup === true)
415
- return body();
416
- budget?.assertUsable({ stage: params.stage ?? "statement" });
417
- guard?.();
418
- const ms = budget === undefined ? null : engineStatementBudgetMs({ budget });
419
- if (ms !== null) {
420
- await runMapped(() => source.unsafe(statementTimeoutStatement({ timeout: ms })));
421
- budget?.assertUsable({ stage: params.stage ?? "statement" });
422
- guard?.();
481
+ const controlTemplates = createTemplateCache({ max: 64 });
482
+ function controlQuery(params) {
483
+ return params.connection(controlTemplates.getTemplate({ text: params.text }).template, ...params.values ?? []);
484
+ }
485
+ function createReservedControl(params) {
486
+ const { reserved, budget, scope } = params;
487
+ const deferred = createDeferredControl({ opening: params.opening === null ? [] : [params.opening] });
488
+ let openingFailure;
489
+ let openingStep = null;
490
+ let openingPending = null;
491
+ let waitTail = null;
492
+ let deadlineTail = Promise.resolve();
493
+ let lastFailure;
494
+ const step = (query) => new Promise((resolve) => resolve(query())).then(undefined, (error) => {
495
+ const mapped = mapBunDriverError({ error });
496
+ if (mapped.meta.sqlstate === "25P02") {
497
+ throw transactionAbortedError({ cause: lastFailure ?? scope.state.aborted?.cause });
423
498
  }
424
- return body();
499
+ if (typeof mapped.meta.sqlstate === "string")
500
+ lastFailure = mapped;
501
+ throw mapped;
502
+ });
503
+ const trackOpening = (sent) => {
504
+ openingStep = sent;
505
+ const settled = sent.then(ignore, (error) => {
506
+ openingFailure = mapBunDriverError({ error });
507
+ }).then(() => {
508
+ if (openingPending === settled) {
509
+ openingPending = null;
510
+ openingStep = null;
511
+ }
512
+ });
513
+ openingPending = settled;
514
+ return settled;
515
+ };
516
+ const ridesOpening = (send) => send.extended && send.effect === "read";
517
+ const sendNow = (send) => {
518
+ const cleanup = send.cleanup === true;
519
+ if (!cleanup) {
520
+ if (openingFailure !== undefined)
521
+ throw transactionAbortedError({ cause: openingFailure });
522
+ budget.assertUsable({ stage: send.stage ?? "statement" });
523
+ send.guard?.();
524
+ }
525
+ const carriesOpening = !cleanup && !deferred.opened();
526
+ const control = cleanup ? [] : [...deferred.take()];
527
+ if (carriesOpening && !ridesOpening(send)) {
528
+ const [opening, ...rest] = control;
529
+ const confirmed = trackOpening(step(() => controlQuery({ connection: reserved, text: opening })));
530
+ const riding = rest.map((text) => {
531
+ const sent = step(() => controlQuery({ connection: reserved, text }));
532
+ sent.catch(ignore);
533
+ return sent;
534
+ });
535
+ const next = { ...send, riding };
536
+ if (budget.totalMs !== null)
537
+ return confirmed.then(() => sendNow(next));
538
+ return enqueue(next);
539
+ }
540
+ const ms = cleanup ? null : engineStatementBudgetMs({ budget });
541
+ const steps = [...send.riding ?? []];
542
+ if (!cleanup && !carriesOpening && openingStep !== null)
543
+ steps.push(openingStep);
544
+ control.forEach((text, index) => {
545
+ const sent = step(() => controlQuery({ connection: reserved, text }));
546
+ if (index === 0 && carriesOpening)
547
+ trackOpening(sent);
548
+ steps.push(sent);
549
+ });
550
+ if (ms !== null)
551
+ steps.push(step(() => controlQuery({ connection: reserved, text: STATEMENT_BOUND_TEXT, values: [String(ms)] })));
552
+ const statement = step(() => send.send(reserved));
553
+ steps.push(statement);
554
+ return Promise.allSettled(steps).then((settled) => {
555
+ const failed = settled.find((outcome) => outcome.status === "rejected");
556
+ if (failed !== undefined)
557
+ throw failed.reason;
558
+ return statement;
559
+ });
560
+ };
561
+ const enqueue = (send) => {
562
+ const prior = waitTail ?? openingPending ?? Promise.resolve();
563
+ let markStarted = ignore;
564
+ const started = new Promise((resolve) => {
565
+ markStarted = resolve;
566
+ });
567
+ waitTail = started;
568
+ return prior.then(() => {
569
+ try {
570
+ return sendNow(send);
571
+ } finally {
572
+ if (waitTail === started)
573
+ waitTail = null;
574
+ markStarted();
575
+ }
576
+ });
577
+ };
578
+ const dispatch = (send) => {
579
+ if (budget.totalMs !== null) {
580
+ const pending = deadlineTail.then(() => sendNow(send));
581
+ deadlineTail = pending.then(ignore, ignore);
582
+ return pending;
583
+ }
584
+ if (openingPending !== null && (waitTail !== null || !ridesOpening(send)))
585
+ return enqueue(send);
586
+ return new Promise((resolve) => resolve(sendNow(send)));
587
+ };
588
+ return {
589
+ deferred,
590
+ dispatch,
591
+ executor: ({ effect, guard }) => Object.assign((strings, ...values) => dispatch({ effect, guard, extended: true, send: (connection) => connection(strings, ...values) }), { unsafe: (text, values) => dispatch({ effect, guard, extended: values !== undefined && values.length > 0, send: (connection) => connection.unsafe(text, values) }) })
425
592
  };
426
- if (budget === undefined || budget.totalMs === null)
427
- return run();
428
- const result = (deadlineTurns.get(budget) ?? Promise.resolve()).then(run);
429
- deadlineTurns.set(budget, result.then(() => {
430
- return;
431
- }, () => {
432
- return;
433
- }));
434
- return result;
435
593
  }
436
594
  let rawSession = null;
437
595
  function createSqlInstance(params) {
438
- const sqlOptions = { max: params.max };
596
+ const sqlOptions = { max: params.max, prepare: usePrepared };
439
597
  if (options?.connectionTimeout !== undefined) {
440
598
  sqlOptions.connectionTimeout = Math.max(1, Math.ceil(options.connectionTimeout / 1000));
599
+ } else if (address.seconds !== undefined) {
600
+ sqlOptions.connectionTimeout = address.seconds;
441
601
  }
442
602
  const startupOptions = buildStartupOptions({
443
603
  planCacheMode: options?.planCacheMode ?? "force_custom_plan",
444
604
  statementTimeout: options?.statementTimeout
445
605
  });
446
- const baseUrl = options?.url ?? process.env.DATABASE_URL;
447
- const url = baseUrl === undefined || startupOptions === undefined ? baseUrl : applyStartupOptions({ url: baseUrl, startupOptions });
448
- if (url === undefined && startupOptions !== undefined) {
449
- sqlOptions.connection = { options: startupOptions };
606
+ const url = startupOptions === undefined ? address.url : applyStartupOptions({ url: address.url, startupOptions });
607
+ if (URL.canParse(url)) {
608
+ const path = new URL(url).pathname;
609
+ if (path.length > 1)
610
+ sqlOptions.database = decodeURIComponent(path.slice(1));
450
611
  }
451
612
  return createBunSql({ url, options: sqlOptions });
452
613
  }
@@ -463,7 +624,7 @@ function bunAdapter(options) {
463
624
  else if (typeof probed === "string" && /^\{\s*"__vibeorm"\s*:\s*1\s*\}$/.test(probed))
464
625
  activeWire = WIRE_TEXT;
465
626
  else {
466
- throw new VibeError6({
627
+ throw new VibeError7({
467
628
  code: "VIBE_ADAPTER",
468
629
  message: "Bun returned an unrecognized JSON wire probe result; no ORM statement was sent",
469
630
  meta: { reason: "json-wire-probe" }
@@ -479,40 +640,44 @@ function bunAdapter(options) {
479
640
  if (results.length === 0)
480
641
  observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect }) });
481
642
  for (const rows of results)
482
- observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect, count: rows.count ?? rows.affectedRows }) });
643
+ observeRowChanges({ scope: params.scope, count: statementRowChanges({ effect: params.effect, count: rows.count ?? rows.affectedRows, rows: rows.length, resultRows: rows }) });
483
644
  }
484
645
  async function runExecute(params) {
485
646
  if (!wireSettled) {
486
- wireProbe ??= probeJsonWire(params.source).finally(() => {
487
- wireProbe = null;
488
- });
489
- await wireProbe;
647
+ if (params.scope === undefined) {
648
+ wireProbe ??= probeJsonWire(params.source).finally(() => {
649
+ wireProbe = null;
650
+ });
651
+ await wireProbe;
652
+ } else {
653
+ await probeJsonWire(params.source);
654
+ }
490
655
  }
491
- const values = serializeParams({ values: params.values });
656
+ const values = serializeParams({ values: params.values, namedStatements: usePrepared });
492
657
  if (usePrepared) {
493
658
  const prepared = templates.getTemplate({ text: params.text });
494
- const bound = bindTemplateValues({ text: params.text, indices: prepared.indices, values });
495
- const result = await runMapped(() => params.source(prepared.template, ...bound));
659
+ const bound = prepared.identityOrder && prepared.indices.length === values.length ? values : bindTemplateValues({ text: params.text, indices: prepared.indices, values });
660
+ const result = bound.length > MAX_BIND_PARAMETERS ? await runMapped(() => params.source.unsafe(params.text, values)) : await runMapped(() => params.source(prepared.template, ...bound));
496
661
  observeResult({ scope: params.scope, effect: params.effect, result });
497
- return finalRows({ result });
662
+ return plainRows({ rows: finalRows({ result }) });
498
663
  }
499
664
  const result = await runMapped(() => params.source.unsafe(params.text, values));
500
665
  observeResult({ scope: params.scope, effect: params.effect, result });
501
- return finalRows({ result });
666
+ return plainRows({ rows: finalRows({ result }) });
502
667
  }
503
668
  async function runExecuteUnsafe(params) {
504
- const values = params.values === undefined ? undefined : serializeParamsRaw({ values: params.values });
669
+ const values = params.values === undefined ? undefined : serializeParamsRaw({ values: params.values, namedStatements: usePrepared });
505
670
  const result = await runMapped(() => params.source.unsafe(params.text, values));
506
671
  observeResult({ scope: params.scope, effect: params.effect, result });
507
672
  const rows = finalRows({ result });
508
- return { rows, affectedRows: readAffectedRows({ rows }) };
673
+ return { rows: plainRows({ rows }), affectedRows: readAffectedRows({ rows }) };
509
674
  }
510
675
  function createAdapter(params) {
511
- const { source, parentMode, scope, budget, lifetime } = params;
676
+ const { source, parentMode, scope, budget, lifetime, control } = params;
512
677
  function assertHandle() {
513
678
  budget?.assertUsable({ stage: "statement" });
514
679
  if (lifetime?.active === false) {
515
- throw new VibeError6({
680
+ throw new VibeError7({
516
681
  code: "VIBE_TRANSACTION",
517
682
  message: "this savepoint callback has finished; its transaction handle has expired",
518
683
  meta: { reason: "expired" }
@@ -521,56 +686,32 @@ function bunAdapter(options) {
521
686
  if (scope.state.poisoned !== null)
522
687
  throw scope.state.poisoned;
523
688
  }
524
- const guardedSource = Object.assign((strings, ...values) => budgetTurn({ source, budget, guard: assertHandle, body: () => source(strings, ...values) }), { unsafe: (text, values) => budgetTurn({ source, budget, guard: assertHandle, body: () => source.unsafe(text, values) }) });
525
- async function transactionViaSavepoint(fn, nestedOptions) {
689
+ async function transactionViaSavepoint(reservedControl, fn, nestedOptions) {
526
690
  refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
527
691
  assertHandle();
528
692
  budget?.assertUsable({ stage: "nested" });
529
- const transactional = source;
530
- const runChild = async (child, childSource) => {
531
- assertHandle();
532
- const childLifetime = { active: true };
533
- try {
534
- return await fn(createAdapter({ source: childSource, parentMode: "tx", scope: child, budget, lifetime: childLifetime }));
535
- } finally {
536
- childLifetime.active = false;
537
- }
538
- };
539
- const driverSavepoint = transactional.savepoint?.bind(transactional);
540
- if (driverSavepoint !== undefined) {
541
- return runNestedSavepoint({
542
- scope,
543
- body: async (child) => {
544
- let callbackFailure;
545
- try {
546
- return await driverSavepoint(async (savepointSql) => {
547
- markSavepointOpened({ scope: child });
548
- try {
549
- return await runChild(child, savepointSql);
550
- } catch (error) {
551
- callbackFailure = new CallbackError(error);
552
- throw callbackFailure;
553
- }
554
- });
555
- } catch (error) {
556
- if (callbackFailure !== undefined && error === callbackFailure)
557
- throw callbackFailure.inner;
558
- const poisoned = callbackFailure !== undefined ? savepointRollbackFailedError({ cause: callbackFailure.inner, rollbackFailure: mapBunDriverError({ error }) }) : unknownOutcome({ cause: mapBunDriverError({ error }), reason: "savepoint-outcome-unknown" });
559
- scope.state.poisoned = poisoned;
560
- throw poisoned;
561
- }
562
- }
693
+ const savepointControl = (statement, cleanup) => {
694
+ if (reservedControl.deferred.routeSavepoint({ text: statement }) !== "send")
695
+ return;
696
+ return reservedControl.dispatch({
697
+ extended: true,
698
+ ...cleanup ? { cleanup: true } : {},
699
+ send: (connection) => controlQuery({ connection, text: statement })
563
700
  });
564
- }
701
+ };
565
702
  return runNestedSavepoint({
566
703
  scope,
567
- execute: (statement) => budgetTurn({
568
- source,
569
- budget,
570
- cleanup: statement.startsWith("ROLLBACK TO SAVEPOINT "),
571
- body: () => runMapped(() => transactional.unsafe(statement))
572
- }),
573
- body: (child) => runChild(child, transactional)
704
+ execute: (statement) => savepointControl(statement, false),
705
+ cleanup: (statement) => savepointControl(statement, true),
706
+ body: async (child) => {
707
+ assertHandle();
708
+ const childLifetime = { active: true };
709
+ try {
710
+ return await fn(createAdapter({ source, parentMode: "tx", scope: child, budget, lifetime: childLifetime, control: reservedControl }));
711
+ } finally {
712
+ childLifetime.active = false;
713
+ }
714
+ }
574
715
  });
575
716
  }
576
717
  async function transactionViaPool(fn, transactionOptions) {
@@ -580,76 +721,76 @@ function bunAdapter(options) {
580
721
  support: BUDGETS.transactionDeadline,
581
722
  provider: PROVIDER
582
723
  });
583
- if (transactionOptions?.isolationLevel === undefined && transactionOptions?.timeout === undefined && transactionOptions?.deadline === undefined) {
584
- const budget = startTransactionBudget({ provider: PROVIDER });
585
- const txScope = createSavepointScope();
586
- let callbackFailure;
587
- try {
588
- const result = await pool.begin(async (txSql) => {
589
- const txAdapter = createAdapter({
590
- source: txSql,
591
- parentMode: "tx",
592
- scope: txScope,
593
- budget
594
- });
595
- try {
596
- return await runRootScope({ scope: txScope, body: () => fn(txAdapter) });
597
- } catch (error) {
598
- callbackFailure = new CallbackError(error);
599
- throw callbackFailure;
600
- }
601
- });
602
- budget.close({ outcome: "committed" });
603
- return result;
604
- } catch (error) {
605
- const knownRollback = callbackFailure !== undefined && error === callbackFailure && txScope.state.poisoned === null;
606
- budget.close({ outcome: knownRollback ? "rolled-back" : "unknown" });
607
- if (callbackFailure !== undefined && error === callbackFailure)
608
- throw callbackFailure.inner;
609
- throw unknownOutcome({ cause: callbackFailure?.inner ?? mapBunDriverError({ error }) });
610
- }
611
- }
612
- const timeoutText = transactionOptions.timeout === undefined ? undefined : statementTimeoutStatement({ timeout: transactionOptions.timeout });
724
+ const readOnly = readOnlyRequested({ options: transactionOptions, provider: PROVIDER });
725
+ const openingOptions = { isolationLevel: transactionOptions?.isolationLevel, readOnly };
726
+ const eagerOpen = transactionOptions?.timeout === undefined ? null : openTransactionStatement({ ...openingOptions, timeout: transactionOptions.timeout });
613
727
  const reserved = await runMapped(() => pool.reserve());
614
728
  const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
615
729
  const txScope = createSavepointScope();
616
- let commitSent = false;
730
+ const control = createReservedControl({
731
+ reserved,
732
+ budget,
733
+ scope: txScope,
734
+ opening: eagerOpen === null ? beginStatement(openingOptions) : null
735
+ });
736
+ const commit = { progress: "not-sent" };
617
737
  let discard = false;
618
738
  try {
619
- await runMapped(() => reserved.unsafe(beginStatement({ isolationLevel: transactionOptions.isolationLevel })));
620
- if (timeoutText !== undefined)
621
- await runMapped(() => reserved.unsafe(timeoutText));
739
+ if (eagerOpen !== null)
740
+ await runMapped(() => reserved.unsafe(eagerOpen));
622
741
  const txAdapter = createAdapter({
623
742
  source: reserved,
624
743
  parentMode: "tx",
625
744
  scope: txScope,
626
- budget
745
+ budget,
746
+ control
627
747
  });
628
748
  const result = await runRootScope({ scope: txScope, body: () => fn(txAdapter) });
629
- await budgetTurn({ source: reserved, budget, stage: "commit", body: () => {
630
- commitSent = true;
631
- return runMapped(() => reserved.unsafe("COMMIT"));
632
- } });
749
+ budget.assertUsable({ stage: "commit" });
750
+ if (!control.deferred.opened()) {
751
+ budget.close({ outcome: "committed" });
752
+ return result;
753
+ }
754
+ const reply = await control.dispatch({
755
+ extended: true,
756
+ effect: "write",
757
+ stage: "commit",
758
+ send: (connection) => {
759
+ commit.progress = "sent";
760
+ return controlQuery({ connection, text: "COMMIT" });
761
+ }
762
+ });
763
+ if (finalRows({ result: reply }).command === "ROLLBACK") {
764
+ commit.progress = "answered";
765
+ throw transactionAbortedError({ cause: txScope.state.aborted?.cause });
766
+ }
633
767
  budget.close({ outcome: "committed" });
634
768
  return result;
635
769
  } catch (error) {
770
+ if (txScope.state.poisoned !== null)
771
+ discard = true;
772
+ if (commit.progress === "sent")
773
+ commit.progress = isConnectionLoss({ error }) ? "unanswered" : "answered";
774
+ let rollback = "not-needed";
636
775
  try {
637
- await reserved.unsafe("ROLLBACK");
638
- budget.close({ outcome: commitSent || txScope.state.poisoned !== null ? "unknown" : "rolled-back" });
776
+ if (control.deferred.opened()) {
777
+ await control.dispatch({ extended: false, cleanup: true, send: (connection) => connection.unsafe("ROLLBACK") });
778
+ rollback = "acknowledged";
779
+ }
639
780
  } catch {
640
781
  discard = true;
641
- budget.close({ outcome: "unknown" });
642
- if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
643
- throw transactionOutcomeUnknownError({
644
- provider: PROVIDER,
645
- totalMs: budget.totalMs,
646
- cause: error
647
- });
648
- }
782
+ rollback = "failed";
649
783
  }
650
- if (budget.outcome === "unknown" && txScope.state.poisoned === null)
651
- throw unknownOutcome({ cause: error });
652
- throw error;
784
+ const own = commit.progress === "unanswered" && txScope.state.poisoned === null ? unknownOutcome({ cause: error }) : error;
785
+ throw settleFailedTransaction({
786
+ error: own,
787
+ provider: PROVIDER,
788
+ budget,
789
+ scope: txScope,
790
+ commit: commit.progress,
791
+ rollback,
792
+ connection: discard ? "discarded" : "returned"
793
+ });
653
794
  } finally {
654
795
  if (discard)
655
796
  await runMapped(() => reserved.close());
@@ -661,6 +802,7 @@ function bunAdapter(options) {
661
802
  dialect: DIALECT,
662
803
  provider: PROVIDER,
663
804
  budgets: BUDGETS,
805
+ roundTrips: ROUND_TRIPS,
664
806
  ...parentMode === "pool" ? {} : { rowChanges: () => {
665
807
  assertHandle();
666
808
  return readRowChanges({ scope });
@@ -674,8 +816,9 @@ function bunAdapter(options) {
674
816
  if (turn !== undefined)
675
817
  await turn;
676
818
  assertHandle();
677
- const result = runExecute({ source: guardedSource, ...parentMode === "pool" ? {} : { scope }, ...execParams });
678
- return parentMode === "pool" ? result : result.catch((error) => abortTransactionOnServerFailure({ scope, error }));
819
+ const executor = control === undefined ? source : control.executor({ effect: execParams.effect, guard: assertHandle });
820
+ const result = runExecute({ source: executor, ...parentMode === "pool" ? {} : { scope }, ...execParams });
821
+ return parentMode === "pool" ? result : result.catch((error) => abortTransactionOnStatementFailure({ scope, error }));
679
822
  },
680
823
  async executeUnsafe(execParams) {
681
824
  assertHandle();
@@ -683,21 +826,35 @@ function bunAdapter(options) {
683
826
  if (turn !== undefined)
684
827
  await turn;
685
828
  assertHandle();
686
- const result = runExecuteUnsafe({ source: guardedSource, ...parentMode === "pool" ? {} : { scope }, ...execParams });
687
- return parentMode === "pool" ? result : result.catch((error) => abortTransactionOnServerFailure({ scope, error }));
829
+ const executor = control === undefined ? source : control.executor({ effect: execParams.effect, guard: assertHandle });
830
+ const result = runExecuteUnsafe({ source: executor, ...parentMode === "pool" ? {} : { scope }, ...execParams });
831
+ return parentMode === "pool" ? result : result.catch((error) => abortTransactionOnStatementFailure({ scope, error }));
688
832
  },
689
833
  async transaction(fn, transactionOptions) {
690
- return parentMode === "pool" ? transactionViaPool(fn, transactionOptions) : transactionViaSavepoint(fn, transactionOptions);
834
+ return control === undefined ? transactionViaPool(fn, transactionOptions) : transactionViaSavepoint(control, fn, transactionOptions);
691
835
  },
692
836
  async connect() {
693
- if (parentMode !== "pool")
694
- return;
837
+ if (parentMode !== "pool") {
838
+ assertHandle();
839
+ throw transactionLifecycleRefusal({ method: "connect" });
840
+ }
695
841
  const reserved = await runMapped(() => source.reserve());
696
- reserved.release();
842
+ try {
843
+ if (!wireSettled && wireProbe === null) {
844
+ wireProbe = probeJsonWire(reserved).finally(() => {
845
+ wireProbe = null;
846
+ });
847
+ await wireProbe;
848
+ }
849
+ } finally {
850
+ reserved.release();
851
+ }
697
852
  },
698
853
  async disconnect() {
699
- if (parentMode !== "pool")
700
- return;
854
+ if (parentMode !== "pool") {
855
+ assertHandle();
856
+ throw transactionLifecycleRefusal({ method: "disconnect" });
857
+ }
701
858
  await runMapped(() => source.close());
702
859
  instance = null;
703
860
  },
@@ -713,29 +870,53 @@ function bunAdapter(options) {
713
870
  scope: createSavepointScope()
714
871
  });
715
872
  }
716
- return {
717
- dialect: DIALECT,
718
- provider: PROVIDER,
719
- budgets: BUDGETS,
720
- get wire() {
721
- return activeWire;
722
- },
873
+ function closeOwnedResources() {
874
+ return runShutdownCleanup({
875
+ steps: [
876
+ async () => {
877
+ if (rawSession === null)
878
+ return;
879
+ const session = rawSession;
880
+ rawSession = null;
881
+ await runMapped(() => session.unsafe("ROLLBACK")).then(() => {
882
+ session.release();
883
+ }, (error) => {
884
+ session.release();
885
+ throw error;
886
+ });
887
+ },
888
+ async () => {
889
+ if (instance === null)
890
+ return;
891
+ const closing = instance;
892
+ instance = null;
893
+ await runMapped(() => closing.close());
894
+ }
895
+ ]
896
+ });
897
+ }
898
+ const lifecycle = createAdapterLifecycleGate({ driver: "bun", cleanup: closeOwnedResources });
899
+ const root = {
723
900
  async withSession({ run }) {
724
901
  const sessionPool = createSqlInstance({ max: SESSION_CONNECTION.max });
725
902
  const close = () => runMapped(() => sessionPool.close({ timeout: SESSION_CONNECTION.closeGraceSeconds }));
726
903
  const reserved = await runMapped(() => sessionPool.reserve()).catch(async (cause) => {
727
- await close();
904
+ await close().catch((cleanupCause) => {
905
+ if (cause instanceof VibeError7)
906
+ throw new VibeError7({ code: cause.code, message: cause.message, meta: { ...cause.meta, cleanupCause }, cause });
907
+ throw cause;
908
+ });
728
909
  throw cause;
729
910
  });
730
911
  return runPinnedSession({
731
912
  run,
732
913
  execute: async (query) => (await runExecuteUnsafe({ source: reserved, ...query })).rows,
733
- isLost: (error) => error instanceof VibeError6 && (typeof error.meta.sqlstate !== "string" || error.meta.sqlstate.startsWith("08") || ["57P01", "57P02", "57P03"].includes(error.meta.sqlstate)),
914
+ isLost: (error) => error instanceof VibeError7 && isConnectionLoss({ error }),
734
915
  release: close
735
916
  });
736
917
  },
737
918
  async execute(params) {
738
- return poolAdapter().execute(params);
919
+ return runExecute({ source: getSql(), ...params });
739
920
  },
740
921
  async executeUnsafe(params) {
741
922
  const control = classifyRawTransactionControl({ text: params.text });
@@ -762,28 +943,41 @@ function bunAdapter(options) {
762
943
  throw error;
763
944
  }
764
945
  }
765
- return poolAdapter().executeUnsafe(params);
946
+ return runExecuteUnsafe({ source: getSql(), ...params });
766
947
  },
767
948
  async transaction(fn, transactionOptions) {
768
949
  return poolAdapter().transaction(fn, transactionOptions);
769
950
  },
770
951
  async connect() {
771
952
  return poolAdapter().connect();
953
+ }
954
+ };
955
+ return {
956
+ dialect: DIALECT,
957
+ provider: PROVIDER,
958
+ budgets: BUDGETS,
959
+ roundTrips: ROUND_TRIPS,
960
+ transactionOutcome: "reported",
961
+ get wire() {
962
+ return activeWire;
772
963
  },
773
- async disconnect() {
774
- if (rawSession !== null) {
775
- const session = rawSession;
776
- rawSession = null;
777
- try {
778
- await session.unsafe("ROLLBACK");
779
- } catch {}
780
- session.release();
781
- }
782
- if (instance === null)
783
- return;
784
- const closing = instance;
785
- instance = null;
786
- await runMapped(() => closing.close());
964
+ withSession({ run }) {
965
+ return lifecycle.admit({ run: () => root.withSession({ run: (executor) => lifecycle.inCallback({ run: () => run(executor) }) }) });
966
+ },
967
+ execute(params) {
968
+ return lifecycle.admit({ run: () => root.execute(params) });
969
+ },
970
+ executeUnsafe(params) {
971
+ return lifecycle.admit({ run: () => root.executeUnsafe(params) });
972
+ },
973
+ transaction(fn, transactionOptions) {
974
+ return lifecycle.admit({ run: () => root.transaction((txAdapter) => lifecycle.inCallback({ run: () => fn(txAdapter) }), transactionOptions) });
975
+ },
976
+ connect() {
977
+ return lifecycle.admit({ run: () => root.connect() });
978
+ },
979
+ disconnect() {
980
+ return lifecycle.disconnect();
787
981
  },
788
982
  formatArrayParam(values) {
789
983
  return toPgArrayLiteral(values);
@@ -798,14 +992,31 @@ function readAffectedRows(params) {
798
992
  return rows.affectedRows;
799
993
  return rows.length;
800
994
  }
995
+ function plainRows(params) {
996
+ return Array.from(params.rows);
997
+ }
801
998
  function finalRows(params) {
802
999
  const { result } = params;
803
1000
  if (result === null)
804
1001
  return [];
805
1002
  return Array.isArray(result[0]) ? result.at(-1) ?? [] : result;
806
1003
  }
1004
+ function isConnectionLoss(params) {
1005
+ const { error } = params;
1006
+ if (!(error instanceof VibeError7))
1007
+ return true;
1008
+ const sqlstate = error.meta.sqlstate;
1009
+ return typeof sqlstate !== "string" || sqlstate.startsWith("08") || ["57P01", "57P02", "57P03"].includes(sqlstate);
1010
+ }
1011
+ function abortTransactionOnStatementFailure(params) {
1012
+ const { scope, error } = params;
1013
+ if (error instanceof VibeError7 && error.meta.driverCode === "ERR_POSTGRES_TOO_MANY_PARAMETERS") {
1014
+ markTransactionAborted({ scope, cause: error });
1015
+ }
1016
+ return abortTransactionOnServerFailure({ scope, error });
1017
+ }
807
1018
  function unknownOutcome(params) {
808
- return new VibeError6({
1019
+ return new VibeError7({
809
1020
  code: "VIBE_TRANSACTION",
810
1021
  message: "Bun could not confirm transaction recovery or completion; the outcome is unknown. Nothing is retried automatically.",
811
1022
  meta: { reason: params.reason ?? "transaction-outcome-unknown", outcome: "unknown" },
@@ -826,7 +1037,7 @@ export {
826
1037
  createSavepointCounter,
827
1038
  createTemplateCache,
828
1039
  engineStatementBudgetMs2 as engineStatementBudgetMs,
829
- isTransactionDeadlineError2 as isTransactionDeadlineError,
1040
+ isTransactionDeadlineError,
830
1041
  mapBunDriverError,
831
1042
  nextSavepointName,
832
1043
  refuseNestedTransactionOptions,
@@ -838,8 +1049,8 @@ export {
838
1049
  toPgArrayLiteral,
839
1050
  transactionClosedError,
840
1051
  transactionDeadlineError,
841
- transactionOutcomeUnknownError2 as transactionOutcomeUnknownError,
1052
+ transactionOutcomeUnknownError,
842
1053
  validateTransactionDeadline2 as validateTransactionDeadline
843
1054
  };
844
1055
 
845
- //# debugId=424C29CF8963522C64756E2164756E21
1056
+ //# debugId=ABB8B79ECB9DC73B64756E2164756E21