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