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.
Files changed (107) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +169 -16
  5. package/connection/connection.js +318 -56
  6. package/connection/copy-stream.d.ts +88 -0
  7. package/connection/copy-stream.js +244 -0
  8. package/connection/cursor.js +20 -13
  9. package/connection/intl-connection.d.ts +113 -5
  10. package/connection/intl-connection.js +500 -70
  11. package/connection/large-object.d.ts +65 -0
  12. package/connection/large-object.js +151 -0
  13. package/connection/logical-replication.d.ts +121 -0
  14. package/connection/logical-replication.js +279 -0
  15. package/connection/pool.d.ts +61 -4
  16. package/connection/pool.js +177 -29
  17. package/connection/portal.d.ts +18 -4
  18. package/connection/portal.js +53 -59
  19. package/connection/prepared-statement.d.ts +22 -1
  20. package/connection/prepared-statement.js +129 -109
  21. package/data-type-map.js +4 -2
  22. package/data-types/bool-type.js +14 -10
  23. package/data-types/box-type.js +15 -8
  24. package/data-types/bytea-type.js +9 -5
  25. package/data-types/char-type.js +11 -5
  26. package/data-types/circle-type.js +15 -7
  27. package/data-types/date-type.js +25 -19
  28. package/data-types/float4-type.js +10 -3
  29. package/data-types/float8-type.js +12 -3
  30. package/data-types/int2-type.js +9 -4
  31. package/data-types/int2-vector-type.js +9 -4
  32. package/data-types/int4-type.js +11 -5
  33. package/data-types/int8-type.js +22 -7
  34. package/data-types/json-type.js +17 -8
  35. package/data-types/jsonb-type.js +20 -14
  36. package/data-types/lseg-type.js +16 -8
  37. package/data-types/numeric-type.d.ts +8 -0
  38. package/data-types/numeric-type.js +118 -9
  39. package/data-types/oid-type.d.ts +0 -1
  40. package/data-types/oid-type.js +9 -10
  41. package/data-types/oid-vector-type.d.ts +9 -0
  42. package/data-types/oid-vector-type.js +51 -0
  43. package/data-types/point-type.js +14 -6
  44. package/data-types/time-type.js +20 -13
  45. package/data-types/timestamp-type.js +32 -25
  46. package/data-types/timestamptz-type.js +33 -21
  47. package/data-types/uuid-type.js +21 -11
  48. package/data-types/varchar-type.js +16 -4
  49. package/index.d.ts +5 -0
  50. package/index.js +5 -0
  51. package/interfaces/command-result.d.ts +2 -1
  52. package/interfaces/data-type.d.ts +5 -3
  53. package/interfaces/database-connection-params.d.ts +108 -0
  54. package/interfaces/query-options.d.ts +23 -0
  55. package/interfaces/script-execute-options.d.ts +23 -0
  56. package/interfaces/script-result.d.ts +3 -2
  57. package/package.json +2 -3
  58. package/protocol/backend.d.ts +6 -1
  59. package/protocol/backend.js +70 -45
  60. package/protocol/buffer-reader.d.ts +0 -2
  61. package/protocol/buffer-reader.js +3 -10
  62. package/protocol/cert-signature.d.ts +12 -0
  63. package/protocol/cert-signature.js +114 -0
  64. package/protocol/frontend.d.ts +16 -0
  65. package/protocol/frontend.js +70 -11
  66. package/protocol/pg-socket.d.ts +220 -9
  67. package/protocol/pg-socket.js +539 -102
  68. package/protocol/pgoutput.d.ts +76 -0
  69. package/protocol/pgoutput.js +155 -0
  70. package/protocol/protocol.d.ts +3 -4
  71. package/protocol/protocol.js +1 -1
  72. package/protocol/sasl.d.ts +11 -1
  73. package/protocol/sasl.js +36 -5
  74. package/protocol/smart-buffer.d.ts +0 -2
  75. package/protocol/smart-buffer.js +11 -16
  76. package/types.d.ts +5 -3
  77. package/util/abort-signal.d.ts +34 -0
  78. package/util/abort-signal.js +76 -0
  79. package/util/array-calculatedim.js +3 -1
  80. package/util/connection-config.js +77 -3
  81. package/util/decode-binaryarray.d.ts +1 -1
  82. package/util/decode-binaryarray.js +15 -6
  83. package/util/encode-binaryarray.js +7 -3
  84. package/util/escape-identifier.d.ts +14 -0
  85. package/util/escape-identifier.js +32 -0
  86. package/util/escape-literal.js +3 -0
  87. package/util/fast-parseint.d.ts +1 -0
  88. package/util/fast-parseint.js +12 -17
  89. package/util/format-datetime.d.ts +10 -0
  90. package/util/format-datetime.js +71 -0
  91. package/util/get-parsers.js +31 -10
  92. package/util/parse-array.js +2 -2
  93. package/util/parse-bytea.d.ts +2 -0
  94. package/util/parse-bytea.js +83 -0
  95. package/util/parse-datetime.d.ts +3 -1
  96. package/util/parse-datetime.js +86 -17
  97. package/util/parse-row.d.ts +3 -1
  98. package/util/parse-row.js +60 -5
  99. package/util/parse-time.js +8 -1
  100. package/util/sql-tag.d.ts +60 -0
  101. package/util/sql-tag.js +202 -0
  102. package/util/starts-transaction.d.ts +38 -0
  103. package/util/starts-transaction.js +148 -0
  104. package/util/stringify-arrayliteral.js +13 -1
  105. package/util/wrap-row-description.js +13 -4
  106. package/util/convert-row-to-object.d.ts +0 -2
  107. package/util/convert-row-to-object.js +0 -9
@@ -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 = new SafeEventEmitter();
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.on('ready', (...args) => this.emit('ready', ...args));
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
- * On the end of the given time, it forces to close the socket and than emits `terminate` event.
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.removeAllListeners();
86
- this._intlCon.statementQueue.clearQueue();
84
+ this._notificationListeners?.removeAllListeners();
87
85
  if (this.state === ConnectionState.CLOSED || this._closing)
88
86
  return;
89
- /* istanbul ignore next */
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
- /* istanbul ignore next */
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
- /* istanbul ignore next */
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
- this.emit('execute', sql, options);
141
- return this._captureErrorStack(this._intlCon.execute(sql, options)).catch((e) => {
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
- /* istanbul ignore next */
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
- this.emit('query', sql, options);
157
- const typeMap = options?.typeMap || GlobalTypeMap;
158
- const paramTypes = options?.params?.map(prm => prm instanceof BindParam ? prm.oid : typeMap.determine(prm));
159
- const statement = await this.prepare(sql, { paramTypes, typeMap }).catch((e) => {
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
- try {
163
- const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
164
- return await this._captureErrorStack(statement.execute({ ...options, params }));
165
- }
166
- finally {
167
- await statement.close();
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
- /* istanbul ignore next */
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
- const registered = !!this._notificationListeners.eventNames().length;
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 (!registered)
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.removeAllListeners(channel);
240
- await this._captureErrorStack(this.query('UNLISTEN ' + channel));
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.removeAllListeners();
244
- await this._captureErrorStack(this.query('UNLISTEN *'));
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.emit(msg.channel, msg);
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
- async _captureErrorStack(promise) {
274
- const stack = new Error().stack;
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
+ }