turbine-orm 0.7.1 → 0.8.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.
@@ -0,0 +1,397 @@
1
+ /**
2
+ * turbine-orm — Real Postgres pipeline protocol implementation
3
+ *
4
+ * Uses the pg extended-query protocol wire methods (parse/bind/describe/execute/sync)
5
+ * exposed on pg.Client's Connection object to send multiple queries in a single
6
+ * TCP flush. This achieves true 1-RTT pipeline execution instead of the sequential
7
+ * await-per-query approach.
8
+ *
9
+ * The approach (listener-swap):
10
+ * 1. Detach the pg.Client's event listeners from the Connection
11
+ * 2. Attach our own state-machine listeners
12
+ * 3. Cork the TCP stream, push all protocol messages, uncork (one TCP write)
13
+ * 4. Drive a state machine over backend response events
14
+ * 5. Restore original listeners and release the client
15
+ *
16
+ * This is the same pattern used by pg-cursor and pg-query-stream, but extended
17
+ * to handle N queries in a single pipeline.
18
+ */
19
+ import Result from 'pg/lib/result';
20
+ import { prepareValue } from 'pg/lib/utils';
21
+ import { wrapPgError } from './errors.js';
22
+ // ---------------------------------------------------------------------------
23
+ // Event names we intercept
24
+ // ---------------------------------------------------------------------------
25
+ /**
26
+ * All backend message event names that the pg Client listens for.
27
+ * We snapshot, detach, and restore listeners for these events.
28
+ */
29
+ const INTERCEPTED_EVENTS = [
30
+ 'readyForQuery',
31
+ 'rowDescription',
32
+ 'dataRow',
33
+ 'commandComplete',
34
+ 'parseComplete',
35
+ 'bindComplete',
36
+ 'errorMessage',
37
+ 'emptyQuery',
38
+ 'portalSuspended',
39
+ 'noData',
40
+ 'notice',
41
+ 'copyInResponse',
42
+ 'copyData',
43
+ ];
44
+ function snapshotListeners(emitter) {
45
+ const map = new Map();
46
+ for (const event of INTERCEPTED_EVENTS) {
47
+ const listeners = emitter.rawListeners(event);
48
+ if (listeners.length > 0) {
49
+ map.set(event, [...listeners]);
50
+ }
51
+ }
52
+ return map;
53
+ }
54
+ function detachListeners(emitter) {
55
+ for (const event of INTERCEPTED_EVENTS) {
56
+ emitter.removeAllListeners(event);
57
+ }
58
+ }
59
+ function restoreListeners(emitter, snapshot) {
60
+ for (const [event, listeners] of snapshot) {
61
+ for (const fn of listeners) {
62
+ emitter.on(event, fn);
63
+ }
64
+ }
65
+ }
66
+ // ---------------------------------------------------------------------------
67
+ // Core pipeline execution
68
+ // ---------------------------------------------------------------------------
69
+ /**
70
+ * Execute multiple queries using the Postgres extended-query pipeline protocol.
71
+ *
72
+ * All protocol messages are buffered into a single TCP write via cork/uncork.
73
+ * The backend processes them in order and sends back results which our state
74
+ * machine collects.
75
+ *
76
+ * @param client - A pg PoolClient with an accessible Connection
77
+ * @param queries - Array of DeferredQuery descriptors
78
+ * @param options - Pipeline options (transactional, timeout)
79
+ * @returns Array of transformed results in the same order as queries
80
+ */
81
+ export async function runPipelined(client, queries, options = {}) {
82
+ const { transactional = true, timeout } = options;
83
+ const connection = client.connection;
84
+ // Snapshot and detach the Client's listeners
85
+ const savedListeners = snapshotListeners(connection);
86
+ detachListeners(connection);
87
+ // Block the Client from processing any queued queries while we own the connection
88
+ const savedReadyForQuery = client.readyForQuery;
89
+ client.readyForQuery = false;
90
+ return new Promise((resolve, reject) => {
91
+ // -----------------------------------------------------------------------
92
+ // State machine
93
+ // -----------------------------------------------------------------------
94
+ /**
95
+ * Total expected commandComplete events:
96
+ * - Transactional: BEGIN + N queries + COMMIT = N + 2
97
+ * - Non-transactional: N queries
98
+ */
99
+ const totalCommands = transactional ? queries.length + 2 : queries.length;
100
+ /**
101
+ * Expected readyForQuery events:
102
+ * - Transactional: 1 (single Sync at end)
103
+ * - Non-transactional: N (one Sync per query)
104
+ */
105
+ const expectedRfq = transactional ? 1 : queries.length;
106
+ // Results array: one Result per query (not counting BEGIN/COMMIT)
107
+ const results = [];
108
+ for (let i = 0; i < queries.length; i++) {
109
+ results.push(new Result(undefined, client._types));
110
+ }
111
+ // commandComplete counter — tracks position across all commands
112
+ let commandIndex = 0;
113
+ // readyForQuery counter
114
+ let rfqCount = 0;
115
+ // First error encountered
116
+ let pipelineError = null;
117
+ // For non-transactional mode: per-query error tracking
118
+ const queryErrors = new Array(queries.length).fill(null);
119
+ // Timeout handle
120
+ let timeoutHandle;
121
+ // Whether cleanup has already been performed
122
+ let cleaned = false;
123
+ /**
124
+ * Map commandComplete index to the corresponding results[] index.
125
+ * In transactional mode: index 0 = BEGIN, 1..N = queries, N+1 = COMMIT
126
+ * In non-transactional mode: index maps 1:1
127
+ */
128
+ function commandToQueryIndex(cmdIdx) {
129
+ if (transactional) {
130
+ if (cmdIdx === 0 || cmdIdx === totalCommands - 1)
131
+ return null;
132
+ return cmdIdx - 1;
133
+ }
134
+ return cmdIdx;
135
+ }
136
+ /** Get the current query-results index for row data */
137
+ function currentQueryIndex() {
138
+ return commandToQueryIndex(commandIndex);
139
+ }
140
+ // -----------------------------------------------------------------------
141
+ // Cleanup: restore listeners
142
+ // -----------------------------------------------------------------------
143
+ function cleanup() {
144
+ if (cleaned)
145
+ return;
146
+ cleaned = true;
147
+ if (timeoutHandle !== undefined) {
148
+ clearTimeout(timeoutHandle);
149
+ timeoutHandle = undefined;
150
+ }
151
+ // Remove our listeners
152
+ detachListeners(connection);
153
+ // Restore original listeners
154
+ restoreListeners(connection, savedListeners);
155
+ // Restore readyForQuery so the Client can process its queue
156
+ client.readyForQuery = savedReadyForQuery;
157
+ }
158
+ // -----------------------------------------------------------------------
159
+ // Finalize: called on final readyForQuery
160
+ // -----------------------------------------------------------------------
161
+ function finalize() {
162
+ cleanup();
163
+ if (transactional && pipelineError) {
164
+ reject(pipelineError);
165
+ return;
166
+ }
167
+ if (!transactional && pipelineError) {
168
+ // In non-transactional mode, attach partial results to the error
169
+ const partialResults = [];
170
+ for (let i = 0; i < queries.length; i++) {
171
+ const qErr = queryErrors[i];
172
+ if (qErr) {
173
+ partialResults.push({ status: 'error', error: qErr });
174
+ }
175
+ else {
176
+ try {
177
+ const q = queries[i];
178
+ partialResults.push({ status: 'ok', value: q.transform(results[i]) });
179
+ }
180
+ catch (transformErr) {
181
+ partialResults.push({ status: 'error', error: transformErr });
182
+ }
183
+ }
184
+ }
185
+ pipelineError.results = partialResults;
186
+ reject(pipelineError);
187
+ return;
188
+ }
189
+ // All succeeded — transform results
190
+ try {
191
+ const transformed = [];
192
+ for (let i = 0; i < queries.length; i++) {
193
+ const q = queries[i];
194
+ transformed.push(q.transform(results[i]));
195
+ }
196
+ resolve(transformed);
197
+ }
198
+ catch (err) {
199
+ reject(err);
200
+ }
201
+ }
202
+ // -----------------------------------------------------------------------
203
+ // Event handlers
204
+ // -----------------------------------------------------------------------
205
+ function onParseComplete() {
206
+ // No action needed — anonymous prepared statements
207
+ }
208
+ function onBindComplete() {
209
+ // No action needed
210
+ }
211
+ function onNoData() {
212
+ // DML without RETURNING — no RowDescription follows. Fine.
213
+ }
214
+ function onRowDescription(msg) {
215
+ const qIdx = currentQueryIndex();
216
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
217
+ results[qIdx].addFields(msg.fields);
218
+ }
219
+ }
220
+ function onDataRow(msg) {
221
+ const qIdx = currentQueryIndex();
222
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
223
+ const result = results[qIdx];
224
+ const row = result.parseRow(msg.fields);
225
+ result.addRow(row);
226
+ }
227
+ }
228
+ function onCommandComplete(msg) {
229
+ const qIdx = currentQueryIndex();
230
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
231
+ results[qIdx].addCommandComplete(msg);
232
+ }
233
+ commandIndex++;
234
+ }
235
+ function onEmptyQuery() {
236
+ // Treat like a commandComplete with no data
237
+ commandIndex++;
238
+ }
239
+ function onErrorMessage(msg) {
240
+ const wrapped = wrapPgError(msg);
241
+ const error = wrapped instanceof Error ? wrapped : new Error(String(wrapped));
242
+ if (transactional) {
243
+ // In transactional mode, the first error aborts everything.
244
+ // Postgres marks the transaction as aborted; subsequent commands
245
+ // until Sync all return errors which we absorb.
246
+ if (!pipelineError) {
247
+ const qIdx = currentQueryIndex();
248
+ if (qIdx !== null && qIdx >= 0 && qIdx < queries.length) {
249
+ error.failedIndex = qIdx;
250
+ error.failedTag = queries[qIdx].tag;
251
+ }
252
+ pipelineError = error;
253
+ }
254
+ }
255
+ else {
256
+ // Non-transactional: record per-query error
257
+ const qIdx = currentQueryIndex();
258
+ if (qIdx !== null && qIdx >= 0 && qIdx < queries.length) {
259
+ queryErrors[qIdx] = error;
260
+ if (!pipelineError) {
261
+ error.failedIndex = qIdx;
262
+ error.failedTag = queries[qIdx].tag;
263
+ pipelineError = error;
264
+ }
265
+ }
266
+ }
267
+ // Advance command index past the failed command
268
+ commandIndex++;
269
+ }
270
+ function onPortalSuspended() {
271
+ // We don't use row-limited portals
272
+ }
273
+ function onReadyForQuery() {
274
+ rfqCount++;
275
+ if (rfqCount >= expectedRfq) {
276
+ finalize();
277
+ }
278
+ }
279
+ // -----------------------------------------------------------------------
280
+ // Attach our listeners
281
+ // -----------------------------------------------------------------------
282
+ connection.on('parseComplete', onParseComplete);
283
+ connection.on('bindComplete', onBindComplete);
284
+ connection.on('noData', onNoData);
285
+ connection.on('rowDescription', onRowDescription);
286
+ connection.on('dataRow', onDataRow);
287
+ connection.on('commandComplete', onCommandComplete);
288
+ connection.on('emptyQuery', onEmptyQuery);
289
+ connection.on('errorMessage', onErrorMessage);
290
+ connection.on('portalSuspended', onPortalSuspended);
291
+ connection.on('readyForQuery', onReadyForQuery);
292
+ connection.on('notice', () => { });
293
+ connection.on('copyInResponse', () => { });
294
+ connection.on('copyData', () => { });
295
+ // -----------------------------------------------------------------------
296
+ // Timeout
297
+ // -----------------------------------------------------------------------
298
+ if (timeout && timeout > 0) {
299
+ timeoutHandle = setTimeout(() => {
300
+ pipelineError = new Error(`[turbine] Pipeline timed out after ${timeout}ms`);
301
+ if (connection.stream.destroy) {
302
+ connection.stream.destroy(pipelineError);
303
+ }
304
+ cleanup();
305
+ reject(pipelineError);
306
+ }, timeout);
307
+ }
308
+ // -----------------------------------------------------------------------
309
+ // Send protocol messages — all in one TCP flush
310
+ // -----------------------------------------------------------------------
311
+ try {
312
+ if (connection.stream.cork) {
313
+ connection.stream.cork();
314
+ }
315
+ if (transactional) {
316
+ // ---- Transactional mode ----
317
+ // BEGIN + N×(parse+bind+describe+execute) + COMMIT + sync
318
+ // One sync = one ReadyForQuery
319
+ // BEGIN
320
+ connection.parse({ text: 'BEGIN', name: '' });
321
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
322
+ connection.execute({ portal: '', rows: 0 });
323
+ // Each query
324
+ for (const q of queries) {
325
+ connection.parse({ text: q.sql, name: '' });
326
+ connection.bind({
327
+ portal: '',
328
+ statement: '',
329
+ values: q.params,
330
+ valueMapper: prepareValue,
331
+ });
332
+ connection.describe({ type: 'P', name: '' });
333
+ connection.execute({ portal: '', rows: 0 });
334
+ }
335
+ // COMMIT
336
+ connection.parse({ text: 'COMMIT', name: '' });
337
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
338
+ connection.execute({ portal: '', rows: 0 });
339
+ // Single Sync
340
+ connection.sync();
341
+ }
342
+ else {
343
+ // ---- Non-transactional mode ----
344
+ // N×(parse+bind+describe+execute+sync)
345
+ // Each sync = one ReadyForQuery
346
+ // All messages still go in one cork/uncork (one TCP flush)
347
+ for (const q of queries) {
348
+ connection.parse({ text: q.sql, name: '' });
349
+ connection.bind({
350
+ portal: '',
351
+ statement: '',
352
+ values: q.params,
353
+ valueMapper: prepareValue,
354
+ });
355
+ connection.describe({ type: 'P', name: '' });
356
+ connection.execute({ portal: '', rows: 0 });
357
+ connection.sync();
358
+ }
359
+ }
360
+ if (connection.stream.uncork) {
361
+ connection.stream.uncork();
362
+ }
363
+ }
364
+ catch (err) {
365
+ cleanup();
366
+ reject(err);
367
+ }
368
+ });
369
+ }
370
+ // ---------------------------------------------------------------------------
371
+ // Capability detection
372
+ // ---------------------------------------------------------------------------
373
+ /**
374
+ * Check whether a pool client supports the extended-query pipeline protocol.
375
+ *
376
+ * Returns true if the client has a Connection object with the required wire
377
+ * protocol methods (parse, bind, describe, execute, sync) and is an EventEmitter.
378
+ *
379
+ * Returns false for HTTP-based drivers (Neon HTTP, Vercel Postgres), mock pools,
380
+ * and any pool that doesn't expose pg internals.
381
+ */
382
+ export function supportsExtendedPipeline(poolClient) {
383
+ if (!poolClient || typeof poolClient !== 'object')
384
+ return false;
385
+ const client = poolClient;
386
+ const conn = client.connection;
387
+ if (!conn || typeof conn !== 'object')
388
+ return false;
389
+ const c = conn;
390
+ return (typeof c.parse === 'function' &&
391
+ typeof c.bind === 'function' &&
392
+ typeof c.describe === 'function' &&
393
+ typeof c.execute === 'function' &&
394
+ typeof c.sync === 'function' &&
395
+ typeof c.on === 'function');
396
+ }
397
+ //# sourceMappingURL=pipeline-submittable.js.map
@@ -7,21 +7,41 @@
7
7
  * How it works:
8
8
  * 1. Each query method (findUnique, count, etc.) can produce a DeferredQuery descriptor
9
9
  * containing the SQL, params, and a transform function.
10
- * 2. pipeline() collects these descriptors, executes them in a single Postgres
11
- * pipeline/transaction, and maps each result through its transform.
10
+ * 2. pipeline() collects these descriptors, checks whether the underlying pool client
11
+ * supports the extended-query pipeline protocol, and either:
12
+ * (a) executes them via real Postgres pipeline protocol (one TCP flush), or
13
+ * (b) falls back to sequential execution on a single connection.
12
14
  *
13
- * In the production Turbine engine, this would go through the Rust proxy which uses
14
- * actual Postgres pipeline protocol (libpq PQpipelineEnter). For the TS SDK prototype,
15
- * we simulate it by running queries concurrently on a single connection or via
16
- * a multi-statement batch.
15
+ * Real pipeline mode uses `src/pipeline-submittable.ts` which drives the pg Connection's
16
+ * wire-protocol methods (parse/bind/describe/execute/sync) directly with listener-swap.
17
+ *
18
+ * Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
19
+ * Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
17
20
  */
18
21
  import type pg from 'pg';
19
22
  import type { DeferredQuery } from './query.js';
23
+ export interface PipelineOptions {
24
+ /**
25
+ * Whether to wrap the pipeline in a transaction (default: true).
26
+ *
27
+ * - `true` (default): All queries execute atomically within BEGIN/COMMIT.
28
+ * If any query fails, the entire batch is rolled back.
29
+ *
30
+ * - `false`: Each query is independent. A failure in one query does NOT
31
+ * affect others. On partial failure, a `PipelineError` is thrown with
32
+ * per-query results in `.results`.
33
+ */
34
+ transactional?: boolean;
35
+ /** Timeout in milliseconds. If exceeded, the connection is destroyed. */
36
+ timeout?: number;
37
+ }
20
38
  /**
21
39
  * Execute multiple deferred queries in a single batch.
22
40
  *
23
- * Uses a single connection from the pool and runs all queries within
24
- * a transaction to guarantee consistency and minimize round-trips.
41
+ * On pg.Pool-backed connections with the standard TCP driver, this uses the
42
+ * real Postgres extended-query pipeline protocol for true 1-RTT execution.
43
+ * On HTTP-based drivers (Neon HTTP, Vercel Postgres, etc.) or mock pools,
44
+ * it falls back to sequential execution on a single connection.
25
45
  *
26
46
  * @example
27
47
  * ```ts
@@ -32,7 +52,15 @@ import type { DeferredQuery } from './query.js';
32
52
  * ]);
33
53
  * ```
34
54
  */
35
- export declare function executePipeline<T extends readonly DeferredQuery<unknown>[]>(pool: pg.Pool, queries: T): Promise<PipelineResults<T>>;
55
+ export declare function executePipeline<T extends readonly DeferredQuery<unknown>[]>(pool: pg.Pool, queries: T, options?: PipelineOptions): Promise<PipelineResults<T>>;
56
+ /**
57
+ * Check whether a pool supports the real pipeline protocol.
58
+ * Call this to determine at runtime whether pipelines will use the fast path
59
+ * or fall back to sequential execution.
60
+ *
61
+ * Note: This acquires and immediately releases a connection to inspect it.
62
+ */
63
+ export declare function pipelineSupported(pool: pg.Pool): Promise<boolean>;
36
64
  /**
37
65
  * Extract the result types from a tuple of DeferredQuery objects.
38
66
  * If you pass [DeferredQuery<User>, DeferredQuery<number>, DeferredQuery<Post[]>],
package/dist/pipeline.js CHANGED
@@ -7,23 +7,71 @@
7
7
  * How it works:
8
8
  * 1. Each query method (findUnique, count, etc.) can produce a DeferredQuery descriptor
9
9
  * containing the SQL, params, and a transform function.
10
- * 2. pipeline() collects these descriptors, executes them in a single Postgres
11
- * pipeline/transaction, and maps each result through its transform.
10
+ * 2. pipeline() collects these descriptors, checks whether the underlying pool client
11
+ * supports the extended-query pipeline protocol, and either:
12
+ * (a) executes them via real Postgres pipeline protocol (one TCP flush), or
13
+ * (b) falls back to sequential execution on a single connection.
12
14
  *
13
- * In the production Turbine engine, this would go through the Rust proxy which uses
14
- * actual Postgres pipeline protocol (libpq PQpipelineEnter). For the TS SDK prototype,
15
- * we simulate it by running queries concurrently on a single connection or via
16
- * a multi-statement batch.
15
+ * Real pipeline mode uses `src/pipeline-submittable.ts` which drives the pg Connection's
16
+ * wire-protocol methods (parse/bind/describe/execute/sync) directly with listener-swap.
17
+ *
18
+ * Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
19
+ * Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
17
20
  */
18
21
  import { wrapPgError } from './errors.js';
22
+ import { runPipelined, supportsExtendedPipeline } from './pipeline-submittable.js';
23
+ /**
24
+ * Execute queries sequentially on an already-acquired connection.
25
+ * This is the fallback path for clients that don't support the extended-query
26
+ * pipeline protocol (HTTP drivers, mocks, etc.).
27
+ *
28
+ * The caller is responsible for acquiring the client and releasing it after
29
+ * this function completes (in the finally block).
30
+ */
31
+ async function runSequential(client, queries, options = {}) {
32
+ const { transactional = true } = options;
33
+ try {
34
+ if (transactional) {
35
+ await client.query('BEGIN');
36
+ }
37
+ const results = [];
38
+ for (const q of queries) {
39
+ let raw;
40
+ try {
41
+ raw = await client.query(q.sql, q.params);
42
+ }
43
+ catch (err) {
44
+ throw wrapPgError(err);
45
+ }
46
+ results.push(q.transform(raw));
47
+ }
48
+ if (transactional) {
49
+ await client.query('COMMIT');
50
+ }
51
+ return results;
52
+ }
53
+ catch (err) {
54
+ if (transactional) {
55
+ try {
56
+ await client.query('ROLLBACK');
57
+ }
58
+ catch {
59
+ // Best-effort rollback
60
+ }
61
+ }
62
+ throw err;
63
+ }
64
+ }
19
65
  // ---------------------------------------------------------------------------
20
- // Pipeline executor
66
+ // Pipeline executor (public)
21
67
  // ---------------------------------------------------------------------------
22
68
  /**
23
69
  * Execute multiple deferred queries in a single batch.
24
70
  *
25
- * Uses a single connection from the pool and runs all queries within
26
- * a transaction to guarantee consistency and minimize round-trips.
71
+ * On pg.Pool-backed connections with the standard TCP driver, this uses the
72
+ * real Postgres extended-query pipeline protocol for true 1-RTT execution.
73
+ * On HTTP-based drivers (Neon HTTP, Vercel Postgres, etc.) or mock pools,
74
+ * it falls back to sequential execution on a single connection.
27
75
  *
28
76
  * @example
29
77
  * ```ts
@@ -34,43 +82,47 @@ import { wrapPgError } from './errors.js';
34
82
  * ]);
35
83
  * ```
36
84
  */
37
- export async function executePipeline(pool, queries) {
85
+ export async function executePipeline(pool, queries, options) {
38
86
  if (queries.length === 0) {
39
87
  return [];
40
88
  }
41
- // Acquire a single connection for the entire batch
89
+ // Acquire a single client — reused for both capability check and execution
42
90
  const client = await pool.connect();
43
91
  try {
44
- // Wrap in a transaction for consistency
45
- await client.query('BEGIN');
46
- // Execute all queries on the same connection — in sequence on a single
47
- // connection this avoids pool checkout overhead, and the Postgres server
48
- // processes them as a tight batch.
49
- //
50
- // Execute queries sequentially on the same connection to avoid the
51
- // "already executing a query" deprecation in pg@8. This is still faster
52
- // than separate pool checkouts because we skip N-1 acquire/release cycles.
53
- // Future: use actual Postgres pipeline protocol for true pipelining.
54
- const results = [];
55
- for (const q of queries) {
56
- let raw;
57
- try {
58
- raw = await client.query(q.sql, q.params);
59
- }
60
- catch (err) {
61
- throw wrapPgError(err);
62
- }
63
- results.push(q.transform(raw));
92
+ if (supportsExtendedPipeline(client)) {
93
+ // Real pipeline path — uses extended-query protocol wire methods
94
+ const pipelineOptions = {
95
+ transactional: options?.transactional ?? true,
96
+ timeout: options?.timeout,
97
+ };
98
+ const results = await runPipelined(client, queries, pipelineOptions);
99
+ return results;
64
100
  }
65
- await client.query('COMMIT');
66
- return results;
67
- }
68
- catch (err) {
69
- await client.query('ROLLBACK');
70
- throw err;
101
+ // Sequential fallback — reuses the same client
102
+ return await runSequential(client, queries, options);
71
103
  }
72
104
  finally {
73
105
  client.release();
74
106
  }
75
107
  }
108
+ /**
109
+ * Check whether a pool supports the real pipeline protocol.
110
+ * Call this to determine at runtime whether pipelines will use the fast path
111
+ * or fall back to sequential execution.
112
+ *
113
+ * Note: This acquires and immediately releases a connection to inspect it.
114
+ */
115
+ export async function pipelineSupported(pool) {
116
+ let client;
117
+ try {
118
+ client = await pool.connect();
119
+ return supportsExtendedPipeline(client);
120
+ }
121
+ catch {
122
+ return false;
123
+ }
124
+ finally {
125
+ client?.release();
126
+ }
127
+ }
76
128
  //# sourceMappingURL=pipeline.js.map