turbine-orm 0.7.1 → 0.9.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.
@@ -134,9 +134,15 @@ class TransactionClient {
134
134
  // Return a minimal pool-compatible object that routes queries
135
135
  // through the transaction client
136
136
  return {
137
- query: async (text, values) => {
137
+ query: async (textOrConfig, values) => {
138
138
  try {
139
- return await client.query(text, values);
139
+ if (typeof textOrConfig === 'string') {
140
+ return await client.query(textOrConfig, values);
141
+ }
142
+ // Object form for prepared statements: { name, text, values }
143
+ // pg.PoolClient.query accepts QueryConfig but the overloads make TS
144
+ // unhappy with the union, so we cast through unknown.
145
+ return await client.query(textOrConfig);
140
146
  }
141
147
  catch (err) {
142
148
  throw (0, errors_js_1.wrapPgError)(err);
@@ -191,9 +197,13 @@ class TurbineClient {
191
197
  }
192
198
  this.logging = config.logging ?? false;
193
199
  this.schema = schema;
200
+ // Respect env var kill switch
201
+ const envDisablePrepared = typeof process !== 'undefined' && process.env?.TURBINE_DISABLE_PREPARED === '1';
194
202
  this.queryOptions = {
195
203
  defaultLimit: config.defaultLimit,
196
204
  warnOnUnlimited: config.warnOnUnlimited,
205
+ preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
206
+ sqlCache: config.sqlCache ?? true,
197
207
  };
198
208
  // Apply NotFoundError message redaction mode (default: safe — values are
199
209
  // stripped from messages to avoid leaking PII into error logs).
@@ -307,13 +317,41 @@ class TurbineClient {
307
317
  /**
308
318
  * Execute multiple queries in a single database round-trip.
309
319
  *
310
- * Pass the result of any `.build*()` method on a table accessor.
320
+ * Two call styles:
321
+ * - `db.pipeline(q1, q2, q3)` — rest params (backward-compatible)
322
+ * - `db.pipeline([q1, q2, q3], { transactional: false })` — array + options
323
+ *
324
+ * On pg.Pool-backed connections with TCP, this uses the real Postgres
325
+ * extended-query pipeline protocol (one TCP flush, one round-trip).
326
+ * On HTTP-based drivers it falls back to sequential execution.
311
327
  */
312
- async pipeline(...queries) {
328
+ async pipeline(...args) {
329
+ let queries;
330
+ let options;
331
+ // Detect which overload was used
332
+ if (args.length > 0 &&
333
+ Array.isArray(args[0]) &&
334
+ args[0].every((item) => item && typeof item === 'object' && 'sql' in item)) {
335
+ // Array form: pipeline([q1, q2], opts?)
336
+ queries = args[0];
337
+ options = args[1];
338
+ }
339
+ else {
340
+ // Rest-param form: pipeline(q1, q2, q3)
341
+ queries = args;
342
+ }
313
343
  if (this.logging) {
314
344
  console.log(`[turbine] Pipeline: ${queries.length} queries — ${queries.map((q) => q.tag).join(', ')}`);
315
345
  }
316
- return (0, pipeline_js_1.executePipeline)(this.pool, queries);
346
+ return (0, pipeline_js_1.executePipeline)(this.pool, queries, options);
347
+ }
348
+ /**
349
+ * Check whether the underlying pool supports the real pipeline protocol.
350
+ * Returns `true` for standard pg.Pool TCP connections, `false` for HTTP
351
+ * drivers (Neon HTTP, Vercel Postgres, etc.) and mock pools.
352
+ */
353
+ async pipelineSupported() {
354
+ return (0, pipeline_js_1.pipelineSupported)(this.pool);
317
355
  }
318
356
  // -------------------------------------------------------------------------
319
357
  // Raw SQL — tagged template literal escape hatch
@@ -6,7 +6,7 @@
6
6
  * All Turbine errors extend TurbineError which includes a `code` property.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
9
+ exports.PipelineError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
10
10
  exports.setErrorMessageMode = setErrorMessageMode;
11
11
  exports.getErrorMessageMode = getErrorMessageMode;
12
12
  exports.wrapPgError = wrapPgError;
@@ -25,6 +25,7 @@ exports.TurbineErrorCode = {
25
25
  CHECK_VIOLATION: 'TURBINE_E011',
26
26
  DEADLOCK_DETECTED: 'TURBINE_E012',
27
27
  SERIALIZATION_FAILURE: 'TURBINE_E013',
28
+ PIPELINE: 'TURBINE_E014',
28
29
  };
29
30
  /** Base error class for all Turbine errors */
30
31
  class TurbineError extends Error {
@@ -350,6 +351,47 @@ class CheckConstraintError extends TurbineError {
350
351
  }
351
352
  }
352
353
  exports.CheckConstraintError = CheckConstraintError;
354
+ /**
355
+ * Thrown when a non-transactional pipeline has partial failures.
356
+ *
357
+ * In non-transactional mode (`{ transactional: false }`), each query executes
358
+ * independently. If one or more queries fail, the pipeline rejects with a
359
+ * `PipelineError` that carries per-query results so callers can inspect which
360
+ * succeeded and which failed.
361
+ *
362
+ * ```ts
363
+ * try {
364
+ * await db.pipeline([q1, q2, q3], { transactional: false });
365
+ * } catch (err) {
366
+ * if (err instanceof PipelineError) {
367
+ * for (const slot of err.results) {
368
+ * if (slot.status === 'error') console.error(slot.error);
369
+ * }
370
+ * }
371
+ * }
372
+ * ```
373
+ */
374
+ class PipelineError extends TurbineError {
375
+ /** Per-query results: each slot is either `{status:'ok', value}` or `{status:'error', error}` */
376
+ results;
377
+ /** Zero-based index of the first query that failed */
378
+ failedIndex;
379
+ /** Tag of the first query that failed (from DeferredQuery.tag) */
380
+ failedTag;
381
+ constructor(opts) {
382
+ const { results, failedIndex, failedTag, cause } = opts;
383
+ const failedCount = results.filter((r) => r.status === 'error').length;
384
+ const message = opts.message ??
385
+ `[turbine] Pipeline completed with ${failedCount} error(s) out of ${results.length} queries` +
386
+ (failedTag ? ` (first failure: ${failedTag} at index ${failedIndex})` : '');
387
+ super(exports.TurbineErrorCode.PIPELINE, message, { cause });
388
+ this.name = 'PipelineError';
389
+ this.results = results;
390
+ this.failedIndex = failedIndex;
391
+ this.failedTag = failedTag;
392
+ }
393
+ }
394
+ exports.PipelineError = PipelineError;
353
395
  /**
354
396
  * Parse column names out of a pg `detail` string like:
355
397
  * "Key (email)=(foo@bar) already exists."
package/dist/cjs/index.js CHANGED
@@ -34,7 +34,7 @@
34
34
  * ```
35
35
  */
36
36
  Object.defineProperty(exports, "__esModule", { value: true });
37
- exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.isDateType = exports.camelToSnake = exports.QueryInterface = exports.executePipeline = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.TurbineClient = exports.TransactionClient = void 0;
37
+ exports.turbineHttp = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.table = exports.defineSchema = exports.column = exports.ColumnBuilder = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.isDateType = exports.camelToSnake = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.PipelineError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.TurbineClient = exports.TransactionClient = void 0;
38
38
  // Client
39
39
  var client_js_1 = require("./client.js");
40
40
  Object.defineProperty(exports, "TransactionClient", { enumerable: true, get: function () { return client_js_1.TransactionClient; } });
@@ -50,6 +50,7 @@ Object.defineProperty(exports, "getErrorMessageMode", { enumerable: true, get: f
50
50
  Object.defineProperty(exports, "MigrationError", { enumerable: true, get: function () { return errors_js_1.MigrationError; } });
51
51
  Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: function () { return errors_js_1.NotFoundError; } });
52
52
  Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
53
+ Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
53
54
  Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
54
55
  Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
55
56
  Object.defineProperty(exports, "setErrorMessageMode", { enumerable: true, get: function () { return errors_js_1.setErrorMessageMode; } });
@@ -68,6 +69,7 @@ Object.defineProperty(exports, "introspect", { enumerable: true, get: function (
68
69
  // Pipeline
69
70
  var pipeline_js_1 = require("./pipeline.js");
70
71
  Object.defineProperty(exports, "executePipeline", { enumerable: true, get: function () { return pipeline_js_1.executePipeline; } });
72
+ Object.defineProperty(exports, "pipelineSupported", { enumerable: true, get: function () { return pipeline_js_1.pipelineSupported; } });
71
73
  // Query builder
72
74
  var query_js_1 = require("./query.js");
73
75
  Object.defineProperty(exports, "QueryInterface", { enumerable: true, get: function () { return query_js_1.QueryInterface; } });
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ /**
3
+ * turbine-orm — Real Postgres pipeline protocol implementation
4
+ *
5
+ * Uses the pg extended-query protocol wire methods (parse/bind/describe/execute/sync)
6
+ * exposed on pg.Client's Connection object to send multiple queries in a single
7
+ * TCP flush. This achieves true 1-RTT pipeline execution instead of the sequential
8
+ * await-per-query approach.
9
+ *
10
+ * The approach (listener-swap):
11
+ * 1. Detach the pg.Client's event listeners from the Connection
12
+ * 2. Attach our own state-machine listeners
13
+ * 3. Cork the TCP stream, push all protocol messages, uncork (one TCP write)
14
+ * 4. Drive a state machine over backend response events
15
+ * 5. Restore original listeners and release the client
16
+ *
17
+ * This is the same pattern used by pg-cursor and pg-query-stream, but extended
18
+ * to handle N queries in a single pipeline.
19
+ */
20
+ var __importDefault = (this && this.__importDefault) || function (mod) {
21
+ return (mod && mod.__esModule) ? mod : { "default": mod };
22
+ };
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ exports.runPipelined = runPipelined;
25
+ exports.supportsExtendedPipeline = supportsExtendedPipeline;
26
+ const result_1 = __importDefault(require("pg/lib/result"));
27
+ const utils_1 = require("pg/lib/utils");
28
+ const errors_js_1 = require("./errors.js");
29
+ // ---------------------------------------------------------------------------
30
+ // Event names we intercept
31
+ // ---------------------------------------------------------------------------
32
+ /**
33
+ * All backend message event names that the pg Client listens for.
34
+ * We snapshot, detach, and restore listeners for these events.
35
+ */
36
+ const INTERCEPTED_EVENTS = [
37
+ 'readyForQuery',
38
+ 'rowDescription',
39
+ 'dataRow',
40
+ 'commandComplete',
41
+ 'parseComplete',
42
+ 'bindComplete',
43
+ 'errorMessage',
44
+ 'emptyQuery',
45
+ 'portalSuspended',
46
+ 'noData',
47
+ 'notice',
48
+ 'copyInResponse',
49
+ 'copyData',
50
+ ];
51
+ function snapshotListeners(emitter) {
52
+ const map = new Map();
53
+ for (const event of INTERCEPTED_EVENTS) {
54
+ const listeners = emitter.rawListeners(event);
55
+ if (listeners.length > 0) {
56
+ map.set(event, [...listeners]);
57
+ }
58
+ }
59
+ return map;
60
+ }
61
+ function detachListeners(emitter) {
62
+ for (const event of INTERCEPTED_EVENTS) {
63
+ emitter.removeAllListeners(event);
64
+ }
65
+ }
66
+ function restoreListeners(emitter, snapshot) {
67
+ for (const [event, listeners] of snapshot) {
68
+ for (const fn of listeners) {
69
+ emitter.on(event, fn);
70
+ }
71
+ }
72
+ }
73
+ // ---------------------------------------------------------------------------
74
+ // Core pipeline execution
75
+ // ---------------------------------------------------------------------------
76
+ /**
77
+ * Execute multiple queries using the Postgres extended-query pipeline protocol.
78
+ *
79
+ * All protocol messages are buffered into a single TCP write via cork/uncork.
80
+ * The backend processes them in order and sends back results which our state
81
+ * machine collects.
82
+ *
83
+ * @param client - A pg PoolClient with an accessible Connection
84
+ * @param queries - Array of DeferredQuery descriptors
85
+ * @param options - Pipeline options (transactional, timeout)
86
+ * @returns Array of transformed results in the same order as queries
87
+ */
88
+ async function runPipelined(client, queries, options = {}) {
89
+ const { transactional = true, timeout } = options;
90
+ const connection = client.connection;
91
+ // Snapshot and detach the Client's listeners
92
+ const savedListeners = snapshotListeners(connection);
93
+ detachListeners(connection);
94
+ // Block the Client from processing any queued queries while we own the connection
95
+ const savedReadyForQuery = client.readyForQuery;
96
+ client.readyForQuery = false;
97
+ return new Promise((resolve, reject) => {
98
+ // -----------------------------------------------------------------------
99
+ // State machine
100
+ // -----------------------------------------------------------------------
101
+ /**
102
+ * Total expected commandComplete events:
103
+ * - Transactional: BEGIN + N queries + COMMIT = N + 2
104
+ * - Non-transactional: N queries
105
+ */
106
+ const totalCommands = transactional ? queries.length + 2 : queries.length;
107
+ /**
108
+ * Expected readyForQuery events:
109
+ * - Transactional: 1 (single Sync at end)
110
+ * - Non-transactional: N (one Sync per query)
111
+ */
112
+ const expectedRfq = transactional ? 1 : queries.length;
113
+ // Results array: one Result per query (not counting BEGIN/COMMIT)
114
+ const results = [];
115
+ for (let i = 0; i < queries.length; i++) {
116
+ results.push(new result_1.default(undefined, client._types));
117
+ }
118
+ // commandComplete counter — tracks position across all commands
119
+ let commandIndex = 0;
120
+ // readyForQuery counter
121
+ let rfqCount = 0;
122
+ // First error encountered
123
+ let pipelineError = null;
124
+ // For non-transactional mode: per-query error tracking
125
+ const queryErrors = new Array(queries.length).fill(null);
126
+ // Timeout handle
127
+ let timeoutHandle;
128
+ // Whether cleanup has already been performed
129
+ let cleaned = false;
130
+ /**
131
+ * Map commandComplete index to the corresponding results[] index.
132
+ * In transactional mode: index 0 = BEGIN, 1..N = queries, N+1 = COMMIT
133
+ * In non-transactional mode: index maps 1:1
134
+ */
135
+ function commandToQueryIndex(cmdIdx) {
136
+ if (transactional) {
137
+ if (cmdIdx === 0 || cmdIdx === totalCommands - 1)
138
+ return null;
139
+ return cmdIdx - 1;
140
+ }
141
+ return cmdIdx;
142
+ }
143
+ /** Get the current query-results index for row data */
144
+ function currentQueryIndex() {
145
+ return commandToQueryIndex(commandIndex);
146
+ }
147
+ // -----------------------------------------------------------------------
148
+ // Cleanup: restore listeners
149
+ // -----------------------------------------------------------------------
150
+ function cleanup() {
151
+ if (cleaned)
152
+ return;
153
+ cleaned = true;
154
+ if (timeoutHandle !== undefined) {
155
+ clearTimeout(timeoutHandle);
156
+ timeoutHandle = undefined;
157
+ }
158
+ // Remove our listeners
159
+ detachListeners(connection);
160
+ // Restore original listeners
161
+ restoreListeners(connection, savedListeners);
162
+ // Restore readyForQuery so the Client can process its queue
163
+ client.readyForQuery = savedReadyForQuery;
164
+ }
165
+ // -----------------------------------------------------------------------
166
+ // Finalize: called on final readyForQuery
167
+ // -----------------------------------------------------------------------
168
+ function finalize() {
169
+ cleanup();
170
+ if (transactional && pipelineError) {
171
+ reject(pipelineError);
172
+ return;
173
+ }
174
+ if (!transactional && pipelineError) {
175
+ // In non-transactional mode, attach partial results to the error
176
+ const partialResults = [];
177
+ for (let i = 0; i < queries.length; i++) {
178
+ const qErr = queryErrors[i];
179
+ if (qErr) {
180
+ partialResults.push({ status: 'error', error: qErr });
181
+ }
182
+ else {
183
+ try {
184
+ const q = queries[i];
185
+ partialResults.push({ status: 'ok', value: q.transform(results[i]) });
186
+ }
187
+ catch (transformErr) {
188
+ partialResults.push({ status: 'error', error: transformErr });
189
+ }
190
+ }
191
+ }
192
+ pipelineError.results = partialResults;
193
+ reject(pipelineError);
194
+ return;
195
+ }
196
+ // All succeeded — transform results
197
+ try {
198
+ const transformed = [];
199
+ for (let i = 0; i < queries.length; i++) {
200
+ const q = queries[i];
201
+ transformed.push(q.transform(results[i]));
202
+ }
203
+ resolve(transformed);
204
+ }
205
+ catch (err) {
206
+ reject(err);
207
+ }
208
+ }
209
+ // -----------------------------------------------------------------------
210
+ // Event handlers
211
+ // -----------------------------------------------------------------------
212
+ function onParseComplete() {
213
+ // No action needed — anonymous prepared statements
214
+ }
215
+ function onBindComplete() {
216
+ // No action needed
217
+ }
218
+ function onNoData() {
219
+ // DML without RETURNING — no RowDescription follows. Fine.
220
+ }
221
+ function onRowDescription(msg) {
222
+ const qIdx = currentQueryIndex();
223
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
224
+ results[qIdx].addFields(msg.fields);
225
+ }
226
+ }
227
+ function onDataRow(msg) {
228
+ const qIdx = currentQueryIndex();
229
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
230
+ const result = results[qIdx];
231
+ const row = result.parseRow(msg.fields);
232
+ result.addRow(row);
233
+ }
234
+ }
235
+ function onCommandComplete(msg) {
236
+ const qIdx = currentQueryIndex();
237
+ if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
238
+ results[qIdx].addCommandComplete(msg);
239
+ }
240
+ commandIndex++;
241
+ }
242
+ function onEmptyQuery() {
243
+ // Treat like a commandComplete with no data
244
+ commandIndex++;
245
+ }
246
+ function onErrorMessage(msg) {
247
+ const wrapped = (0, errors_js_1.wrapPgError)(msg);
248
+ const error = wrapped instanceof Error ? wrapped : new Error(String(wrapped));
249
+ if (transactional) {
250
+ // In transactional mode, the first error aborts everything.
251
+ // Postgres marks the transaction as aborted; subsequent commands
252
+ // until Sync all return errors which we absorb.
253
+ if (!pipelineError) {
254
+ const qIdx = currentQueryIndex();
255
+ if (qIdx !== null && qIdx >= 0 && qIdx < queries.length) {
256
+ error.failedIndex = qIdx;
257
+ error.failedTag = queries[qIdx].tag;
258
+ }
259
+ pipelineError = error;
260
+ }
261
+ }
262
+ else {
263
+ // Non-transactional: record per-query error
264
+ const qIdx = currentQueryIndex();
265
+ if (qIdx !== null && qIdx >= 0 && qIdx < queries.length) {
266
+ queryErrors[qIdx] = error;
267
+ if (!pipelineError) {
268
+ error.failedIndex = qIdx;
269
+ error.failedTag = queries[qIdx].tag;
270
+ pipelineError = error;
271
+ }
272
+ }
273
+ }
274
+ // Advance command index past the failed command
275
+ commandIndex++;
276
+ }
277
+ function onPortalSuspended() {
278
+ // We don't use row-limited portals
279
+ }
280
+ function onReadyForQuery() {
281
+ rfqCount++;
282
+ if (rfqCount >= expectedRfq) {
283
+ finalize();
284
+ }
285
+ }
286
+ // -----------------------------------------------------------------------
287
+ // Attach our listeners
288
+ // -----------------------------------------------------------------------
289
+ connection.on('parseComplete', onParseComplete);
290
+ connection.on('bindComplete', onBindComplete);
291
+ connection.on('noData', onNoData);
292
+ connection.on('rowDescription', onRowDescription);
293
+ connection.on('dataRow', onDataRow);
294
+ connection.on('commandComplete', onCommandComplete);
295
+ connection.on('emptyQuery', onEmptyQuery);
296
+ connection.on('errorMessage', onErrorMessage);
297
+ connection.on('portalSuspended', onPortalSuspended);
298
+ connection.on('readyForQuery', onReadyForQuery);
299
+ connection.on('notice', () => { });
300
+ connection.on('copyInResponse', () => { });
301
+ connection.on('copyData', () => { });
302
+ // -----------------------------------------------------------------------
303
+ // Timeout
304
+ // -----------------------------------------------------------------------
305
+ if (timeout && timeout > 0) {
306
+ timeoutHandle = setTimeout(() => {
307
+ pipelineError = new Error(`[turbine] Pipeline timed out after ${timeout}ms`);
308
+ if (connection.stream.destroy) {
309
+ connection.stream.destroy(pipelineError);
310
+ }
311
+ cleanup();
312
+ reject(pipelineError);
313
+ }, timeout);
314
+ }
315
+ // -----------------------------------------------------------------------
316
+ // Send protocol messages — all in one TCP flush
317
+ // -----------------------------------------------------------------------
318
+ try {
319
+ if (connection.stream.cork) {
320
+ connection.stream.cork();
321
+ }
322
+ if (transactional) {
323
+ // ---- Transactional mode ----
324
+ // BEGIN + N×(parse+bind+describe+execute) + COMMIT + sync
325
+ // One sync = one ReadyForQuery
326
+ // BEGIN
327
+ connection.parse({ text: 'BEGIN', name: '' });
328
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: utils_1.prepareValue });
329
+ connection.execute({ portal: '', rows: 0 });
330
+ // Each query
331
+ for (const q of queries) {
332
+ connection.parse({ text: q.sql, name: '' });
333
+ connection.bind({
334
+ portal: '',
335
+ statement: '',
336
+ values: q.params,
337
+ valueMapper: utils_1.prepareValue,
338
+ });
339
+ connection.describe({ type: 'P', name: '' });
340
+ connection.execute({ portal: '', rows: 0 });
341
+ }
342
+ // COMMIT
343
+ connection.parse({ text: 'COMMIT', name: '' });
344
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: utils_1.prepareValue });
345
+ connection.execute({ portal: '', rows: 0 });
346
+ // Single Sync
347
+ connection.sync();
348
+ }
349
+ else {
350
+ // ---- Non-transactional mode ----
351
+ // N×(parse+bind+describe+execute+sync)
352
+ // Each sync = one ReadyForQuery
353
+ // All messages still go in one cork/uncork (one TCP flush)
354
+ for (const q of queries) {
355
+ connection.parse({ text: q.sql, name: '' });
356
+ connection.bind({
357
+ portal: '',
358
+ statement: '',
359
+ values: q.params,
360
+ valueMapper: utils_1.prepareValue,
361
+ });
362
+ connection.describe({ type: 'P', name: '' });
363
+ connection.execute({ portal: '', rows: 0 });
364
+ connection.sync();
365
+ }
366
+ }
367
+ if (connection.stream.uncork) {
368
+ connection.stream.uncork();
369
+ }
370
+ }
371
+ catch (err) {
372
+ cleanup();
373
+ reject(err);
374
+ }
375
+ });
376
+ }
377
+ // ---------------------------------------------------------------------------
378
+ // Capability detection
379
+ // ---------------------------------------------------------------------------
380
+ /**
381
+ * Check whether a pool client supports the extended-query pipeline protocol.
382
+ *
383
+ * Returns true if the client has a Connection object with the required wire
384
+ * protocol methods (parse, bind, describe, execute, sync) and is an EventEmitter.
385
+ *
386
+ * Returns false for HTTP-based drivers (Neon HTTP, Vercel Postgres), mock pools,
387
+ * and any pool that doesn't expose pg internals.
388
+ */
389
+ function supportsExtendedPipeline(poolClient) {
390
+ if (!poolClient || typeof poolClient !== 'object')
391
+ return false;
392
+ const client = poolClient;
393
+ const conn = client.connection;
394
+ if (!conn || typeof conn !== 'object')
395
+ return false;
396
+ const c = conn;
397
+ return (typeof c.parse === 'function' &&
398
+ typeof c.bind === 'function' &&
399
+ typeof c.describe === 'function' &&
400
+ typeof c.execute === 'function' &&
401
+ typeof c.sync === 'function' &&
402
+ typeof c.on === 'function');
403
+ }