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.
- package/README.md +62 -40
- package/dist/cjs/cli/index.js +102 -10
- package/dist/cjs/cli/migrate.js +50 -13
- package/dist/cjs/cli/studio-ui.generated.js +6 -0
- package/dist/cjs/cli/studio.js +641 -0
- package/dist/cjs/client.js +43 -5
- package/dist/cjs/errors.js +43 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/pipeline-submittable.js +403 -0
- package/dist/cjs/pipeline.js +90 -37
- package/dist/cjs/query.js +865 -141
- package/dist/cjs/schema-builder.js +23 -3
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +103 -11
- package/dist/cli/migrate.d.ts +16 -0
- package/dist/cli/migrate.js +49 -13
- package/dist/cli/studio-ui.generated.d.ts +2 -0
- package/dist/cli/studio-ui.generated.js +4 -0
- package/dist/cli/studio.d.ts +75 -0
- package/dist/cli/studio.js +627 -0
- package/dist/client.d.ts +32 -3
- package/dist/client.js +44 -6
- package/dist/errors.d.ts +44 -0
- package/dist/errors.js +41 -0
- package/dist/index.d.ts +2 -2
- 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 +142 -6
- package/dist/query.js +863 -141
- package/dist/schema-builder.js +23 -3
- package/package.json +8 -4
package/dist/cjs/client.js
CHANGED
|
@@ -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 (
|
|
137
|
+
query: async (textOrConfig, values) => {
|
|
138
138
|
try {
|
|
139
|
-
|
|
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
|
-
*
|
|
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(...
|
|
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
|
package/dist/cjs/errors.js
CHANGED
|
@@ -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
|
+
}
|