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 +49 -0
- package/connection/connection.js +7 -2
- package/connection/intl-connection.d.ts +1 -0
- package/connection/intl-connection.js +4 -3
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/data-types/date-type.js +8 -1
- package/package.json +1 -1
- package/protocol/frontend.d.ts +14 -1
- package/protocol/frontend.js +39 -5
- package/protocol/pg-socket.d.ts +27 -3
- package/protocol/pg-socket.js +132 -34
- package/protocol/smart-buffer.d.ts +5 -4
- package/protocol/smart-buffer.js +21 -1
- package/protocol/workerd-socket.d.ts +87 -0
- package/protocol/workerd-socket.js +245 -0
- package/util/resolved-bind-type.d.ts +15 -0
- package/util/resolved-bind-type.js +85 -0
- package/util/runtime.d.ts +6 -0
- package/util/runtime.js +19 -0
- package/util/stringify-arrayliteral.js +9 -2
- package/util/unspecified-param.d.ts +14 -0
- package/util/unspecified-param.js +40 -0
- package/util/unspecified-value.d.ts +26 -0
- package/util/unspecified-value.js +63 -0
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
|
|
package/connection/connection.js
CHANGED
|
@@ -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
|
-
:
|
|
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.
|
|
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.
|
|
5
|
+
export const version = '3.14.0';
|
|
6
6
|
export { DataFormat };
|
|
7
7
|
/**
|
|
8
8
|
* What columns come back in unless something asks otherwise.
|
package/data-types/date-type.js
CHANGED
|
@@ -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
|
-
|
|
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
package/protocol/frontend.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/protocol/frontend.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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
|
}
|
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -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:
|
|
91
|
+
protected _negotiateTls(socket: ClientSocket, target: {
|
|
69
92
|
host: string;
|
|
70
93
|
port?: number;
|
|
71
|
-
}, onReady: (socket:
|
|
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 {};
|
package/protocol/pg-socket.js
CHANGED
|
@@ -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
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
255
|
-
|
|
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
|
|
338
|
+
const before = [];
|
|
264
339
|
if (args.before)
|
|
265
|
-
this._pushUtilityStatement(
|
|
266
|
-
|
|
340
|
+
this._pushUtilityStatement(before, args.before, args.bind);
|
|
341
|
+
const after = [];
|
|
267
342
|
if (args.after)
|
|
268
|
-
this._pushUtilityStatement(
|
|
269
|
-
|
|
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
|
|
341
|
-
|
|
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
|
|
431
|
+
const before = [];
|
|
348
432
|
if (args.before)
|
|
349
|
-
this._pushUtilityStatement(
|
|
350
|
-
|
|
433
|
+
this._pushUtilityStatement(before, args.before, args.bind);
|
|
434
|
+
const after = [];
|
|
351
435
|
if (args.after)
|
|
352
|
-
this._pushUtilityStatement(
|
|
353
|
-
|
|
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(
|
|
816
|
-
? 'SASL: channelBinding is "require" but the
|
|
817
|
-
:
|
|
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
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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;
|