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
@@ -2,10 +2,12 @@ import crypto from 'node:crypto';
2
2
  import net from 'node:net';
3
3
  import path from 'node:path';
4
4
  import tls from 'node:tls';
5
+ import DoublyLinked from 'doublylinked';
5
6
  import promisify from 'putil-promisify';
6
7
  import { ConnectionState } from '../constants.js';
7
8
  import { SafeEventEmitter } from '../safe-event-emitter.js';
8
9
  import { Backend } from './backend.js';
10
+ import { signatureHashOfCertificate } from './cert-signature.js';
9
11
  import { DatabaseError } from './database-error.js';
10
12
  import { Frontend } from './frontend.js';
11
13
  import { Protocol } from './protocol.js';
@@ -22,6 +24,13 @@ export class PgSocket extends SafeEventEmitter {
22
24
  _saslSession;
23
25
  _processID;
24
26
  _secretKey;
27
+ _captureQueue = new DoublyLinked();
28
+ _pendingWrites = [];
29
+ _flushScheduled = false;
30
+ _hosts = [];
31
+ _hostIndex = 0;
32
+ _sessionAttrsChecked = false;
33
+ _standbyState;
25
34
  constructor(options) {
26
35
  super();
27
36
  this.options = options;
@@ -45,8 +54,17 @@ export class PgSocket extends SafeEventEmitter {
45
54
  connect() {
46
55
  if (this._socket)
47
56
  return;
48
- this._state = ConnectionState.CONNECTING;
49
57
  const options = this.options;
58
+ this._hosts = options.hosts?.length
59
+ ? options.hosts
60
+ : [{ host: options.host || 'localhost', port: options.port }];
61
+ this._hostIndex = 0;
62
+ this._connectToHost();
63
+ }
64
+ _connectToHost() {
65
+ this._sessionAttrsChecked = false;
66
+ this._state = ConnectionState.CONNECTING;
67
+ const target = this._hosts[this._hostIndex];
50
68
  const socket = (this._socket = new net.Socket());
51
69
  const errorHandler = (err) => {
52
70
  this._state = ConnectionState.CLOSED;
@@ -54,49 +72,128 @@ export class PgSocket extends SafeEventEmitter {
54
72
  this._reset();
55
73
  socket.destroy();
56
74
  this._socket = undefined;
75
+ if (this._hostIndex + 1 < this._hosts.length) {
76
+ this._hostIndex++;
77
+ this._connectToHost();
78
+ return;
79
+ }
57
80
  this.emit('error', err);
58
81
  };
59
82
  const connectHandler = () => {
60
83
  socket.setTimeout(0);
61
84
  if (this.options.keepAlive || this.options.keepAlive == null)
62
85
  socket.setKeepAlive(true);
63
- socket.write(this._frontend.getSSLRequestMessage());
64
- socket.once('data', x => {
86
+ this._negotiateTls(socket, target, readySocket => {
87
+ this._socket = readySocket;
65
88
  this._removeListeners();
66
- if (x.toString() === 'S') {
67
- const tslOptions = { ...options.ssl, socket };
68
- if (options.host && net.isIP(options.host) === 0)
69
- tslOptions.servername = options.host;
70
- const tlsSocket = (this._socket = tls.connect(tslOptions));
71
- tlsSocket.once('error', errorHandler);
72
- tlsSocket.once('secureConnect', () => {
73
- this._removeListeners();
74
- this._handleConnect();
75
- });
76
- return;
77
- }
78
- if (x.toString() === 'N') {
79
- if (options.requireSSL) {
80
- return errorHandler(new Error('Server does not support SSL connections'));
81
- }
82
- this._removeListeners();
83
- this._handleConnect();
84
- return;
85
- }
86
- return errorHandler(new Error('There was an error establishing an SSL connection'));
87
- });
89
+ this._handleConnect();
90
+ }, errorHandler);
88
91
  };
89
92
  socket.setNoDelay(true);
90
- socket.setTimeout(options.connectTimeoutMs || 30000, () => errorHandler(new Error('Connection timed out')));
93
+ socket.setTimeout(this.options.connectTimeoutMs || 30000, () => errorHandler(new Error('Connection timed out')));
91
94
  socket.once('error', errorHandler);
92
95
  socket.once('connect', connectHandler);
93
96
  this.emit('connecting');
94
- const port = options.port || DEFAULT_PORT_NUMBER;
95
- if (options.host && options.host.startsWith('/')) {
96
- socket.connect(path.join(options.host, '/.s.PGSQL.' + port));
97
+ const port = target.port || DEFAULT_PORT_NUMBER;
98
+ if (target.host && target.host.startsWith('/')) {
99
+ socket.connect(path.join(target.host, '/.s.PGSQL.' + port));
97
100
  }
98
101
  else
99
- socket.connect(options.port || DEFAULT_PORT_NUMBER, options.host || 'localhost');
102
+ socket.connect(port, target.host || 'localhost');
103
+ }
104
+ /**
105
+ * Negotiates TLS on an already TCP-connected `socket`, if configured -
106
+ * shared between the long-lived connection above (_connectToHost) and
107
+ * cancel() below, which needs the exact same host/TLS handling for its
108
+ * own short-lived socket, just without ever sending a StartupMessage
109
+ * afterwards. Resolves `onReady` with whichever socket ends up ready to
110
+ * speak the PostgreSQL protocol: the plain one, or its TLS upgrade.
111
+ */
112
+ _negotiateTls(socket, target, onReady, onError) {
113
+ const options = this.options;
114
+ const startTls = () => {
115
+ const tlsOptions = { ...options.ssl, socket };
116
+ if (target.host && net.isIP(target.host) === 0)
117
+ tlsOptions.servername = target.host;
118
+ if (options.sslNegotiation === 'direct')
119
+ tlsOptions.ALPNProtocols = ['postgresql'];
120
+ const tlsSocket = tls.connect(tlsOptions);
121
+ tlsSocket.once('error', onError);
122
+ tlsSocket.once('secureConnect', () => onReady(tlsSocket));
123
+ };
124
+ const wantsSSL = !!options.ssl ||
125
+ !!options.requireSSL ||
126
+ options.sslNegotiation === 'direct';
127
+ if (!wantsSSL)
128
+ return onReady(socket);
129
+ if (options.sslNegotiation === 'direct') {
130
+ // Straight into the handshake: no SSLRequest, nothing in the clear.
131
+ return startTls();
132
+ }
133
+ socket.write(this._frontend.getSSLRequestMessage());
134
+ socket.once('data', x => {
135
+ const command = x.toString();
136
+ if (command === 'S')
137
+ return startTls();
138
+ if (command === 'N') {
139
+ if (options.requireSSL) {
140
+ return onError(new Error('Server does not support SSL connections'));
141
+ }
142
+ return onReady(socket);
143
+ }
144
+ return onError(new Error('There was an error establishing an SSL connection'));
145
+ });
146
+ }
147
+ /**
148
+ * Asks the server to cancel whatever this session is currently running.
149
+ *
150
+ * Opens its own short-lived connection and closes it again: a backend busy
151
+ * with a query is not reading its own socket, so the request cannot travel
152
+ * down the connection it is meant to interrupt. The server answers nothing
153
+ * - it either finds a matching session and signals it or does not - so
154
+ * this resolves once the bytes are out, and the cancelled query reports
155
+ * the outcome itself, as an ordinary error on its own connection.
156
+ *
157
+ * No StartupMessage or authentication is ever sent here - the
158
+ * CancelRequest is the only message this socket carries, and it is
159
+ * self-authorized by the processID/secretKey pair BackendKeyData handed
160
+ * out to the session being cancelled, not by a separate login. TLS is
161
+ * still negotiated via _negotiateTls() when configured, because a
162
+ * `hostssl` pg_hba.conf rule can reject a plaintext connection outright
163
+ * before the server ever reads what request it carries - and the target
164
+ * is `_hosts[_hostIndex]`, the host this session actually ended up
165
+ * connected to, not `options.host`, which after a multi-host failover may
166
+ * name a candidate this session never used.
167
+ */
168
+ cancel() {
169
+ const processID = this._processID;
170
+ const secretKey = this._secretKey;
171
+ // Nothing to cancel before the session is established.
172
+ if (processID == null || secretKey == null)
173
+ return Promise.resolve();
174
+ const data = this._frontend.getCancelRequestMessage(processID, secretKey);
175
+ const target = this._hosts[this._hostIndex];
176
+ return new Promise((resolve, reject) => {
177
+ const socket = new net.Socket();
178
+ const fail = (err) => {
179
+ socket.destroy();
180
+ reject(err);
181
+ };
182
+ const send = (readySocket) => {
183
+ readySocket.end(data, () => {
184
+ readySocket.destroy();
185
+ resolve();
186
+ });
187
+ };
188
+ socket.setNoDelay(true);
189
+ socket.once('error', fail);
190
+ socket.once('connect', () => this._negotiateTls(socket, target, send, fail));
191
+ const port = target.port || DEFAULT_PORT_NUMBER;
192
+ if (target.host && target.host.startsWith('/'))
193
+ socket.connect(path.join(target.host, '/.s.PGSQL.' + port));
194
+ else
195
+ socket.connect(port, target.host || 'localhost');
196
+ });
100
197
  }
101
198
  close() {
102
199
  if (!this._socket || this._socket.destroyed) {
@@ -113,35 +210,150 @@ export class PgSocket extends SafeEventEmitter {
113
210
  socket.once('close', () => this._handleClose());
114
211
  socket.destroy();
115
212
  }
116
- sendParseMessage(args, cb) {
117
- if (this.listenerCount('debug'))
118
- this.emit('debug', { location: 'PgSocket.sendParseMessage', args });
119
- this._send(this._frontend.getParseMessage(args), cb);
213
+ sendExecuteMessage(args, cb) {
214
+ return this._sendAndCapture(this._frontend.getExecuteMessage(args), cb, 'sendExecuteMessage', args);
120
215
  }
121
- sendBindMessage(args, cb) {
122
- if (this.listenerCount('debug'))
123
- this.emit('debug', { location: 'PgSocket.sendBindMessage', args });
124
- this._send(this._frontend.getBindMessage(args), cb);
216
+ sendCloseMessage(args, cb) {
217
+ return this._sendAndCapture(this._frontend.getCloseMessage(args), cb, 'sendCloseMessage', args);
125
218
  }
126
- sendDescribeMessage(args, cb) {
127
- if (this.listenerCount('debug'))
128
- this.emit('debug', { location: 'PgSocket.sendDescribeMessage', args });
129
- this._send(this._frontend.getDescribeMessage(args), cb);
219
+ /**
220
+ * Sends Parse+Bind+Describe+Execute+Sync as a single write with a single
221
+ * FIFO capture entry, instead of 5 separate sendXMessage() round trips -
222
+ * the one-shot Extended Query fast path (IntlConnection.queryOnce()).
223
+ * Callers pass unnamed statement/portal (omit statement/portal/name) so
224
+ * no Close message is needed at all: PostgreSQL auto-clears an unnamed
225
+ * statement/portal at the next Parse/Bind referencing the unnamed name.
226
+ */
227
+ sendExtendedQueryMessages(args, cb) {
228
+ const data = [
229
+ this._frontend.getParseMessage(args.parse),
230
+ this._frontend.getBindMessage(args.bind),
231
+ this._frontend.getDescribeMessage(args.describe),
232
+ this._frontend.getExecuteMessage(args.execute),
233
+ this._frontend.getSyncMessage(),
234
+ ];
235
+ return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
130
236
  }
131
- sendExecuteMessage(args, cb) {
132
- if (this.listenerCount('debug'))
133
- this.emit('debug', { location: 'PgSocket.sendDescribeMessage', args });
134
- this._send(this._frontend.getExecuteMessage(args), cb);
237
+ /**
238
+ * Parse + Describe(statement) + Sync as a single round trip - used by
239
+ * PreparedStatement.prepare() instead of a separate awaited Parse then a
240
+ * separate awaited Sync, and fetches the RowDescription/NoData in the
241
+ * same round trip so PreparedStatement can cache it instead of every
242
+ * later execute() re-Describing its own portal.
243
+ */
244
+ sendPrepareMessages(args, cb) {
245
+ const data = [
246
+ this._frontend.getParseMessage(args.parse),
247
+ this._frontend.getDescribeMessage(args.describe),
248
+ this._frontend.getSyncMessage(),
249
+ ];
250
+ return this._sendAndCapture(data, cb, 'sendPrepareMessages', args);
135
251
  }
136
- sendCloseMessage(args, cb) {
137
- if (this.listenerCount('debug'))
138
- this.emit('debug', { location: 'PgSocket.sendCloseMessage', args });
139
- this._send(this._frontend.getCloseMessage(args), cb);
252
+ /**
253
+ * Bind + Execute + Sync as a single round trip, against an UNNAMED
254
+ * portal (args.bind.portal/args.execute.portal left unset) - used by
255
+ * PreparedStatement._execute() to reuse an already-prepared (named)
256
+ * statement without the per-call Describe/Close that Portal's own
257
+ * bind()/retrieveFields()/execute()/close() sequence needs for a
258
+ * freshly-created named portal. No Close message is needed: PostgreSQL
259
+ * auto-clears an unnamed portal at the next Bind naming it.
260
+ */
261
+ sendBindExecuteMessages(args, cb) {
262
+ const data = [
263
+ this._frontend.getBindMessage(args.bind),
264
+ this._frontend.getExecuteMessage(args.execute),
265
+ this._frontend.getSyncMessage(),
266
+ ];
267
+ return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
268
+ }
269
+ /**
270
+ * Bind + Describe(portal) + Flush as a single round trip (NOT Sync - see
271
+ * Portal.bindAndRetrieveFields()'s doc comment for why a Sync here would
272
+ * be unsafe: it would commit an implicit/autocommit transaction and
273
+ * destroy the just-bound named portal before any fetch() could use it).
274
+ * Used by PreparedStatement._execute()'s cursor branch instead of two
275
+ * separately-awaited Portal.bind()/retrieveFields() round trips.
276
+ */
277
+ sendBindDescribeMessages(args, cb) {
278
+ const data = [
279
+ this._frontend.getBindMessage(args.bind),
280
+ this._frontend.getDescribeMessage(args.describe),
281
+ this._frontend.getFlushMessage(),
282
+ ];
283
+ return this._sendAndCapture(data, cb, 'sendBindDescribeMessages', args);
284
+ }
285
+ /**
286
+ * Close + Sync under ONE capture, rather than a Close capture followed by
287
+ * a separate Sync capture. That split is unsafe after an ErrorResponse:
288
+ * the server then skips every message until Sync, so the only reply is
289
+ * the Sync's ReadyForQuery - which the FIFO hands to the *Close* capture
290
+ * (the older entry), making it fail with "unexpected response message
291
+ * (Z)" and leaving the Sync capture orphaned, to silently swallow the
292
+ * first message of whatever request comes next. With one capture,
293
+ * ReadyForQuery is simply this request's end marker whether or not the
294
+ * Close itself was honoured.
295
+ */
296
+ sendCloseAndSyncMessages(args, cb) {
297
+ const data = [
298
+ this._frontend.getCloseMessage(args),
299
+ this._frontend.getSyncMessage(),
300
+ ];
301
+ return this._sendAndCapture(data, cb, 'sendCloseAndSyncMessages', args);
302
+ }
303
+ /**
304
+ * Close(portal) + Close(statement) + Sync as a single round trip - used
305
+ * by PreparedStatement._maybeCloseWithPortal() when a Cursor's close()
306
+ * brings the owning PreparedStatement's refcount to 0, instead of two
307
+ * separately-batched Close+Sync round trips (Portal.close() then
308
+ * PreparedStatement._close()). Sync here is safe/intended: this is final
309
+ * teardown, no portal needs to survive past it.
310
+ */
311
+ sendClosePortalAndStatementMessages(args, cb) {
312
+ const data = [
313
+ this._frontend.getCloseMessage(args.portal),
314
+ this._frontend.getCloseMessage(args.statement),
315
+ this._frontend.getSyncMessage(),
316
+ ];
317
+ return this._sendAndCapture(data, cb, 'sendClosePortalAndStatementMessages', args);
140
318
  }
141
319
  sendQueryMessage(sql, cb) {
142
- if (this.listenerCount('debug'))
143
- this.emit('debug', { location: 'PgSocket.sendQueryMessage', sql });
144
- this._send(this._frontend.getQueryMessage(sql), cb);
320
+ return this._sendAndCapture(this._frontend.getQueryMessage(sql), cb, 'sendQueryMessage', sql);
321
+ }
322
+ /**
323
+ * COPY IN data. Unlike every other send*() here this pushes nothing onto
324
+ * the capture queue: while a COPY is in progress the server answers no
325
+ * individual CopyData message, so the response still belongs to the
326
+ * Query capture that opened the copy.
327
+ *
328
+ * Returns false when the socket's buffer is full, exactly as
329
+ * net.Socket.write() does - the caller is expected to stop writing until
330
+ * `cb` fires, which is what CopyFromStream hands straight to its own
331
+ * Writable callback.
332
+ */
333
+ sendCopyData(data, cb) {
334
+ return this._sendCopy(this._frontend.getCopyDataMessage(data), cb);
335
+ }
336
+ /** Ends a COPY IN normally; the server replies CommandComplete. */
337
+ sendCopyDone(cb) {
338
+ return this._sendCopy(this._frontend.getCopyDoneMessage(), cb);
339
+ }
340
+ /**
341
+ * Aborts a COPY IN. The server discards the copy, reports `message` as an
342
+ * ErrorResponse and returns to its normal state - without this a failed
343
+ * import would leave the connection waiting for data forever.
344
+ */
345
+ sendCopyFail(message, cb) {
346
+ return this._sendCopy(this._frontend.getCopyFailMessage(message), cb);
347
+ }
348
+ /**
349
+ * Stops reading from the socket, so a COPY OUT consumer that cannot keep
350
+ * up doesn't buffer the whole export in memory. Safe to call repeatedly.
351
+ */
352
+ pause() {
353
+ this._socket?.pause();
354
+ }
355
+ resume() {
356
+ this._socket?.resume();
145
357
  }
146
358
  sendFlushMessage(cb) {
147
359
  if (this.listenerCount('debug'))
@@ -153,46 +365,31 @@ export class PgSocket extends SafeEventEmitter {
153
365
  this.emit('debug', { location: 'PgSocket.sendTerminateMessage' });
154
366
  this._send(this._frontend.getTerminateMessage(), cb);
155
367
  }
156
- sendSyncMessage() {
157
- if (this.listenerCount('debug'))
158
- this.emit('debug', { location: 'PgSocket.sendSyncMessage' });
159
- this._send(this._frontend.getSyncMessage());
368
+ sendSyncMessage(cb) {
369
+ return this._sendAndCapture(this._frontend.getSyncMessage(), cb, 'sendSyncMessage', undefined);
160
370
  }
161
- capture(callback) {
162
- if (this._state === ConnectionState.CLOSING ||
163
- this._state === ConnectionState.CLOSED) {
164
- return Promise.reject(new Error('Connection closed'));
165
- }
166
- if (this._state !== ConnectionState.READY)
167
- return Promise.reject(new Error('Connection is not ready'));
371
+ /**
372
+ * The only way anything outside this class can receive a correlated
373
+ * backend response. Sends `data` and pushes `cb` onto the FIFO capture
374
+ * queue in the same call, so a response can never arrive before its
375
+ * capturer is registered, and a capturer can never be registered without
376
+ * a message actually going out for it - there is no separate, decoupled
377
+ * "start listening" step to race or misuse.
378
+ */
379
+ _sendAndCapture(data, cb, location, args) {
380
+ if (typeof cb !== 'function')
381
+ throw new TypeError(`${location}() requires a CaptureCallback`);
382
+ if (this.listenerCount('debug'))
383
+ this.emit('debug', { location: `PgSocket.${location}`, args });
168
384
  return new Promise((resolve, reject) => {
169
- const done = (err, result) => {
170
- this.removeListener('close', closeHandler);
171
- this.removeListener('error', errorHandler);
172
- this.removeListener('message', msgHandler);
173
- if (err)
174
- reject(err);
175
- else
176
- resolve(result);
177
- };
178
- const errorHandler = (err) => {
179
- this.removeListener('close', closeHandler);
180
- this.removeListener('message', msgHandler);
181
- reject(err);
182
- };
183
- const closeHandler = () => {
184
- this.removeListener('error', errorHandler);
185
- this.removeListener('message', msgHandler);
186
- reject(new Error('Connection closed'));
187
- };
188
- const msgHandler = (code, msg) => {
189
- const x = callback(code, msg, done);
190
- if (promisify.isPromise(x))
191
- x.catch(err => done(err));
192
- };
193
- this.once('close', closeHandler);
194
- this.once('error', errorHandler);
195
- this.on('message', msgHandler);
385
+ const entry = { callback: cb, resolve, reject };
386
+ this._captureQueue.push(entry);
387
+ if (!this._send(data)) {
388
+ // Nothing else can have run between the push above and here (fully
389
+ // synchronous), so the entry just pushed is still the tail.
390
+ this._captureQueue.pop();
391
+ reject(new Error('Socket is not writable'));
392
+ }
196
393
  });
197
394
  }
198
395
  _removeListeners() {
@@ -209,6 +406,9 @@ export class PgSocket extends SafeEventEmitter {
209
406
  this._processID = undefined;
210
407
  this._secretKey = undefined;
211
408
  this._saslSession = undefined;
409
+ // Anything still queued belonged to the connection being reset - it
410
+ // must never reach a freshly connected socket.
411
+ this._pendingWrites = [];
212
412
  }
213
413
  _handleConnect() {
214
414
  const socket = this._socket;
@@ -223,20 +423,99 @@ export class PgSocket extends SafeEventEmitter {
223
423
  user: this.options.user || 'postgres',
224
424
  database: this.options.database || '',
225
425
  application_name: this.options.applicationName || '',
426
+ ...(this.options.replication
427
+ ? { replication: this.options.replication }
428
+ : undefined),
226
429
  }));
227
430
  }
431
+ /**
432
+ * Called once the server is ready. With `targetSessionAttrs` set, the
433
+ * session has to be checked before it is handed over: a server that does
434
+ * not match is dropped and the next candidate tried, which is how
435
+ * `read-write` finds whichever node is currently the primary.
436
+ */
437
+ _finishConnect() {
438
+ const wanted = this.options.targetSessionAttrs;
439
+ if (!wanted || this._sessionAttrsChecked) {
440
+ this.emit('ready');
441
+ return;
442
+ }
443
+ this._sessionAttrsChecked = true;
444
+ const reported = this._sessionParameters;
445
+ if (reported.in_hot_standby != null &&
446
+ reported.default_transaction_read_only != null) {
447
+ this._applySessionAttrs(wanted, reported.in_hot_standby === 'on', reported.default_transaction_read_only === 'on');
448
+ return;
449
+ }
450
+ this.sendQueryMessage('select pg_catalog.pg_is_in_recovery()::text as a,' +
451
+ " current_setting('transaction_read_only') as b", (code, msg, done) => {
452
+ if (code === Protocol.BackendMessageCode.DataRow) {
453
+ // Two text columns, each length-prefixed within the row buffer.
454
+ const data = msg.data;
455
+ const aLen = data.readInt32BE(0);
456
+ const a = data.toString('utf8', 4, 4 + aLen);
457
+ const bOffset = 4 + aLen;
458
+ const bLen = data.readInt32BE(bOffset);
459
+ const b = data.toString('utf8', bOffset + 4, bOffset + 4 + bLen);
460
+ this._standbyState = { standby: a === 'true', readOnly: b === 'on' };
461
+ }
462
+ else if (code === Protocol.BackendMessageCode.ReadyForQuery) {
463
+ done(undefined);
464
+ const st = this._standbyState;
465
+ this._applySessionAttrs(wanted, !!st?.standby, !!st?.readOnly);
466
+ }
467
+ }).catch(err => this.emit('error', err));
468
+ }
469
+ /** Accepts this server, or drops it and moves to the next candidate. */
470
+ _applySessionAttrs(wanted, standby, readOnly) {
471
+ const rejected = (wanted === 'read-write' && readOnly) ||
472
+ (wanted === 'read-only' && !readOnly) ||
473
+ (wanted === 'primary' && standby) ||
474
+ (wanted === 'standby' && !standby) ||
475
+ // prefer-standby settles for a primary, but only once nothing else
476
+ // is left to try.
477
+ (wanted === 'prefer-standby' &&
478
+ !standby &&
479
+ this._hostIndex + 1 < this._hosts.length);
480
+ if (!rejected) {
481
+ this.emit('ready');
482
+ return;
483
+ }
484
+ const target = this._hosts[this._hostIndex];
485
+ if (this._hostIndex + 1 >= this._hosts.length) {
486
+ this.close();
487
+ this.emit('error', new Error(`No server matched target_session_attrs "${wanted}" ` +
488
+ `(last tried ${target.host}:${target.port ?? this.options.port ?? DEFAULT_PORT_NUMBER})`));
489
+ return;
490
+ }
491
+ this._hostIndex++;
492
+ const socket = this._socket;
493
+ this._removeListeners();
494
+ this._socket = undefined;
495
+ this._reset();
496
+ socket?.destroy();
497
+ this._connectToHost();
498
+ }
228
499
  _handleClose() {
500
+ this._failPendingCaptures(new Error('Connection closed'));
229
501
  this._reset();
230
502
  this._socket = undefined;
231
503
  this._state = ConnectionState.CLOSED;
232
504
  this.emit('close');
233
505
  }
234
506
  _handleError(err) {
507
+ this._failPendingCaptures(err instanceof Error ? err : new Error(String(err)));
235
508
  if (this._state !== ConnectionState.READY) {
236
509
  this._socket?.end();
237
510
  }
238
511
  this.emit('error', err);
239
512
  }
513
+ /** Rejects and clears every still-open capture, e.g. on close/error. */
514
+ _failPendingCaptures(err) {
515
+ const pending = this._captureQueue;
516
+ this._captureQueue = new DoublyLinked();
517
+ pending.forEach(entry => entry.reject(err));
518
+ }
240
519
  _handleData(data) {
241
520
  this._backend.parse(data, (code, payload) => {
242
521
  try {
@@ -245,7 +524,7 @@ export class PgSocket extends SafeEventEmitter {
245
524
  this._handleAuthenticationMessage(payload);
246
525
  break;
247
526
  case Protocol.BackendMessageCode.ErrorResponse:
248
- this.emit('error', new DatabaseError(payload));
527
+ this._dispatch(code, new DatabaseError(payload));
249
528
  break;
250
529
  case Protocol.BackendMessageCode.NoticeResponse:
251
530
  this.emit('notice', payload);
@@ -262,18 +541,18 @@ export class PgSocket extends SafeEventEmitter {
262
541
  case Protocol.BackendMessageCode.ReadyForQuery:
263
542
  if (this._state !== ConnectionState.READY) {
264
543
  this._state = ConnectionState.READY;
265
- this.emit('ready');
544
+ this._finishConnect();
266
545
  }
267
546
  else
268
- this.emit('message', code, payload);
547
+ this._dispatch(code, payload);
269
548
  break;
270
549
  case Protocol.BackendMessageCode.CommandComplete: {
271
550
  const msg = this._handleCommandComplete(payload);
272
- this.emit('message', code, msg);
551
+ this._dispatch(code, msg);
273
552
  break;
274
553
  }
275
554
  default:
276
- this.emit('message', code, payload);
555
+ this._dispatch(code, payload);
277
556
  }
278
557
  }
279
558
  catch (e) {
@@ -281,6 +560,44 @@ export class PgSocket extends SafeEventEmitter {
281
560
  }
282
561
  });
283
562
  }
563
+ /**
564
+ * Routes one backend message to the capture at the front of the FIFO -
565
+ * the oldest still-open request, i.e. the one whose turn it is given the
566
+ * order messages were written to the socket. This *is* the request/
567
+ * response correlation, since the wire protocol has no id of its own.
568
+ * The same entry keeps receiving messages (ParseComplete, BindComplete,
569
+ * RowDescription, DataRow*, CommandComplete, ReadyForQuery, ...) until
570
+ * its callback calls `done()`, at which point it is dequeued and the
571
+ * next entry becomes the one messages are routed to.
572
+ */
573
+ _dispatch(code, payload) {
574
+ const entry = this._captureQueue.head?.value;
575
+ if (!entry) {
576
+ if (payload instanceof DatabaseError) {
577
+ this.emit('error', payload);
578
+ return;
579
+ }
580
+ this.emit('error', new Error(`PgSocket: received message code=${String(code)} with an empty capture queue`));
581
+ return;
582
+ }
583
+ const done = (err, result) => {
584
+ if (this._captureQueue.head?.value !== entry)
585
+ return;
586
+ this._captureQueue.shift();
587
+ if (err)
588
+ entry.reject(err);
589
+ else
590
+ entry.resolve(result);
591
+ };
592
+ try {
593
+ const x = entry.callback(code, payload, done);
594
+ if (promisify.isPromise(x))
595
+ x.catch(err => done(err));
596
+ }
597
+ catch (err) {
598
+ done(err);
599
+ }
600
+ }
284
601
  _resolvePassword(cb) {
285
602
  (async () => {
286
603
  const pass = typeof this.options.password === 'function'
@@ -310,10 +627,21 @@ export class PgSocket extends SafeEventEmitter {
310
627
  });
311
628
  break;
312
629
  case Protocol.AuthenticationMessageKind.SASL: {
313
- if (!msg.mechanisms.includes('SCRAM-SHA-256')) {
314
- throw new Error('SASL: Only mechanism SCRAM-SHA-256 is currently supported');
630
+ const mode = this.options.channelBinding || 'prefer';
631
+ const tlsSocket = mode !== 'disable' && this._socket instanceof tls.TLSSocket
632
+ ? this._socket
633
+ : undefined;
634
+ const offersBinding = !!msg.mechanisms?.includes('SCRAM-SHA-256-PLUS');
635
+ const useBinding = !!tlsSocket && offersBinding;
636
+ if (mode === 'require' && !useBinding) {
637
+ throw new Error(tlsSocket
638
+ ? 'SASL: channelBinding is "require" but the server does not offer SCRAM-SHA-256-PLUS'
639
+ : 'SASL: channelBinding is "require" but the connection is not using TLS');
640
+ }
641
+ if (!useBinding && !msg.mechanisms?.includes('SCRAM-SHA-256')) {
642
+ throw new Error('SASL: Only mechanisms SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are supported');
315
643
  }
316
- const saslSession = (this._saslSession = SASL.createSession(this.options.user || '', 'SCRAM-SHA-256'));
644
+ const saslSession = (this._saslSession = SASL.createSession(this.options.user || '', useBinding ? 'SCRAM-SHA-256-PLUS' : 'SCRAM-SHA-256', useBinding ? this._channelBindingData(tlsSocket) : undefined, !!tlsSocket));
317
645
  this._send(this._frontend.getSASLMessage(saslSession));
318
646
  break;
319
647
  }
@@ -337,9 +665,26 @@ export class PgSocket extends SafeEventEmitter {
337
665
  break;
338
666
  }
339
667
  default:
340
- break;
668
+ throw new Error(`Authentication method "${msg.kind}" is not supported. ` +
669
+ 'Supported methods are cleartext password, MD5 and SCRAM-SHA-256 ' +
670
+ '(with or without channel binding).');
341
671
  }
342
672
  }
673
+ /**
674
+ * tls-server-end-point: the server certificate hashed with the algorithm
675
+ * its own signature used (RFC 5929), which is why the DER has to be read
676
+ * rather than one of Node's fixed fingerprints taken.
677
+ */
678
+ _channelBindingData(socket) {
679
+ const cert = socket.getPeerX509Certificate?.();
680
+ if (!cert)
681
+ throw new Error('SASL: SCRAM-SHA-256-PLUS needs the server certificate, which this connection did not provide');
682
+ const der = cert.raw;
683
+ return crypto
684
+ .createHash(signatureHashOfCertificate(der))
685
+ .update(der)
686
+ .digest();
687
+ }
343
688
  _handleParameterStatus(msg) {
344
689
  this._sessionParameters[msg.name] = msg.value;
345
690
  }
@@ -360,9 +705,101 @@ export class PgSocket extends SafeEventEmitter {
360
705
  result.rowCount = parseInt(m[2], 10);
361
706
  return result;
362
707
  }
708
+ /**
709
+ * Queues `data` for the socket instead of writing it straight through:
710
+ * everything handed to _send() during the same synchronous burst (e.g.
711
+ * many concurrent query() calls fired via Promise.all over one
712
+ * connection) is flushed together on the next tick, corked, so Node
713
+ * turns the whole batch into a single underlying writev rather than one
714
+ * socket write per call. Measured against postgres.js, which coalesces
715
+ * the same way at the application level (its connection.js write()/
716
+ * nextWrite() pair, flushing on a 1KB threshold or setImmediate): 50
717
+ * concurrent prepared executes cost it 3 socket writes where this cost
718
+ * us 50, and that write path was the single largest non-idle item in a
719
+ * CPU profile of the scenario.
720
+ *
721
+ * nextTick rather than setImmediate, deliberately: it still batches a
722
+ * whole synchronous burst, but a lone query doesn't have to wait for the
723
+ * event loop's check phase before its bytes go out - which would trade
724
+ * away the sequential/single-query scenarios to win the concurrent ones.
725
+ *
726
+ * Returns whether the socket could accept the data at all; the write
727
+ * itself is reported through `cb` (and a socket that dies before the
728
+ * flush rejects every pending capture via _failPendingCaptures()).
729
+ */
363
730
  _send(data, cb) {
364
- if (this._socket && this._socket.writable) {
365
- this._socket.write(data, cb);
731
+ if (!this._socket || !this._socket.writable)
732
+ return false;
733
+ if (Array.isArray(data)) {
734
+ const l = data.length;
735
+ for (let i = 0; i < l; i++)
736
+ this._pendingWrites.push({
737
+ data: data[i],
738
+ cb: i === l - 1 ? cb : undefined,
739
+ });
740
+ }
741
+ else
742
+ this._pendingWrites.push({ data, cb });
743
+ if (this._captureQueue.length > 1) {
744
+ if (!this._flushScheduled) {
745
+ this._flushScheduled = true;
746
+ process.nextTick(this._flushPendingWrites);
747
+ }
748
+ }
749
+ else
750
+ this._flushPendingWrites();
751
+ return true;
752
+ }
753
+ /**
754
+ * Writes COPY bytes straight through instead of queueing them for the
755
+ * next tick like _send() does. Two reasons: during a copy the capture
756
+ * queue holds exactly one entry (the Query that opened it), so _send()'s
757
+ * "batch only when more than one request is in flight" test would never
758
+ * fire and every chunk would be written on its own anyway; and the
759
+ * caller needs write()'s own return value to know when to stop, which a
760
+ * deferred write cannot give it.
761
+ */
762
+ _sendCopy(data, cb) {
763
+ const socket = this._socket;
764
+ if (!socket || !socket.writable)
765
+ return false;
766
+ this._flushPendingWrites();
767
+ if (!Array.isArray(data))
768
+ return socket.write(data, cb);
769
+ socket.cork();
770
+ try {
771
+ const l = data.length;
772
+ let writable = true;
773
+ let i;
774
+ for (i = 0; i < l; i++)
775
+ writable = socket.write(data[i], i === l - 1 ? cb : undefined);
776
+ return writable;
777
+ }
778
+ finally {
779
+ socket.uncork();
366
780
  }
367
781
  }
782
+ /** Arrow property, not a method: used as a bare process.nextTick callback. */
783
+ _flushPendingWrites = () => {
784
+ this._flushScheduled = false;
785
+ const pending = this._pendingWrites;
786
+ if (!pending.length)
787
+ return;
788
+ this._pendingWrites = [];
789
+ const socket = this._socket;
790
+ if (!socket || !socket.writable)
791
+ return;
792
+ socket.cork();
793
+ try {
794
+ const l = pending.length;
795
+ let i;
796
+ for (i = 0; i < l; i++) {
797
+ const { data, cb } = pending[i];
798
+ socket.write(data, cb);
799
+ }
800
+ }
801
+ finally {
802
+ socket.uncork();
803
+ }
804
+ };
368
805
  }