postgrejs 3.5.0 → 3.6.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 (43) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +22 -4
  3. package/connection/connection.js +13 -7
  4. package/connection/cursor.js +22 -1
  5. package/connection/intl-connection.d.ts +1 -1
  6. package/connection/intl-connection.js +110 -41
  7. package/connection/pool.d.ts +22 -0
  8. package/connection/pool.js +73 -9
  9. package/connection/portal.d.ts +1 -1
  10. package/connection/portal.js +3 -2
  11. package/connection/prepared-statement.js +8 -4
  12. package/data-type-map.d.ts +11 -0
  13. package/data-type-map.js +20 -2
  14. package/data-types/char-type.js +15 -0
  15. package/data-types/date-type.js +4 -18
  16. package/data-types/float4-type.js +79 -1
  17. package/data-types/json-type.js +7 -12
  18. package/data-types/jsonb-type.js +7 -12
  19. package/data-types/time-type.js +2 -15
  20. package/data-types/timestamp-type.js +8 -29
  21. package/data-types/timestamptz-type.js +7 -31
  22. package/index.d.ts +1 -0
  23. package/index.js +1 -0
  24. package/interfaces/batch-result.d.ts +1 -1
  25. package/interfaces/command-result.d.ts +21 -1
  26. package/interfaces/data-mapping-options.d.ts +16 -0
  27. package/interfaces/data-type.d.ts +11 -0
  28. package/interfaces/query-options.d.ts +25 -3
  29. package/package.json +1 -1
  30. package/protocol/connection-lost-error.d.ts +40 -0
  31. package/protocol/connection-lost-error.js +54 -0
  32. package/protocol/frontend.d.ts +7 -0
  33. package/protocol/frontend.js +5 -3
  34. package/protocol/pg-socket.d.ts +7 -0
  35. package/protocol/pg-socket.js +26 -2
  36. package/util/channel-name.d.ts +24 -0
  37. package/util/channel-name.js +44 -0
  38. package/util/get-parsers.d.ts +2 -1
  39. package/util/get-parsers.js +16 -1
  40. package/util/resolve-column-formats.d.ts +33 -0
  41. package/util/resolve-column-formats.js +67 -0
  42. package/util/wrap-row-description.d.ts +2 -1
  43. package/util/wrap-row-description.js +7 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,45 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.5.0](https://github.com/panates/postgrejs/compare/v3.4.0...v3.5.0) -
3
+ ### [v3.6.1](https://github.com/panates/postgrejs/compare/v3.6.0...v3.6.1) -
4
+
5
+ #### 🚀 New Features
6
+
7
+ - feat: report a pooled connection that dies @Eray Hanoğlu
8
+
9
+ #### 🛠 Refactoring and Updates
10
+
11
+ - refactor: give a lost connection its own error type @Eray Hanoğlu
12
+
13
+ #### 💬 General Changes
14
+
15
+ - ci: pin the runner image and stop naming the PostgreSQL it ships @Eray Hanoğlu
16
+
17
+ ### [v3.6.0](https://github.com/panates/postgrejs/compare/v3.5.0...v3.6.0) - 20 September 2026
18
+
19
+ #### 🚀 New Features
20
+
21
+ - feat: make fetchAsString request text from the server @Eray Hanoğlu
22
+
23
+ #### 🪲 Fixes
24
+
25
+ - fix: decode float4 as the number the server would print @Eray Hanoğlu
26
+ - fix: report rowsAffected for MERGE @Eray Hanoğlu
27
+ - fix: quote LISTEN channel names instead of restricting them @Eray Hanoğlu
28
+ - fix: stop a pooled cursor's connection going back into the pool @Eray Hanoğlu
29
+ - fix: stop inferring "char" for one-character strings @Eray Hanoğlu
30
+ - fix: copy both indexes when a DataTypeMap is built from another @Eray Hanoğlu
31
+ - fix: emit 'notification' once per NOTIFY @Eray Hanoğlu
32
+ - fix: accept single-character savepoint names @Eray Hanoğlu
33
+
34
+ #### 📖 Documentation Changes
35
+
36
+ - docs: record what 3.6 changes for existing code @Eray Hanoğlu
37
+
38
+ #### 💬 General Changes
39
+
40
+ - feat!: fetch every row by default instead of the first 100 @Eray Hanoğlu
41
+
42
+ ### [v3.5.0](https://github.com/panates/postgrejs/compare/v3.4.0...v3.5.0) - 16 September 2026
4
43
 
5
44
  #### 🚀 New Features
6
45
 
package/README.md CHANGED
@@ -59,6 +59,9 @@ $ npm install postgrejs --save
59
59
  Please read :small_orange_diamond: [DOCUMENTATION](https://www.postgrejs.com/) :small_orange_diamond: for detailed
60
60
  usage.
61
61
 
62
+ Upgrading from 3.5? See [doc/MIGRATION-v3.5-to-v3.6.md](doc/MIGRATION-v3.5-to-v3.6.md) - `query()` no longer stops at
63
+ 100 rows, and `fetchAsString` now returns the server's own text.
64
+
62
65
  ## Library Overview
63
66
 
64
67
  - **Language:** Pure JavaScript, with no native/binary dependencies to compile or ship.
@@ -71,7 +74,11 @@ usage.
71
74
  ## Features
72
75
 
73
76
  - **Connection Management:** Supports both single connection and advanced pooling, providing scalability and efficient
74
- resource management.
77
+ resource management. A connection that dies rather than being closed says so: `'close'` carries the reason, and a
78
+ pooled one is reported on the pool's `'destroy'` and `'error'` events - so an admin kill or a failover is
79
+ distinguishable from an ordinary eviction, including when the connection was sitting idle with no query to reject.
80
+ The reason is a `ConnectionLostError` carrying the backend's `processID`, and `code` `'08006'` so branching on it
81
+ needs no `instanceof`.
75
82
  - **Binary Wire Protocol:** Implements the full binary wire protocol for all PostgreSQL data types, ensuring robust and
76
83
  efficient data handling.
77
84
  - **Prepared Statements:** Named prepared statements for optimized query execution, and a per-connection cache that
@@ -82,16 +89,23 @@ usage.
82
89
  statement leaves the transaction usable instead of aborting the whole block. The `SAVEPOINT`/`RELEASE` pair travels
83
90
  in the statement's own round trip rather than costing one each - 36µs for the pair, against 507µs sent separately.
84
91
  `rollbackOnError: false` to opt out.
92
+ - **Complete Results:** `query()` returns every row the statement produced. A `fetchCount` limit is a limit rather
93
+ than a silent cap: the result carries `suspended: true` when the server stopped at it, so a prefix can never be
94
+ mistaken for the whole answer. `0` means unlimited, as it does in the protocol.
85
95
  - **Cursors:** Features fast double-link cache cursors for efficient data retrieval, iterable with `for await` a row
86
- at a time and closed when the loop ends - by exhaustion, a `break`, or a throw.
96
+ at a time and closed when the loop ends - by exhaustion, a `break`, or a throw. A cursor reads through a portal, and
97
+ a portal lives as long as the transaction that created it - `Pool.query()` keeps its connection out of the pool
98
+ until the cursor closes, and on a bare `Connection` open the cursor inside a transaction if anything else will run
99
+ on that connection meanwhile.
87
100
  - **Scoped Transactions:** `transaction(fn)` commits when the callback returns and rolls back when it throws, on a
88
101
  connection or straight from the pool. A call made while a transaction is already open takes a savepoint rather than
89
102
  a second `BEGIN`, so a nested scope that fails rolls back its own work and leaves the outer transaction standing.
90
103
  - **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
91
104
  reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
92
105
  - **Notifications:** High-level implementation for PostgreSQL notifications (LISTEN/NOTIFY), enabling real-time data
93
- updates.
94
- - **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements.
106
+ updates. Channel names are quoted rather than pattern-matched, so any name PostgreSQL accepts works.
107
+ - **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements. A type can set
108
+ `inferrable: false` to stay out of parameter type inference while still decoding its own columns.
95
109
  - **Parameter Binding:** Bind parameters with OID mappings for precise and efficient query execution.
96
110
  - **Array Handling:** Supports multidimensional arrays with fast binary encoding/decoding.
97
111
  - **Performance Optimization:** Low memory utilization and boosted performance through the use of shared buffers.
@@ -119,6 +133,10 @@ usage.
119
133
  `rollbackPrepared()`, from any connection.
120
134
  - **Cancellation:** Any call takes an `AbortSignal`, which also gives per-query timeouts via `AbortSignal.timeout()`.
121
135
  - **Flexible Data Retrieval:** Can return both array and object rows to suit different data processing needs.
136
+ - **Values as Text:** `fetchAsString: [DataTypeOIDs.int8]` asks the server for those columns in its own text format
137
+ and hands the bytes back unparsed - so `count(*)` arrives as `'3'`, one consistent type where a decoded `int8` is a
138
+ `number` or a `BigInt` depending on magnitude. Any OID, arrays included. The string is PostgreSQL's own rendering
139
+ rather than one reconstructed from the binary value, so it cannot drift from what the server would print.
122
140
  - **Resource Management:** Auto disposal of resources with the "using" syntax
123
141
  ([TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)), ensuring
124
142
  efficient resource cleanup.
@@ -2,6 +2,8 @@ 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
4
  import { withAbortSignal } from '../util/abort-signal.js';
5
+ import { normalizeChannelName } from '../util/channel-name.js';
6
+ import { escapeIdentifier } from '../util/escape-identifier.js';
5
7
  import { QueryRequest } from '../util/sql-tag.js';
6
8
  import { BindParam } from './bind-param.js';
7
9
  import { IntlConnection } from './intl-connection.js';
@@ -599,8 +601,7 @@ export class Connection extends SafeEventEmitter {
599
601
  return this._captureErrorStack(this._intlCon.releaseSavepoint(name, immediate));
600
602
  }
601
603
  async listen(channel, callback) {
602
- if (!/^[A-Z]\w+$/i.test(channel))
603
- throw new TypeError(`Invalid channel name`);
604
+ channel = normalizeChannelName(channel);
604
605
  if (!this._notificationListeners) {
605
606
  this._notificationListeners = new SafeEventEmitter();
606
607
  this._intlCon.on('notification', (msg) => this._handleNotification(msg));
@@ -613,14 +614,13 @@ export class Connection extends SafeEventEmitter {
613
614
  const alreadyListening = !!this._notificationListeners.listenerCount(channel);
614
615
  this._notificationListeners.on(channel, callback);
615
616
  if (!alreadyListening)
616
- await this._captureErrorStack(this.query('LISTEN ' + channel));
617
+ await this._captureErrorStack(this.query('LISTEN ' + escapeIdentifier(channel)));
617
618
  }
618
619
  async unListen(channel) {
619
- if (!/^[A-Z]\w+$/i.test(channel))
620
- throw new TypeError(`Invalid channel name`);
620
+ channel = normalizeChannelName(channel);
621
621
  if (this._notificationListeners?.listenerCount(channel)) {
622
622
  this._notificationListeners?.removeAllListeners(channel);
623
- await this._captureErrorStack(this.query('UNLISTEN ' + channel));
623
+ await this._captureErrorStack(this.query('UNLISTEN ' + escapeIdentifier(channel)));
624
624
  }
625
625
  }
626
626
  async unListenAll() {
@@ -682,7 +682,13 @@ export class Connection extends SafeEventEmitter {
682
682
  return new LargeObject(this, oid, Number(opened.rows?.[0][0]), ownsTransaction);
683
683
  }
684
684
  _handleNotification(msg) {
685
- this.emit('notification', msg);
685
+ // Deliberately no `this.emit('notification', msg)` here:
686
+ // IntlConnection.emit() re-emits every event on its owner, which is
687
+ // this Connection, so the raw event has already been delivered by the
688
+ // time this runs. Emitting it again fired 'notification' twice for one
689
+ // NOTIFY - but only once listen() had been called, since that is what
690
+ // installs this handler, so the same listener saw one event or two
691
+ // depending on whether anything else had subscribed.
686
692
  this._notificationListeners?.emit(msg.channel, msg);
687
693
  }
688
694
  async _close() {
@@ -69,7 +69,28 @@ export class Cursor extends SafeEventEmitter {
69
69
  await this._taskQueue
70
70
  .enqueue(async () => {
71
71
  const queryOptions = this._queryOptions;
72
- const r = await portal.execute(queryOptions.fetchCount || 100);
72
+ let r;
73
+ try {
74
+ r = await portal.execute(queryOptions.fetchCount ?? 100);
75
+ }
76
+ catch (e) {
77
+ // 34000 here means the portal is gone, and the only thing that
78
+ // takes a portal away mid-cursor is another statement on this
79
+ // connection: its Sync ends the implicit transaction the portal
80
+ // lives in. The server's own message says nothing about why, and
81
+ // a caller cannot act on it without knowing. Closing first keeps
82
+ // the statement from leaking and lets whoever is holding the
83
+ // connection for this cursor (Pool.query()) have it back.
84
+ if (e?.code === '34000') {
85
+ await this.close().catch(() => undefined);
86
+ e.message +=
87
+ ' - the portal was destroyed by another statement running on' +
88
+ ' the same connection, which ends the implicit transaction it' +
89
+ ' lives in. Open the cursor inside an explicit transaction, or' +
90
+ ' give it a connection of its own.';
91
+ }
92
+ throw e;
93
+ }
73
94
  if (r && r.rows && r.rows.length) {
74
95
  const rows = r.rows;
75
96
  if (this._parsers) {
@@ -245,7 +245,7 @@ export declare class IntlConnection extends SafeEventEmitter {
245
245
  * so repeated executes of the same statement don't rebuild them.
246
246
  * Shared by executeReused() and executeBatchReused().
247
247
  */
248
- protected _resolveReusedParsers(cachedFields: Protocol.RowDescription[], typeMap: DataTypeMap, options: QueryOptions): {
248
+ protected _resolveReusedParsers(cachedFields: Protocol.RowDescription[], typeMap: DataTypeMap, options: QueryOptions, columnFormat: Protocol.DataFormat | Protocol.DataFormat[]): {
249
249
  parsers: AnyParseFunction[];
250
250
  resultFields: FieldInfo[];
251
251
  };
@@ -9,6 +9,7 @@ import { getConnectionConfig } from '../util/connection-config.js';
9
9
  import { buildCopySql, buildProbeSql, writeCopyBinaryRows, } from '../util/copy-from-rows.js';
10
10
  import { escapeLiteral } from '../util/escape-literal.js';
11
11
  import { getParsers } from '../util/get-parsers.js';
12
+ import { fetchAsStringEqual, resolveColumnFormats, } from '../util/resolve-column-formats.js';
12
13
  import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
13
14
  import { wrapRowDescription } from '../util/wrap-row-description.js';
14
15
  import { CopyFromStream, CopyToStream } from './copy-stream.js';
@@ -27,6 +28,21 @@ function columnFormatsEqual(a, b) {
27
28
  return false;
28
29
  return true;
29
30
  }
31
+ /**
32
+ * Whether a command tag's row count describes rows the statement changed,
33
+ * rather than rows it returned or moved. SELECT, FETCH, MOVE and COPY all
34
+ * carry a count of their own that would be misleading as `rowsAffected`.
35
+ *
36
+ * MERGE has reported its own total (inserted + updated + deleted) since
37
+ * PostgreSQL 15; _handleCommandComplete() has always parsed it, and this
38
+ * is what stops it being dropped on the floor.
39
+ */
40
+ function reportsRowsAffected(command) {
41
+ return (command === 'INSERT' ||
42
+ command === 'UPDATE' ||
43
+ command === 'DELETE' ||
44
+ command === 'MERGE');
45
+ }
30
46
  /**
31
47
  * Uses of the same SQL before it is worth preparing.
32
48
  *
@@ -80,7 +96,9 @@ export class IntlConnection extends SafeEventEmitter {
80
96
  this._config = Object.freeze(getConnectionConfig(config));
81
97
  this.socket = new PgSocket(this._config);
82
98
  this.socket.on('error', err => this._onError(err));
83
- this.socket.on('close', () => this.emit('close'));
99
+ // The reason travels with it: 'close' on its own cannot tell an
100
+ // ordinary shutdown from the backend being terminated under us.
101
+ this.socket.on('close', (reason) => this.emit('close', reason));
84
102
  this.socket.on('notification', payload => this.emit('notification', payload));
85
103
  this.socket.on('connecting', () => this.emit('connecting'));
86
104
  this._onErrorSavePoint = 'SP_' + Math.round(Math.random() * 100000000);
@@ -147,7 +165,7 @@ export class IntlConnection extends SafeEventEmitter {
147
165
  // IntlConnection TWICE per close() call - confirmed live: a single
148
166
  // close() left two listeners registered on the far side (e.g. two
149
167
  // NOTIFY deliveries after Pool's reconnect re-subscribed twice).
150
- this.socket.once('close', resolve);
168
+ this.socket.once('close', () => resolve());
151
169
  this.socket.sendTerminateMessage(() => {
152
170
  this.socket.close();
153
171
  });
@@ -267,7 +285,7 @@ export class IntlConnection extends SafeEventEmitter {
267
285
  * nesting the existing one.
268
286
  */
269
287
  async savepoint(name) {
270
- if (!(name && name.match(/^[a-zA-Z]\w+$/)))
288
+ if (!(name && name.match(/^[a-zA-Z]\w*$/)))
271
289
  throw new Error(`Invalid savepoint "${name}"`);
272
290
  const depth = (this._savepointDepths.get(name) || 0) + 1;
273
291
  this._savepointDepths.set(name, depth);
@@ -299,7 +317,7 @@ export class IntlConnection extends SafeEventEmitter {
299
317
  * @param immediate - Ignores the tracked depth and releases right away.
300
318
  */
301
319
  async releaseSavepoint(name, immediate) {
302
- if (!(name && name.match(/^[a-zA-Z]\w+$/)))
320
+ if (!(name && name.match(/^[a-zA-Z]\w*$/)))
303
321
  throw new Error(`Invalid savepoint "${name}"`);
304
322
  const depth = this._savepointDepths.get(name) || 0;
305
323
  if (!immediate && depth > 1) {
@@ -359,7 +377,7 @@ export class IntlConnection extends SafeEventEmitter {
359
377
  * unwind.
360
378
  */
361
379
  async rollbackToSavepoint(name) {
362
- if (!(name && name.match(/^[a-zA-Z]\w+$/)))
380
+ if (!(name && name.match(/^[a-zA-Z]\w*$/)))
363
381
  throw new Error(`Invalid savepoint "${name}"`);
364
382
  this._savepointDepths.delete(name);
365
383
  await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
@@ -523,8 +541,11 @@ export class IntlConnection extends SafeEventEmitter {
523
541
  break;
524
542
  case Protocol.BackendMessageCode.RowDescription:
525
543
  fields = msg.fields;
526
- parsers = getParsers(typeMap, fields);
527
- current.fields = wrapRowDescription(typeMap, fields, DataFormat.text);
544
+ // Simple Query has no Bind at all, so every column is
545
+ // already text - a fetchAsString column simply comes back
546
+ // unparsed.
547
+ parsers = getParsers(typeMap, fields, options);
548
+ current.fields = wrapRowDescription(typeMap, fields, DataFormat.text, options);
528
549
  current.rows = [];
529
550
  break;
530
551
  case Protocol.BackendMessageCode.DataRow:
@@ -539,11 +560,8 @@ export class IntlConnection extends SafeEventEmitter {
539
560
  case Protocol.BackendMessageCode.CommandComplete:
540
561
  // Ignore BEGIN command that we added to sql
541
562
  current.command = msg.command;
542
- if (current.command === 'DELETE' ||
543
- current.command === 'INSERT' ||
544
- current.command === 'UPDATE') {
563
+ if (reportsRowsAffected(current.command))
545
564
  current.rowsAffected = msg.rowCount;
546
- }
547
565
  if (timingEnabled)
548
566
  current.executeTime = performance.now() - currentStart;
549
567
  if (current.rows)
@@ -659,7 +677,13 @@ export class IntlConnection extends SafeEventEmitter {
659
677
  return result;
660
678
  }
661
679
  }
662
- if (!this._earnsAName(key))
680
+ // A fetchAsString query needs its RowDescription before the Bind that
681
+ // asks for those columns as text, and only a named statement has one
682
+ // in hand ahead of time. Preparing on first sight pays a single
683
+ // Parse+Describe round trip per connection instead of leaving every
684
+ // call until the statement earns a name asking for the whole row as
685
+ // text (what queryOnce() falls back to).
686
+ if (!options.fetchAsString?.length && !this._earnsAName(key))
663
687
  return this.queryOnce(sql, paramTypes, params, options, savepoint);
664
688
  const name = 'C_' + ++this._preparedCounter;
665
689
  // A Parse that fails caches nothing and reports itself; the statement
@@ -734,7 +758,24 @@ export class IntlConnection extends SafeEventEmitter {
734
758
  let resultFields;
735
759
  let commandTag;
736
760
  let error;
761
+ let suspended = false;
737
762
  const rowDecoder = resolveRowDecoder(options);
763
+ // fetchAsString names OIDs, but Bind's result format codes are
764
+ // positional - and this path's Describe answers only after the Bind
765
+ // that would have to carry them. With no column types to map the
766
+ // list onto, the whole row is asked for as text instead: the listed
767
+ // columns then arrive exactly as the server renders them, which is
768
+ // what was asked for, and the rest cost a text decode.
769
+ //
770
+ // Only `prepare: false` normally reaches here with a list set -
771
+ // _queryCached() prepares a fetchAsString query on first sight
772
+ // precisely so it can send per-column codes from the second call on
773
+ // (from the first, for every connection that has seen the SQL).
774
+ const columnFormat = options.fetchAsString?.length
775
+ ? DataFormat.text
776
+ : options.columnFormat != null
777
+ ? options.columnFormat
778
+ : DEFAULT_COLUMN_FORMAT;
738
779
  // See executeReused() for what a savepoint riding along does to the
739
780
  // CommandComplete stream.
740
781
  let leadingCommandTags = savepoint ? 1 : 0;
@@ -742,9 +783,15 @@ export class IntlConnection extends SafeEventEmitter {
742
783
  await this.socket
743
784
  .sendExtendedQueryMessages({
744
785
  parse: { sql, paramTypes },
745
- bind: { typeMap, paramTypes, params, queryOptions: options },
786
+ bind: {
787
+ typeMap,
788
+ paramTypes,
789
+ params,
790
+ queryOptions: options,
791
+ columnFormat,
792
+ },
746
793
  describe: { type: 'P' },
747
- execute: { fetchCount: options.fetchCount || 100 },
794
+ execute: { fetchCount: options.fetchCount ?? 0 },
748
795
  before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
749
796
  after: savepoint ? 'RELEASE ' + savepoint : undefined,
750
797
  }, (code, msg, done) => {
@@ -753,11 +800,17 @@ export class IntlConnection extends SafeEventEmitter {
753
800
  case Protocol.BackendMessageCode.BindComplete:
754
801
  case Protocol.BackendMessageCode.NoData:
755
802
  case Protocol.BackendMessageCode.NoticeResponse:
803
+ break;
804
+ // The server stopped at the fetchCount limit rather than
805
+ // running out of rows. The Sync that closes this call
806
+ // discards the portal, so the rest is gone - all that can be
807
+ // done is say the result is a prefix.
756
808
  case Protocol.BackendMessageCode.PortalSuspended:
809
+ suspended = true;
757
810
  break;
758
811
  case Protocol.BackendMessageCode.RowDescription:
759
- parsers = getParsers(typeMap, msg.fields);
760
- resultFields = wrapRowDescription(typeMap, msg.fields, options.columnFormat || DEFAULT_COLUMN_FORMAT);
812
+ parsers = getParsers(typeMap, msg.fields, options);
813
+ resultFields = wrapRowDescription(typeMap, msg.fields, columnFormat, options);
761
814
  result.fields = resultFields;
762
815
  result.rowType = resolveRowType(options);
763
816
  break;
@@ -796,11 +849,10 @@ export class IntlConnection extends SafeEventEmitter {
796
849
  rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
797
850
  }
798
851
  }
799
- if (result.command === 'DELETE' ||
800
- result.command === 'INSERT' ||
801
- result.command === 'UPDATE') {
852
+ if (suspended)
853
+ result.suspended = true;
854
+ if (reportsRowsAffected(result.command))
802
855
  result.rowsAffected = commandTag?.rowCount;
803
- }
804
856
  if (timingEnabled)
805
857
  result.executeTime = performance.now() - startTime;
806
858
  return result;
@@ -873,25 +925,24 @@ export class IntlConnection extends SafeEventEmitter {
873
925
  * so repeated executes of the same statement don't rebuild them.
874
926
  * Shared by executeReused() and executeBatchReused().
875
927
  */
876
- _resolveReusedParsers(cachedFields, typeMap, options) {
877
- const columnFormat = options.columnFormat != null
878
- ? options.columnFormat
879
- : DEFAULT_COLUMN_FORMAT;
928
+ _resolveReusedParsers(cachedFields, typeMap, options, columnFormat) {
880
929
  const cached = this._executeReusedParserCache.get(cachedFields);
881
930
  if (cached &&
882
931
  cached.typeMap === typeMap &&
883
- columnFormatsEqual(cached.columnFormat, columnFormat)) {
932
+ columnFormatsEqual(cached.columnFormat, columnFormat) &&
933
+ fetchAsStringEqual(cached.fetchAsString, options.fetchAsString)) {
884
934
  return { parsers: cached.parsers, resultFields: cached.resultFields };
885
935
  }
886
936
  const fields = cachedFields.map((f, i) => ({
887
937
  ...f,
888
938
  format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
889
939
  }));
890
- const parsers = getParsers(typeMap, fields);
891
- const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
940
+ const parsers = getParsers(typeMap, fields, options);
941
+ const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
892
942
  this._executeReusedParserCache.set(cachedFields, {
893
943
  typeMap,
894
944
  columnFormat,
945
+ fetchAsString: options.fetchAsString,
895
946
  parsers,
896
947
  resultFields,
897
948
  });
@@ -974,6 +1025,16 @@ export class IntlConnection extends SafeEventEmitter {
974
1025
  if (key != null && plan.name)
975
1026
  named.set(key, plan);
976
1027
  }
1028
+ // Only a statement whose columns are already known can carry
1029
+ // per-column codes; the rest fall back to asking for the whole row
1030
+ // as text, exactly as queryOnce() does and for the same reason.
1031
+ plan.columnFormat = plan.fields
1032
+ ? resolveColumnFormats(plan.fields, options)
1033
+ : options.fetchAsString?.length
1034
+ ? DataFormat.text
1035
+ : options.columnFormat != null
1036
+ ? options.columnFormat
1037
+ : DEFAULT_COLUMN_FORMAT;
977
1038
  plans.push(plan);
978
1039
  statements.push({
979
1040
  // Per statement, not per pipeline: the statements differ, so a
@@ -988,6 +1049,7 @@ export class IntlConnection extends SafeEventEmitter {
988
1049
  paramTypes: r.paramTypes,
989
1050
  params: r.params,
990
1051
  queryOptions: options,
1052
+ columnFormat: plan.columnFormat,
991
1053
  },
992
1054
  describe: sendDescribe ? { type: 'P' } : undefined,
993
1055
  // Unlimited, never options.fetchCount: a suspended portal would
@@ -1025,7 +1087,7 @@ export class IntlConnection extends SafeEventEmitter {
1025
1087
  const fields = pendingFields || plan.fields;
1026
1088
  const result = { command: msg?.command };
1027
1089
  if (fields) {
1028
- const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options);
1090
+ const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options, plan.columnFormat ?? DEFAULT_COLUMN_FORMAT);
1029
1091
  result.fields = resultFields;
1030
1092
  result.rowType = resolveRowType(options);
1031
1093
  if (!result.command)
@@ -1038,9 +1100,7 @@ export class IntlConnection extends SafeEventEmitter {
1038
1100
  }
1039
1101
  result.rows = rows;
1040
1102
  }
1041
- if (result.command === 'DELETE' ||
1042
- result.command === 'INSERT' ||
1043
- result.command === 'UPDATE')
1103
+ if (reportsRowsAffected(result.command))
1044
1104
  result.rowsAffected = msg.rowCount;
1045
1105
  pendingFields = undefined;
1046
1106
  pendingRows = undefined;
@@ -1120,8 +1180,9 @@ export class IntlConnection extends SafeEventEmitter {
1120
1180
  const rowDecoder = resolveRowDecoder(options);
1121
1181
  let parsers;
1122
1182
  let resultFields;
1183
+ const columnFormat = resolveColumnFormats(cachedFields, options);
1123
1184
  if (cachedFields) {
1124
- const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
1185
+ const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
1125
1186
  parsers = resolved.parsers;
1126
1187
  resultFields = resolved.resultFields;
1127
1188
  }
@@ -1134,6 +1195,7 @@ export class IntlConnection extends SafeEventEmitter {
1134
1195
  paramTypes,
1135
1196
  params,
1136
1197
  queryOptions: options,
1198
+ columnFormat,
1137
1199
  }));
1138
1200
  this.runningQueryCount++;
1139
1201
  await this.socket
@@ -1155,9 +1217,7 @@ export class IntlConnection extends SafeEventEmitter {
1155
1217
  const item = {};
1156
1218
  if (msg?.command)
1157
1219
  item.command = msg.command;
1158
- if (item.command === 'DELETE' ||
1159
- item.command === 'INSERT' ||
1160
- item.command === 'UPDATE')
1220
+ if (reportsRowsAffected(item.command))
1161
1221
  item.rowsAffected = msg.rowCount;
1162
1222
  if (pendingRows && parsers && resultFields) {
1163
1223
  const l = pendingRows.length;
@@ -1225,9 +1285,15 @@ export class IntlConnection extends SafeEventEmitter {
1225
1285
  let resultFields;
1226
1286
  let commandTag;
1227
1287
  let error;
1288
+ let suspended = false;
1228
1289
  const rowDecoder = resolveRowDecoder(options);
1290
+ // The whole point of reusing a prepared statement here: its
1291
+ // RowDescription is already in hand, so fetchAsString's OID list can
1292
+ // become the per-column result format codes this Bind carries, with
1293
+ // no extra round trip to find out what the columns are.
1294
+ const columnFormat = resolveColumnFormats(cachedFields, options);
1229
1295
  if (cachedFields) {
1230
- const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
1296
+ const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
1231
1297
  parsers = resolved.parsers;
1232
1298
  resultFields = resolved.resultFields;
1233
1299
  result.fields = resultFields;
@@ -1247,8 +1313,9 @@ export class IntlConnection extends SafeEventEmitter {
1247
1313
  paramTypes,
1248
1314
  params,
1249
1315
  queryOptions: options,
1316
+ columnFormat,
1250
1317
  },
1251
- execute: { fetchCount: options.fetchCount || 100 },
1318
+ execute: { fetchCount: options.fetchCount ?? 0 },
1252
1319
  before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
1253
1320
  after: savepoint ? 'RELEASE ' + savepoint : undefined,
1254
1321
  }, (code, msg, done) => {
@@ -1256,7 +1323,10 @@ export class IntlConnection extends SafeEventEmitter {
1256
1323
  case Protocol.BackendMessageCode.ParseComplete:
1257
1324
  case Protocol.BackendMessageCode.BindComplete:
1258
1325
  case Protocol.BackendMessageCode.NoticeResponse:
1326
+ break;
1327
+ // See queryOnce() for what a suspended portal means here.
1259
1328
  case Protocol.BackendMessageCode.PortalSuspended:
1329
+ suspended = true;
1260
1330
  break;
1261
1331
  case Protocol.BackendMessageCode.DataRow:
1262
1332
  rows.push(msg);
@@ -1297,11 +1367,10 @@ export class IntlConnection extends SafeEventEmitter {
1297
1367
  rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
1298
1368
  }
1299
1369
  }
1300
- if (result.command === 'DELETE' ||
1301
- result.command === 'INSERT' ||
1302
- result.command === 'UPDATE') {
1370
+ if (suspended)
1371
+ result.suspended = true;
1372
+ if (reportsRowsAffected(result.command))
1303
1373
  result.rowsAffected = commandTag?.rowCount;
1304
- }
1305
1374
  if (timingEnabled)
1306
1375
  result.executeTime = performance.now() - startTime;
1307
1376
  return result;
@@ -37,6 +37,14 @@ export declare class Pool extends SafeEventEmitter {
37
37
  protected readonly _notificationListeners: SafeEventEmitter;
38
38
  protected _notificationConnection?: Connection;
39
39
  protected readonly _pipelineSlots: PipelineSlot[];
40
+ /**
41
+ * Why a connection is about to be destroyed, for the ones that were not
42
+ * destroyed on purpose. lightning-pool's own 'destroy' carries the
43
+ * resource and nothing else, so the reason is recorded on the way in -
44
+ * when the connection reports its close - and read back out when the
45
+ * pool reports the removal.
46
+ */
47
+ protected readonly _destroyReasons: WeakMap<IntlConnection, Error>;
40
48
  protected _pipelineMaxQueries: number;
41
49
  protected _pipelineMaxConnections: number;
42
50
  readonly config: PoolConfiguration;
@@ -91,6 +99,20 @@ export declare class Pool extends SafeEventEmitter {
91
99
  listen(channel: string, callback: NotificationCallback): Promise<void>;
92
100
  unListen(channel: string): Promise<void>;
93
101
  unListenAll(): Promise<void>;
102
+ /**
103
+ * A pooled connection reported that its socket closed. `reason` is set
104
+ * only when that was not asked for - see PgSocket._handleClose().
105
+ */
106
+ protected _onConnectionClosed(intlCon: IntlConnection, reason?: Error): void;
107
+ /**
108
+ * The pool has removed a connection. `destroy` fires the same way for an
109
+ * ordinary eviction (idle timeout, close(), a failed validate) as for a
110
+ * connection that died, so the reason is what tells them apart - an
111
+ * admin kill, a failover and a network fault were otherwise
112
+ * indistinguishable from a timeout, with nothing at all reported when
113
+ * the connection had no query in flight to reject.
114
+ */
115
+ protected _onConnectionDestroyed(intlCon: IntlConnection): void;
94
116
  protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
95
117
  /**
96
118
  * Picks the pooled connection a pipelined query should ride on, or