postgrejs 2.23.1 → 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +152 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +152 -13
  5. package/connection/connection.js +296 -49
  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 +68 -3
  10. package/connection/intl-connection.js +414 -57
  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 +117 -97
  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 +112 -8
  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/data-type.d.ts +5 -3
  52. package/interfaces/database-connection-params.d.ts +95 -0
  53. package/interfaces/query-options.d.ts +17 -0
  54. package/interfaces/script-execute-options.d.ts +17 -0
  55. package/package.json +2 -3
  56. package/protocol/backend.d.ts +6 -1
  57. package/protocol/backend.js +70 -45
  58. package/protocol/buffer-reader.d.ts +0 -2
  59. package/protocol/buffer-reader.js +3 -10
  60. package/protocol/cert-signature.d.ts +12 -0
  61. package/protocol/cert-signature.js +114 -0
  62. package/protocol/frontend.d.ts +16 -0
  63. package/protocol/frontend.js +57 -6
  64. package/protocol/pg-socket.d.ts +220 -9
  65. package/protocol/pg-socket.js +539 -102
  66. package/protocol/pgoutput.d.ts +76 -0
  67. package/protocol/pgoutput.js +155 -0
  68. package/protocol/protocol.d.ts +3 -4
  69. package/protocol/protocol.js +1 -1
  70. package/protocol/sasl.d.ts +11 -1
  71. package/protocol/sasl.js +35 -4
  72. package/protocol/smart-buffer.d.ts +0 -2
  73. package/protocol/smart-buffer.js +11 -16
  74. package/types.d.ts +5 -3
  75. package/util/abort-signal.d.ts +24 -0
  76. package/util/abort-signal.js +63 -0
  77. package/util/array-calculatedim.js +3 -1
  78. package/util/connection-config.js +77 -3
  79. package/util/decode-binaryarray.d.ts +1 -1
  80. package/util/decode-binaryarray.js +15 -6
  81. package/util/encode-binaryarray.js +7 -3
  82. package/util/escape-identifier.d.ts +14 -0
  83. package/util/escape-identifier.js +32 -0
  84. package/util/escape-literal.js +3 -0
  85. package/util/fast-parseint.d.ts +1 -0
  86. package/util/fast-parseint.js +12 -17
  87. package/util/format-datetime.d.ts +10 -0
  88. package/util/format-datetime.js +71 -0
  89. package/util/get-parsers.js +31 -10
  90. package/util/parse-array.js +2 -2
  91. package/util/parse-bytea.d.ts +2 -0
  92. package/util/parse-bytea.js +83 -0
  93. package/util/parse-datetime.d.ts +3 -1
  94. package/util/parse-datetime.js +86 -17
  95. package/util/parse-row.d.ts +3 -1
  96. package/util/parse-row.js +60 -5
  97. package/util/parse-time.js +8 -1
  98. package/util/sql-tag.d.ts +60 -0
  99. package/util/sql-tag.js +202 -0
  100. package/util/starts-transaction.d.ts +38 -0
  101. package/util/starts-transaction.js +148 -0
  102. package/util/stringify-arrayliteral.js +13 -1
  103. package/util/wrap-row-description.js +13 -4
  104. package/util/convert-row-to-object.d.ts +0 -2
  105. package/util/convert-row-to-object.js +0 -9
@@ -1,34 +1,31 @@
1
- import { ConnectionState } from '../constants.js';
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 = new SafeEventEmitter();
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.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));
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
- * On the end of the given time, it forces to close the socket and than emits `terminate` event.
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.removeAllListeners();
86
- this._intlCon.statementQueue.clearQueue();
85
+ this._notificationListeners?.removeAllListeners();
87
86
  if (this.state === ConnectionState.CLOSED || this._closing)
88
87
  return;
89
- /* istanbul ignore next */
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
- /* istanbul ignore next */
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
- /* istanbul ignore next */
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
- /* 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,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
- 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) => {
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
- 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();
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
- /* istanbul ignore next */
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
- const registered = !!this._notificationListeners.eventNames().length;
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 (!registered)
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.removeAllListeners(channel);
240
- await this._captureErrorStack(this.query('UNLISTEN ' + channel));
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.removeAllListeners();
244
- await this._captureErrorStack(this.query('UNLISTEN *'));
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.emit(msg.channel, msg);
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
- async _captureErrorStack(promise) {
274
- const stack = new Error().stack;
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
+ }