@vibeorm/adapter-pg 2.0.0-alpha.1 → 2.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAqB1E,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE/D,CAAC;AAYH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAK9E;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CA0BtE;AAED,wEAAwE;AACxE,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMnE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAIhE;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CA2B1E,CAAC;AAEH,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAE/D,CAAC;AAYH;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,MAAM,GAAG,SAAS,CAK9E;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE;IAAE,KAAK,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CA0BtE;AAED,wEAAwE;AACxE,wBAAsB,SAAS,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAMnE"}
package/dist/index.d.ts CHANGED
@@ -11,6 +11,16 @@ export type PgQueryConfig = {
11
11
  readonly text: string;
12
12
  readonly values?: unknown[];
13
13
  readonly name?: string;
14
+ /**
15
+ * QUERY-LOCAL result parsers ({@link EXACT_TYPES}). node-postgres reads this
16
+ * off the query config, so nothing outside VibeORM's own statements is
17
+ * affected — a caller's pool keeps the driver's documented behaviour.
18
+ */
19
+ readonly types?: PgTypeParsers;
20
+ };
21
+ /** The `pg-types` surface a query config may carry. */
22
+ export type PgTypeParsers = {
23
+ getTypeParser(oid: number, format?: "text" | "binary"): (value: string) => unknown;
14
24
  };
15
25
  /** The parts of a `pg` result this adapter reads. */
16
26
  export type PgQueryResultLike = {
@@ -20,13 +30,27 @@ export type PgQueryResultLike = {
20
30
  /** The parts of a `pg` PoolClient this adapter uses. */
21
31
  export type PgPoolClientLike = {
22
32
  query(config: PgQueryConfig): Promise<PgQueryResultLike>;
23
- release(): void;
33
+ /**
34
+ * Hand the client back. A TRUTHY argument tells node-postgres to DESTROY it
35
+ * instead of pooling it — used when a rollback failed and the session state
36
+ * can no longer be asserted (EPIC 6). Optional so existing test doubles that
37
+ * declare `release(): void` still satisfy this type.
38
+ */
39
+ release(destroy?: boolean): void;
40
+ /**
41
+ * EventEmitter surface. A real `pg.PoolClient` has it; test doubles need not,
42
+ * so both members are optional and the adapter feature-detects them.
43
+ */
44
+ on?(event: "error", listener: (error: unknown) => void): void;
45
+ removeListener?(event: "error", listener: (error: unknown) => void): void;
24
46
  };
25
47
  /** The parts of a `pg` Pool this adapter uses — satisfied by a real `pg.Pool`. */
26
48
  export type PgPoolLike = {
27
49
  connect(): Promise<PgPoolClientLike>;
28
50
  query(config: PgQueryConfig): Promise<PgQueryResultLike>;
29
51
  end(): Promise<void>;
52
+ /** EventEmitter surface; optional for the same reason as on {@link PgPoolClientLike}. */
53
+ on?(event: "error", listener: (error: unknown, client: PgPoolClientLike) => void): void;
30
54
  };
31
55
  /**
32
56
  * Options for {@link pgAdapter}. Either point it at a connection string (the
@@ -85,4 +109,6 @@ export { createStatementNameCache } from "./statement-cache.ts";
85
109
  export type { StatementNameCache } from "./statement-cache.ts";
86
110
  export { ISOLATION_LEVEL_SQL, beginStatement, classifyRawTransactionControl, refuseNestedTransactionOptions, statementTimeoutStatement, } from "./transaction-sql.ts";
87
111
  export type { RawTransactionControl } from "./transaction-sql.ts";
112
+ export { DEFAULT_DEADLINE_CLOCK, engineStatementBudgetMs, isTransactionDeadlineError, startTransactionBudget, transactionClosedError, transactionDeadlineError, transactionOutcomeUnknownError, validateTransactionDeadline, } from "./transaction-budget.ts";
113
+ export type { BudgetStage, DeadlineClock, TransactionBudget, TransactionOutcome } from "./transaction-budget.ts";
88
114
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAiD,MAAM,kBAAkB,CAAC;AAgBvG,qFAAqF;AACrF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD,OAAO,IAAI,IAAI,CAAC;CACjB,CAAC;AAEF,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrC,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB,CAAC;AAOF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,gGAAgG;IAChG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAcF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAyOrE;AAID,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9G,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAEV,eAAe,EAIhB,MAAM,kBAAkB,CAAC;AA8B1B,qFAAqF;AACrF,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC,CAAC;AAEF,uDAAuD;AACvD,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;CACpF,CAAC;AAEF,qDAAqD;AACrD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IACzC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CAClC,CAAC;AAEF,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD;;;;;OAKG;IACH,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;IACjC;;;OAGG;IACH,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;IAC9D,cAAc,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;CAC3E,CAAC;AAEF,kFAAkF;AAClF,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IACrC,KAAK,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACzD,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACrB,yFAAyF;IACzF,EAAE,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,KAAK,IAAI,GAAG,IAAI,CAAC;CACzF,CAAC;AA6CF;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC;;;OAGG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;IAC3B,6EAA6E;IAC7E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,gGAAgG;IAChG,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;IACtC,gFAAgF;IAChF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AA2EF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,SAAS,CAAC,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAgVrE;AAID,OAAO,EAAE,uBAAuB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9G,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC5E,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAChE,YAAY,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC/D,OAAO,EACL,mBAAmB,EACnB,cAAc,EACd,6BAA6B,EAC7B,8BAA8B,EAC9B,yBAAyB,GAC1B,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,0BAA0B,EAC1B,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,8BAA8B,EAC9B,2BAA2B,GAC5B,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,aAAa,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ var PG_SQLSTATE_ERROR_CODES = Object.freeze({
10
10
  "23502": "VIBE_VALIDATION",
11
11
  "23514": "VIBE_VALIDATION",
12
12
  "22P02": "VIBE_VALIDATION",
13
+ "42501": "VIBE_PERMISSION_DENIED",
13
14
  "40001": "VIBE_TRANSACTION",
14
15
  "40P01": "VIBE_TRANSACTION",
15
16
  "57014": "VIBE_TRANSACTION"
@@ -93,6 +94,9 @@ function serializeParamsRaw(params) {
93
94
  });
94
95
  }
95
96
 
97
+ // packages/adapter-pg/src/savepoint-gate.ts
98
+ import { AsyncLocalStorage } from "async_hooks";
99
+
96
100
  // packages/adapter-pg/src/savepoints.ts
97
101
  function createSavepointCounter() {
98
102
  return { n: 0 };
@@ -101,26 +105,123 @@ function nextSavepointName(params) {
101
105
  return `vibeorm_sp_${params.counter.n++}`;
102
106
  }
103
107
 
108
+ // packages/adapter-pg/src/savepoint-gate.ts
109
+ var CURRENT = new AsyncLocalStorage;
110
+ function createSavepointScope() {
111
+ return { depth: 0, state: { counter: createSavepointCounter(), open: null } };
112
+ }
113
+ function runRootScope(params) {
114
+ return CURRENT.run(params.scope, params.body);
115
+ }
116
+ function scopeTurn(params) {
117
+ const open = params.scope.state.open;
118
+ if (mayProceed({ scope: params.scope, open }))
119
+ return;
120
+ return waitForTurn({ scope: params.scope });
121
+ }
122
+ async function runNestedSavepoint(params) {
123
+ const { scope, execute, body } = params;
124
+ const node = await acquire({ scope });
125
+ try {
126
+ if (execute !== undefined)
127
+ await execute(`SAVEPOINT ${node.name}`);
128
+ let result;
129
+ try {
130
+ result = await CURRENT.run(node.scope, () => body(node.scope));
131
+ } catch (error) {
132
+ await drainDescendants({ node });
133
+ if (execute !== undefined) {
134
+ try {
135
+ await execute(`ROLLBACK TO SAVEPOINT ${node.name}`);
136
+ } catch {}
137
+ }
138
+ throw error;
139
+ }
140
+ await drainDescendants({ node });
141
+ if (execute !== undefined)
142
+ await execute(`RELEASE SAVEPOINT ${node.name}`);
143
+ return result;
144
+ } finally {
145
+ closeNode({ node });
146
+ }
147
+ }
148
+ function mayProceed(params) {
149
+ const { scope, open } = params;
150
+ if (open === null)
151
+ return true;
152
+ if (open.scope === scope)
153
+ return true;
154
+ if (open.scope.depth < scope.depth)
155
+ return true;
156
+ return CURRENT.getStore() === open.scope;
157
+ }
158
+ async function waitForTurn(params) {
159
+ for (;; ) {
160
+ const open = params.scope.state.open;
161
+ if (open === null || mayProceed({ scope: params.scope, open }))
162
+ return;
163
+ await open.settled;
164
+ }
165
+ }
166
+ async function acquire(params) {
167
+ const { scope } = params;
168
+ for (;; ) {
169
+ const open = scope.state.open;
170
+ if (mayProceed({ scope, open }))
171
+ return pushNode({ scope, parent: open });
172
+ await open.settled;
173
+ }
174
+ }
175
+ function pushNode(params) {
176
+ const { scope, parent } = params;
177
+ const state = scope.state;
178
+ let settle = () => {
179
+ return;
180
+ };
181
+ const settled = new Promise((resolve) => {
182
+ settle = () => resolve();
183
+ });
184
+ const node = {
185
+ scope: { depth: (parent === null ? 0 : parent.scope.depth) + 1, state },
186
+ name: nextSavepointName({ counter: state.counter }),
187
+ parent,
188
+ settled,
189
+ settle
190
+ };
191
+ state.open = node;
192
+ return node;
193
+ }
194
+ function closeNode(params) {
195
+ const { node } = params;
196
+ const state = node.scope.state;
197
+ if (state.open === node)
198
+ state.open = node.parent;
199
+ node.settle();
200
+ }
201
+ async function drainDescendants(params) {
202
+ const state = params.node.scope.state;
203
+ for (;; ) {
204
+ const open = state.open;
205
+ if (open === null || open === params.node || open.scope.depth <= params.node.scope.depth)
206
+ return;
207
+ await open.settled;
208
+ }
209
+ }
210
+
104
211
  // packages/adapter-pg/src/statement-cache.ts
105
212
  function createStatementNameCache(params) {
106
- const { max } = params;
213
+ const max = Math.max(0, params.max);
107
214
  const names = new Map;
108
215
  let counter = 0;
109
216
  return {
110
217
  getName(nameParams) {
111
218
  const { text } = nameParams;
112
219
  const existing = names.get(text);
113
- if (existing !== undefined) {
114
- names.delete(text);
115
- names.set(text, existing);
220
+ if (existing !== undefined)
116
221
  return existing;
117
- }
222
+ if (names.size >= max)
223
+ return;
118
224
  const name = `vibeorm_${counter++}`;
119
- if (names.size >= max) {
120
- const oldest = names.keys().next().value;
121
- if (oldest !== undefined)
122
- names.delete(oldest);
123
- }
124
225
  names.set(text, name);
125
226
  return name;
126
227
  },
@@ -130,8 +231,127 @@ function createStatementNameCache(params) {
130
231
  };
131
232
  }
132
233
 
133
- // packages/adapter-pg/src/transaction-sql.ts
234
+ // packages/adapter-pg/src/transaction-budget.ts
134
235
  import { VibeError as VibeError2 } from "@vibeorm/schema";
236
+ var DEFAULT_DEADLINE_CLOCK = () => Date.now();
237
+ function validateTransactionDeadline(params) {
238
+ const { options, support, provider } = params;
239
+ const deadline = options?.deadline;
240
+ if (deadline === undefined)
241
+ return;
242
+ if (support === "unsupported") {
243
+ throw new VibeError2({
244
+ code: "VIBE_UNSUPPORTED_CAPABILITY",
245
+ message: `${provider} cannot enforce a transaction deadline \u2014 remove the \`deadline\` option`,
246
+ meta: { provider, feature: "transactionDeadline", supported: support }
247
+ });
248
+ }
249
+ if (!Number.isInteger(deadline.totalMs) || deadline.totalMs <= 0) {
250
+ throw new VibeError2({
251
+ code: "VIBE_VALIDATION",
252
+ message: `transaction deadline \`totalMs\` must be a positive integer of milliseconds, received ${String(deadline.totalMs)}`,
253
+ meta: { provider, feature: "transactionDeadline", totalMs: deadline.totalMs }
254
+ });
255
+ }
256
+ const requested = deadline.enforcement ?? "between-statements";
257
+ if (requested === "cancel-running-statements" && support !== "cancel-running-statements") {
258
+ throw new VibeError2({
259
+ code: "VIBE_UNSUPPORTED_CAPABILITY",
260
+ message: `${provider} cannot cancel a statement that is already running, so the requested ` + "`cancel-running-statements` deadline guarantee is refused rather than silently downgraded \u2014 " + 'ask for "between-statements" if a budget checked between statements is enough',
261
+ meta: { provider, feature: "transactionDeadline", requested, supported: support }
262
+ });
263
+ }
264
+ }
265
+ function startTransactionBudget(params) {
266
+ const { options, provider } = params;
267
+ const clock = params.clock ?? DEFAULT_DEADLINE_CLOCK;
268
+ const deadline = options?.deadline;
269
+ const totalMs = deadline === undefined ? null : deadline.totalMs;
270
+ const enforcement = deadline === undefined ? null : deadline.enforcement ?? "between-statements";
271
+ const expiresAt = totalMs === null ? null : clock() + totalMs;
272
+ let outcome = null;
273
+ const budget = {
274
+ enforcement,
275
+ totalMs,
276
+ statementTimeoutMs: options?.timeout ?? null,
277
+ remainingMs() {
278
+ return expiresAt === null ? null : Math.max(0, expiresAt - clock());
279
+ },
280
+ expired() {
281
+ return expiresAt !== null && clock() >= expiresAt;
282
+ },
283
+ assertUsable(assertParams) {
284
+ if (outcome !== null) {
285
+ throw transactionClosedError({ provider, outcome, stage: assertParams.stage });
286
+ }
287
+ if (expiresAt !== null && clock() >= expiresAt) {
288
+ throw transactionDeadlineError({
289
+ provider,
290
+ stage: assertParams.stage,
291
+ totalMs,
292
+ overdueMs: clock() - expiresAt
293
+ });
294
+ }
295
+ },
296
+ close(closeParams) {
297
+ if (outcome === null)
298
+ outcome = closeParams.outcome;
299
+ },
300
+ get outcome() {
301
+ return outcome;
302
+ }
303
+ };
304
+ return budget;
305
+ }
306
+ function engineStatementBudgetMs(params) {
307
+ const { budget } = params;
308
+ if (budget.enforcement !== "cancel-running-statements")
309
+ return null;
310
+ const remaining = budget.remainingMs();
311
+ if (remaining === null)
312
+ return null;
313
+ const totalBound = Math.max(1, Math.ceil(remaining));
314
+ return budget.statementTimeoutMs !== null && budget.statementTimeoutMs > 0 ? Math.min(totalBound, budget.statementTimeoutMs) : totalBound;
315
+ }
316
+ function transactionDeadlineError(params) {
317
+ const { provider, stage, totalMs, overdueMs } = params;
318
+ const what = stage === "commit" ? "the transaction was rolled back instead of committed" : "the statement was never sent and the transaction is being rolled back";
319
+ return new VibeError2({
320
+ code: "VIBE_TRANSACTION",
321
+ message: `transaction deadline of ${totalMs} ms expired ${Math.max(0, Math.round(overdueMs))} ms ago \u2014 ${what}. ` + "Nothing is retried automatically: work the callback did outside the database may already have happened.",
322
+ meta: { provider, reason: "transaction-deadline", stage, totalMs, overdueMs: Math.max(0, Math.round(overdueMs)) }
323
+ });
324
+ }
325
+ function transactionClosedError(params) {
326
+ const { provider, outcome, stage } = params;
327
+ return new VibeError2({
328
+ code: "VIBE_TRANSACTION",
329
+ message: `this transaction has already finished (${outcome}), so its client can no longer be used \u2014 ` + "a handle kept past the end of `$transaction` would run outside the transaction, on a connection " + "that now belongs to someone else",
330
+ meta: { provider, reason: "transaction-closed", outcome, stage }
331
+ });
332
+ }
333
+ function transactionOutcomeUnknownError(params) {
334
+ const { provider, totalMs, cause } = params;
335
+ return new VibeError2({
336
+ code: "VIBE_TRANSACTION",
337
+ message: `transaction deadline of ${totalMs} ms expired and the rollback that should have cleaned up failed \u2014 ` + "the commit state of this transaction is UNKNOWN. It is not retried automatically; inspect the data " + "before deciding.",
338
+ meta: {
339
+ provider,
340
+ reason: "transaction-outcome-unknown",
341
+ outcome: "unknown",
342
+ totalMs,
343
+ rollbackFailed: true
344
+ },
345
+ cause
346
+ });
347
+ }
348
+ function isTransactionDeadlineError(error) {
349
+ const reason = error?.meta?.reason;
350
+ return reason === "transaction-deadline";
351
+ }
352
+
353
+ // packages/adapter-pg/src/transaction-sql.ts
354
+ import { VibeError as VibeError3 } from "@vibeorm/schema";
135
355
  var ISOLATION_LEVEL_SQL = Object.freeze({
136
356
  ReadCommitted: "READ COMMITTED",
137
357
  RepeatableRead: "REPEATABLE READ",
@@ -144,7 +364,7 @@ function beginStatement(params) {
144
364
  function statementTimeoutStatement(params) {
145
365
  const { timeout } = params;
146
366
  if (!Number.isInteger(timeout) || timeout < 0) {
147
- throw new VibeError2({
367
+ throw new VibeError3({
148
368
  code: "VIBE_VALIDATION",
149
369
  message: `transaction timeout must be a non-negative integer of milliseconds, received ${String(timeout)}`,
150
370
  meta: { timeout }
@@ -170,6 +390,8 @@ function refuseNestedTransactionOptions(params) {
170
390
  passed.push("isolationLevel");
171
391
  if (options.timeout !== undefined)
172
392
  passed.push("timeout");
393
+ if (options.deadline !== undefined)
394
+ passed.push("deadline");
173
395
  if (passed.length === 0)
174
396
  return;
175
397
  const meta = { provider, nested: true };
@@ -177,23 +399,55 @@ function refuseNestedTransactionOptions(params) {
177
399
  meta.isolationLevel = options.isolationLevel;
178
400
  if (options.timeout !== undefined)
179
401
  meta.timeout = options.timeout;
180
- throw new VibeError2({
402
+ if (options.deadline !== undefined)
403
+ meta.deadline = options.deadline;
404
+ throw new VibeError3({
181
405
  code: "VIBE_VALIDATION",
182
- message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins, and a nested timeout is not enforceable; pass options on the top-level $transaction`,
406
+ message: `nested transactions run as savepoints and cannot honor ${passed.join(" or ")} \u2014 a savepoint cannot change the isolation level of the transaction it joins, a nested timeout is not enforceable, and a nested deadline would need its own session setting on a connection it does not own (it inherits the top-level one instead); pass options on the top-level $transaction`,
183
407
  meta
184
408
  });
185
409
  }
186
410
 
187
411
  // packages/adapter-pg/src/index.ts
412
+ var POOL_STATEMENT_NAMES = new WeakMap;
413
+ function guardBackgroundErrors(params) {
414
+ const { client } = params;
415
+ if (typeof client.on !== "function")
416
+ return () => {
417
+ return;
418
+ };
419
+ const swallow = () => {};
420
+ client.on("error", swallow);
421
+ return () => {
422
+ client.removeListener?.("error", swallow);
423
+ };
424
+ }
188
425
  var DIALECT = "postgres";
189
426
  var PROVIDER = "postgresql";
190
427
  var WIRE = Object.freeze({ dateTime: "native", json: "parsed", numbers: "native" });
428
+ var BUDGETS = Object.freeze({
429
+ acquireTimeout: true,
430
+ statementTimeout: "all-statements",
431
+ transactionDeadline: "cancel-running-statements"
432
+ });
433
+ var NUMERIC_ARRAY_OID = 1231;
434
+ var TEXT_ARRAY_OID = 1009;
435
+ var EXACT_TYPES = {
436
+ getTypeParser(oid, format) {
437
+ if (oid === NUMERIC_ARRAY_OID && (format === undefined || format === "text")) {
438
+ return driverTypeParser(TEXT_ARRAY_OID, "text");
439
+ }
440
+ return driverTypeParser(oid, format);
441
+ }
442
+ };
443
+ var driverTypeParser = pg.types.getTypeParser;
191
444
  function pgAdapter(options) {
192
445
  const usePrepared = options?.preparedStatements ?? false;
193
- const statementNames = createStatementNameCache({ max: options?.stmtCacheMax ?? 1000 });
446
+ const stmtCacheMax = options?.stmtCacheMax ?? 1000;
194
447
  let pool = null;
195
448
  let ownsPool = false;
196
449
  let rawSession = null;
450
+ let rawSessionUnguard = null;
197
451
  function getPool() {
198
452
  if (pool !== null)
199
453
  return pool;
@@ -214,12 +468,24 @@ function pgAdapter(options) {
214
468
  }
215
469
  const PoolCtor = pg.Pool;
216
470
  pool = new PoolCtor(config);
471
+ pool.on?.("error", () => {});
217
472
  ownsPool = true;
218
473
  return pool;
219
474
  }
475
+ function statementNamesFor(params) {
476
+ const existing = POOL_STATEMENT_NAMES.get(params.pool);
477
+ if (existing !== undefined)
478
+ return existing;
479
+ const created = createStatementNameCache({ max: stmtCacheMax });
480
+ POOL_STATEMENT_NAMES.set(params.pool, created);
481
+ return created;
482
+ }
220
483
  function buildQueryConfig(params) {
221
484
  const values = serializeParams({ values: params.values });
222
- return usePrepared ? { name: statementNames.getName({ text: params.text }), text: params.text, values } : { text: params.text, values };
485
+ if (!usePrepared)
486
+ return { text: params.text, values, types: EXACT_TYPES };
487
+ const name = statementNamesFor({ pool: getPool() }).getName({ text: params.text });
488
+ return name === undefined ? { text: params.text, values, types: EXACT_TYPES } : { name, text: params.text, values, types: EXACT_TYPES };
223
489
  }
224
490
  async function runExecute(params) {
225
491
  const config = buildQueryConfig({ text: params.text, values: params.values });
@@ -228,35 +494,46 @@ function pgAdapter(options) {
228
494
  }
229
495
  async function runExecuteUnsafe(params) {
230
496
  const values = params.values === undefined ? undefined : serializeParamsRaw({ values: params.values });
231
- const result = await runMapped(() => params.queryable.query({ text: params.text, values }));
497
+ const result = await runMapped(() => params.queryable.query({ text: params.text, values, types: EXACT_TYPES }));
232
498
  return { rows: result.rows, affectedRows: result.rowCount ?? 0 };
233
499
  }
234
500
  function createClientAdapter(params) {
235
- const { client, savepointCounter } = params;
501
+ const { client, scope, budget } = params;
502
+ async function applyEngineBudget() {
503
+ const ms = engineStatementBudgetMs({ budget });
504
+ if (ms === null)
505
+ return;
506
+ await runMapped(() => client.query({ text: statementTimeoutStatement({ timeout: ms }) }));
507
+ }
236
508
  return {
237
509
  dialect: DIALECT,
238
510
  provider: PROVIDER,
239
511
  wire: WIRE,
512
+ budgets: BUDGETS,
240
513
  async execute(execParams) {
514
+ const turn = scopeTurn({ scope });
515
+ if (turn !== undefined)
516
+ await turn;
517
+ budget.assertUsable({ stage: "statement" });
518
+ await applyEngineBudget();
241
519
  return runExecute({ queryable: client, ...execParams });
242
520
  },
243
521
  async executeUnsafe(execParams) {
522
+ const turn = scopeTurn({ scope });
523
+ if (turn !== undefined)
524
+ await turn;
525
+ budget.assertUsable({ stage: "statement" });
526
+ await applyEngineBudget();
244
527
  return runExecuteUnsafe({ queryable: client, ...execParams });
245
528
  },
246
529
  async transaction(fn, nestedOptions) {
247
530
  refuseNestedTransactionOptions({ options: nestedOptions, provider: PROVIDER });
248
- const savepoint = nextSavepointName({ counter: savepointCounter });
249
- await runMapped(() => client.query({ text: `SAVEPOINT ${savepoint}` }));
250
- try {
251
- const result = await fn(createClientAdapter({ client, savepointCounter }));
252
- await runMapped(() => client.query({ text: `RELEASE SAVEPOINT ${savepoint}` }));
253
- return result;
254
- } catch (error) {
255
- try {
256
- await client.query({ text: `ROLLBACK TO SAVEPOINT ${savepoint}` });
257
- } catch {}
258
- throw error;
259
- }
531
+ budget.assertUsable({ stage: "nested" });
532
+ return runNestedSavepoint({
533
+ scope,
534
+ execute: (statement) => runMapped(() => client.query({ text: statement })),
535
+ body: (child) => fn(createClientAdapter({ client, scope: child, budget }))
536
+ });
260
537
  },
261
538
  async connect() {},
262
539
  async disconnect() {},
@@ -269,6 +546,7 @@ function pgAdapter(options) {
269
546
  dialect: DIALECT,
270
547
  provider: PROVIDER,
271
548
  wire: WIRE,
549
+ budgets: BUDGETS,
272
550
  async execute(params) {
273
551
  return runExecute({ queryable: getPool(), ...params });
274
552
  },
@@ -277,10 +555,13 @@ function pgAdapter(options) {
277
555
  if (rawSession !== null) {
278
556
  if (control === "close") {
279
557
  const session = rawSession;
558
+ const unguard = rawSessionUnguard;
280
559
  rawSession = null;
560
+ rawSessionUnguard = null;
281
561
  try {
282
562
  return await runExecuteUnsafe({ queryable: session, ...params });
283
563
  } finally {
564
+ unguard?.();
284
565
  session.release();
285
566
  }
286
567
  }
@@ -288,11 +569,14 @@ function pgAdapter(options) {
288
569
  }
289
570
  if (control === "open") {
290
571
  const client = await runMapped(() => getPool().connect());
572
+ const unguard = guardBackgroundErrors({ client });
291
573
  try {
292
574
  const result = await runExecuteUnsafe({ queryable: client, ...params });
293
575
  rawSession = client;
576
+ rawSessionUnguard = unguard;
294
577
  return result;
295
578
  } catch (error) {
579
+ unguard();
296
580
  client.release();
297
581
  throw error;
298
582
  }
@@ -300,24 +584,49 @@ function pgAdapter(options) {
300
584
  return runExecuteUnsafe({ queryable: getPool(), ...params });
301
585
  },
302
586
  async transaction(fn, transactionOptions) {
587
+ validateTransactionDeadline({
588
+ options: transactionOptions,
589
+ support: BUDGETS.transactionDeadline,
590
+ provider: PROVIDER
591
+ });
303
592
  const client = await runMapped(() => getPool().connect());
304
- const savepointCounter = createSavepointCounter();
593
+ const unguard = guardBackgroundErrors({ client });
594
+ const scope = createSavepointScope();
595
+ const budget = startTransactionBudget({ options: transactionOptions, provider: PROVIDER });
596
+ let discard = false;
305
597
  try {
306
598
  await runMapped(() => client.query({ text: beginStatement({ isolationLevel: transactionOptions?.isolationLevel }) }));
307
599
  if (transactionOptions?.timeout !== undefined) {
308
600
  const text = statementTimeoutStatement({ timeout: transactionOptions.timeout });
309
601
  await runMapped(() => client.query({ text }));
310
602
  }
311
- const result = await fn(createClientAdapter({ client, savepointCounter }));
603
+ const result = await runRootScope({
604
+ scope,
605
+ body: () => fn(createClientAdapter({ client, scope, budget }))
606
+ });
607
+ budget.assertUsable({ stage: "commit" });
312
608
  await runMapped(() => client.query({ text: "COMMIT" }));
609
+ budget.close({ outcome: "committed" });
313
610
  return result;
314
611
  } catch (error) {
315
612
  try {
316
613
  await client.query({ text: "ROLLBACK" });
317
- } catch {}
614
+ budget.close({ outcome: "rolled-back" });
615
+ } catch {
616
+ discard = true;
617
+ budget.close({ outcome: "unknown" });
618
+ if (budget.totalMs !== null && isTransactionDeadlineError(error)) {
619
+ throw transactionOutcomeUnknownError({
620
+ provider: PROVIDER,
621
+ totalMs: budget.totalMs,
622
+ cause: error
623
+ });
624
+ }
625
+ }
318
626
  throw error;
319
627
  } finally {
320
- client.release();
628
+ unguard();
629
+ client.release(discard);
321
630
  }
322
631
  },
323
632
  async connect() {
@@ -327,10 +636,13 @@ function pgAdapter(options) {
327
636
  async disconnect() {
328
637
  if (rawSession !== null) {
329
638
  const session = rawSession;
639
+ const unguard = rawSessionUnguard;
330
640
  rawSession = null;
641
+ rawSessionUnguard = null;
331
642
  try {
332
643
  await session.query({ text: "ROLLBACK" });
333
644
  } catch {}
645
+ unguard?.();
334
646
  session.release();
335
647
  }
336
648
  if (pool !== null && ownsPool) {
@@ -345,7 +657,12 @@ function pgAdapter(options) {
345
657
  };
346
658
  }
347
659
  export {
660
+ validateTransactionDeadline,
661
+ transactionOutcomeUnknownError,
662
+ transactionDeadlineError,
663
+ transactionClosedError,
348
664
  statementTimeoutStatement,
665
+ startTransactionBudget,
349
666
  serializeParamsRaw,
350
667
  serializeParams,
351
668
  refuseNestedTransactionOptions,
@@ -353,6 +670,8 @@ export {
353
670
  pgAdapter,
354
671
  nextSavepointName,
355
672
  mapPgDriverError,
673
+ isTransactionDeadlineError,
674
+ engineStatementBudgetMs,
356
675
  createStatementNameCache,
357
676
  createSavepointCounter,
358
677
  classifyRawTransactionControl,
@@ -360,7 +679,8 @@ export {
360
679
  PgArrayParam,
361
680
  PG_SQLSTATE_REASONS,
362
681
  PG_SQLSTATE_ERROR_CODES,
363
- ISOLATION_LEVEL_SQL
682
+ ISOLATION_LEVEL_SQL,
683
+ DEFAULT_DEADLINE_CLOCK
364
684
  };
365
685
 
366
- //# debugId=7F872087B9FB07A764756E2164756E21
686
+ //# debugId=17E44C1AF7E3C0D164756E2164756E21