postgrejs 2.23.0 → 3.0.1
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/CHANGELOG.md +162 -1
- package/LICENSE +26 -17
- package/README.md +195 -175
- package/connection/connection.d.ts +152 -13
- package/connection/connection.js +296 -49
- package/connection/copy-stream.d.ts +88 -0
- package/connection/copy-stream.js +244 -0
- package/connection/cursor.js +20 -13
- package/connection/intl-connection.d.ts +68 -3
- package/connection/intl-connection.js +414 -57
- package/connection/large-object.d.ts +65 -0
- package/connection/large-object.js +151 -0
- package/connection/logical-replication.d.ts +121 -0
- package/connection/logical-replication.js +279 -0
- package/connection/pool.d.ts +61 -4
- package/connection/pool.js +177 -29
- package/connection/portal.d.ts +18 -4
- package/connection/portal.js +53 -59
- package/connection/prepared-statement.d.ts +22 -1
- package/connection/prepared-statement.js +117 -97
- package/data-type-map.d.ts +2 -2
- package/data-type-map.js +4 -2
- package/data-types/bool-type.js +14 -10
- package/data-types/box-type.js +15 -8
- package/data-types/bytea-type.js +9 -5
- package/data-types/char-type.js +11 -5
- package/data-types/circle-type.js +15 -7
- package/data-types/date-type.js +25 -19
- package/data-types/float4-type.js +10 -3
- package/data-types/float8-type.js +12 -3
- package/data-types/int2-type.js +9 -4
- package/data-types/int2-vector-type.js +10 -4
- package/data-types/int4-type.js +11 -5
- package/data-types/int8-type.js +22 -7
- package/data-types/json-type.js +17 -8
- package/data-types/jsonb-type.js +20 -14
- package/data-types/lseg-type.js +16 -8
- package/data-types/numeric-type.d.ts +8 -0
- package/data-types/numeric-type.js +112 -8
- package/data-types/oid-type.d.ts +0 -1
- package/data-types/oid-type.js +9 -10
- package/data-types/oid-vector-type.d.ts +9 -0
- package/data-types/oid-vector-type.js +51 -0
- package/data-types/point-type.js +14 -6
- package/data-types/time-type.js +20 -13
- package/data-types/timestamp-type.js +32 -25
- package/data-types/timestamptz-type.d.ts +1 -1
- package/data-types/timestamptz-type.js +34 -21
- package/data-types/uuid-type.js +21 -11
- package/data-types/varchar-type.js +16 -4
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/interfaces/data-type.d.ts +5 -3
- package/interfaces/database-connection-params.d.ts +95 -0
- package/interfaces/query-options.d.ts +17 -0
- package/interfaces/script-execute-options.d.ts +17 -0
- package/package.json +3 -4
- package/protocol/backend.d.ts +6 -1
- package/protocol/backend.js +70 -45
- package/protocol/buffer-reader.d.ts +0 -2
- package/protocol/buffer-reader.js +3 -10
- package/protocol/cert-signature.d.ts +12 -0
- package/protocol/cert-signature.js +114 -0
- package/protocol/database-error.js +1 -0
- package/protocol/frontend.d.ts +16 -0
- package/protocol/frontend.js +58 -6
- package/protocol/pg-socket.d.ts +221 -10
- package/protocol/pg-socket.js +539 -102
- package/protocol/pgoutput.d.ts +76 -0
- package/protocol/pgoutput.js +155 -0
- package/protocol/protocol.d.ts +3 -4
- package/protocol/protocol.js +1 -1
- package/protocol/sasl.d.ts +11 -1
- package/protocol/sasl.js +35 -4
- package/protocol/smart-buffer.d.ts +0 -2
- package/protocol/smart-buffer.js +11 -16
- package/types.d.ts +5 -3
- package/util/abort-signal.d.ts +24 -0
- package/util/abort-signal.js +63 -0
- package/util/array-calculatedim.js +3 -1
- package/util/bigint-methods.js +1 -1
- package/util/connection-config.js +77 -3
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +15 -6
- package/util/encode-binaryarray.js +7 -3
- package/util/escape-identifier.d.ts +14 -0
- package/util/escape-identifier.js +32 -0
- package/util/escape-literal.js +3 -0
- package/util/fast-parseint.d.ts +1 -0
- package/util/fast-parseint.js +12 -17
- package/util/format-datetime.d.ts +10 -0
- package/util/format-datetime.js +71 -0
- package/util/get-parsers.js +31 -10
- package/util/parse-array.js +2 -2
- package/util/parse-bytea.d.ts +2 -0
- package/util/parse-bytea.js +83 -0
- package/util/parse-datetime.d.ts +3 -1
- package/util/parse-datetime.js +86 -17
- package/util/parse-row.d.ts +3 -1
- package/util/parse-row.js +60 -5
- package/util/parse-time.js +8 -1
- package/util/sql-tag.d.ts +60 -0
- package/util/sql-tag.js +202 -0
- package/util/starts-transaction.d.ts +38 -0
- package/util/starts-transaction.js +148 -0
- package/util/stringify-arrayliteral.js +13 -1
- package/util/wrap-row-description.js +13 -4
- package/util/convert-row-to-object.d.ts +0 -2
- package/util/convert-row-to-object.js +0 -9
package/connection/connection.js
CHANGED
|
@@ -1,34 +1,31 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { coerceToBoolean } from 'putil-varhelpers';
|
|
2
|
+
import { ConnectionState, DataTypeOIDs } from '../constants.js';
|
|
2
3
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
3
4
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
5
|
+
import { withAbortSignal } from '../util/abort-signal.js';
|
|
6
|
+
import { QueryRequest } from '../util/sql-tag.js';
|
|
4
7
|
import { BindParam } from './bind-param.js';
|
|
5
8
|
import { IntlConnection } from './intl-connection.js';
|
|
9
|
+
import { LargeObject, LargeObjectMode } from './large-object.js';
|
|
6
10
|
import { PreparedStatement } from './prepared-statement.js';
|
|
11
|
+
const CAPTURE_STACK_TRACE_LIMIT = 5;
|
|
7
12
|
export class Connection extends SafeEventEmitter {
|
|
8
13
|
_pool;
|
|
9
14
|
_intlCon;
|
|
10
|
-
_notificationListeners
|
|
15
|
+
_notificationListeners;
|
|
11
16
|
_closing = false;
|
|
12
17
|
constructor(arg0, arg1) {
|
|
13
18
|
super();
|
|
14
19
|
if (arg0 &&
|
|
15
20
|
typeof arg0 === 'object' &&
|
|
16
21
|
typeof arg0.acquire === 'function') {
|
|
17
|
-
if (!(arg1 instanceof IntlConnection))
|
|
18
|
-
throw new TypeError('Invalid argument');
|
|
19
22
|
this._pool = arg0;
|
|
20
23
|
this._intlCon = arg1;
|
|
21
24
|
}
|
|
22
25
|
else {
|
|
23
26
|
this._intlCon = new IntlConnection(arg0);
|
|
24
27
|
}
|
|
25
|
-
this._intlCon.
|
|
26
|
-
this._intlCon.on('error', (...args) => this.emit('error', ...args));
|
|
27
|
-
this._intlCon.on('close', (...args) => this.emit('close', ...args));
|
|
28
|
-
this._intlCon.on('connecting', (...args) => this.emit('connecting', ...args));
|
|
29
|
-
this._intlCon.on('ready', (...args) => this.emit('ready', ...args));
|
|
30
|
-
this._intlCon.on('terminate', (...args) => this.emit('terminate', ...args));
|
|
31
|
-
this._intlCon.on('notification', (msg) => this._handleNotification(msg));
|
|
28
|
+
this._intlCon.owner = this;
|
|
32
29
|
}
|
|
33
30
|
/**
|
|
34
31
|
* Returns configuration object
|
|
@@ -37,35 +34,38 @@ export class Connection extends SafeEventEmitter {
|
|
|
37
34
|
return this._intlCon.config;
|
|
38
35
|
}
|
|
39
36
|
/**
|
|
40
|
-
* Returns true if connection is in a transaction
|
|
37
|
+
* Returns true if the connection is in a transaction
|
|
41
38
|
*/
|
|
42
39
|
get inTransaction() {
|
|
43
40
|
return this._intlCon.inTransaction;
|
|
44
41
|
}
|
|
45
42
|
/**
|
|
46
|
-
* Returns current state of the connection
|
|
43
|
+
* Returns the current state of the connection
|
|
47
44
|
*/
|
|
48
45
|
get state() {
|
|
49
46
|
return this._intlCon.state;
|
|
50
47
|
}
|
|
51
48
|
/**
|
|
52
|
-
* Returns processId of current session
|
|
49
|
+
* Returns processId of the current session
|
|
53
50
|
*/
|
|
54
51
|
get processID() {
|
|
55
52
|
return this._intlCon.processID;
|
|
56
53
|
}
|
|
57
54
|
/**
|
|
58
|
-
* Returns information parameters for current session
|
|
55
|
+
* Returns information parameters for the current session
|
|
59
56
|
*/
|
|
60
57
|
get sessionParameters() {
|
|
61
58
|
return this._intlCon.sessionParameters;
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
64
|
-
* Returns secret key of current session
|
|
61
|
+
* Returns the secret key of the current session
|
|
65
62
|
*/
|
|
66
63
|
get secretKey() {
|
|
67
64
|
return this._intlCon.secretKey;
|
|
68
65
|
}
|
|
66
|
+
get runningQueryCount() {
|
|
67
|
+
return this._intlCon.runningQueryCount;
|
|
68
|
+
}
|
|
69
69
|
/**
|
|
70
70
|
* Connects to the server
|
|
71
71
|
*/
|
|
@@ -77,16 +77,15 @@ export class Connection extends SafeEventEmitter {
|
|
|
77
77
|
/**
|
|
78
78
|
* Closes connection. You can define how long time the connection will
|
|
79
79
|
* wait for active queries before terminating the connection.
|
|
80
|
-
*
|
|
80
|
+
* At the end of the given time, it forces to close the socket and then emits the ` terminate ` event.
|
|
81
81
|
*
|
|
82
82
|
* @param terminateWait {number} - Determines how long the connection will wait for active queries before terminating.
|
|
83
83
|
*/
|
|
84
84
|
async close(terminateWait) {
|
|
85
|
-
this._notificationListeners
|
|
86
|
-
this._intlCon.statementQueue.clearQueue();
|
|
85
|
+
this._notificationListeners?.removeAllListeners();
|
|
87
86
|
if (this.state === ConnectionState.CLOSED || this._closing)
|
|
88
87
|
return;
|
|
89
|
-
/*
|
|
88
|
+
/* c8 ignore start */
|
|
90
89
|
if (this.listenerCount('debug')) {
|
|
91
90
|
this.emit('debug', {
|
|
92
91
|
location: 'Connection.close',
|
|
@@ -94,13 +93,14 @@ export class Connection extends SafeEventEmitter {
|
|
|
94
93
|
message: `[${this.processID}] closing`,
|
|
95
94
|
});
|
|
96
95
|
}
|
|
96
|
+
/* c8 ignore stop */
|
|
97
97
|
this._closing = true;
|
|
98
98
|
if (this._intlCon.refCount > 0 &&
|
|
99
99
|
typeof terminateWait === 'number' &&
|
|
100
100
|
terminateWait > 0) {
|
|
101
101
|
const startTime = Date.now();
|
|
102
102
|
return this._captureErrorStack(new Promise((resolve, reject) => {
|
|
103
|
-
/*
|
|
103
|
+
/* c8 ignore start */
|
|
104
104
|
if (this.listenerCount('debug')) {
|
|
105
105
|
this.emit('debug', {
|
|
106
106
|
location: 'Connection.close',
|
|
@@ -108,12 +108,13 @@ export class Connection extends SafeEventEmitter {
|
|
|
108
108
|
message: `[${this.processID}] waiting active queries`,
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
|
+
/* c8 ignore stop */
|
|
111
112
|
const timer = setInterval(() => {
|
|
112
113
|
if (this._intlCon.refCount <= 0 ||
|
|
113
114
|
Date.now() > startTime + terminateWait) {
|
|
114
115
|
clearInterval(timer);
|
|
115
116
|
if (this._intlCon.refCount > 0) {
|
|
116
|
-
/*
|
|
117
|
+
/* c8 ignore start */
|
|
117
118
|
if (this.listenerCount('debug')) {
|
|
118
119
|
this.emit('debug', {
|
|
119
120
|
location: 'Connection.close',
|
|
@@ -121,6 +122,7 @@ export class Connection extends SafeEventEmitter {
|
|
|
121
122
|
message: `[${this.processID}] terminate`,
|
|
122
123
|
});
|
|
123
124
|
}
|
|
125
|
+
/* c8 ignore stop */
|
|
124
126
|
this.emit('terminate');
|
|
125
127
|
}
|
|
126
128
|
this._close().then(resolve).catch(reject);
|
|
@@ -137,14 +139,22 @@ export class Connection extends SafeEventEmitter {
|
|
|
137
139
|
* @param options {ScriptExecuteOptions} - Execute options
|
|
138
140
|
*/
|
|
139
141
|
async execute(sql, options) {
|
|
142
|
+
if (typeof sql === 'object' && sql instanceof QueryRequest)
|
|
143
|
+
sql = sql.stringify({ ...options, typeMap: options?.typeMap });
|
|
140
144
|
this.emit('execute', sql, options);
|
|
141
|
-
return this._captureErrorStack(this._intlCon.execute(sql, options)).catch((e) => {
|
|
145
|
+
return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._captureErrorStack(this._intlCon.execute(sql, options), this.execute, options?.asyncErrorHandling).catch((e) => {
|
|
142
146
|
throw this._handleError(e, sql);
|
|
143
|
-
});
|
|
147
|
+
}));
|
|
144
148
|
}
|
|
145
149
|
async query(sql, options) {
|
|
150
|
+
if (sql instanceof QueryRequest) {
|
|
151
|
+
if (options?.params)
|
|
152
|
+
throw new TypeError('A statement built with sql`` carries its own parameters; passing `params` as well is ambiguous');
|
|
153
|
+
options = { ...options, params: sql.params };
|
|
154
|
+
sql = sql.sql;
|
|
155
|
+
}
|
|
146
156
|
this._intlCon.assertConnected();
|
|
147
|
-
/*
|
|
157
|
+
/* c8 ignore start */
|
|
148
158
|
if (this.listenerCount('debug')) {
|
|
149
159
|
this.emit('debug', {
|
|
150
160
|
location: 'Connection.query',
|
|
@@ -153,19 +163,78 @@ export class Connection extends SafeEventEmitter {
|
|
|
153
163
|
sql,
|
|
154
164
|
});
|
|
155
165
|
}
|
|
166
|
+
/* c8 ignore stop */
|
|
156
167
|
this.emit('query', sql, options);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._query(sql, options));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Runs a `COPY ... TO STDOUT` and returns its output as a stream of the
|
|
172
|
+
* raw bytes the server sends - text, CSV or binary, whichever the
|
|
173
|
+
* statement asked for. Nothing is decoded and nothing is copied.
|
|
174
|
+
*
|
|
175
|
+
* Resolves as soon as the server accepts the copy, so a large export is
|
|
176
|
+
* never held in memory; a consumer that falls behind pauses the socket
|
|
177
|
+
* rather than buffering. `rowCount` is set before the stream ends.
|
|
178
|
+
*
|
|
179
|
+
* ```ts
|
|
180
|
+
* const out = await connection.copyTo(`COPY users TO STDOUT (FORMAT csv)`);
|
|
181
|
+
* await pipeline(out, fs.createWriteStream('users.csv'));
|
|
182
|
+
* console.log(out.rowCount);
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* The stream must be consumed or destroyed: until the copy finishes the
|
|
186
|
+
* connection is still mid-statement.
|
|
187
|
+
*
|
|
188
|
+
* @param sql {string} - A COPY ... TO STDOUT statement
|
|
189
|
+
*/
|
|
190
|
+
async copyTo(sql) {
|
|
191
|
+
/* c8 ignore start */
|
|
192
|
+
if (this.listenerCount('debug')) {
|
|
193
|
+
this.emit('debug', {
|
|
194
|
+
location: 'Connection.copyTo',
|
|
195
|
+
connection: this,
|
|
196
|
+
message: `[${this.processID}] copyTo | ${sql}`,
|
|
197
|
+
sql,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
/* c8 ignore stop */
|
|
201
|
+
this.emit('execute', sql);
|
|
202
|
+
return await this._captureErrorStack(this._intlCon.copyTo(sql)).catch((e) => {
|
|
160
203
|
throw this._handleError(e, sql);
|
|
161
204
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Runs a `COPY ... FROM STDIN` and returns a stream to feed it. Whatever
|
|
208
|
+
* is written is forwarded verbatim, so the statement's own FORMAT decides
|
|
209
|
+
* the encoding.
|
|
210
|
+
*
|
|
211
|
+
* ```ts
|
|
212
|
+
* const inp = await connection.copyFrom(`COPY users FROM STDIN (FORMAT csv)`);
|
|
213
|
+
* await pipeline(fs.createReadStream('users.csv'), inp);
|
|
214
|
+
* console.log(inp.rowCount);
|
|
215
|
+
* ```
|
|
216
|
+
*
|
|
217
|
+
* 'finish' means the server accepted the copy, not merely that the last
|
|
218
|
+
* byte was written. If the source fails, `pipeline()` destroys the stream
|
|
219
|
+
* and a CopyFail is sent, so the connection stays usable.
|
|
220
|
+
*
|
|
221
|
+
* @param sql {string} - A COPY ... FROM STDIN statement
|
|
222
|
+
*/
|
|
223
|
+
async copyFrom(sql) {
|
|
224
|
+
/* c8 ignore start */
|
|
225
|
+
if (this.listenerCount('debug')) {
|
|
226
|
+
this.emit('debug', {
|
|
227
|
+
location: 'Connection.copyFrom',
|
|
228
|
+
connection: this,
|
|
229
|
+
message: `[${this.processID}] copyFrom | ${sql}`,
|
|
230
|
+
sql,
|
|
231
|
+
});
|
|
168
232
|
}
|
|
233
|
+
/* c8 ignore stop */
|
|
234
|
+
this.emit('execute', sql);
|
|
235
|
+
return await this._captureErrorStack(this._intlCon.copyFrom(sql)).catch((e) => {
|
|
236
|
+
throw this._handleError(e, sql);
|
|
237
|
+
});
|
|
169
238
|
}
|
|
170
239
|
/**
|
|
171
240
|
* Creates a PreparedStatement instance
|
|
@@ -173,7 +242,7 @@ export class Connection extends SafeEventEmitter {
|
|
|
173
242
|
* @param options {StatementPrepareOptions} - Options
|
|
174
243
|
*/
|
|
175
244
|
async prepare(sql, options) {
|
|
176
|
-
/*
|
|
245
|
+
/* c8 ignore start */
|
|
177
246
|
if (this.listenerCount('debug')) {
|
|
178
247
|
this.emit('debug', {
|
|
179
248
|
location: 'Connection.prepare',
|
|
@@ -182,8 +251,61 @@ export class Connection extends SafeEventEmitter {
|
|
|
182
251
|
sql,
|
|
183
252
|
});
|
|
184
253
|
}
|
|
254
|
+
/* c8 ignore stop */
|
|
185
255
|
return await this._captureErrorStack(PreparedStatement.prepare(this, sql, options));
|
|
186
256
|
}
|
|
257
|
+
/**
|
|
258
|
+
* Creates a large object and opens it for reading and writing.
|
|
259
|
+
*
|
|
260
|
+
* A large object holds binary data outside any row, reachable a piece at
|
|
261
|
+
* a time instead of whole the way a `bytea` column is - which is what it
|
|
262
|
+
* is for, along with a 4TB ceiling rather than roughly 1GB.
|
|
263
|
+
*
|
|
264
|
+
* ```ts
|
|
265
|
+
* const lo = await connection.createLargeObject();
|
|
266
|
+
* await pipeline(fs.createReadStream('video.mp4'), lo.writable());
|
|
267
|
+
* await lo.close();
|
|
268
|
+
* await table.insert({ videoOid: lo.oid });
|
|
269
|
+
* ```
|
|
270
|
+
*
|
|
271
|
+
* Nothing links the object to the row that names it: dropping the row
|
|
272
|
+
* leaves the data behind, so unlinkLargeObject() has to be called when it
|
|
273
|
+
* is no longer wanted. PostgreSQL ships `vacuumlo` for finding the ones
|
|
274
|
+
* that were not.
|
|
275
|
+
*/
|
|
276
|
+
async createLargeObject(mode = LargeObjectMode.readWrite) {
|
|
277
|
+
const ownsTransaction = await this._beginForLargeObject();
|
|
278
|
+
const created = await this.query('select lo_creat(-1) as oid');
|
|
279
|
+
const oid = Number(created.rows?.[0][0]);
|
|
280
|
+
return this._openLargeObject(oid, mode, ownsTransaction);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Opens an existing large object by its OID. See createLargeObject() for
|
|
284
|
+
* how the transaction is handled.
|
|
285
|
+
*/
|
|
286
|
+
async openLargeObject(oid, mode = LargeObjectMode.read) {
|
|
287
|
+
const ownsTransaction = await this._beginForLargeObject();
|
|
288
|
+
return this._openLargeObject(oid, mode, ownsTransaction);
|
|
289
|
+
}
|
|
290
|
+
/** Deletes a large object and its data. */
|
|
291
|
+
async unlinkLargeObject(oid) {
|
|
292
|
+
await this.query('select lo_unlink($1)', {
|
|
293
|
+
params: [new BindParam(DataTypeOIDs.oid, oid)],
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Asks the server to cancel whatever this connection is currently running.
|
|
298
|
+
*
|
|
299
|
+
* Travels on its own short-lived connection, since a backend busy with a
|
|
300
|
+
* query is not reading its own socket. It is a request, not a guarantee -
|
|
301
|
+
* the statement may finish first - and the cancelled call reports the
|
|
302
|
+
* outcome itself, rejecting with SQLSTATE 57014 if the server acted on it.
|
|
303
|
+
* Prefer the per-call `signal` option, which does this and reports the
|
|
304
|
+
* abort to the right caller.
|
|
305
|
+
*/
|
|
306
|
+
cancel() {
|
|
307
|
+
return this._intlCon.cancel();
|
|
308
|
+
}
|
|
187
309
|
/**
|
|
188
310
|
* Starts a transaction
|
|
189
311
|
*/
|
|
@@ -202,6 +324,41 @@ export class Connection extends SafeEventEmitter {
|
|
|
202
324
|
rollback() {
|
|
203
325
|
return this._captureErrorStack(this._intlCon.rollback());
|
|
204
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Ends the current transaction as a prepared one, for two-phase commit.
|
|
329
|
+
*
|
|
330
|
+
* The transaction stops belonging to this session and waits under `name`
|
|
331
|
+
* until it is finished by commitPrepared() or rollbackPrepared() - which
|
|
332
|
+
* may run on another connection, in another process, after this one is
|
|
333
|
+
* gone. That is the point: it lets several databases agree to commit
|
|
334
|
+
* before any of them actually does.
|
|
335
|
+
*
|
|
336
|
+
* ```ts
|
|
337
|
+
* await connection.startTransaction();
|
|
338
|
+
* await connection.query(sql`insert into t values (${1})`);
|
|
339
|
+
* await connection.prepareTransaction('tx1');
|
|
340
|
+
* // ...later, anywhere:
|
|
341
|
+
* await other.commitPrepared('tx1');
|
|
342
|
+
* ```
|
|
343
|
+
*
|
|
344
|
+
* PostgreSQL ships with `max_prepared_transactions` at zero, so this
|
|
345
|
+
* fails until the server is configured for it.
|
|
346
|
+
*/
|
|
347
|
+
prepareTransaction(name) {
|
|
348
|
+
return this._captureErrorStack(this._intlCon.prepareTransaction(name));
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Commits a transaction left waiting by prepareTransaction(), by name.
|
|
352
|
+
* Runs outside any transaction and needs no connection to the session
|
|
353
|
+
* that prepared it.
|
|
354
|
+
*/
|
|
355
|
+
commitPrepared(name) {
|
|
356
|
+
return this._captureErrorStack(this._intlCon.commitPrepared(name));
|
|
357
|
+
}
|
|
358
|
+
/** Discards a transaction left waiting by prepareTransaction(), by name. */
|
|
359
|
+
rollbackPrepared(name) {
|
|
360
|
+
return this._captureErrorStack(this._intlCon.rollbackPrepared(name));
|
|
361
|
+
}
|
|
205
362
|
/**
|
|
206
363
|
* Starts transaction and creates a savepoint
|
|
207
364
|
* @param name {string} - Name of the savepoint
|
|
@@ -212,7 +369,7 @@ export class Connection extends SafeEventEmitter {
|
|
|
212
369
|
return this._captureErrorStack(this._intlCon.savepoint(name));
|
|
213
370
|
}
|
|
214
371
|
/**
|
|
215
|
-
* Rolls back current transaction to given savepoint
|
|
372
|
+
* Rolls back the current transaction to given savepoint
|
|
216
373
|
* @param name {string} - Name of the savepoint
|
|
217
374
|
*/
|
|
218
375
|
rollbackToSavepoint(name) {
|
|
@@ -228,26 +385,89 @@ export class Connection extends SafeEventEmitter {
|
|
|
228
385
|
async listen(channel, callback) {
|
|
229
386
|
if (!/^[A-Z]\w+$/i.test(channel))
|
|
230
387
|
throw new TypeError(`Invalid channel name`);
|
|
231
|
-
|
|
388
|
+
if (!this._notificationListeners) {
|
|
389
|
+
this._notificationListeners = new SafeEventEmitter();
|
|
390
|
+
this._intlCon.on('notification', (msg) => this._handleNotification(msg));
|
|
391
|
+
}
|
|
392
|
+
// Bug: this used to check whether ANY channel already had a listener,
|
|
393
|
+
// not this one specifically - so a second, different channel added
|
|
394
|
+
// after the first never got its own LISTEN sent at all, and never
|
|
395
|
+
// received a notification for it (verified live: only the first
|
|
396
|
+
// channel ever showed up in pg_stat_activity's query text).
|
|
397
|
+
const alreadyListening = !!this._notificationListeners.listenerCount(channel);
|
|
232
398
|
this._notificationListeners.on(channel, callback);
|
|
233
|
-
if (!
|
|
399
|
+
if (!alreadyListening)
|
|
234
400
|
await this._captureErrorStack(this.query('LISTEN ' + channel));
|
|
235
401
|
}
|
|
236
402
|
async unListen(channel) {
|
|
237
403
|
if (!/^[A-Z]\w+$/i.test(channel))
|
|
238
404
|
throw new TypeError(`Invalid channel name`);
|
|
239
|
-
this._notificationListeners
|
|
240
|
-
|
|
405
|
+
if (this._notificationListeners?.listenerCount(channel)) {
|
|
406
|
+
this._notificationListeners?.removeAllListeners(channel);
|
|
407
|
+
await this._captureErrorStack(this.query('UNLISTEN ' + channel));
|
|
408
|
+
}
|
|
241
409
|
}
|
|
242
410
|
async unListenAll() {
|
|
243
|
-
this._notificationListeners
|
|
244
|
-
|
|
411
|
+
if (this._notificationListeners?.eventNames().length) {
|
|
412
|
+
this._notificationListeners.removeAllListeners();
|
|
413
|
+
await this._captureErrorStack(this.query('UNLISTEN *'));
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
async _query(sql, options) {
|
|
417
|
+
const typeMap = options?.typeMap || GlobalTypeMap;
|
|
418
|
+
const paramTypes = options?.params?.map(prm => prm instanceof BindParam ? prm.oid : typeMap.determine(prm));
|
|
419
|
+
const effectiveAutoCommit = options?.autoCommit != null
|
|
420
|
+
? options.autoCommit
|
|
421
|
+
: this._intlCon.config.autoCommit;
|
|
422
|
+
if (!options?.cursor &&
|
|
423
|
+
!this._intlCon.inTransaction &&
|
|
424
|
+
effectiveAutoCommit !== false) {
|
|
425
|
+
const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
|
|
426
|
+
return await this._captureErrorStack(this._intlCon.queryOnce(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
|
|
427
|
+
throw this._handleError(e, sql);
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const statement = await this.prepare(sql, { paramTypes, typeMap }).catch((e) => {
|
|
431
|
+
throw this._handleError(e, sql);
|
|
432
|
+
});
|
|
433
|
+
try {
|
|
434
|
+
const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
|
|
435
|
+
return await this._captureErrorStack(statement.execute({ ...options, params }), this.query, options?.asyncErrorHandling);
|
|
436
|
+
}
|
|
437
|
+
finally {
|
|
438
|
+
await statement.close();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* A large object's descriptor is only valid inside the transaction that
|
|
443
|
+
* opened it. One is started here when there is none, and reported so that
|
|
444
|
+
* close() commits only what it started - a transaction the caller opened
|
|
445
|
+
* stays theirs. Mirrors what savepoint() already does.
|
|
446
|
+
*/
|
|
447
|
+
async _beginForLargeObject() {
|
|
448
|
+
if (this.inTransaction)
|
|
449
|
+
return false;
|
|
450
|
+
await this.startTransaction();
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
async _openLargeObject(oid, mode, ownsTransaction) {
|
|
454
|
+
const opened = await this.query('select lo_open($1, $2) as fd', {
|
|
455
|
+
params: [
|
|
456
|
+
new BindParam(DataTypeOIDs.oid, oid),
|
|
457
|
+
new BindParam(DataTypeOIDs.int4, mode),
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
return new LargeObject(this, oid, Number(opened.rows?.[0][0]), ownsTransaction);
|
|
245
461
|
}
|
|
246
462
|
_handleNotification(msg) {
|
|
247
463
|
this.emit('notification', msg);
|
|
248
|
-
this._notificationListeners
|
|
464
|
+
this._notificationListeners?.emit(msg.channel, msg);
|
|
249
465
|
}
|
|
250
466
|
async _close() {
|
|
467
|
+
if (this._notificationListeners?.eventNames().length)
|
|
468
|
+
await this.unListenAll();
|
|
469
|
+
if (this.inTransaction)
|
|
470
|
+
await this.rollback();
|
|
251
471
|
if (this._pool) {
|
|
252
472
|
await this._captureErrorStack(this._pool.release(this));
|
|
253
473
|
this.emit('release');
|
|
@@ -270,18 +490,45 @@ export class Connection extends SafeEventEmitter {
|
|
|
270
490
|
}
|
|
271
491
|
return err;
|
|
272
492
|
}
|
|
273
|
-
|
|
274
|
-
|
|
493
|
+
/**
|
|
494
|
+
* Remembers where a call came from, so the error it may reject with points
|
|
495
|
+
* at the caller instead of at an internal async frame.
|
|
496
|
+
*
|
|
497
|
+
* `entry` is the public method to cut the trace at: everything from it
|
|
498
|
+
* inwards is this library's own plumbing and is dropped, which matters
|
|
499
|
+
* because the captured depth is deliberately small - spending four of five
|
|
500
|
+
* frames on our own call chain would leave one for the caller, and any
|
|
501
|
+
* helper of theirs would push the real call site off the end.
|
|
502
|
+
*
|
|
503
|
+
* Skippable via `asyncErrorHandling: false` - capturing costs a real,
|
|
504
|
+
* measurable slice of CPU time once many calls are in flight at once
|
|
505
|
+
* (a pipelined burst on one connection), where it has to compete with
|
|
506
|
+
* every other call for the same core instead of hiding behind network
|
|
507
|
+
* wait the way it does for a single sequential call.
|
|
508
|
+
*
|
|
509
|
+
* `asyncErrorHandling` is the per-call override (execute()/query()'s own
|
|
510
|
+
* option of the same name); when omitted, the connection's own
|
|
511
|
+
* `DatabaseConnectionParams.asyncErrorHandling` decides.
|
|
512
|
+
*/
|
|
513
|
+
async _captureErrorStack(promise, entry, asyncErrorHandling) {
|
|
514
|
+
const enabled = asyncErrorHandling != null
|
|
515
|
+
? asyncErrorHandling
|
|
516
|
+
: coerceToBoolean(this._intlCon.config.asyncErrorHandling, true);
|
|
517
|
+
if (!enabled)
|
|
518
|
+
return promise;
|
|
519
|
+
const stackHolder = {};
|
|
520
|
+
const originalStackTraceLimit = Error.stackTraceLimit;
|
|
521
|
+
Error.stackTraceLimit = CAPTURE_STACK_TRACE_LIMIT;
|
|
522
|
+
Error.captureStackTrace(stackHolder, entry || this._captureErrorStack);
|
|
523
|
+
Error.stackTraceLimit = originalStackTraceLimit;
|
|
275
524
|
return promise.catch(e => {
|
|
525
|
+
const stack = stackHolder.stack;
|
|
276
526
|
if (e instanceof Error && stack) {
|
|
277
527
|
if (e.stack && stack) {
|
|
278
528
|
e.stack =
|
|
279
529
|
e.stack.substring(0, e.stack.indexOf('\n')) +
|
|
280
530
|
'\n' +
|
|
281
|
-
stack
|
|
282
|
-
.split('\n')
|
|
283
|
-
.filter((x, i) => i && !x.includes('._captureErrorStack'))
|
|
284
|
-
.join('\n');
|
|
531
|
+
stack.split('\n').slice(1).join('\n');
|
|
285
532
|
}
|
|
286
533
|
}
|
|
287
534
|
throw e;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Readable, Writable } from 'node:stream';
|
|
2
|
+
import type { DataFormat } from '../constants.js';
|
|
3
|
+
import type { CaptureCallback, PgSocket } from '../protocol/pg-socket.js';
|
|
4
|
+
/**
|
|
5
|
+
* A COPY ... TO STDOUT in progress, as a Readable of the raw bytes the
|
|
6
|
+
* server sends - text, CSV or binary, whichever the statement asked for.
|
|
7
|
+
* No decoding happens here: the payload is handed on exactly as it arrived,
|
|
8
|
+
* without a copy.
|
|
9
|
+
*
|
|
10
|
+
* The stream must be consumed (or destroyed): until the copy finishes the
|
|
11
|
+
* connection is still mid-statement, and an unread stream pauses the socket
|
|
12
|
+
* rather than buffering the whole export in memory.
|
|
13
|
+
*/
|
|
14
|
+
export declare class CopyToStream extends Readable {
|
|
15
|
+
/** Format of the copy as a whole, from CopyOutResponse. */
|
|
16
|
+
overallFormat?: DataFormat;
|
|
17
|
+
/** Per-column formats, from CopyOutResponse. */
|
|
18
|
+
columnFormats?: DataFormat[];
|
|
19
|
+
/**
|
|
20
|
+
* Rows the server reported for the copy. Set before 'end' is emitted, so
|
|
21
|
+
* it is always readable once the stream has finished.
|
|
22
|
+
*/
|
|
23
|
+
rowCount?: number;
|
|
24
|
+
protected readonly _socket: PgSocket;
|
|
25
|
+
protected _error?: Error;
|
|
26
|
+
protected _started?: (err?: Error) => void;
|
|
27
|
+
protected _discarding: boolean;
|
|
28
|
+
protected _settled: boolean;
|
|
29
|
+
protected _paused: boolean;
|
|
30
|
+
constructor(socket: PgSocket);
|
|
31
|
+
/**
|
|
32
|
+
* Handles the copy's backend messages. Passed straight to
|
|
33
|
+
* PgSocket.sendQueryMessage() by IntlConnection.copyTo().
|
|
34
|
+
*/
|
|
35
|
+
readonly capture: CaptureCallback;
|
|
36
|
+
/**
|
|
37
|
+
* Resolves once the server has accepted the copy, rejects if it never
|
|
38
|
+
* starts one.
|
|
39
|
+
*/
|
|
40
|
+
waitStarted(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Reports a failure that happened outside the message flow (the socket
|
|
43
|
+
* dying, say). A no-op once the copy has already settled, so the normal
|
|
44
|
+
* error path is never reported twice.
|
|
45
|
+
*/
|
|
46
|
+
fail(err: Error): void;
|
|
47
|
+
_read(): void;
|
|
48
|
+
_destroy(err: Error | null, callback: (err?: Error | null) => void): void;
|
|
49
|
+
protected _finish(done: (err?: Error) => void): void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A COPY ... FROM STDIN in progress, as a Writable. Whatever is written is
|
|
53
|
+
* forwarded as CopyData without being copied or reinterpreted, so the
|
|
54
|
+
* caller decides the encoding by the format its statement asked for.
|
|
55
|
+
*
|
|
56
|
+
* 'finish' means the server accepted the whole copy: the stream waits for
|
|
57
|
+
* CommandComplete rather than for the last byte to leave, so rowCount is
|
|
58
|
+
* set by the time a `pipeline()` over it resolves. Destroying the stream
|
|
59
|
+
* (which `pipeline()` does when the source fails) sends CopyFail, leaving
|
|
60
|
+
* the connection usable rather than stuck waiting for data.
|
|
61
|
+
*/
|
|
62
|
+
export declare class CopyFromStream extends Writable {
|
|
63
|
+
/** Rows the server accepted. Set before 'finish' is emitted. */
|
|
64
|
+
rowCount?: number;
|
|
65
|
+
protected readonly _socket: PgSocket;
|
|
66
|
+
protected _error?: Error;
|
|
67
|
+
protected _started?: (err?: Error) => void;
|
|
68
|
+
protected _completed?: (err?: Error) => void;
|
|
69
|
+
protected _copyEnded: boolean;
|
|
70
|
+
protected _settled: boolean;
|
|
71
|
+
constructor(socket: PgSocket);
|
|
72
|
+
/**
|
|
73
|
+
* Handles the copy's backend messages. Passed straight to
|
|
74
|
+
* PgSocket.sendQueryMessage() by IntlConnection.copyFrom().
|
|
75
|
+
*/
|
|
76
|
+
readonly capture: CaptureCallback;
|
|
77
|
+
/**
|
|
78
|
+
* Resolves once the server is ready for data, rejects if it never asks
|
|
79
|
+
* for any.
|
|
80
|
+
*/
|
|
81
|
+
waitStarted(): Promise<void>;
|
|
82
|
+
/** See CopyToStream.fail(). */
|
|
83
|
+
fail(err: Error): void;
|
|
84
|
+
_write(chunk: Buffer, encoding: BufferEncoding, callback: (err?: Error | null) => void): void;
|
|
85
|
+
_final(callback: (err?: Error | null) => void): void;
|
|
86
|
+
_destroy(err: Error | null, callback: (err?: Error | null) => void): void;
|
|
87
|
+
protected _finish(done: (err?: Error) => void): void;
|
|
88
|
+
}
|