postgrejs 3.12.2 โ†’ 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -98,6 +98,32 @@ Each adapter hands the compiled SQL straight to this client, with no `pg` left a
98
98
  compared query by query against the driver it replaces - and, where the project ships a suite of its own, run
99
99
  against that too. So adopting one is a change to how the connection is created and nothing else.
100
100
 
101
+ ### ๐ŸŒ Runs Where You Run
102
+
103
+ <!-- Same card shape as the adapters above, for the same reason: GitHub strips CSS, and a table
104
+ is the one layout that survives everywhere the README is read. -->
105
+ <table>
106
+ <tr>
107
+ <td align="center" width="230" valign="top">
108
+ <a href="https://www.postgrejs.com/docs/getting-started/benchmarks"><img src="https://www.postgrejs.com/img/nodejs-logo.svg" alt="Node.js" height="46"></a><br><br>
109
+ <b><a href="https://www.postgrejs.com/docs/getting-started/benchmarks">Node.js</a></b><br>
110
+ </td>
111
+ <td align="center" width="230" valign="top">
112
+ <a href="https://www.postgrejs.com/docs/getting-started/benchmarks-bun"><img src="https://www.postgrejs.com/img/bun-logo.svg" alt="Bun" height="46"></a><br><br>
113
+ <b><a href="https://www.postgrejs.com/docs/getting-started/benchmarks-bun">Bun</a></b><br>
114
+ </td>
115
+ <td align="center" width="230" valign="top">
116
+ <a href="https://www.postgrejs.com/docs/guides/cloudflare-workers"><img src="https://www.postgrejs.com/img/cloudflare-logo.svg" alt="Cloudflare Workers" height="46"></a><br><br>
117
+ <b><a href="https://www.postgrejs.com/docs/guides/cloudflare-workers">Cloudflare Workers</a></b><br>
118
+ </td>
119
+ </tr>
120
+ </table>
121
+
122
+ Node and Bun run **the same test suite** in CI on every push - Node across 20, 22, 24 and 26 against PostgreSQL 12,
123
+ 16 and 18, Bun against 18 - so neither is a target that merely ought to work. Workers is not in that matrix yet: it
124
+ was measured by hand against PostgreSQL 18, a plain connection and a TLS one, and what the runtime does and does not
125
+ allow is written down in [doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
126
+
101
127
  ## Installation
102
128
 
103
129
  ```bash
@@ -133,6 +159,7 @@ in ยท ๐ŸŸก partial or needs a separate package ยท โŒ not supported.
133
159
  | Packages to install | 1 | 4 <sup>1</sup> | 1 |
134
160
  | Module system | ESM | ESM/CJS | ESM/CJS |
135
161
  | Language | TS | JS <sup>2</sup> | JS <sup>3</sup> |
162
+ | Cloudflare Workers | โœ… <sup>28</sup> | ๐ŸŸก <sup>29</sup> | โœ… <sup>30</sup> |
136
163
  | ***Wire protocol*** | | | |
137
164
  | Protocol version | 3.2 | 3.0 | 3.0 |
138
165
  | Simple Query protocol | โœ… | โœ… | โœ… |
@@ -271,6 +298,25 @@ in ยท ๐ŸŸก partial or needs a separate package ยท โŒ not supported.
271
298
  input parser is what sorts and deduplicates a vector and what defines a query's grammar, so encoding either here
272
299
  would make the same text mean one thing written as a literal and another bound as a parameter. They are sent as
273
300
  text instead, which is exact.
301
+ - <sup>28</sup> PostgreJS runs on workerd under `nodejs_compat` with nothing installed and no build of its own.
302
+ Measured end to end: a plain connection against PostgreSQL 18, and a TLS one against a managed PostgreSQL 18 whose
303
+ certificate comes from Let's Encrypt - the same server refusing the connection outright (`28000 connection is
304
+ insecure`) when `sslmode` is dropped, which is what makes the encrypted one mean something. TLS opens a socket of
305
+ the runtime's own and upgrades it with `startTls()`, since `tls.connect({ socket })` is not something workerd can
306
+ carry out. Three limits are the runtime's, and all three libraries have them: `ca`, `cert` and `rejectUnauthorized`
307
+ have nowhere to go (workerd's TLS options are `{ expectedServerHostname?: string }`, so a self-signed certificate
308
+ cannot be trusted and a local server cannot be reached over TLS at all); `sslnegotiation=direct` cannot announce the
309
+ `postgresql` ALPN protocol, and is refused by name here; and SCRAM channel binding has no server certificate to bind
310
+ to, so `channelBinding: 'require'` is refused and the default `prefer` authenticates without it. Hyperdrive is the
311
+ other way in, terminating TLS in Cloudflare's own network - checked here against its binding. See
312
+ [doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
313
+ - <sup>29</sup> pg reaches workerd through `pg-cloudflare`, a separate package it carries as an optional dependency
314
+ and selects at runtime (`lib/stream.js`, on `navigator.userAgent`). That package wraps `cloudflare:sockets` and
315
+ upgrades with its own `startTls()`, so TLS is available where PostgreJS's is not - at the cost of a second package
316
+ and of nothing happening at all if it was not installed.
317
+ - <sup>30</sup> postgres.js ships a build for the runtime: a `workerd` condition in `exports` pointing at `cf/`, whose
318
+ polyfills shim `net`/`tls` onto `cloudflare:sockets` including `startTls({ servername })`. One package, the right
319
+ code chosen by the resolver, TLS included.
274
320
 
275
321
  ## Benchmarks
276
322
 
@@ -302,6 +348,9 @@ you open an issue please provide version of NodeJS and PostgreSQL server.
302
348
  - Bun - the same test suite runs under Bun in CI against PostgreSQL 18 on every push, so Bun is a supported target
303
349
  rather than an untested coincidence. (Coverage is collected on the Node matrix only: `c8` instruments V8's
304
350
  coverage APIs, which JavaScriptCore doesn't have.)
351
+ - Cloudflare Workers - with `nodejs_compat`, and without TLS: the runtime has no way to trust a server
352
+ certificate, so a database behind TLS is reached through Hyperdrive instead. See
353
+ [doc/CLOUDFLARE-WORKERS.md](doc/CLOUDFLARE-WORKERS.md).
305
354
 
306
355
  ## License
307
356
 
@@ -5,7 +5,7 @@ import { withAbortSignal } from '../util/abort-signal.js';
5
5
  import { normalizeChannelName } from '../util/channel-name.js';
6
6
  import { escapeIdentifier } from '../util/escape-identifier.js';
7
7
  import { QueryRequest } from '../util/sql-tag.js';
8
- import { isUnspecifiedParam } from '../util/unspecified-param.js';
8
+ import { isUnspecifiedParam, needsNumericParam, } from '../util/unspecified-param.js';
9
9
  import { BindParam } from './bind-param.js';
10
10
  import { IntlConnection } from './intl-connection.js';
11
11
  import { LargeObject, LargeObjectMode } from './large-object.js';
@@ -807,7 +807,12 @@ export class Connection extends SafeEventEmitter {
807
807
  // out as is written in getBindMessage().
808
808
  isUnspecifiedParam(prm)
809
809
  ? 0
810
- : typeMap.determine(prm));
810
+ : // A non-integer is declared numeric rather than the float8
811
+ // its value suggests - see `needsNumericParam()` for the
812
+ // cast that makes the difference, and what it costs.
813
+ needsNumericParam(prm)
814
+ ? DataTypeOIDs.numeric
815
+ : typeMap.determine(prm));
811
816
  // A money parameter is written as an int64 of minor units, so the
812
817
  // scale has to be known before the Bind goes out - the probe fired
813
818
  // at connect answers in time for every result, but not necessarily
@@ -24,6 +24,7 @@ import { CopyFromStream, CopyToStream } from './copy-stream.js';
24
24
  interface PreparedCacheEntry {
25
25
  name: string;
26
26
  fields?: Protocol.RowDescription[];
27
+ resolvedParamTypes?: OID[];
27
28
  }
28
29
  /** See IntlConnection._decodeDeferredRows(). */
29
30
  interface DeferredRows {
@@ -15,6 +15,7 @@ import { escapeLiteral } from '../util/escape-literal.js';
15
15
  import { fetchAsStringEqual, fetchAsStringNamesElement, fetchAsStringNamesOid, validateFetchAsString, } from '../util/fetch-as-string.js';
16
16
  import { getParsers } from '../util/get-parsers.js';
17
17
  import { resolveColumnFormats } from '../util/resolve-column-formats.js';
18
+ import { resolvedBindTypes } from '../util/resolved-bind-type.js';
18
19
  import { dataRowBytes, resolveRowDecoder, resolveRowType, } from '../util/row-decoder.js';
19
20
  import { requireTimeZone } from '../util/temporal.js';
20
21
  import { isTransactionCommand, refusesSavepoint, } from '../util/transaction-command.js';
@@ -983,7 +984,7 @@ export class IntlConnection extends SafeEventEmitter {
983
984
  this._preparedCache.delete(key);
984
985
  this._preparedCache.set(key, cached);
985
986
  try {
986
- return await this.executeReused(cached.name, cached.fields, paramTypes, params, options, savepoint);
987
+ return await this.executeReused(cached.name, cached.fields, resolvedBindTypes(paramTypes, params, cached.resolvedParamTypes), params, options, savepoint);
987
988
  }
988
989
  catch (e) {
989
990
  if (!isStaleCachedPlan(e))
@@ -1052,8 +1053,8 @@ export class IntlConnection extends SafeEventEmitter {
1052
1053
  _prepareForCache(key, sql, paramTypes) {
1053
1054
  const name = 'C_' + ++this._preparedCounter;
1054
1055
  return (async () => {
1055
- const { fields } = await this.prepareOnce(sql, paramTypes, name);
1056
- const entry = { name, fields };
1056
+ const { fields, resolvedParamTypes } = await this.prepareOnce(sql, paramTypes, name);
1057
+ const entry = { name, fields, resolvedParamTypes };
1057
1058
  await this._evictPreparedIfFull();
1058
1059
  this._preparedCache.set(key, entry);
1059
1060
  return entry;
package/constants.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Protocol } from './protocol/protocol.js';
2
2
  import DataFormat = Protocol.DataFormat;
3
- export declare const version = "3.12.2";
3
+ export declare const version = "3.14.0";
4
4
  export { DataFormat };
5
5
  /**
6
6
  * What columns come back in unless something asks otherwise.
package/constants.js CHANGED
@@ -2,7 +2,7 @@ import { Protocol } from './protocol/protocol.js';
2
2
  var DataFormat = Protocol.DataFormat;
3
3
  // @ts-ignore
4
4
  Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
5
- export const version = '3.12.2';
5
+ export const version = '3.14.0';
6
6
  export { DataFormat };
7
7
  /**
8
8
  * What columns come back in unless something asks otherwise.
@@ -28,7 +28,14 @@ export const DateType = {
28
28
  ? v.getTime()
29
29
  : v.getTime() - v.getTimezoneOffset() * 60 * 1000;
30
30
  n = (n - timeShift) / 1000 / 86400;
31
- const t = Math.trunc(n + Number.EPSILON);
31
+ /* Floor, not trunc: a date before 2000-01-01 makes `n` negative, and
32
+ truncation there rounds *towards* the epoch - every pre-2000
33
+ instant with a time of day was stored one day late, silently.
34
+ `1999-12-31T22:00Z` under TZ=UTC went in as 2000-01-01 where the
35
+ text path, and `pg`, both say 1999-12-31. The epsilon stays: it is
36
+ what keeps an exact midnight from landing a hair under its own
37
+ day and flooring to the one before. */
38
+ const t = Math.floor(n + Number.EPSILON);
32
39
  buf.writeInt32BE(t);
33
40
  },
34
41
  decodeBinary(v, offset = 0, _len, options) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.12.2",
4
+ "version": "3.14.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -101,7 +101,20 @@ export declare class Frontend {
101
101
  getSASLMessage(sasl: SASL.Session): Buffer;
102
102
  getSASLFinalMessage(session: SASL.Session): Buffer;
103
103
  getParseMessage(args: Frontend.ParseMessageArgs): Buffer;
104
- getBindMessage(args: Frontend.BindMessageArgs): Buffer;
104
+ /**
105
+ * Bind, carrying the parameter values - the one message in a statement
106
+ * whose size is the caller's data rather than a few dozen bytes.
107
+ *
108
+ * @param args The portal, statement, parameters and result formats.
109
+ * @param copy When false, the returned buffer is a view into the
110
+ * shared send buffer rather than a copy of it. It stays valid only
111
+ * until the next message is written, so a caller passing false must
112
+ * consume it - `Buffer.concat()`, a write - before building anything
113
+ * else, and must never hand it to the socket, which does not copy
114
+ * synchronously.
115
+ * @returns The Bind message.
116
+ */
117
+ getBindMessage(args: Frontend.BindMessageArgs, copy?: boolean): Buffer;
105
118
  getDescribeMessage(args: Frontend.DescribeMessageArgs): Buffer;
106
119
  getExecuteMessage(args: Frontend.ExecuteMessageArgs): Buffer;
107
120
  getCloseMessage(args: Frontend.CloseMessageArgs): Buffer;
@@ -4,6 +4,7 @@ import { arrayLeaf } from '../util/array-leaf.js';
4
4
  import { encodeBinaryArray } from '../util/encode-binaryarray.js';
5
5
  import { formatDateParam } from '../util/format-datetime.js';
6
6
  import { writeArrayLiteral } from '../util/stringify-arrayliteral.js';
7
+ import { resolveToPostgres, unspecifiedText, } from '../util/unspecified-value.js';
7
8
  import { Protocol } from './protocol.js';
8
9
  import { SmartBuffer } from './smart-buffer.js';
9
10
  const DataFormat = Protocol.DataFormat;
@@ -131,7 +132,28 @@ export class Frontend {
131
132
  }
132
133
  return setLengthAndFlush(io, 1);
133
134
  }
134
- getBindMessage(args) {
135
+ /**
136
+ * Bind, carrying the parameter values - the one message in a statement
137
+ * whose size is the caller's data rather than a few dozen bytes.
138
+ *
139
+ * @param args The portal, statement, parameters and result formats.
140
+ * @param copy When false, the returned buffer is a view into the
141
+ * shared send buffer rather than a copy of it. It stays valid only
142
+ * until the next message is written, so a caller passing false must
143
+ * consume it - `Buffer.concat()`, a write - before building anything
144
+ * else, and must never hand it to the socket, which does not copy
145
+ * synchronously.
146
+ * @returns The Bind message.
147
+ */
148
+ /* The copy exists for buffer reuse, not for the socket: every message
149
+ is built in this one SmartBuffer, so message N+1's start() would
150
+ overwrite N's bytes before they were assembled. Nothing flushed here
151
+ ever reaches socket.write() - only the concat of them does, which is
152
+ checked in a test. So a caller that builds the small messages first
153
+ and this one last can take a view and let the concat read it, which
154
+ is one copy of the payload rather than two: measured on a 100 000
155
+ element int4[], 2148 KB of copying a call against 1074. */
156
+ getBindMessage(args, copy = true) {
135
157
  if (args.portal && args.portal.length > 63)
136
158
  throw new Error('Portal name length must be lower than 63');
137
159
  if (args.statement && args.statement.length > 63)
@@ -166,6 +188,18 @@ export class Frontend {
166
188
  }
167
189
  const dataTypeOid = paramTypes ? paramTypes[i] : undefined;
168
190
  const dt = dataTypeOid ? args.typeMap.get(dataTypeOid) : undefined;
191
+ // No declared type: the value writes itself if it knows how, and
192
+ // what it hands back decides which branch below takes it - a
193
+ // toPostgres() returning a Date belongs in the Date branch, not
194
+ // in `'' + v`. A declared type is left alone; it owns its own
195
+ // encoding.
196
+ if (!dt) {
197
+ v = resolveToPostgres(v);
198
+ if (v === null || v === undefined) {
199
+ io.writeInt32BE(-1);
200
+ continue;
201
+ }
202
+ }
169
203
  if (dt) {
170
204
  if (typeof dt.encodeAsNull === 'function' &&
171
205
  dt.encodeAsNull(v, queryOptions)) {
@@ -220,7 +254,7 @@ export class Frontend {
220
254
  io.buffer.writeInt32BE(io.size - dataOffset, dataOffset - 4); // Update length
221
255
  }
222
256
  else {
223
- io.writeLString('' + v, 'utf8');
257
+ io.writeLString(unspecifiedText(v), 'utf8');
224
258
  }
225
259
  }
226
260
  }
@@ -242,7 +276,7 @@ export class Frontend {
242
276
  }
243
277
  else
244
278
  io.writeUInt16BE(0);
245
- return setLengthAndFlush(io, 1);
279
+ return setLengthAndFlush(io, 1, copy);
246
280
  }
247
281
  getDescribeMessage(args) {
248
282
  if (args.name && args.name.length > 63) {
@@ -348,7 +382,7 @@ export class Frontend {
348
382
  return StaticSyncBuffer;
349
383
  }
350
384
  }
351
- function setLengthAndFlush(io, lengthOffset) {
385
+ function setLengthAndFlush(io, lengthOffset, copy = true) {
352
386
  io.buffer.writeUInt32BE(io.size - lengthOffset, lengthOffset);
353
- return io.flush();
387
+ return io.flush(copy);
354
388
  }
@@ -1,4 +1,3 @@
1
- import net from 'node:net';
2
1
  import tls from 'node:tls';
3
2
  import { ConnectionState } from '../constants.js';
4
3
  import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
@@ -20,6 +19,29 @@ export interface SocketError extends Error {
20
19
  * is asked for, and a queue that pairs each response with whoever sent
21
20
  * the statement that caused it.
22
21
  */
22
+ /**
23
+ * What this client needs a socket to be, whoever provides one.
24
+ *
25
+ * `net.Socket`, its TLS upgrade, and `WorkerdSocket` all satisfy it.
26
+ */
27
+ interface ClientSocket {
28
+ readonly destroyed: boolean;
29
+ readonly writable: boolean;
30
+ write(data: any, cb?: (err?: Error | null) => void): boolean;
31
+ destroy(err?: Error): any;
32
+ end(...args: any[]): any;
33
+ pause(): any;
34
+ resume(): any;
35
+ cork(): void;
36
+ uncork(): void;
37
+ setNoDelay(enable?: boolean): any;
38
+ setKeepAlive(enable?: boolean): any;
39
+ setTimeout(ms: number, cb?: () => void): any;
40
+ connect(...args: any[]): any;
41
+ on(event: string, listener: (...args: any[]) => void): any;
42
+ once(event: string, listener: (...args: any[]) => void): any;
43
+ removeAllListeners(event?: string): any;
44
+ }
23
45
  export declare class PgSocket extends SafeEventEmitter {
24
46
  options: ConnectionConfiguration;
25
47
  private _state;
@@ -29,6 +51,7 @@ export declare class PgSocket extends SafeEventEmitter {
29
51
  * bare. Cleared by _reset().
30
52
  */
31
53
  private _closeReason?;
54
+ /** Whichever kind of socket this runtime connects with. */
32
55
  private _socket?;
33
56
  private _backend;
34
57
  private _frontend;
@@ -65,10 +88,10 @@ export declare class PgSocket extends SafeEventEmitter {
65
88
  * afterwards. Resolves `onReady` with whichever socket ends up ready to
66
89
  * speak the PostgreSQL protocol: the plain one, or its TLS upgrade.
67
90
  */
68
- protected _negotiateTls(socket: net.Socket, target: {
91
+ protected _negotiateTls(socket: ClientSocket, target: {
69
92
  host: string;
70
93
  port?: number;
71
- }, onReady: (socket: net.Socket | tls.TLSSocket) => void, onError: (err: Error) => void): void;
94
+ }, onReady: (socket: ClientSocket) => void, onError: (err: Error) => void): void;
72
95
  /**
73
96
  * Asks the server to cancel whatever this session is currently running.
74
97
  *
@@ -337,3 +360,4 @@ export declare class PgSocket extends SafeEventEmitter {
337
360
  */
338
361
  protected _sendCopy(data: Buffer | Buffer[], cb?: Callback): boolean;
339
362
  }
363
+ export {};
@@ -6,6 +6,7 @@ import DoublyLinked from 'doublylinked';
6
6
  import promisify from 'putil-promisify';
7
7
  import { ConnectionState } from '../constants.js';
8
8
  import { SafeEventEmitter } from '../safe-event-emitter.js';
9
+ import { isWorkerd } from '../util/runtime.js';
9
10
  import { Backend } from './backend.js';
10
11
  import { signatureHashOfCertificate } from './cert-signature.js';
11
12
  import { ConnectionLostError } from './connection-lost-error.js';
@@ -13,13 +14,20 @@ import { DatabaseError } from './database-error.js';
13
14
  import { Frontend } from './frontend.js';
14
15
  import { Protocol } from './protocol.js';
15
16
  import { SASL } from './sasl.js';
17
+ import { WorkerdSocket } from './workerd-socket.js';
16
18
  const DEFAULT_PORT_NUMBER = 5432;
17
19
  const COMMAND_RESULT_PATTERN = /^([^\d]+)(?: (\d+)(?: (\d+))?)?$/;
18
20
  /**
19
- * The connected socket: the handshake, the authentication, TLS where it
20
- * is asked for, and a queue that pairs each response with whoever sent
21
- * the statement that caused it.
21
+ * Whether a connection is going to ask for TLS at all.
22
+ *
23
+ * @param options The connection's own options.
24
+ * @returns True when any of the three ways of asking is set.
22
25
  */
26
+ /* Read in two places that have to agree: which socket to open, and
27
+ whether to negotiate once it is open. */
28
+ function wantsTls(options) {
29
+ return (!!options.ssl || !!options.requireSSL || options.sslNegotiation === 'direct');
30
+ }
23
31
  export class PgSocket extends SafeEventEmitter {
24
32
  options;
25
33
  _state = ConnectionState.CLOSED;
@@ -29,6 +37,7 @@ export class PgSocket extends SafeEventEmitter {
29
37
  * bare. Cleared by _reset().
30
38
  */
31
39
  _closeReason;
40
+ /** Whichever kind of socket this runtime connects with. */
32
41
  _socket;
33
42
  _backend = new Backend();
34
43
  _frontend;
@@ -86,7 +95,17 @@ export class PgSocket extends SafeEventEmitter {
86
95
  this._sessionAttrsChecked = false;
87
96
  this._state = ConnectionState.CONNECTING;
88
97
  const target = this._hosts[this._hostIndex];
89
- const socket = (this._socket = new net.Socket());
98
+ /* Workerd cannot upgrade a `net.Socket`, and the socket that can be
99
+ upgraded has to be opened as one from the start - `secureTransport:
100
+ 'starttls'` is a connect-time choice, not a later one. So the kind
101
+ of socket is decided here, by whether TLS is going to be asked for
102
+ at all. The plain path stays on `node:net`, which works there. */
103
+ const socket = (this._socket =
104
+ isWorkerd() &&
105
+ wantsTls(this.options) &&
106
+ this.options.sslNegotiation !== 'direct'
107
+ ? new WorkerdSocket(true)
108
+ : new net.Socket());
90
109
  const errorHandler = (err) => {
91
110
  this._state = ConnectionState.CLOSED;
92
111
  this._removeListeners();
@@ -133,7 +152,14 @@ export class PgSocket extends SafeEventEmitter {
133
152
  _negotiateTls(socket, target, onReady, onError) {
134
153
  const options = this.options;
135
154
  const startTls = () => {
136
- const tlsOptions = { ...options.ssl, socket };
155
+ /* Only ever reached off workerd - the branch below hands that
156
+ runtime to its own upgrade - so the socket here is the
157
+ `net.Socket` `tls.connect()` wants, which the structural type
158
+ cannot say on its own. */
159
+ const tlsOptions = {
160
+ ...options.ssl,
161
+ socket: socket,
162
+ };
137
163
  if (target.host && net.isIP(target.host) === 0)
138
164
  tlsOptions.servername = target.host;
139
165
  if (options.sslNegotiation === 'direct')
@@ -142,11 +168,51 @@ export class PgSocket extends SafeEventEmitter {
142
168
  tlsSocket.once('error', onError);
143
169
  tlsSocket.once('secureConnect', () => onReady(tlsSocket));
144
170
  };
145
- const wantsSSL = !!options.ssl ||
146
- !!options.requireSSL ||
147
- options.sslNegotiation === 'direct';
148
- if (!wantsSSL)
171
+ if (!wantsTls(options))
149
172
  return onReady(socket);
173
+ /* Workerd upgrades through the socket it already has rather than by
174
+ wrapping it: `tls.connect({ socket })` does nothing it can carry
175
+ out, and measured, attempting it answers "Network connection
176
+ lost." after the server has already agreed to TLS. The exchange
177
+ either side of the upgrade is the same one as below - this branch
178
+ exists because the call in the middle is a different call.
179
+
180
+ One limit is the runtime's and no client escapes it: workerd's TLS
181
+ options are `{ expectedServerHostname?: string }` and nothing
182
+ else, so nothing here can say which certificate to trust, and a
183
+ server whose certificate does not chain to a public CA cannot be
184
+ reached over TLS at all - which is every local PostgreSQL with a
185
+ self-signed certificate. */
186
+ /* Direct negotiation is TLS from the first byte, and PostgreSQL 17
187
+ only accepts it when the client announces the `postgresql`
188
+ protocol over ALPN. Workerd's socket options are
189
+ `{ secureTransport, allowHalfOpen }` - there is nowhere to say it,
190
+ and the server answers by closing, which surfaces as an internal
191
+ error with a reference number. Refused by name instead. */
192
+ if (isWorkerd() && options.sslNegotiation === 'direct')
193
+ return onError(new Error('sslNegotiation "direct" is not available on Cloudflare Workers: it needs the ' +
194
+ '"postgresql" ALPN protocol, which the runtime offers no way to announce. ' +
195
+ 'Leave sslNegotiation unset to negotiate with SSLRequest instead.'));
196
+ if (socket instanceof WorkerdSocket) {
197
+ const upgrade = () => {
198
+ socket.once('error', onError);
199
+ socket.once('secureConnect', () => onReady(socket));
200
+ socket.startTls(target.host && net.isIP(target.host) === 0 ? target.host : undefined);
201
+ };
202
+ socket.write(this._frontend.getSSLRequestMessage());
203
+ socket.readOnce((x) => {
204
+ const command = x?.toString();
205
+ if (command === 'S')
206
+ return upgrade();
207
+ if (command === 'N') {
208
+ if (options.requireSSL)
209
+ return onError(new Error('Server does not support SSL connections'));
210
+ return onReady(socket);
211
+ }
212
+ onError(new Error('Unexpected response from server: ' + command));
213
+ });
214
+ return;
215
+ }
150
216
  if (options.sslNegotiation === 'direct') {
151
217
  // Straight into the handshake: no SSLRequest, nothing in the clear.
152
218
  return startTls();
@@ -250,23 +316,41 @@ export class PgSocket extends SafeEventEmitter {
250
316
  * statement/portal at the next Parse/Bind referencing the unnamed name.
251
317
  */
252
318
  sendExtendedQueryMessages(args, cb) {
319
+ /* The Bind is built LAST and taken as a view, then concatenated here
320
+ rather than in _send(): it carries the caller's data, where the
321
+ other four messages are a few dozen bytes, and the copy flush()
322
+ makes exists only because this one SmartBuffer is reused for the
323
+ next message. Building the small ones first leaves nothing to
324
+ overwrite it before Buffer.concat() reads it, so the payload is
325
+ copied once instead of twice - measured on a 100 000-element
326
+ int4[], 2148 KB of copying a call against 1074, with the wire bytes
327
+ and the stored value unchanged. The view never reaches the socket;
328
+ only the concat does. */
329
+ const parse = this._frontend.getParseMessage(args.parse);
330
+ const describe = this._frontend.getDescribeMessage(args.describe);
331
+ const execute = this._frontend.getExecuteMessage(args.execute);
332
+ const sync = this._frontend.getSyncMessage();
253
333
  if (!args.before && !args.after) {
254
- const data = [
255
- this._frontend.getParseMessage(args.parse),
256
- this._frontend.getBindMessage(args.bind),
257
- this._frontend.getDescribeMessage(args.describe),
258
- this._frontend.getExecuteMessage(args.execute),
259
- this._frontend.getSyncMessage(),
260
- ];
334
+ const bind = this._frontend.getBindMessage(args.bind, false);
335
+ const data = Buffer.concat([parse, bind, describe, execute, sync]);
261
336
  return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
262
337
  }
263
- const data = [];
338
+ const before = [];
264
339
  if (args.before)
265
- this._pushUtilityStatement(data, args.before, args.bind);
266
- data.push(this._frontend.getParseMessage(args.parse), this._frontend.getBindMessage(args.bind), this._frontend.getDescribeMessage(args.describe), this._frontend.getExecuteMessage(args.execute));
340
+ this._pushUtilityStatement(before, args.before, args.bind);
341
+ const after = [];
267
342
  if (args.after)
268
- this._pushUtilityStatement(data, args.after, args.bind);
269
- data.push(this._frontend.getSyncMessage());
343
+ this._pushUtilityStatement(after, args.after, args.bind);
344
+ const bind = this._frontend.getBindMessage(args.bind, false);
345
+ const data = Buffer.concat([
346
+ ...before,
347
+ parse,
348
+ bind,
349
+ describe,
350
+ execute,
351
+ ...after,
352
+ sync,
353
+ ]);
270
354
  return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
271
355
  }
272
356
  /**
@@ -336,21 +420,22 @@ export class PgSocket extends SafeEventEmitter {
336
420
  * auto-clears an unnamed portal at the next Bind naming it.
337
421
  */
338
422
  sendBindExecuteMessages(args, cb) {
423
+ // See sendExtendedQueryMessages() for why the Bind is built last.
424
+ const execute = this._frontend.getExecuteMessage(args.execute);
425
+ const sync = this._frontend.getSyncMessage();
339
426
  if (!args.before && !args.after) {
340
- const data = [
341
- this._frontend.getBindMessage(args.bind),
342
- this._frontend.getExecuteMessage(args.execute),
343
- this._frontend.getSyncMessage(),
344
- ];
427
+ const bind = this._frontend.getBindMessage(args.bind, false);
428
+ const data = Buffer.concat([bind, execute, sync]);
345
429
  return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
346
430
  }
347
- const data = [];
431
+ const before = [];
348
432
  if (args.before)
349
- this._pushUtilityStatement(data, args.before, args.bind);
350
- data.push(this._frontend.getBindMessage(args.bind), this._frontend.getExecuteMessage(args.execute));
433
+ this._pushUtilityStatement(before, args.before, args.bind);
434
+ const after = [];
351
435
  if (args.after)
352
- this._pushUtilityStatement(data, args.after, args.bind);
353
- data.push(this._frontend.getSyncMessage());
436
+ this._pushUtilityStatement(after, args.after, args.bind);
437
+ const bind = this._frontend.getBindMessage(args.bind, false);
438
+ const data = Buffer.concat([...before, bind, execute, ...after, sync]);
354
439
  return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
355
440
  }
356
441
  /**
@@ -806,15 +891,28 @@ export class PgSocket extends SafeEventEmitter {
806
891
  break;
807
892
  case Protocol.AuthenticationMessageKind.SASL: {
808
893
  const mode = this.options.channelBinding || 'prefer';
894
+ /* Encrypted and "can be bound to" are two questions on workerd,
895
+ where they are one on Node. Binding mixes a hash of the
896
+ server's certificate into the SCRAM proof, and that runtime
897
+ hands out no certificate - so a connection can be perfectly
898
+ secure there and still have nothing to bind to. Saying "not
899
+ using TLS" for it, which is what the one test used to answer,
900
+ sends the reader looking in the wrong place. */
901
+ const secure = this._socket instanceof tls.TLSSocket ||
902
+ (this._socket instanceof WorkerdSocket && this._socket.secure);
809
903
  const tlsSocket = mode !== 'disable' && this._socket instanceof tls.TLSSocket
810
904
  ? this._socket
811
905
  : undefined;
812
906
  const offersBinding = !!msg.mechanisms?.includes('SCRAM-SHA-256-PLUS');
813
907
  const useBinding = !!tlsSocket && offersBinding;
814
908
  if (mode === 'require' && !useBinding) {
815
- throw new Error(tlsSocket
816
- ? 'SASL: channelBinding is "require" but the server does not offer SCRAM-SHA-256-PLUS'
817
- : 'SASL: channelBinding is "require" but the connection is not using TLS');
909
+ throw new Error(!secure
910
+ ? 'SASL: channelBinding is "require" but the connection is not using TLS'
911
+ : !tlsSocket
912
+ ? 'SASL: channelBinding is "require", but this runtime does not expose the ' +
913
+ 'server certificate, so there is nothing to bind to. Cloudflare Workers is ' +
914
+ 'one; use "prefer", which is the default.'
915
+ : 'SASL: channelBinding is "require" but the server does not offer SCRAM-SHA-256-PLUS');
818
916
  }
819
917
  if (!useBinding && !msg.mechanisms?.includes('SCRAM-SHA-256')) {
820
918
  throw new Error('SASL: Only mechanisms SCRAM-SHA-256 and SCRAM-SHA-256-PLUS are supported');
@@ -9,11 +9,12 @@ export interface SmartBufferConfig {
9
9
  houseKeepMs?: number;
10
10
  }
11
11
  /**
12
- * Adds the PostgreSQL wire format's two string encodings on top of
13
- * flexy-buffer's `FlexyBuffer`: a NUL-terminated C string, and a
14
- * length-prefixed string where a `null`/`undefined` value is written as a
15
- * length of -1 with no bytes following.
12
+ * The most a buffer may ever hold, given how much memory the machine has.
13
+ *
14
+ * @param totalMemory Bytes of memory, or 0 when that cannot be known.
15
+ * @returns Half of it, capped - or the cap alone when it is unknown.
16
16
  */
17
+ export declare function maxSizeFor(totalMemory: number): number;
17
18
  export declare class SmartBuffer extends FlexyBuffer {
18
19
  static DEFAULT_PAGE_SIZE: number;
19
20
  static DEFAULT_MAX_SIZE: number;