postgrejs 2.23.0 → 3.0.1

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 (109) hide show
  1. package/CHANGELOG.md +162 -1
  2. package/LICENSE +26 -17
  3. package/README.md +195 -175
  4. package/connection/connection.d.ts +152 -13
  5. package/connection/connection.js +296 -49
  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 +68 -3
  10. package/connection/intl-connection.js +414 -57
  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 +117 -97
  21. package/data-type-map.d.ts +2 -2
  22. package/data-type-map.js +4 -2
  23. package/data-types/bool-type.js +14 -10
  24. package/data-types/box-type.js +15 -8
  25. package/data-types/bytea-type.js +9 -5
  26. package/data-types/char-type.js +11 -5
  27. package/data-types/circle-type.js +15 -7
  28. package/data-types/date-type.js +25 -19
  29. package/data-types/float4-type.js +10 -3
  30. package/data-types/float8-type.js +12 -3
  31. package/data-types/int2-type.js +9 -4
  32. package/data-types/int2-vector-type.js +10 -4
  33. package/data-types/int4-type.js +11 -5
  34. package/data-types/int8-type.js +22 -7
  35. package/data-types/json-type.js +17 -8
  36. package/data-types/jsonb-type.js +20 -14
  37. package/data-types/lseg-type.js +16 -8
  38. package/data-types/numeric-type.d.ts +8 -0
  39. package/data-types/numeric-type.js +112 -8
  40. package/data-types/oid-type.d.ts +0 -1
  41. package/data-types/oid-type.js +9 -10
  42. package/data-types/oid-vector-type.d.ts +9 -0
  43. package/data-types/oid-vector-type.js +51 -0
  44. package/data-types/point-type.js +14 -6
  45. package/data-types/time-type.js +20 -13
  46. package/data-types/timestamp-type.js +32 -25
  47. package/data-types/timestamptz-type.d.ts +1 -1
  48. package/data-types/timestamptz-type.js +34 -21
  49. package/data-types/uuid-type.js +21 -11
  50. package/data-types/varchar-type.js +16 -4
  51. package/index.d.ts +5 -0
  52. package/index.js +5 -0
  53. package/interfaces/data-type.d.ts +5 -3
  54. package/interfaces/database-connection-params.d.ts +95 -0
  55. package/interfaces/query-options.d.ts +17 -0
  56. package/interfaces/script-execute-options.d.ts +17 -0
  57. package/package.json +3 -4
  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/database-error.js +1 -0
  65. package/protocol/frontend.d.ts +16 -0
  66. package/protocol/frontend.js +58 -6
  67. package/protocol/pg-socket.d.ts +221 -10
  68. package/protocol/pg-socket.js +539 -102
  69. package/protocol/pgoutput.d.ts +76 -0
  70. package/protocol/pgoutput.js +155 -0
  71. package/protocol/protocol.d.ts +3 -4
  72. package/protocol/protocol.js +1 -1
  73. package/protocol/sasl.d.ts +11 -1
  74. package/protocol/sasl.js +35 -4
  75. package/protocol/smart-buffer.d.ts +0 -2
  76. package/protocol/smart-buffer.js +11 -16
  77. package/types.d.ts +5 -3
  78. package/util/abort-signal.d.ts +24 -0
  79. package/util/abort-signal.js +63 -0
  80. package/util/array-calculatedim.js +3 -1
  81. package/util/bigint-methods.js +1 -1
  82. package/util/connection-config.js +77 -3
  83. package/util/decode-binaryarray.d.ts +1 -1
  84. package/util/decode-binaryarray.js +15 -6
  85. package/util/encode-binaryarray.js +7 -3
  86. package/util/escape-identifier.d.ts +14 -0
  87. package/util/escape-identifier.js +32 -0
  88. package/util/escape-literal.js +3 -0
  89. package/util/fast-parseint.d.ts +1 -0
  90. package/util/fast-parseint.js +12 -17
  91. package/util/format-datetime.d.ts +10 -0
  92. package/util/format-datetime.js +71 -0
  93. package/util/get-parsers.js +31 -10
  94. package/util/parse-array.js +2 -2
  95. package/util/parse-bytea.d.ts +2 -0
  96. package/util/parse-bytea.js +83 -0
  97. package/util/parse-datetime.d.ts +3 -1
  98. package/util/parse-datetime.js +86 -17
  99. package/util/parse-row.d.ts +3 -1
  100. package/util/parse-row.js +60 -5
  101. package/util/parse-time.js +8 -1
  102. package/util/sql-tag.d.ts +60 -0
  103. package/util/sql-tag.js +202 -0
  104. package/util/starts-transaction.d.ts +38 -0
  105. package/util/starts-transaction.js +148 -0
  106. package/util/stringify-arrayliteral.js +13 -1
  107. package/util/wrap-row-description.js +13 -4
  108. package/util/convert-row-to-object.d.ts +0 -2
  109. package/util/convert-row-to-object.js +0 -9
@@ -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)})`));
@@ -8,9 +8,11 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
8
8
  private readonly _connection;
9
9
  private readonly _sql;
10
10
  private readonly _name;
11
- private readonly _paramTypes;
11
+ private readonly _paramTypes?;
12
+ private _fields?;
12
13
  protected _onErrorSavePoint: string;
13
14
  private _refCount;
15
+ private _closed;
14
16
  constructor(connection: Connection, sql: string, paramTypes?: OID[]);
15
17
  static prepare(connection: Connection, sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
16
18
  get connection(): Connection;
@@ -18,9 +20,28 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
18
20
  get sql(): string;
19
21
  get paramTypes(): Maybe<Maybe<OID>[]>;
20
22
  execute(options?: QueryOptions): Promise<QueryResult>;
23
+ protected _executeWithTransaction(options?: QueryOptions): Promise<QueryResult>;
21
24
  close(): Promise<void>;
25
+ /**
26
+ * Asks the server to cancel whatever this statement's connection is
27
+ * currently running. See Connection.cancel(); prefer the per-call `signal`
28
+ * option, which reports the abort to the caller that asked for it.
29
+ */
22
30
  cancel(): Promise<void>;
23
31
  protected _execute(options?: QueryOptions): Promise<QueryResult>;
32
+ /**
33
+ * Called by Cursor.close() instead of separately awaiting
34
+ * Portal.close() then this.close(). Returns false (no wire action taken,
35
+ * caller must still close just the portal itself) when either this
36
+ * statement was already fully closed by someone else (mirrors close()'s
37
+ * own `if (this._closed) return;` idempotency guard - re-sending
38
+ * Close(statement) for an already-closed name would error) or the
39
+ * decremented refcount is still > 0 (statement still referenced
40
+ * elsewhere - e.g. another cursor from the same PreparedStatement, or
41
+ * the statement's own +1 from prepare()). Returns true only when it
42
+ * actually performed the combined Close(portal)+Close(statement)+Sync.
43
+ */
44
+ _maybeCloseWithPortal(portalName: string): Promise<boolean>;
24
45
  protected _close(): Promise<void>;
25
46
  [Symbol.asyncDispose](): Promise<void>;
26
47
  }