postgrejs 3.0.4 β†’ 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 CHANGED
@@ -1,6 +1,17 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.0.4](https://github.com/panates/postgrejs/compare/v3.0.3...v3.0.4) -
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
4
15
 
5
16
  ### [v3.0.3](https://github.com/panates/postgrejs/compare/v3.0.2...v3.0.3) - 8 September 2026
6
17
 
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 2.23.1, pg 8.23.0, postgres.js 3.4.9**. βœ… built
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<number>;
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
@@ -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<number>;
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,7 @@
1
+ export interface FunctionCallResult {
2
+ /**
3
+ * The function's return value, in the wire format requested via
4
+ * `FunctionCallOptions.resultFormat` - `null` if it returned SQL NULL.
5
+ */
6
+ result: Buffer | null;
7
+ }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.0.4",
4
+ "version": "3.1.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -169,10 +169,13 @@ function parseAuthentication(io, code, len) {
169
169
  }
170
170
  }
171
171
  function parseBackendKeyData(io) {
172
- return {
173
- processID: io.readUInt32BE(),
174
- secretKey: io.readUInt32BE(),
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, code, len) {
230
- return {
231
- result: io.readBuffer(len - 4),
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
- return {
236
- supportedVersionMinor: io.readUInt32BE(),
237
- numberOfNotSupportedVersions: io.readUInt32BE(),
238
- option: io.readCString('utf8'),
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 = {
@@ -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: number): Buffer;
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.
@@ -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(Protocol.VERSION_MINOR);
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(16) // Length of message contents in bytes, including self.
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
- .writeUInt32BE(secretKey)
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.
@@ -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<number>;
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
  /**
@@ -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;
@@ -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
- secretKey: number;
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
- result: Buffer;
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
- numberOfNotSupportedVersions: number;
153
- option: string;
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;
@@ -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) {