postgrejs 3.0.3 β 3.1.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/CHANGELOG.md +14 -1
- package/README.md +11 -1
- package/connection/connection.d.ts +31 -2
- package/connection/connection.js +29 -0
- package/connection/intl-connection.d.ts +17 -1
- package/connection/intl-connection.js +51 -0
- package/index.d.ts +2 -0
- package/index.js +2 -0
- package/interfaces/database-connection-params.d.ts +12 -0
- package/interfaces/function-call-options.d.ts +14 -0
- package/interfaces/function-call-options.js +1 -0
- package/interfaces/function-call-result.d.ts +7 -0
- package/interfaces/function-call-result.js +1 -0
- package/package.json +1 -1
- package/protocol/backend.js +20 -13
- package/protocol/frontend.d.ts +27 -2
- package/protocol/frontend.js +30 -4
- package/protocol/pg-socket.d.ts +13 -1
- package/protocol/pg-socket.js +36 -1
- package/protocol/protocol.d.ts +27 -4
- package/protocol/protocol.js +10 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.0
|
|
3
|
+
### [v3.1.0](https://github.com/panates/postgrejs/compare/v3.0.4...v3.1.0) -
|
|
4
|
+
|
|
5
|
+
#### π New Features
|
|
6
|
+
|
|
7
|
+
- feat: implement the legacy Function Call sub-protocol ('F'/'V') @Eray HanoΔlu
|
|
8
|
+
- feat: support protocol 3.2's longer cancellation key (PostgreSQL 18+) @Eray HanoΔlu
|
|
9
|
+
|
|
10
|
+
#### πͺ² Fixes
|
|
11
|
+
|
|
12
|
+
- fix: correctly parse NegotiateProtocolVersion and expose it on Connection @Eray HanoΔlu
|
|
13
|
+
|
|
14
|
+
### [v3.0.4](https://github.com/panates/postgrejs/compare/v3.0.3...v3.0.4) - 9 September 2026
|
|
15
|
+
|
|
16
|
+
### [v3.0.3](https://github.com/panates/postgrejs/compare/v3.0.2...v3.0.3) - 8 September 2026
|
|
4
17
|
|
|
5
18
|
### [v3.0.2](https://github.com/panates/postgrejs/compare/v3.0.1...v3.0.2) - 7 September 2026
|
|
6
19
|
|
package/README.md
CHANGED
|
@@ -100,12 +100,18 @@ usage.
|
|
|
100
100
|
- **Resource Management:** Auto disposal of resources with the "using" syntax
|
|
101
101
|
([TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)), ensuring
|
|
102
102
|
efficient resource cleanup.
|
|
103
|
+
- **Long Cancel Key:** Opt in to protocol 3.2 (PostgreSQL 18+) with `longCancelKey`, so a `cancel()` in progress can't
|
|
104
|
+
be forged by an attacker guessing a short secret key.
|
|
105
|
+
- **Legacy Function Call Protocol:** `callFunction()` calls a function by OID directly over the wire, bypassing SQL
|
|
106
|
+
entirely - kept for protocol completeness even though `SELECT func(...)` covers the same ground.
|
|
107
|
+
- **Graceful Protocol Renegotiation:** A server that doesn't recognize a requested protocol version or startup option
|
|
108
|
+
reports back instead of erroring out, surfaced on `Connection.protocolNegotiation`.
|
|
103
109
|
|
|
104
110
|
## Feature Comparison
|
|
105
111
|
|
|
106
112
|
How PostgreJS compares to [`pg`](https://github.com/brianc/node-postgres) (node-postgres) and
|
|
107
113
|
[`postgres`](https://github.com/porsager/postgres) (postgres.js). Every row was checked against the libraries' own
|
|
108
|
-
source rather than their documentation β versions compared: **PostgreJS
|
|
114
|
+
source rather than their documentation β versions compared: **PostgreJS 3.1.0, pg 8.23.0, postgres.js 3.4.9**. β
built
|
|
109
115
|
in Β· π‘ partial or needs a separate package Β· β not supported.
|
|
110
116
|
|
|
111
117
|
| Feature | PostgreJS | pg | postgres.js |
|
|
@@ -115,11 +121,15 @@ in Β· π‘ partial or needs a separate package Β· β not supported.
|
|
|
115
121
|
| Module system | ESM | ESM/CJS | ESM/CJS |
|
|
116
122
|
| Language | TS | JS <sup>2</sup> | JS <sup>3</sup> |
|
|
117
123
|
| ***Wire protocol*** | | | |
|
|
124
|
+
| Protocol version | 3.2 | 3.0 | 3.0 |
|
|
118
125
|
| Simple Query protocol | β
| β
| β
|
|
|
119
126
|
| Extended Query protocol | β
| β
| β
|
|
|
120
127
|
| Text wire format | β
| β
| β
|
|
|
121
128
|
| Binary wire format | β
| π‘ <sup>4</sup> | β <sup>5</sup> |
|
|
122
129
|
| Per-column format selection | β
| β | β |
|
|
130
|
+
| Long cancel key (opt-in) | β
| β | β |
|
|
131
|
+
| Legacy Function Call protocol | β
| β | β |
|
|
132
|
+
| Graceful protocol renegotiation | β
| β | β |
|
|
123
133
|
| ***High-level API*** | | | |
|
|
124
134
|
| Object and array row modes | β
| β
| β
|
|
|
125
135
|
| Dynamic SQL helpers | β
`sql` tag | β | β
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ConnectionState } from '../constants.js';
|
|
2
2
|
import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
|
|
3
|
+
import type { FunctionCallOptions } from '../interfaces/function-call-options.js';
|
|
4
|
+
import type { FunctionCallResult } from '../interfaces/function-call-result.js';
|
|
3
5
|
import type { QueryOptions } from '../interfaces/query-options.js';
|
|
4
6
|
import type { QueryResult } from '../interfaces/query-result.js';
|
|
5
7
|
import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.js';
|
|
@@ -8,7 +10,7 @@ import type { StatementPrepareOptions } from '../interfaces/statement-prepare-op
|
|
|
8
10
|
import type { DatabaseError } from '../protocol/database-error.js';
|
|
9
11
|
import type { Protocol } from '../protocol/protocol.js';
|
|
10
12
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
11
|
-
import type { Maybe } from '../types.js';
|
|
13
|
+
import type { Maybe, OID } from '../types.js';
|
|
12
14
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
13
15
|
import type { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
14
16
|
import { IntlConnection } from './intl-connection.js';
|
|
@@ -44,10 +46,20 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
|
|
|
44
46
|
* Returns information parameters for the current session
|
|
45
47
|
*/
|
|
46
48
|
get sessionParameters(): Record<string, string>;
|
|
49
|
+
/**
|
|
50
|
+
* The server's NegotiateProtocolVersion reply, if it sent one during
|
|
51
|
+
* connect() - undefined means the server fully recognized everything
|
|
52
|
+
* this client's startup packet asked for (protocol minor version, any
|
|
53
|
+
* `_pq_.*` options). Present only when the server is older/stricter
|
|
54
|
+
* than what was requested, or didn't recognize one of the options -
|
|
55
|
+
* check it after connect() if a feature gated behind such an option
|
|
56
|
+
* doesn't seem to have taken effect.
|
|
57
|
+
*/
|
|
58
|
+
get protocolNegotiation(): Maybe<Protocol.NegotiateProtocolVersionMessage>;
|
|
47
59
|
/**
|
|
48
60
|
* Returns the secret key of the current session
|
|
49
61
|
*/
|
|
50
|
-
get secretKey(): Maybe<
|
|
62
|
+
get secretKey(): Maybe<Buffer>;
|
|
51
63
|
get runningQueryCount(): number;
|
|
52
64
|
/**
|
|
53
65
|
* Connects to the server
|
|
@@ -68,6 +80,23 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
|
|
|
68
80
|
* @param options {ScriptExecuteOptions} - Execute options
|
|
69
81
|
*/
|
|
70
82
|
execute(sql: string | QueryRequest, options?: ScriptExecuteOptions): Promise<ScriptResult>;
|
|
83
|
+
/**
|
|
84
|
+
* The legacy Function Call sub-protocol - calls a function by OID
|
|
85
|
+
* directly, bypassing SQL entirely. Superseded by `SELECT func(...)`
|
|
86
|
+
* over the Simple/Extended Query protocols (what `execute()`/`query()`
|
|
87
|
+
* use, and what every current PostgreSQL client uses exclusively) -
|
|
88
|
+
* kept only for wire-protocol completeness. Arguments and the result
|
|
89
|
+
* travel as raw wire-format bytes, not JS values: the caller is
|
|
90
|
+
* responsible for encoding/decoding them (see a `DataType`'s own
|
|
91
|
+
* `encodeBinary`/`decodeBinary` for the format a given OID expects).
|
|
92
|
+
*
|
|
93
|
+
* @param functionId - OID of the function to call
|
|
94
|
+
* @param args - Each argument's own already-encoded wire bytes, or
|
|
95
|
+
* `null` for SQL NULL
|
|
96
|
+
*/
|
|
97
|
+
callFunction(functionId: OID, args: Maybe<Buffer>[], options?: FunctionCallOptions & {
|
|
98
|
+
signal?: AbortSignal;
|
|
99
|
+
}): Promise<FunctionCallResult>;
|
|
71
100
|
query(sql: string | QueryRequest, options?: QueryOptions): Promise<QueryResult>;
|
|
72
101
|
/**
|
|
73
102
|
* Runs a `COPY ... TO STDOUT` and returns its output as a stream of the
|
package/connection/connection.js
CHANGED
|
@@ -56,6 +56,18 @@ export class Connection extends SafeEventEmitter {
|
|
|
56
56
|
get sessionParameters() {
|
|
57
57
|
return this._intlCon.sessionParameters;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The server's NegotiateProtocolVersion reply, if it sent one during
|
|
61
|
+
* connect() - undefined means the server fully recognized everything
|
|
62
|
+
* this client's startup packet asked for (protocol minor version, any
|
|
63
|
+
* `_pq_.*` options). Present only when the server is older/stricter
|
|
64
|
+
* than what was requested, or didn't recognize one of the options -
|
|
65
|
+
* check it after connect() if a feature gated behind such an option
|
|
66
|
+
* doesn't seem to have taken effect.
|
|
67
|
+
*/
|
|
68
|
+
get protocolNegotiation() {
|
|
69
|
+
return this._intlCon.protocolNegotiation;
|
|
70
|
+
}
|
|
59
71
|
/**
|
|
60
72
|
* Returns the secret key of the current session
|
|
61
73
|
*/
|
|
@@ -146,6 +158,23 @@ export class Connection extends SafeEventEmitter {
|
|
|
146
158
|
throw this._handleError(e, sql);
|
|
147
159
|
}));
|
|
148
160
|
}
|
|
161
|
+
/**
|
|
162
|
+
* The legacy Function Call sub-protocol - calls a function by OID
|
|
163
|
+
* directly, bypassing SQL entirely. Superseded by `SELECT func(...)`
|
|
164
|
+
* over the Simple/Extended Query protocols (what `execute()`/`query()`
|
|
165
|
+
* use, and what every current PostgreSQL client uses exclusively) -
|
|
166
|
+
* kept only for wire-protocol completeness. Arguments and the result
|
|
167
|
+
* travel as raw wire-format bytes, not JS values: the caller is
|
|
168
|
+
* responsible for encoding/decoding them (see a `DataType`'s own
|
|
169
|
+
* `encodeBinary`/`decodeBinary` for the format a given OID expects).
|
|
170
|
+
*
|
|
171
|
+
* @param functionId - OID of the function to call
|
|
172
|
+
* @param args - Each argument's own already-encoded wire bytes, or
|
|
173
|
+
* `null` for SQL NULL
|
|
174
|
+
*/
|
|
175
|
+
callFunction(functionId, args, options) {
|
|
176
|
+
return withAbortSignal(options?.signal, () => this._intlCon.cancel(), () => this._captureErrorStack(this._intlCon.callFunction(functionId, args, options), this.callFunction));
|
|
177
|
+
}
|
|
149
178
|
async query(sql, options) {
|
|
150
179
|
if (sql instanceof QueryRequest) {
|
|
151
180
|
if (options?.params)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ConnectionState } from '../constants.js';
|
|
2
2
|
import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
|
|
3
|
+
import type { FunctionCallOptions } from '../interfaces/function-call-options.js';
|
|
4
|
+
import type { FunctionCallResult } from '../interfaces/function-call-result.js';
|
|
3
5
|
import type { QueryOptions } from '../interfaces/query-options.js';
|
|
4
6
|
import type { QueryResult } from '../interfaces/query-result.js';
|
|
5
7
|
import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.js';
|
|
@@ -27,11 +29,25 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
27
29
|
get state(): ConnectionState;
|
|
28
30
|
get refCount(): number;
|
|
29
31
|
get processID(): Maybe<number>;
|
|
30
|
-
get secretKey(): Maybe<
|
|
32
|
+
get secretKey(): Maybe<Buffer>;
|
|
33
|
+
/** See `PgSocket.protocolNegotiation`. */
|
|
34
|
+
get protocolNegotiation(): Maybe<Protocol.NegotiateProtocolVersionMessage>;
|
|
31
35
|
get sessionParameters(): Record<string, string>;
|
|
32
36
|
connect(): Promise<void>;
|
|
33
37
|
close(): Promise<void>;
|
|
34
38
|
execute(sql: string, options?: ScriptExecuteOptions, cb?: (event: string, ...args: any[]) => void): Promise<ScriptResult>;
|
|
39
|
+
/**
|
|
40
|
+
* The legacy Function Call sub-protocol: calls a function by OID
|
|
41
|
+
* directly, bypassing SQL entirely. PostgreSQL itself calls this
|
|
42
|
+
* superseded by `SELECT func(...)` over the Simple/Extended Query
|
|
43
|
+
* protocols (what this driver uses everywhere else, and what every
|
|
44
|
+
* other current client uses exclusively) - kept only for wire-protocol
|
|
45
|
+
* completeness. Arguments and the result travel as raw wire-format
|
|
46
|
+
* bytes rather than through this driver's usual typed encode/decode
|
|
47
|
+
* pipeline - the caller is responsible for both (see a `DataType`'s own
|
|
48
|
+
* `encodeBinary`/`decodeBinary` for the format a given OID expects).
|
|
49
|
+
*/
|
|
50
|
+
callFunction(functionId: OID, args: Maybe<Buffer>[], options?: FunctionCallOptions): Promise<FunctionCallResult>;
|
|
35
51
|
/**
|
|
36
52
|
* Starts a transaction, or - if one is already running - marks a nested
|
|
37
53
|
* level of it. Each call increments `_transactionDepth`; only the
|
|
@@ -64,6 +64,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
64
64
|
get secretKey() {
|
|
65
65
|
return this.socket.secretKey;
|
|
66
66
|
}
|
|
67
|
+
/** See `PgSocket.protocolNegotiation`. */
|
|
68
|
+
get protocolNegotiation() {
|
|
69
|
+
return this.socket.protocolNegotiation;
|
|
70
|
+
}
|
|
67
71
|
get sessionParameters() {
|
|
68
72
|
return this.socket.sessionParameters;
|
|
69
73
|
}
|
|
@@ -156,6 +160,53 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
156
160
|
throw e;
|
|
157
161
|
}
|
|
158
162
|
}
|
|
163
|
+
/**
|
|
164
|
+
* The legacy Function Call sub-protocol: calls a function by OID
|
|
165
|
+
* directly, bypassing SQL entirely. PostgreSQL itself calls this
|
|
166
|
+
* superseded by `SELECT func(...)` over the Simple/Extended Query
|
|
167
|
+
* protocols (what this driver uses everywhere else, and what every
|
|
168
|
+
* other current client uses exclusively) - kept only for wire-protocol
|
|
169
|
+
* completeness. Arguments and the result travel as raw wire-format
|
|
170
|
+
* bytes rather than through this driver's usual typed encode/decode
|
|
171
|
+
* pipeline - the caller is responsible for both (see a `DataType`'s own
|
|
172
|
+
* `encodeBinary`/`decodeBinary` for the format a given OID expects).
|
|
173
|
+
*/
|
|
174
|
+
async callFunction(functionId, args, options = {}) {
|
|
175
|
+
this.assertConnected();
|
|
176
|
+
this.ref();
|
|
177
|
+
try {
|
|
178
|
+
let result;
|
|
179
|
+
let error;
|
|
180
|
+
return await this.socket.sendFunctionCallMessage({
|
|
181
|
+
functionId,
|
|
182
|
+
args,
|
|
183
|
+
argFormats: options.argFormats,
|
|
184
|
+
resultFormat: options.resultFormat,
|
|
185
|
+
}, (code, msg, done) => {
|
|
186
|
+
switch (code) {
|
|
187
|
+
case Protocol.BackendMessageCode.ErrorResponse:
|
|
188
|
+
error = msg;
|
|
189
|
+
break;
|
|
190
|
+
case Protocol.BackendMessageCode.FunctionCallResponse:
|
|
191
|
+
result = msg;
|
|
192
|
+
break;
|
|
193
|
+
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
194
|
+
this.transactionStatus = msg.status;
|
|
195
|
+
if (error) {
|
|
196
|
+
done(error);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
done(undefined, result);
|
|
200
|
+
break;
|
|
201
|
+
default:
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
this.unref();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
159
210
|
/**
|
|
160
211
|
* Starts a transaction, or - if one is already running - marks a nested
|
|
161
212
|
* level of it. Each call increments `_transactionDepth`; only the
|
package/index.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export * from './interfaces/data-mapping-options.js';
|
|
|
13
13
|
export * from './interfaces/data-type.js';
|
|
14
14
|
export * from './interfaces/database-connection-params.js';
|
|
15
15
|
export * from './interfaces/field-info.js';
|
|
16
|
+
export * from './interfaces/function-call-options.js';
|
|
17
|
+
export * from './interfaces/function-call-result.js';
|
|
16
18
|
export * from './interfaces/query-options.js';
|
|
17
19
|
export * from './interfaces/query-result.js';
|
|
18
20
|
export * from './interfaces/script-execute-options.js';
|
package/index.js
CHANGED
|
@@ -13,6 +13,8 @@ export * from './interfaces/data-mapping-options.js';
|
|
|
13
13
|
export * from './interfaces/data-type.js';
|
|
14
14
|
export * from './interfaces/database-connection-params.js';
|
|
15
15
|
export * from './interfaces/field-info.js';
|
|
16
|
+
export * from './interfaces/function-call-options.js';
|
|
17
|
+
export * from './interfaces/function-call-result.js';
|
|
16
18
|
export * from './interfaces/query-options.js';
|
|
17
19
|
export * from './interfaces/query-result.js';
|
|
18
20
|
export * from './interfaces/script-execute-options.js';
|
|
@@ -61,6 +61,18 @@ export interface DatabaseConnectionParams {
|
|
|
61
61
|
* usual case with `sslmode=require` or a self-signed certificate.
|
|
62
62
|
*/
|
|
63
63
|
channelBinding?: 'prefer' | 'require' | 'disable';
|
|
64
|
+
/**
|
|
65
|
+
* Requests protocol 3.2 (PostgreSQL 18+) instead of 3.0, so a
|
|
66
|
+
* `cancel()` in progress on this connection can't be forged by an
|
|
67
|
+
* attacker guessing a 4-byte secret key - 3.2 lets the server hand out
|
|
68
|
+
* one up to 256 bytes instead. Off by default: an older server simply
|
|
69
|
+
* reports back that it doesn't support 3.2 (see
|
|
70
|
+
* `Connection.protocolNegotiation`) and the session proceeds at 3.0
|
|
71
|
+
* exactly as if this had never been set, so turning it on is never a
|
|
72
|
+
* connection-breaking choice - just not something every server rewards
|
|
73
|
+
* yet.
|
|
74
|
+
*/
|
|
75
|
+
longCancelKey?: boolean;
|
|
64
76
|
requireSSL?: boolean;
|
|
65
77
|
ssl?: TlsConnectionOptions;
|
|
66
78
|
timezone?: string;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DataFormat } from '../constants.js';
|
|
2
|
+
export interface FunctionCallOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Format of each argument, applied positionally - omit for text (the
|
|
5
|
+
* default) applied to every argument, a single entry to apply it to all
|
|
6
|
+
* of them, or one entry per argument.
|
|
7
|
+
*/
|
|
8
|
+
argFormats?: DataFormat[];
|
|
9
|
+
/**
|
|
10
|
+
* Format the function's return value comes back in.
|
|
11
|
+
* @default DataFormat.text
|
|
12
|
+
*/
|
|
13
|
+
resultFormat?: DataFormat;
|
|
14
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
package/protocol/backend.js
CHANGED
|
@@ -169,10 +169,13 @@ function parseAuthentication(io, code, len) {
|
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
171
|
function parseBackendKeyData(io) {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
172
|
+
const processID = io.readUInt32BE();
|
|
173
|
+
// No length prefix of its own: this BufferReader is scoped to exactly
|
|
174
|
+
// this message's body (see Backend.parse()), so the secret key is
|
|
175
|
+
// simply whatever bytes remain - 4 of them before protocol 3.2, up to
|
|
176
|
+
// 256 with it (see VERSION_MINOR_LONG_CANCEL_KEY).
|
|
177
|
+
const secretKey = io.readBuffer();
|
|
178
|
+
return { processID, secretKey };
|
|
176
179
|
}
|
|
177
180
|
function parseCommandComplete(io) {
|
|
178
181
|
return {
|
|
@@ -226,17 +229,21 @@ function parseNotificationResponse(io) {
|
|
|
226
229
|
payload: io.readCString(),
|
|
227
230
|
};
|
|
228
231
|
}
|
|
229
|
-
function parseFunctionCallResponse(io
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
232
|
+
function parseFunctionCallResponse(io) {
|
|
233
|
+
// A length prefix ahead of the value itself (-1 for SQL NULL, with no
|
|
234
|
+
// bytes following) - not deducible from the outer message length alone,
|
|
235
|
+
// unlike most fixed-shape messages this codebase parses.
|
|
236
|
+
const len = io.readInt32BE();
|
|
237
|
+
return { result: len < 0 ? null : io.readBuffer(len) };
|
|
233
238
|
}
|
|
234
239
|
function parseNegotiateProtocolVersion(io) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
+
const supportedVersionMinor = io.readUInt32BE();
|
|
241
|
+
const count = io.readUInt32BE();
|
|
242
|
+
const unrecognizedOptions = new Array(count);
|
|
243
|
+
let i;
|
|
244
|
+
for (i = 0; i < count; i++)
|
|
245
|
+
unrecognizedOptions[i] = io.readCString('utf8');
|
|
246
|
+
return { supportedVersionMinor, unrecognizedOptions };
|
|
240
247
|
}
|
|
241
248
|
function parseParameterDescription(io) {
|
|
242
249
|
const out = {
|
package/protocol/frontend.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { DataTypeMap } from '../data-type-map.js';
|
|
2
2
|
import type { QueryOptions } from '../interfaces/query-options.js';
|
|
3
3
|
import type { Maybe, OID } from '../types.js';
|
|
4
|
+
import { Protocol } from './protocol.js';
|
|
4
5
|
import type { SASL } from './sasl.js';
|
|
5
6
|
import { type SmartBufferConfig } from './smart-buffer.js';
|
|
6
7
|
export interface FrontendOptions {
|
|
@@ -37,19 +38,42 @@ export declare namespace Frontend {
|
|
|
37
38
|
type: 'P' | 'S';
|
|
38
39
|
name?: string;
|
|
39
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The legacy Function Call sub-protocol - calls a function by OID
|
|
43
|
+
* directly, bypassing SQL entirely. Superseded by `SELECT func(...)`
|
|
44
|
+
* over the Simple/Extended Query protocols (which every current
|
|
45
|
+
* PostgreSQL client, this one included for everything else, uses
|
|
46
|
+
* exclusively) - kept only for wire-protocol completeness. Arguments
|
|
47
|
+
* and the result travel as raw wire-format bytes, not JS values: the
|
|
48
|
+
* caller is responsible for encoding/decoding them (see a `DataType`'s
|
|
49
|
+
* own `encodeBinary`/`decodeBinary` for the format a given OID expects).
|
|
50
|
+
*/
|
|
51
|
+
interface FunctionCallMessageArgs {
|
|
52
|
+
functionId: OID;
|
|
53
|
+
/**
|
|
54
|
+
* Format of each argument in `args`, applied positionally - empty
|
|
55
|
+
* means text for all of them, a single entry applies to all, or it
|
|
56
|
+
* must have exactly one entry per argument.
|
|
57
|
+
*/
|
|
58
|
+
argFormats?: Protocol.DataFormat[];
|
|
59
|
+
/** `null` for an SQL NULL argument. */
|
|
60
|
+
args: Maybe<Buffer>[];
|
|
61
|
+
/** Format the function's return value comes back in. @default text */
|
|
62
|
+
resultFormat?: Protocol.DataFormat;
|
|
63
|
+
}
|
|
40
64
|
}
|
|
41
65
|
export declare class Frontend {
|
|
42
66
|
private _io;
|
|
43
67
|
constructor(options?: FrontendOptions);
|
|
44
68
|
getSSLRequestMessage(): Buffer;
|
|
45
|
-
getStartupMessage(args: Frontend.StartupMessageArgs): Buffer;
|
|
69
|
+
getStartupMessage(args: Frontend.StartupMessageArgs, minorVersion?: number): Buffer;
|
|
46
70
|
/**
|
|
47
71
|
* Asks the server to cancel whatever the given session is currently
|
|
48
72
|
* running. Sent on its own fresh connection - a backend busy with a query
|
|
49
73
|
* is not reading its own socket, which is the whole reason this cannot go
|
|
50
74
|
* down the normal one.
|
|
51
75
|
*/
|
|
52
|
-
getCancelRequestMessage(processID: number, secretKey:
|
|
76
|
+
getCancelRequestMessage(processID: number, secretKey: Buffer): Buffer;
|
|
53
77
|
getPasswordMessage(password: string): Buffer;
|
|
54
78
|
getSASLMessage(sasl: SASL.Session): Buffer;
|
|
55
79
|
getSASLFinalMessage(session: SASL.Session): Buffer;
|
|
@@ -59,6 +83,7 @@ export declare class Frontend {
|
|
|
59
83
|
getExecuteMessage(args: Frontend.ExecuteMessageArgs): Buffer;
|
|
60
84
|
getCloseMessage(args: Frontend.CloseMessageArgs): Buffer;
|
|
61
85
|
getQueryMessage(sql: string): Buffer;
|
|
86
|
+
getFunctionCallMessage(args: Frontend.FunctionCallMessageArgs): Buffer;
|
|
62
87
|
/**
|
|
63
88
|
* Returns the CopyData header and the caller's payload as two buffers
|
|
64
89
|
* rather than one, so a bulk import never copies the caller's bytes.
|
package/protocol/frontend.js
CHANGED
|
@@ -46,12 +46,12 @@ export class Frontend {
|
|
|
46
46
|
.writeUInt16BE(5679)
|
|
47
47
|
.flush();
|
|
48
48
|
}
|
|
49
|
-
getStartupMessage(args) {
|
|
49
|
+
getStartupMessage(args, minorVersion = Protocol.VERSION_MINOR) {
|
|
50
50
|
const io = this._io
|
|
51
51
|
.start()
|
|
52
52
|
.writeInt32BE(0) // Preserve length
|
|
53
53
|
.writeInt16BE(Protocol.VERSION_MAJOR)
|
|
54
|
-
.writeInt16BE(
|
|
54
|
+
.writeInt16BE(minorVersion);
|
|
55
55
|
const entries = Object.entries(args);
|
|
56
56
|
const l = entries.length;
|
|
57
57
|
let k;
|
|
@@ -75,11 +75,11 @@ export class Frontend {
|
|
|
75
75
|
getCancelRequestMessage(processID, secretKey) {
|
|
76
76
|
return this._io
|
|
77
77
|
.start()
|
|
78
|
-
.writeUInt32BE(
|
|
78
|
+
.writeUInt32BE(12 + secretKey.length) // Length of message contents in bytes, including self.
|
|
79
79
|
.writeUInt16BE(1234)
|
|
80
80
|
.writeUInt16BE(5678)
|
|
81
81
|
.writeUInt32BE(processID)
|
|
82
|
-
.
|
|
82
|
+
.writeBuffer(secretKey)
|
|
83
83
|
.flush();
|
|
84
84
|
}
|
|
85
85
|
getPasswordMessage(password) {
|
|
@@ -273,6 +273,32 @@ export class Frontend {
|
|
|
273
273
|
.writeCString(sql || '', 'utf8');
|
|
274
274
|
return setLengthAndFlush(io, 1);
|
|
275
275
|
}
|
|
276
|
+
getFunctionCallMessage(args) {
|
|
277
|
+
const { functionId, args: values, argFormats, resultFormat } = args;
|
|
278
|
+
const formats = argFormats || [];
|
|
279
|
+
const fl = formats.length;
|
|
280
|
+
const l = values.length;
|
|
281
|
+
let i;
|
|
282
|
+
const io = this._io
|
|
283
|
+
.start()
|
|
284
|
+
.writeInt8(Protocol.FrontendMessageCode.FunctionCall)
|
|
285
|
+
.writeInt32BE(0) // Preserve header
|
|
286
|
+
.writeInt32BE(functionId)
|
|
287
|
+
.writeInt16BE(fl);
|
|
288
|
+
for (i = 0; i < fl; i++)
|
|
289
|
+
io.writeInt16BE(formats[i]);
|
|
290
|
+
io.writeInt16BE(l);
|
|
291
|
+
let v;
|
|
292
|
+
for (i = 0; i < l; i++) {
|
|
293
|
+
v = values[i];
|
|
294
|
+
if (v == null)
|
|
295
|
+
io.writeInt32BE(-1);
|
|
296
|
+
else
|
|
297
|
+
io.writeInt32BE(v.length).writeBuffer(v);
|
|
298
|
+
}
|
|
299
|
+
io.writeInt16BE(resultFormat ?? DataFormat.text);
|
|
300
|
+
return setLengthAndFlush(io, 1);
|
|
301
|
+
}
|
|
276
302
|
/**
|
|
277
303
|
* Returns the CopyData header and the caller's payload as two buffers
|
|
278
304
|
* rather than one, so a bulk import never copies the caller's bytes.
|
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -20,6 +20,7 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
20
20
|
private _saslSession?;
|
|
21
21
|
private _processID?;
|
|
22
22
|
private _secretKey?;
|
|
23
|
+
private _protocolNegotiation?;
|
|
23
24
|
private _captureQueue;
|
|
24
25
|
private _pendingWrites;
|
|
25
26
|
private _flushScheduled;
|
|
@@ -30,7 +31,15 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
30
31
|
constructor(options: ConnectionConfiguration);
|
|
31
32
|
get state(): ConnectionState;
|
|
32
33
|
get processID(): Maybe<number>;
|
|
33
|
-
get secretKey(): Maybe<
|
|
34
|
+
get secretKey(): Maybe<Buffer>;
|
|
35
|
+
/**
|
|
36
|
+
* The server's NegotiateProtocolVersion reply, if it sent one -
|
|
37
|
+
* undefined means everything this client asked for in its startup
|
|
38
|
+
* packet (protocol minor version, any `_pq_.*` options) was fully
|
|
39
|
+
* recognized. Present only when the server is older/stricter than what
|
|
40
|
+
* was requested, or didn't recognize one of the startup options.
|
|
41
|
+
*/
|
|
42
|
+
get protocolNegotiation(): Maybe<Protocol.NegotiateProtocolVersionMessage>;
|
|
34
43
|
get sessionParameters(): Record<string, string>;
|
|
35
44
|
connect(): void;
|
|
36
45
|
protected _connectToHost(): void;
|
|
@@ -71,6 +80,8 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
71
80
|
close(): void;
|
|
72
81
|
sendExecuteMessage(args: Frontend.ExecuteMessageArgs, cb: CaptureCallback): Promise<any>;
|
|
73
82
|
sendCloseMessage(args: Frontend.CloseMessageArgs, cb: CaptureCallback): Promise<any>;
|
|
83
|
+
/** The legacy Function Call sub-protocol - see `Frontend.FunctionCallMessageArgs`. */
|
|
84
|
+
sendFunctionCallMessage(args: Frontend.FunctionCallMessageArgs, cb: CaptureCallback): Promise<any>;
|
|
74
85
|
/**
|
|
75
86
|
* Sends Parse+Bind+Describe+Execute+Sync as a single write with a single
|
|
76
87
|
* FIFO capture entry, instead of 5 separate sendXMessage() round trips -
|
|
@@ -221,6 +232,7 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
221
232
|
*/
|
|
222
233
|
protected _channelBindingData(socket: tls.TLSSocket): Buffer;
|
|
223
234
|
protected _handleParameterStatus(msg: Protocol.ParameterStatusMessage): void;
|
|
235
|
+
protected _handleNegotiateProtocolVersion(msg: Protocol.NegotiateProtocolVersionMessage): void;
|
|
224
236
|
protected _handleBackendKeyData(msg: Protocol.BackendKeyDataMessage): void;
|
|
225
237
|
protected _handleCommandComplete(msg: any): Protocol.CommandCompleteMessage;
|
|
226
238
|
/**
|
package/protocol/pg-socket.js
CHANGED
|
@@ -24,6 +24,7 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
24
24
|
_saslSession;
|
|
25
25
|
_processID;
|
|
26
26
|
_secretKey;
|
|
27
|
+
_protocolNegotiation;
|
|
27
28
|
_captureQueue = new DoublyLinked();
|
|
28
29
|
_pendingWrites = [];
|
|
29
30
|
_flushScheduled = false;
|
|
@@ -48,6 +49,16 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
48
49
|
get secretKey() {
|
|
49
50
|
return this._secretKey;
|
|
50
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The server's NegotiateProtocolVersion reply, if it sent one -
|
|
54
|
+
* undefined means everything this client asked for in its startup
|
|
55
|
+
* packet (protocol minor version, any `_pq_.*` options) was fully
|
|
56
|
+
* recognized. Present only when the server is older/stricter than what
|
|
57
|
+
* was requested, or didn't recognize one of the startup options.
|
|
58
|
+
*/
|
|
59
|
+
get protocolNegotiation() {
|
|
60
|
+
return this._protocolNegotiation;
|
|
61
|
+
}
|
|
51
62
|
get sessionParameters() {
|
|
52
63
|
return this._sessionParameters;
|
|
53
64
|
}
|
|
@@ -216,6 +227,10 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
216
227
|
sendCloseMessage(args, cb) {
|
|
217
228
|
return this._sendAndCapture(this._frontend.getCloseMessage(args), cb, 'sendCloseMessage', args);
|
|
218
229
|
}
|
|
230
|
+
/** The legacy Function Call sub-protocol - see `Frontend.FunctionCallMessageArgs`. */
|
|
231
|
+
sendFunctionCallMessage(args, cb) {
|
|
232
|
+
return this._sendAndCapture(this._frontend.getFunctionCallMessage(args), cb, 'sendFunctionCallMessage', args);
|
|
233
|
+
}
|
|
219
234
|
/**
|
|
220
235
|
* Sends Parse+Bind+Describe+Execute+Sync as a single write with a single
|
|
221
236
|
* FIFO capture entry, instead of 5 separate sendXMessage() round trips -
|
|
@@ -426,7 +441,9 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
426
441
|
...(this.options.replication
|
|
427
442
|
? { replication: this.options.replication }
|
|
428
443
|
: undefined),
|
|
429
|
-
}
|
|
444
|
+
}, this.options.longCancelKey
|
|
445
|
+
? Protocol.VERSION_MINOR_LONG_CANCEL_KEY
|
|
446
|
+
: undefined));
|
|
430
447
|
}
|
|
431
448
|
/**
|
|
432
449
|
* Called once the server is ready. With `targetSessionAttrs` set, the
|
|
@@ -535,6 +552,9 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
535
552
|
case Protocol.BackendMessageCode.ParameterStatus:
|
|
536
553
|
this._handleParameterStatus(payload);
|
|
537
554
|
break;
|
|
555
|
+
case Protocol.BackendMessageCode.NegotiateProtocolVersion:
|
|
556
|
+
this._handleNegotiateProtocolVersion(payload);
|
|
557
|
+
break;
|
|
538
558
|
case Protocol.BackendMessageCode.BackendKeyData:
|
|
539
559
|
this._handleBackendKeyData(payload);
|
|
540
560
|
break;
|
|
@@ -688,6 +708,21 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
688
708
|
_handleParameterStatus(msg) {
|
|
689
709
|
this._sessionParameters[msg.name] = msg.value;
|
|
690
710
|
}
|
|
711
|
+
_handleNegotiateProtocolVersion(msg) {
|
|
712
|
+
this._protocolNegotiation = msg;
|
|
713
|
+
/* c8 ignore start */
|
|
714
|
+
if (this.listenerCount('debug')) {
|
|
715
|
+
this.emit('debug', {
|
|
716
|
+
location: 'PgSocket._handleNegotiateProtocolVersion',
|
|
717
|
+
message: `server supports protocol 3.${msg.supportedVersionMinor}` +
|
|
718
|
+
(msg.unrecognizedOptions.length
|
|
719
|
+
? `; did not recognize startup option(s): ${msg.unrecognizedOptions.join(', ')}`
|
|
720
|
+
: ''),
|
|
721
|
+
...msg,
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/* c8 ignore stop */
|
|
725
|
+
}
|
|
691
726
|
_handleBackendKeyData(msg) {
|
|
692
727
|
this._processID = msg.processID;
|
|
693
728
|
this._secretKey = msg.secretKey;
|
package/protocol/protocol.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
export declare namespace Protocol {
|
|
2
2
|
const VERSION_MAJOR = 3;
|
|
3
3
|
const VERSION_MINOR = 0;
|
|
4
|
+
/**
|
|
5
|
+
* Minor version 3.2 (PostgreSQL 18+) differs from 3.0 only in that
|
|
6
|
+
* BackendKeyData's secret key - and so CancelRequest's - can be up to
|
|
7
|
+
* 256 bytes instead of always exactly 4. A server that doesn't support
|
|
8
|
+
* 3.2 replies with NegotiateProtocolVersion naming the highest minor
|
|
9
|
+
* version it does support (see `PgSocket.protocolNegotiation`) and the
|
|
10
|
+
* session simply proceeds at that version instead - requesting 3.2 is
|
|
11
|
+
* never a connection-breaking choice, only ever a possible no-op.
|
|
12
|
+
*/
|
|
13
|
+
const VERSION_MINOR_LONG_CANCEL_KEY = 2;
|
|
4
14
|
enum BackendMessageCode {
|
|
5
15
|
Authentication = 82,// R
|
|
6
16
|
BackendKeyData = 75,// K
|
|
@@ -99,7 +109,14 @@ export declare namespace Protocol {
|
|
|
99
109
|
}
|
|
100
110
|
interface BackendKeyDataMessage {
|
|
101
111
|
processID: number;
|
|
102
|
-
|
|
112
|
+
/**
|
|
113
|
+
* Always 4 bytes before protocol 3.2, up to 256 with it - see
|
|
114
|
+
* `VERSION_MINOR_LONG_CANCEL_KEY`. Not a fixed-width int, unlike
|
|
115
|
+
* before: the field carries whatever bytes the server sent, in
|
|
116
|
+
* whatever order it sent them, to be relayed back to CancelRequest
|
|
117
|
+
* unchanged rather than interpreted as a number.
|
|
118
|
+
*/
|
|
119
|
+
secretKey: Buffer;
|
|
103
120
|
}
|
|
104
121
|
interface CommandCompleteMessage {
|
|
105
122
|
command: string;
|
|
@@ -145,12 +162,18 @@ export declare namespace Protocol {
|
|
|
145
162
|
payload: string;
|
|
146
163
|
}
|
|
147
164
|
interface FunctionCallResponseMessage {
|
|
148
|
-
|
|
165
|
+
/** `null` when the function returned SQL NULL. */
|
|
166
|
+
result: Buffer | null;
|
|
149
167
|
}
|
|
150
168
|
interface NegotiateProtocolVersionMessage {
|
|
169
|
+
/** Newest minor protocol version the server supports. */
|
|
151
170
|
supportedVersionMinor: number;
|
|
152
|
-
|
|
153
|
-
|
|
171
|
+
/**
|
|
172
|
+
* Startup packet options this client sent that the server didn't
|
|
173
|
+
* recognize - empty when the only mismatch is the protocol minor
|
|
174
|
+
* version itself.
|
|
175
|
+
*/
|
|
176
|
+
unrecognizedOptions: string[];
|
|
154
177
|
}
|
|
155
178
|
interface ParameterDescriptionMessage {
|
|
156
179
|
parameterCount: number;
|
package/protocol/protocol.js
CHANGED
|
@@ -2,6 +2,16 @@ export var Protocol;
|
|
|
2
2
|
(function (Protocol) {
|
|
3
3
|
Protocol.VERSION_MAJOR = 3;
|
|
4
4
|
Protocol.VERSION_MINOR = 0;
|
|
5
|
+
/**
|
|
6
|
+
* Minor version 3.2 (PostgreSQL 18+) differs from 3.0 only in that
|
|
7
|
+
* BackendKeyData's secret key - and so CancelRequest's - can be up to
|
|
8
|
+
* 256 bytes instead of always exactly 4. A server that doesn't support
|
|
9
|
+
* 3.2 replies with NegotiateProtocolVersion naming the highest minor
|
|
10
|
+
* version it does support (see `PgSocket.protocolNegotiation`) and the
|
|
11
|
+
* session simply proceeds at that version instead - requesting 3.2 is
|
|
12
|
+
* never a connection-breaking choice, only ever a possible no-op.
|
|
13
|
+
*/
|
|
14
|
+
Protocol.VERSION_MINOR_LONG_CANCEL_KEY = 2;
|
|
5
15
|
// https://www.postgresql.org/docs/9.3/protocol-message-formats.html
|
|
6
16
|
let BackendMessageCode;
|
|
7
17
|
(function (BackendMessageCode) {
|