turbine-orm 0.7.0 → 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.
- package/README.md +134 -40
- package/dist/cjs/cli/index.js +72 -3
- package/dist/cjs/cli/loader.js +129 -0
- package/dist/cjs/cli/migrate.js +33 -9
- package/dist/cjs/client.js +92 -8
- package/dist/cjs/errors.js +177 -4
- package/dist/cjs/generate.js +120 -9
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/pipeline-submittable.js +403 -0
- package/dist/cjs/pipeline.js +90 -37
- package/dist/cjs/query.js +943 -137
- package/dist/cjs/schema-builder.js +57 -6
- package/dist/cjs/schema-sql.js +85 -19
- package/dist/cjs/serverless.js +8 -7
- package/dist/cli/index.js +72 -3
- package/dist/cli/loader.d.ts +45 -0
- package/dist/cli/loader.js +91 -0
- package/dist/cli/migrate.d.ts +7 -1
- package/dist/cli/migrate.js +33 -9
- package/dist/cli/ui.d.ts +1 -1
- package/dist/client.d.ts +47 -3
- package/dist/client.js +94 -10
- package/dist/errors.d.ts +132 -1
- package/dist/errors.js +171 -3
- package/dist/generate.d.ts +6 -0
- package/dist/generate.js +120 -10
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/pipeline-submittable.d.ts +94 -0
- package/dist/pipeline-submittable.js +397 -0
- package/dist/pipeline.d.ts +37 -9
- package/dist/pipeline.js +89 -37
- package/dist/query.d.ts +268 -17
- package/dist/query.js +941 -137
- package/dist/schema-builder.d.ts +36 -3
- package/dist/schema-builder.js +57 -6
- package/dist/schema-sql.js +85 -19
- package/dist/serverless.d.ts +8 -7
- package/dist/serverless.js +8 -7
- package/package.json +3 -3
|
@@ -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
|
package/dist/pipeline.d.ts
CHANGED
|
@@ -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,
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
24
|
-
*
|
|
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,
|
|
11
|
-
*
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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
|
-
*
|
|
26
|
-
*
|
|
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
|
|
89
|
+
// Acquire a single client — reused for both capability check and execution
|
|
42
90
|
const client = await pool.connect();
|
|
43
91
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
66
|
-
return
|
|
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
|