postgrejs 2.23.1 → 3.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/CHANGELOG.md +171 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +169 -16
  5. package/connection/connection.js +318 -56
  6. package/connection/copy-stream.d.ts +88 -0
  7. package/connection/copy-stream.js +244 -0
  8. package/connection/cursor.js +20 -13
  9. package/connection/intl-connection.d.ts +113 -5
  10. package/connection/intl-connection.js +500 -70
  11. package/connection/large-object.d.ts +65 -0
  12. package/connection/large-object.js +151 -0
  13. package/connection/logical-replication.d.ts +121 -0
  14. package/connection/logical-replication.js +279 -0
  15. package/connection/pool.d.ts +61 -4
  16. package/connection/pool.js +177 -29
  17. package/connection/portal.d.ts +18 -4
  18. package/connection/portal.js +53 -59
  19. package/connection/prepared-statement.d.ts +22 -1
  20. package/connection/prepared-statement.js +129 -109
  21. package/data-type-map.js +4 -2
  22. package/data-types/bool-type.js +14 -10
  23. package/data-types/box-type.js +15 -8
  24. package/data-types/bytea-type.js +9 -5
  25. package/data-types/char-type.js +11 -5
  26. package/data-types/circle-type.js +15 -7
  27. package/data-types/date-type.js +25 -19
  28. package/data-types/float4-type.js +10 -3
  29. package/data-types/float8-type.js +12 -3
  30. package/data-types/int2-type.js +9 -4
  31. package/data-types/int2-vector-type.js +9 -4
  32. package/data-types/int4-type.js +11 -5
  33. package/data-types/int8-type.js +22 -7
  34. package/data-types/json-type.js +17 -8
  35. package/data-types/jsonb-type.js +20 -14
  36. package/data-types/lseg-type.js +16 -8
  37. package/data-types/numeric-type.d.ts +8 -0
  38. package/data-types/numeric-type.js +118 -9
  39. package/data-types/oid-type.d.ts +0 -1
  40. package/data-types/oid-type.js +9 -10
  41. package/data-types/oid-vector-type.d.ts +9 -0
  42. package/data-types/oid-vector-type.js +51 -0
  43. package/data-types/point-type.js +14 -6
  44. package/data-types/time-type.js +20 -13
  45. package/data-types/timestamp-type.js +32 -25
  46. package/data-types/timestamptz-type.js +33 -21
  47. package/data-types/uuid-type.js +21 -11
  48. package/data-types/varchar-type.js +16 -4
  49. package/index.d.ts +5 -0
  50. package/index.js +5 -0
  51. package/interfaces/command-result.d.ts +2 -1
  52. package/interfaces/data-type.d.ts +5 -3
  53. package/interfaces/database-connection-params.d.ts +108 -0
  54. package/interfaces/query-options.d.ts +23 -0
  55. package/interfaces/script-execute-options.d.ts +23 -0
  56. package/interfaces/script-result.d.ts +3 -2
  57. package/package.json +2 -3
  58. package/protocol/backend.d.ts +6 -1
  59. package/protocol/backend.js +70 -45
  60. package/protocol/buffer-reader.d.ts +0 -2
  61. package/protocol/buffer-reader.js +3 -10
  62. package/protocol/cert-signature.d.ts +12 -0
  63. package/protocol/cert-signature.js +114 -0
  64. package/protocol/frontend.d.ts +16 -0
  65. package/protocol/frontend.js +70 -11
  66. package/protocol/pg-socket.d.ts +220 -9
  67. package/protocol/pg-socket.js +539 -102
  68. package/protocol/pgoutput.d.ts +76 -0
  69. package/protocol/pgoutput.js +155 -0
  70. package/protocol/protocol.d.ts +3 -4
  71. package/protocol/protocol.js +1 -1
  72. package/protocol/sasl.d.ts +11 -1
  73. package/protocol/sasl.js +36 -5
  74. package/protocol/smart-buffer.d.ts +0 -2
  75. package/protocol/smart-buffer.js +11 -16
  76. package/types.d.ts +5 -3
  77. package/util/abort-signal.d.ts +34 -0
  78. package/util/abort-signal.js +76 -0
  79. package/util/array-calculatedim.js +3 -1
  80. package/util/connection-config.js +77 -3
  81. package/util/decode-binaryarray.d.ts +1 -1
  82. package/util/decode-binaryarray.js +15 -6
  83. package/util/encode-binaryarray.js +7 -3
  84. package/util/escape-identifier.d.ts +14 -0
  85. package/util/escape-identifier.js +32 -0
  86. package/util/escape-literal.js +3 -0
  87. package/util/fast-parseint.d.ts +1 -0
  88. package/util/fast-parseint.js +12 -17
  89. package/util/format-datetime.d.ts +10 -0
  90. package/util/format-datetime.js +71 -0
  91. package/util/get-parsers.js +31 -10
  92. package/util/parse-array.js +2 -2
  93. package/util/parse-bytea.d.ts +2 -0
  94. package/util/parse-bytea.js +83 -0
  95. package/util/parse-datetime.d.ts +3 -1
  96. package/util/parse-datetime.js +86 -17
  97. package/util/parse-row.d.ts +3 -1
  98. package/util/parse-row.js +60 -5
  99. package/util/parse-time.js +8 -1
  100. package/util/sql-tag.d.ts +60 -0
  101. package/util/sql-tag.js +202 -0
  102. package/util/starts-transaction.d.ts +38 -0
  103. package/util/starts-transaction.js +148 -0
  104. package/util/stringify-arrayliteral.js +13 -1
  105. package/util/wrap-row-description.js +13 -4
  106. package/util/convert-row-to-object.d.ts +0 -2
  107. package/util/convert-row-to-object.js +0 -9
@@ -6,27 +6,54 @@ import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.
6
6
  import type { ScriptResult } from '../interfaces/script-result.js';
7
7
  import type { StatementPrepareOptions } from '../interfaces/statement-prepare-options.js';
8
8
  import { SafeEventEmitter } from '../safe-event-emitter.js';
9
+ import { QueryRequest } from '../util/sql-tag.js';
9
10
  import { Connection, type NotificationCallback } from './connection.js';
10
11
  import { IntlConnection } from './intl-connection.js';
11
12
  import type { PreparedStatement } from './prepared-statement.js';
13
+ export interface PoolPipelineOptions {
14
+ pipeline?: boolean;
15
+ }
16
+ export type PoolQueryOptions = QueryOptions & PoolPipelineOptions;
17
+ export type PoolScriptExecuteOptions = ScriptExecuteOptions & PoolPipelineOptions;
18
+ /**
19
+ * One pooled connection the pipelined path is currently borrowing.
20
+ *
21
+ * A slot joins the list the moment its acquire starts rather than when it
22
+ * finishes, so callers arriving while a connection is still being opened
23
+ * queue onto `promise` instead of starting a second acquire of their own.
24
+ * `load` counts queries handed to this slot that have not settled yet - it
25
+ * is incremented synchronously, at selection time, which `runningQueryCount`
26
+ * cannot be: everything in a Promise.all() burst picks its connection before
27
+ * any of them reaches execute(), so at that point every connection still
28
+ * reports zero running queries.
29
+ */
30
+ interface PipelineSlot {
31
+ connection?: Connection;
32
+ promise: Promise<Connection>;
33
+ load: number;
34
+ }
12
35
  export declare class Pool extends SafeEventEmitter {
13
36
  protected readonly _pool: LightningPool<IntlConnection>;
14
37
  protected readonly _notificationListeners: SafeEventEmitter;
15
38
  protected _notificationConnection?: Connection;
39
+ protected readonly _pipelineSlots: PipelineSlot[];
40
+ protected _pipelineMaxQueries: number;
41
+ protected _pipelineMaxConnections: number;
16
42
  readonly config: PoolConfiguration;
17
43
  constructor(config?: PoolConfiguration | string);
18
44
  /**
19
- * Returns number of connections that are currently acquired
45
+ * Returns the number of connections that are currently acquired
20
46
  */
21
47
  get acquiredConnections(): number;
22
48
  /**
23
- * Returns number of unused connections in the pool
49
+ * Returns the number of unused connections in the pool
24
50
  */
25
51
  get idleConnections(): number;
26
52
  /**
27
53
  * Returns total number of connections in the pool regardless of whether they are idle or in use
28
54
  */
29
55
  get totalConnections(): number;
56
+ start(): void;
30
57
  /**
31
58
  * Obtains a connection from the connection pool
32
59
  */
@@ -38,15 +65,45 @@ export declare class Pool extends SafeEventEmitter {
38
65
  /**
39
66
  * Executes a script
40
67
  */
41
- execute(sql: string, options?: ScriptExecuteOptions): Promise<ScriptResult>;
68
+ execute(sql: string | QueryRequest, options?: PoolScriptExecuteOptions): Promise<ScriptResult>;
42
69
  /**
43
70
  * Executes a query
44
71
  */
45
- query(sql: string, options?: QueryOptions): Promise<QueryResult>;
72
+ query(sql: string | QueryRequest, options?: PoolQueryOptions): Promise<QueryResult>;
46
73
  prepare(sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
47
74
  release(connection: Connection): Promise<void>;
48
75
  listen(channel: string, callback: NotificationCallback): Promise<void>;
49
76
  unListen(channel: string): Promise<void>;
50
77
  unListenAll(): Promise<void>;
78
+ protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
79
+ /**
80
+ * Picks the pooled connection a pipelined query should ride on, or
81
+ * undefined when the caller should take a connection of its own.
82
+ *
83
+ * Deliberately synchronous: it must never await, because everything in a
84
+ * Promise.all() burst has to pick its connection and reach execute()
85
+ * within a single event loop tick for PgSocket to coalesce the whole
86
+ * burst into one write per connection. Opening another connection is
87
+ * started here but never awaited by the caller that triggered it.
88
+ */
89
+ protected _acquireShared(): PipelineSlot | undefined;
90
+ /**
91
+ * Starts borrowing one more connection and returns its slot right away,
92
+ * already carrying the caller that triggered the growth.
93
+ */
94
+ protected _growPipeline(): PipelineSlot;
95
+ /**
96
+ * Stops new queries joining a slot. Queries already on it are unaffected.
97
+ */
98
+ protected _dropPipelineSlot(slot: PipelineSlot): void;
99
+ /**
100
+ * Backstop for a transaction startsTransaction() could not see coming -
101
+ * one opened inside a stored procedure, for instance. The connection is
102
+ * still sound, but nothing else may join it while it is in a
103
+ * transaction, so the slot goes away and the connection returns to the
104
+ * pool once its own queries finish.
105
+ */
106
+ protected _checkSharedTransaction(slot: PipelineSlot, connection: Connection): void;
51
107
  protected _initNotificationConnection(): Promise<void>;
52
108
  }
109
+ export {};
@@ -3,12 +3,17 @@ import { coerceToBoolean, coerceToInt } from 'putil-varhelpers';
3
3
  import { ConnectionState } from '../constants.js';
4
4
  import { SafeEventEmitter } from '../safe-event-emitter.js';
5
5
  import { getConnectionConfig } from '../util/connection-config.js';
6
+ import { QueryRequest } from '../util/sql-tag.js';
7
+ import { startsCopy, startsTransaction } from '../util/starts-transaction.js';
6
8
  import { Connection } from './connection.js';
7
9
  import { getIntlConnection, IntlConnection } from './intl-connection.js';
8
10
  export class Pool extends SafeEventEmitter {
9
11
  _pool;
10
12
  _notificationListeners = new SafeEventEmitter();
11
13
  _notificationConnection;
14
+ _pipelineSlots = [];
15
+ _pipelineMaxQueries;
16
+ _pipelineMaxConnections;
12
17
  config;
13
18
  constructor(config) {
14
19
  super();
@@ -25,9 +30,11 @@ export class Pool extends SafeEventEmitter {
25
30
  poolOptions.min = coerceToInt(cfg.min, 0);
26
31
  poolOptions.minIdle = coerceToInt(cfg.minIdle, 0);
27
32
  poolOptions.validation = coerceToBoolean(cfg.validation, false);
33
+ this._pipelineMaxQueries = Math.max(coerceToInt(cfg.pipelineMaxQueries, 100), 1);
34
+ this._pipelineMaxConnections = Math.max(coerceToInt(cfg.pipelineMaxConnections, poolOptions.max), 0);
28
35
  const poolFactory = {
29
36
  create: async () => {
30
- /* istanbul ignore next */
37
+ /* c8 ignore start */
31
38
  if (this.listenerCount('debug')) {
32
39
  this.emit('debug', {
33
40
  location: 'Pool.factory.create',
@@ -35,10 +42,11 @@ export class Pool extends SafeEventEmitter {
35
42
  message: `new connection creating`,
36
43
  });
37
44
  }
45
+ /* c8 ignore stop */
38
46
  const intlCon = new IntlConnection(cfg);
39
47
  await intlCon.connect();
40
48
  intlCon.on('close', () => this._pool.destroy(intlCon));
41
- /* istanbul ignore next */
49
+ /* c8 ignore start */
42
50
  if (this.listenerCount('debug')) {
43
51
  this.emit('debug', {
44
52
  location: 'Pool.factory.create',
@@ -46,10 +54,11 @@ export class Pool extends SafeEventEmitter {
46
54
  message: `[${intlCon.processID}] connection created`,
47
55
  });
48
56
  }
57
+ /* c8 ignore stop */
49
58
  return intlCon;
50
59
  },
51
60
  destroy: intlCon => {
52
- /* istanbul ignore next */
61
+ /* c8 ignore start */
53
62
  if (this.listenerCount('debug')) {
54
63
  this.emit('debug', {
55
64
  location: 'Pool.factory.destroy',
@@ -57,10 +66,11 @@ export class Pool extends SafeEventEmitter {
57
66
  message: `[${intlCon.processID}] connection destroy`,
58
67
  });
59
68
  }
69
+ /* c8 ignore stop */
60
70
  return intlCon.close();
61
71
  },
62
- reset: async (intlCon) => {
63
- /* istanbul ignore next */
72
+ reset: (intlCon) => {
73
+ /* c8 ignore start */
64
74
  if (this.listenerCount('debug')) {
65
75
  this.emit('debug', {
66
76
  location: 'Pool.factory.reset',
@@ -68,19 +78,14 @@ export class Pool extends SafeEventEmitter {
68
78
  message: `[${intlCon.processID}] connection reset`,
69
79
  });
70
80
  }
71
- try {
72
- if (intlCon.state === ConnectionState.READY) {
73
- await intlCon.execute('ROLLBACK;UNLISTEN *');
74
- }
75
- }
76
- finally {
77
- intlCon.removeAllListeners();
78
- intlCon.once('close', () => this._pool.destroy(intlCon));
79
- intlCon._refCount = 0;
80
- }
81
+ /* c8 ignore stop */
82
+ intlCon.owner = undefined;
83
+ intlCon.removeAllListeners();
84
+ intlCon.once('close', () => this._pool.destroy(intlCon));
85
+ intlCon._refCount = 0;
81
86
  },
82
87
  validate: async (intlCon) => {
83
- /* istanbul ignore next */
88
+ /* c8 ignore start */
84
89
  if (this.listenerCount('debug')) {
85
90
  this.emit('debug', {
86
91
  location: 'Pool.factory.validate',
@@ -88,6 +93,7 @@ export class Pool extends SafeEventEmitter {
88
93
  message: `[${intlCon.processID}] connection validate`,
89
94
  });
90
95
  }
96
+ /* c8 ignore stop */
91
97
  if (intlCon.state !== ConnectionState.READY)
92
98
  throw new Error('Connection is not active');
93
99
  await intlCon.execute('select 1;');
@@ -98,16 +104,15 @@ export class Pool extends SafeEventEmitter {
98
104
  this._pool.on('error', (...args) => this.emit('error', ...args));
99
105
  this._pool.on('acquire', (...args) => this.emit('acquire', ...args));
100
106
  this._pool.on('destroy', (...args) => this.emit('destroy', ...args));
101
- this._pool.start();
102
107
  }
103
108
  /**
104
- * Returns number of connections that are currently acquired
109
+ * Returns the number of connections that are currently acquired
105
110
  */
106
111
  get acquiredConnections() {
107
112
  return this._pool.acquired;
108
113
  }
109
114
  /**
110
- * Returns number of unused connections in the pool
115
+ * Returns the number of unused connections in the pool
111
116
  */
112
117
  get idleConnections() {
113
118
  return this._pool.available;
@@ -118,12 +123,15 @@ export class Pool extends SafeEventEmitter {
118
123
  get totalConnections() {
119
124
  return this._pool.size;
120
125
  }
126
+ start() {
127
+ return this._pool.start();
128
+ }
121
129
  /**
122
130
  * Obtains a connection from the connection pool
123
131
  */
124
132
  async acquire() {
125
133
  const intlCon = await this._pool.acquire();
126
- /* istanbul ignore next */
134
+ /* c8 ignore start */
127
135
  if (this.listenerCount('debug')) {
128
136
  this.emit('debug', {
129
137
  location: 'Pool.acquire',
@@ -131,8 +139,9 @@ export class Pool extends SafeEventEmitter {
131
139
  message: `[${intlCon.processID}] acquired`,
132
140
  });
133
141
  }
142
+ /* c8 ignore stop */
134
143
  const connection = new Connection(this, intlCon);
135
- /* istanbul ignore next */
144
+ /* c8 ignore next */
136
145
  if (this.listenerCount('debug'))
137
146
  connection.on('debug', (...args) => this.emit('debug', ...args));
138
147
  if (this.listenerCount('execute'))
@@ -154,24 +163,63 @@ export class Pool extends SafeEventEmitter {
154
163
  * Executes a script
155
164
  */
156
165
  async execute(sql, options) {
157
- const connection = await this.acquire();
166
+ if (sql instanceof QueryRequest)
167
+ sql = sql.stringify({ ...options, typeMap: options?.typeMap });
168
+ const slot = this._canPipeline(options?.pipeline, sql, options?.autoCommit, options?.signal)
169
+ ? this._acquireShared()
170
+ : undefined;
171
+ if (!slot) {
172
+ const connection = await this.acquire();
173
+ try {
174
+ return await connection.execute(sql, options);
175
+ }
176
+ finally {
177
+ await this.release(connection);
178
+ }
179
+ }
158
180
  try {
159
- return await connection.execute(sql, options);
181
+ const shared = slot.connection || (await slot.promise);
182
+ const result = await shared.execute(sql, options);
183
+ this._checkSharedTransaction(slot, shared);
184
+ return result;
160
185
  }
161
186
  finally {
162
- await this.release(connection);
187
+ slot.load--;
163
188
  }
164
189
  }
165
190
  /**
166
191
  * Executes a query
167
192
  */
168
193
  async query(sql, options) {
169
- const connection = await this.acquire();
194
+ if (sql instanceof QueryRequest) {
195
+ if (options?.params)
196
+ throw new TypeError('A statement built with sql`` carries its own parameters; passing `params` as well is ambiguous');
197
+ options = { ...options, params: sql.params };
198
+ sql = sql.sql;
199
+ }
200
+ // A cursor hands the caller something that outlives this call and must
201
+ // keep its own portal on its own connection, so it can never share.
202
+ const slot = !options?.cursor &&
203
+ this._canPipeline(options?.pipeline, sql, options?.autoCommit, options?.signal)
204
+ ? this._acquireShared()
205
+ : undefined;
206
+ if (!slot) {
207
+ const connection = await this.acquire();
208
+ try {
209
+ return await connection.query(sql, options);
210
+ }
211
+ finally {
212
+ await this.release(connection);
213
+ }
214
+ }
170
215
  try {
171
- return await connection.query(sql, options);
216
+ const shared = slot.connection || (await slot.promise);
217
+ const result = await shared.query(sql, options);
218
+ this._checkSharedTransaction(slot, shared);
219
+ return result;
172
220
  }
173
221
  finally {
174
- await this.release(connection);
222
+ slot.load--;
175
223
  }
176
224
  }
177
225
  async prepare(sql, options) {
@@ -186,8 +234,20 @@ export class Pool extends SafeEventEmitter {
186
234
  async listen(channel, callback) {
187
235
  if (!/^[A-Z]\w+$/i.test(channel))
188
236
  throw new TypeError(`Invalid channel name`);
237
+ // Bug: _initNotificationConnection() only ever bootstraps the shared
238
+ // connection and registers every channel known at that moment - it
239
+ // returns immediately once that connection already exists, so a second,
240
+ // different channel added afterwards was recorded here but never
241
+ // reached the server at all (same root cause fixed in
242
+ // Connection.listen(), one layer up).
243
+ const alreadyListening = !!this._notificationListeners.listenerCount(channel);
189
244
  this._notificationListeners.on(channel, callback);
190
- await this._initNotificationConnection();
245
+ if (!this._notificationConnection) {
246
+ await this._initNotificationConnection();
247
+ }
248
+ else if (!alreadyListening) {
249
+ await this._notificationConnection.listen(channel, callback);
250
+ }
191
251
  }
192
252
  async unListen(channel) {
193
253
  if (!/^[A-Z]\w+$/i.test(channel))
@@ -207,6 +267,86 @@ export class Pool extends SafeEventEmitter {
207
267
  await conn.close();
208
268
  }
209
269
  }
270
+ _canPipeline(pipeline, sql, autoCommit, signal) {
271
+ return (pipeline === true &&
272
+ !signal &&
273
+ this._pipelineMaxQueries > 1 &&
274
+ this._pipelineMaxConnections > 0 &&
275
+ autoCommit !== false &&
276
+ this.config.autoCommit !== false &&
277
+ !startsTransaction(sql) &&
278
+ !startsCopy(sql));
279
+ }
280
+ /**
281
+ * Picks the pooled connection a pipelined query should ride on, or
282
+ * undefined when the caller should take a connection of its own.
283
+ *
284
+ * Deliberately synchronous: it must never await, because everything in a
285
+ * Promise.all() burst has to pick its connection and reach execute()
286
+ * within a single event loop tick for PgSocket to coalesce the whole
287
+ * burst into one write per connection. Opening another connection is
288
+ * started here but never awaited by the caller that triggered it.
289
+ */
290
+ _acquireShared() {
291
+ const slots = this._pipelineSlots;
292
+ const l = slots.length;
293
+ let best;
294
+ let slot;
295
+ let i;
296
+ for (i = 0; i < l; i++) {
297
+ slot = slots[i];
298
+ /* c8 ignore next */
299
+ if (slot.connection?.inTransaction)
300
+ continue;
301
+ if (!best || slot.load < best.load)
302
+ best = slot;
303
+ }
304
+ if ((!best || best.load > 0) && l < this._pipelineMaxConnections)
305
+ return this._growPipeline();
306
+ if (!best || best.load >= this._pipelineMaxQueries)
307
+ return undefined;
308
+ best.load++;
309
+ return best;
310
+ }
311
+ /**
312
+ * Starts borrowing one more connection and returns its slot right away,
313
+ * already carrying the caller that triggered the growth.
314
+ */
315
+ _growPipeline() {
316
+ const slot = { load: 1 };
317
+ slot.promise = this.acquire().then(connection => {
318
+ slot.connection = connection;
319
+ connection.once('idle', () => {
320
+ this._dropPipelineSlot(slot);
321
+ this.release(connection).catch(e => this.emit('error', e));
322
+ });
323
+ return connection;
324
+ }, e => {
325
+ this._dropPipelineSlot(slot);
326
+ throw e;
327
+ });
328
+ this._pipelineSlots.push(slot);
329
+ return slot;
330
+ }
331
+ /**
332
+ * Stops new queries joining a slot. Queries already on it are unaffected.
333
+ */
334
+ _dropPipelineSlot(slot) {
335
+ const i = this._pipelineSlots.indexOf(slot);
336
+ if (i >= 0)
337
+ this._pipelineSlots.splice(i, 1);
338
+ }
339
+ /**
340
+ * Backstop for a transaction startsTransaction() could not see coming -
341
+ * one opened inside a stored procedure, for instance. The connection is
342
+ * still sound, but nothing else may join it while it is in a
343
+ * transaction, so the slot goes away and the connection returns to the
344
+ * pool once its own queries finish.
345
+ */
346
+ _checkSharedTransaction(slot, connection) {
347
+ if (connection.inTransaction)
348
+ this._dropPipelineSlot(slot);
349
+ }
210
350
  async _initNotificationConnection() {
211
351
  if (this._notificationConnection)
212
352
  return;
@@ -226,7 +366,15 @@ export class Pool extends SafeEventEmitter {
226
366
  setTimeout(() => {
227
367
  if (!this._notificationListeners.eventNames().length)
228
368
  return;
229
- conn.connect().catch(() => reConnect());
369
+ // Bug: a successful reconnect never re-issued LISTEN for anything -
370
+ // Connection.close() clears `conn`'s own _notificationListeners,
371
+ // and registerEvents() (which reads the Pool's, a separate
372
+ // instance) was only ever called once, right after the first
373
+ // connect(). A connection drop silently ended every subscription.
374
+ conn
375
+ .connect()
376
+ .then(registerEvents)
377
+ .catch(() => reConnect());
230
378
  }, 500).unref();
231
379
  };
232
380
  await conn.connect();
@@ -5,19 +5,33 @@ import type { Connection } from './connection.js';
5
5
  import type { PreparedStatement } from './prepared-statement.js';
6
6
  export interface PortalExecuteResult {
7
7
  code: Protocol.BackendMessageCode;
8
- rows?: any[];
8
+ rows?: Protocol.DataRowMessage[];
9
9
  command?: string;
10
10
  rowCount?: number;
11
11
  }
12
12
  export declare class Portal {
13
13
  private readonly _statement;
14
14
  private readonly _name?;
15
- private _columnFormat;
16
15
  constructor(statement: PreparedStatement, name: string);
17
16
  get connection(): Connection;
18
17
  get name(): Maybe<string>;
19
- bind(params: Maybe<any[]>, queryOptions: QueryOptions): Promise<void>;
20
- retrieveFields(): Promise<Protocol.RowDescription[]>;
18
+ /**
19
+ * Bind + Describe(portal) collapsed into one round trip instead of two
20
+ * separately-awaited calls. Ends in Flush, not Sync - a Sync here would
21
+ * commit an implicit (autocommit) transaction and destroy this portal
22
+ * before any fetch() could use it (proved live: Bind+Describe+Sync with
23
+ * no explicit BEGIN left transactionStatus 'I' and a later Execute on the
24
+ * same named portal failed with "portal ... does not exist").
25
+ */
26
+ bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions): Promise<Protocol.RowDescription[]>;
21
27
  execute(fetchCount?: number): Promise<PortalExecuteResult>;
28
+ /**
29
+ * Close + Sync as one request (see PgSocket.sendCloseAndSyncMessages()
30
+ * for why they must share a single capture): ReadyForQuery is the end
31
+ * marker, CloseComplete is informational on the way there. That also
32
+ * makes this safe to call as cleanup after a failed Bind - the server is
33
+ * in error state then, skips the Close entirely, and answers only the
34
+ * Sync.
35
+ */
22
36
  close(): Promise<void>;
23
37
  }
@@ -1,11 +1,9 @@
1
- import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
2
1
  import { GlobalTypeMap } from '../data-type-map.js';
3
2
  import { Protocol } from '../protocol/protocol.js';
4
3
  import { getIntlConnection } from './intl-connection.js';
5
4
  export class Portal {
6
5
  _statement;
7
6
  _name;
8
- _columnFormat = DEFAULT_COLUMN_FORMAT;
9
7
  constructor(statement, name) {
10
8
  this._statement = statement;
11
9
  this._name = name;
@@ -16,49 +14,32 @@ export class Portal {
16
14
  get name() {
17
15
  return this._name;
18
16
  }
19
- async bind(params, queryOptions) {
17
+ /**
18
+ * Bind + Describe(portal) collapsed into one round trip instead of two
19
+ * separately-awaited calls. Ends in Flush, not Sync - a Sync here would
20
+ * commit an implicit (autocommit) transaction and destroy this portal
21
+ * before any fetch() could use it (proved live: Bind+Describe+Sync with
22
+ * no explicit BEGIN left transactionStatus 'I' and a later Execute on the
23
+ * same named portal failed with "portal ... does not exist").
24
+ */
25
+ async bindAndRetrieveFields(params, queryOptions) {
20
26
  const intoCon = getIntlConnection(this.connection);
21
27
  intoCon.ref();
22
28
  try {
23
29
  const socket = intoCon.socket;
24
- this._columnFormat =
25
- queryOptions.columnFormat != null
26
- ? queryOptions.columnFormat
27
- : Protocol.DataFormat.binary;
28
- socket.sendBindMessage({
29
- typeMap: queryOptions.typeMap || GlobalTypeMap,
30
- statement: this._statement.name,
31
- portal: this.name,
32
- paramTypes: this._statement.paramTypes,
33
- params,
34
- queryOptions,
35
- });
36
- socket.sendFlushMessage();
37
- return await socket.capture(async (code, msg, done) => {
30
+ return await socket.sendBindDescribeMessages({
31
+ bind: {
32
+ typeMap: queryOptions.typeMap || GlobalTypeMap,
33
+ statement: this._statement.name,
34
+ portal: this.name,
35
+ paramTypes: this._statement.paramTypes,
36
+ params,
37
+ queryOptions,
38
+ },
39
+ describe: { type: 'P', name: this.name },
40
+ }, (code, msg, done) => {
38
41
  switch (code) {
39
42
  case Protocol.BackendMessageCode.BindComplete:
40
- done();
41
- break;
42
- case Protocol.BackendMessageCode.NoticeResponse:
43
- break;
44
- default:
45
- done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
46
- }
47
- });
48
- }
49
- finally {
50
- intoCon.unref();
51
- }
52
- }
53
- async retrieveFields() {
54
- const intoCon = getIntlConnection(this.connection);
55
- intoCon.ref();
56
- try {
57
- const socket = intoCon.socket;
58
- socket.sendDescribeMessage({ type: 'P', name: this.name });
59
- socket.sendFlushMessage();
60
- return await socket.capture(async (code, msg, done) => {
61
- switch (code) {
62
43
  case Protocol.BackendMessageCode.NoticeResponse:
63
44
  break;
64
45
  case Protocol.BackendMessageCode.NoData:
@@ -67,6 +48,9 @@ export class Portal {
67
48
  case Protocol.BackendMessageCode.RowDescription:
68
49
  done(undefined, msg.fields);
69
50
  break;
51
+ case Protocol.BackendMessageCode.ErrorResponse:
52
+ done(msg);
53
+ break;
70
54
  default:
71
55
  done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
72
56
  }
@@ -81,13 +65,11 @@ export class Portal {
81
65
  intoCon.ref();
82
66
  try {
83
67
  const socket = intoCon.socket;
84
- socket.sendExecuteMessage({
68
+ const rows = [];
69
+ const executePromise = socket.sendExecuteMessage({
85
70
  portal: this.name,
86
71
  fetchCount: fetchCount || 100,
87
- });
88
- socket.sendFlushMessage();
89
- const rows = [];
90
- return await socket.capture(async (code, msg, done) => {
72
+ }, (code, msg, done) => {
91
73
  switch (code) {
92
74
  case Protocol.BackendMessageCode.NoticeResponse:
93
75
  break;
@@ -95,16 +77,12 @@ export class Portal {
95
77
  done(undefined, { code });
96
78
  break;
97
79
  case Protocol.BackendMessageCode.DataRow:
98
- if (Array.isArray(this._columnFormat)) {
99
- rows.push(msg.columns.map((buf, i) => this._columnFormat[i] ===
100
- Protocol.DataFormat.text
101
- ? buf.toString('utf8')
102
- : buf));
103
- }
104
- else if (this._columnFormat === Protocol.DataFormat.binary)
105
- rows.push(msg.columns);
106
- else
107
- rows.push(msg.columns.map((buf) => buf.toString('utf8')));
80
+ // msg is the whole DataRowMessage (columnCount + one raw
81
+ // buffer for the whole row) - per-column boundary-finding
82
+ // and decode now happens lazily in parseRow/get-parsers.ts
83
+ // instead of eagerly here, so this can just pass it straight
84
+ // through regardless of this._columnFormat's shape.
85
+ rows.push(msg);
108
86
  break;
109
87
  case Protocol.BackendMessageCode.PortalSuspended:
110
88
  done(undefined, { code, rows });
@@ -117,31 +95,47 @@ export class Portal {
117
95
  rowCount: msg.rowCount,
118
96
  });
119
97
  break;
98
+ case Protocol.BackendMessageCode.ErrorResponse:
99
+ done(msg);
100
+ break;
120
101
  default:
121
102
  done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
122
103
  }
123
104
  });
105
+ socket.sendFlushMessage();
106
+ return await executePromise;
124
107
  }
125
108
  finally {
126
109
  intoCon.unref();
127
110
  }
128
111
  }
112
+ /**
113
+ * Close + Sync as one request (see PgSocket.sendCloseAndSyncMessages()
114
+ * for why they must share a single capture): ReadyForQuery is the end
115
+ * marker, CloseComplete is informational on the way there. That also
116
+ * makes this safe to call as cleanup after a failed Bind - the server is
117
+ * in error state then, skips the Close entirely, and answers only the
118
+ * Sync.
119
+ */
129
120
  async close() {
130
121
  const intoCon = getIntlConnection(this.connection);
131
122
  intoCon.ref();
132
123
  try {
133
124
  const socket = intoCon.socket;
134
- socket.sendCloseMessage({ type: 'P', name: this.name });
135
- socket.sendSyncMessage();
136
- return await socket.capture(async (code, msg, done) => {
125
+ let error;
126
+ await socket.sendCloseAndSyncMessages({ type: 'P', name: this.name }, (code, msg, done) => {
137
127
  switch (code) {
128
+ case Protocol.BackendMessageCode.CloseComplete:
138
129
  case Protocol.BackendMessageCode.NoticeResponse:
139
130
  break;
140
- case Protocol.BackendMessageCode.CloseComplete:
131
+ case Protocol.BackendMessageCode.ErrorResponse:
132
+ // Deferred: only ReadyForQuery is guaranteed to arrive
133
+ // exactly once, so done() waits for it.
134
+ error = msg;
141
135
  break;
142
136
  case Protocol.BackendMessageCode.ReadyForQuery:
143
137
  intoCon.transactionStatus = msg.status;
144
- done();
138
+ done(error);
145
139
  break;
146
140
  default:
147
141
  done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));