postgrejs 3.5.0 → 3.6.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.
- package/CHANGELOG.md +40 -1
- package/README.md +22 -4
- package/connection/connection.js +13 -7
- package/connection/cursor.js +22 -1
- package/connection/intl-connection.d.ts +1 -1
- package/connection/intl-connection.js +110 -41
- package/connection/pool.d.ts +22 -0
- package/connection/pool.js +73 -9
- package/connection/portal.d.ts +1 -1
- package/connection/portal.js +3 -2
- package/connection/prepared-statement.js +8 -4
- package/data-type-map.d.ts +11 -0
- package/data-type-map.js +20 -2
- package/data-types/char-type.js +15 -0
- package/data-types/date-type.js +4 -18
- package/data-types/float4-type.js +79 -1
- package/data-types/json-type.js +7 -12
- package/data-types/jsonb-type.js +7 -12
- package/data-types/time-type.js +2 -15
- package/data-types/timestamp-type.js +8 -29
- package/data-types/timestamptz-type.js +7 -31
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/batch-result.d.ts +1 -1
- package/interfaces/command-result.d.ts +21 -1
- package/interfaces/data-mapping-options.d.ts +16 -0
- package/interfaces/data-type.d.ts +11 -0
- package/interfaces/query-options.d.ts +25 -3
- package/package.json +1 -1
- package/protocol/connection-lost-error.d.ts +40 -0
- package/protocol/connection-lost-error.js +54 -0
- package/protocol/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- package/protocol/pg-socket.d.ts +7 -0
- package/protocol/pg-socket.js +26 -2
- package/util/channel-name.d.ts +24 -0
- package/util/channel-name.js +44 -0
- package/util/get-parsers.d.ts +2 -1
- package/util/get-parsers.js +16 -1
- package/util/resolve-column-formats.d.ts +33 -0
- package/util/resolve-column-formats.js +67 -0
- package/util/wrap-row-description.d.ts +2 -1
- package/util/wrap-row-description.js +7 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.
|
|
3
|
+
### [v3.6.1](https://github.com/panates/postgrejs/compare/v3.6.0...v3.6.1) -
|
|
4
|
+
|
|
5
|
+
#### 🚀 New Features
|
|
6
|
+
|
|
7
|
+
- feat: report a pooled connection that dies @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### 🛠 Refactoring and Updates
|
|
10
|
+
|
|
11
|
+
- refactor: give a lost connection its own error type @Eray Hanoğlu
|
|
12
|
+
|
|
13
|
+
#### 💬 General Changes
|
|
14
|
+
|
|
15
|
+
- ci: pin the runner image and stop naming the PostgreSQL it ships @Eray Hanoğlu
|
|
16
|
+
|
|
17
|
+
### [v3.6.0](https://github.com/panates/postgrejs/compare/v3.5.0...v3.6.0) - 20 September 2026
|
|
18
|
+
|
|
19
|
+
#### 🚀 New Features
|
|
20
|
+
|
|
21
|
+
- feat: make fetchAsString request text from the server @Eray Hanoğlu
|
|
22
|
+
|
|
23
|
+
#### 🪲 Fixes
|
|
24
|
+
|
|
25
|
+
- fix: decode float4 as the number the server would print @Eray Hanoğlu
|
|
26
|
+
- fix: report rowsAffected for MERGE @Eray Hanoğlu
|
|
27
|
+
- fix: quote LISTEN channel names instead of restricting them @Eray Hanoğlu
|
|
28
|
+
- fix: stop a pooled cursor's connection going back into the pool @Eray Hanoğlu
|
|
29
|
+
- fix: stop inferring "char" for one-character strings @Eray Hanoğlu
|
|
30
|
+
- fix: copy both indexes when a DataTypeMap is built from another @Eray Hanoğlu
|
|
31
|
+
- fix: emit 'notification' once per NOTIFY @Eray Hanoğlu
|
|
32
|
+
- fix: accept single-character savepoint names @Eray Hanoğlu
|
|
33
|
+
|
|
34
|
+
#### 📖 Documentation Changes
|
|
35
|
+
|
|
36
|
+
- docs: record what 3.6 changes for existing code @Eray Hanoğlu
|
|
37
|
+
|
|
38
|
+
#### 💬 General Changes
|
|
39
|
+
|
|
40
|
+
- feat!: fetch every row by default instead of the first 100 @Eray Hanoğlu
|
|
41
|
+
|
|
42
|
+
### [v3.5.0](https://github.com/panates/postgrejs/compare/v3.4.0...v3.5.0) - 16 September 2026
|
|
4
43
|
|
|
5
44
|
#### 🚀 New Features
|
|
6
45
|
|
package/README.md
CHANGED
|
@@ -59,6 +59,9 @@ $ npm install postgrejs --save
|
|
|
59
59
|
Please read :small_orange_diamond: [DOCUMENTATION](https://www.postgrejs.com/) :small_orange_diamond: for detailed
|
|
60
60
|
usage.
|
|
61
61
|
|
|
62
|
+
Upgrading from 3.5? See [doc/MIGRATION-v3.5-to-v3.6.md](doc/MIGRATION-v3.5-to-v3.6.md) - `query()` no longer stops at
|
|
63
|
+
100 rows, and `fetchAsString` now returns the server's own text.
|
|
64
|
+
|
|
62
65
|
## Library Overview
|
|
63
66
|
|
|
64
67
|
- **Language:** Pure JavaScript, with no native/binary dependencies to compile or ship.
|
|
@@ -71,7 +74,11 @@ usage.
|
|
|
71
74
|
## Features
|
|
72
75
|
|
|
73
76
|
- **Connection Management:** Supports both single connection and advanced pooling, providing scalability and efficient
|
|
74
|
-
resource management.
|
|
77
|
+
resource management. A connection that dies rather than being closed says so: `'close'` carries the reason, and a
|
|
78
|
+
pooled one is reported on the pool's `'destroy'` and `'error'` events - so an admin kill or a failover is
|
|
79
|
+
distinguishable from an ordinary eviction, including when the connection was sitting idle with no query to reject.
|
|
80
|
+
The reason is a `ConnectionLostError` carrying the backend's `processID`, and `code` `'08006'` so branching on it
|
|
81
|
+
needs no `instanceof`.
|
|
75
82
|
- **Binary Wire Protocol:** Implements the full binary wire protocol for all PostgreSQL data types, ensuring robust and
|
|
76
83
|
efficient data handling.
|
|
77
84
|
- **Prepared Statements:** Named prepared statements for optimized query execution, and a per-connection cache that
|
|
@@ -82,16 +89,23 @@ usage.
|
|
|
82
89
|
statement leaves the transaction usable instead of aborting the whole block. The `SAVEPOINT`/`RELEASE` pair travels
|
|
83
90
|
in the statement's own round trip rather than costing one each - 36µs for the pair, against 507µs sent separately.
|
|
84
91
|
`rollbackOnError: false` to opt out.
|
|
92
|
+
- **Complete Results:** `query()` returns every row the statement produced. A `fetchCount` limit is a limit rather
|
|
93
|
+
than a silent cap: the result carries `suspended: true` when the server stopped at it, so a prefix can never be
|
|
94
|
+
mistaken for the whole answer. `0` means unlimited, as it does in the protocol.
|
|
85
95
|
- **Cursors:** Features fast double-link cache cursors for efficient data retrieval, iterable with `for await` a row
|
|
86
|
-
at a time and closed when the loop ends - by exhaustion, a `break`, or a throw.
|
|
96
|
+
at a time and closed when the loop ends - by exhaustion, a `break`, or a throw. A cursor reads through a portal, and
|
|
97
|
+
a portal lives as long as the transaction that created it - `Pool.query()` keeps its connection out of the pool
|
|
98
|
+
until the cursor closes, and on a bare `Connection` open the cursor inside a transaction if anything else will run
|
|
99
|
+
on that connection meanwhile.
|
|
87
100
|
- **Scoped Transactions:** `transaction(fn)` commits when the callback returns and rolls back when it throws, on a
|
|
88
101
|
connection or straight from the pool. A call made while a transaction is already open takes a savepoint rather than
|
|
89
102
|
a second `BEGIN`, so a nested scope that fails rolls back its own work and leaves the outer transaction standing.
|
|
90
103
|
- **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
|
|
91
104
|
reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
|
|
92
105
|
- **Notifications:** High-level implementation for PostgreSQL notifications (LISTEN/NOTIFY), enabling real-time data
|
|
93
|
-
updates.
|
|
94
|
-
- **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements.
|
|
106
|
+
updates. Channel names are quoted rather than pattern-matched, so any name PostgreSQL accepts works.
|
|
107
|
+
- **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements. A type can set
|
|
108
|
+
`inferrable: false` to stay out of parameter type inference while still decoding its own columns.
|
|
95
109
|
- **Parameter Binding:** Bind parameters with OID mappings for precise and efficient query execution.
|
|
96
110
|
- **Array Handling:** Supports multidimensional arrays with fast binary encoding/decoding.
|
|
97
111
|
- **Performance Optimization:** Low memory utilization and boosted performance through the use of shared buffers.
|
|
@@ -119,6 +133,10 @@ usage.
|
|
|
119
133
|
`rollbackPrepared()`, from any connection.
|
|
120
134
|
- **Cancellation:** Any call takes an `AbortSignal`, which also gives per-query timeouts via `AbortSignal.timeout()`.
|
|
121
135
|
- **Flexible Data Retrieval:** Can return both array and object rows to suit different data processing needs.
|
|
136
|
+
- **Values as Text:** `fetchAsString: [DataTypeOIDs.int8]` asks the server for those columns in its own text format
|
|
137
|
+
and hands the bytes back unparsed - so `count(*)` arrives as `'3'`, one consistent type where a decoded `int8` is a
|
|
138
|
+
`number` or a `BigInt` depending on magnitude. Any OID, arrays included. The string is PostgreSQL's own rendering
|
|
139
|
+
rather than one reconstructed from the binary value, so it cannot drift from what the server would print.
|
|
122
140
|
- **Resource Management:** Auto disposal of resources with the "using" syntax
|
|
123
141
|
([TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)), ensuring
|
|
124
142
|
efficient resource cleanup.
|
package/connection/connection.js
CHANGED
|
@@ -2,6 +2,8 @@ import { ConnectionState, DataTypeOIDs } from '../constants.js';
|
|
|
2
2
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
3
3
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
4
4
|
import { withAbortSignal } from '../util/abort-signal.js';
|
|
5
|
+
import { normalizeChannelName } from '../util/channel-name.js';
|
|
6
|
+
import { escapeIdentifier } from '../util/escape-identifier.js';
|
|
5
7
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
6
8
|
import { BindParam } from './bind-param.js';
|
|
7
9
|
import { IntlConnection } from './intl-connection.js';
|
|
@@ -599,8 +601,7 @@ export class Connection extends SafeEventEmitter {
|
|
|
599
601
|
return this._captureErrorStack(this._intlCon.releaseSavepoint(name, immediate));
|
|
600
602
|
}
|
|
601
603
|
async listen(channel, callback) {
|
|
602
|
-
|
|
603
|
-
throw new TypeError(`Invalid channel name`);
|
|
604
|
+
channel = normalizeChannelName(channel);
|
|
604
605
|
if (!this._notificationListeners) {
|
|
605
606
|
this._notificationListeners = new SafeEventEmitter();
|
|
606
607
|
this._intlCon.on('notification', (msg) => this._handleNotification(msg));
|
|
@@ -613,14 +614,13 @@ export class Connection extends SafeEventEmitter {
|
|
|
613
614
|
const alreadyListening = !!this._notificationListeners.listenerCount(channel);
|
|
614
615
|
this._notificationListeners.on(channel, callback);
|
|
615
616
|
if (!alreadyListening)
|
|
616
|
-
await this._captureErrorStack(this.query('LISTEN ' + channel));
|
|
617
|
+
await this._captureErrorStack(this.query('LISTEN ' + escapeIdentifier(channel)));
|
|
617
618
|
}
|
|
618
619
|
async unListen(channel) {
|
|
619
|
-
|
|
620
|
-
throw new TypeError(`Invalid channel name`);
|
|
620
|
+
channel = normalizeChannelName(channel);
|
|
621
621
|
if (this._notificationListeners?.listenerCount(channel)) {
|
|
622
622
|
this._notificationListeners?.removeAllListeners(channel);
|
|
623
|
-
await this._captureErrorStack(this.query('UNLISTEN ' + channel));
|
|
623
|
+
await this._captureErrorStack(this.query('UNLISTEN ' + escapeIdentifier(channel)));
|
|
624
624
|
}
|
|
625
625
|
}
|
|
626
626
|
async unListenAll() {
|
|
@@ -682,7 +682,13 @@ export class Connection extends SafeEventEmitter {
|
|
|
682
682
|
return new LargeObject(this, oid, Number(opened.rows?.[0][0]), ownsTransaction);
|
|
683
683
|
}
|
|
684
684
|
_handleNotification(msg) {
|
|
685
|
-
this.emit('notification', msg)
|
|
685
|
+
// Deliberately no `this.emit('notification', msg)` here:
|
|
686
|
+
// IntlConnection.emit() re-emits every event on its owner, which is
|
|
687
|
+
// this Connection, so the raw event has already been delivered by the
|
|
688
|
+
// time this runs. Emitting it again fired 'notification' twice for one
|
|
689
|
+
// NOTIFY - but only once listen() had been called, since that is what
|
|
690
|
+
// installs this handler, so the same listener saw one event or two
|
|
691
|
+
// depending on whether anything else had subscribed.
|
|
686
692
|
this._notificationListeners?.emit(msg.channel, msg);
|
|
687
693
|
}
|
|
688
694
|
async _close() {
|
package/connection/cursor.js
CHANGED
|
@@ -69,7 +69,28 @@ export class Cursor extends SafeEventEmitter {
|
|
|
69
69
|
await this._taskQueue
|
|
70
70
|
.enqueue(async () => {
|
|
71
71
|
const queryOptions = this._queryOptions;
|
|
72
|
-
|
|
72
|
+
let r;
|
|
73
|
+
try {
|
|
74
|
+
r = await portal.execute(queryOptions.fetchCount ?? 100);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
// 34000 here means the portal is gone, and the only thing that
|
|
78
|
+
// takes a portal away mid-cursor is another statement on this
|
|
79
|
+
// connection: its Sync ends the implicit transaction the portal
|
|
80
|
+
// lives in. The server's own message says nothing about why, and
|
|
81
|
+
// a caller cannot act on it without knowing. Closing first keeps
|
|
82
|
+
// the statement from leaking and lets whoever is holding the
|
|
83
|
+
// connection for this cursor (Pool.query()) have it back.
|
|
84
|
+
if (e?.code === '34000') {
|
|
85
|
+
await this.close().catch(() => undefined);
|
|
86
|
+
e.message +=
|
|
87
|
+
' - the portal was destroyed by another statement running on' +
|
|
88
|
+
' the same connection, which ends the implicit transaction it' +
|
|
89
|
+
' lives in. Open the cursor inside an explicit transaction, or' +
|
|
90
|
+
' give it a connection of its own.';
|
|
91
|
+
}
|
|
92
|
+
throw e;
|
|
93
|
+
}
|
|
73
94
|
if (r && r.rows && r.rows.length) {
|
|
74
95
|
const rows = r.rows;
|
|
75
96
|
if (this._parsers) {
|
|
@@ -245,7 +245,7 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
245
245
|
* so repeated executes of the same statement don't rebuild them.
|
|
246
246
|
* Shared by executeReused() and executeBatchReused().
|
|
247
247
|
*/
|
|
248
|
-
protected _resolveReusedParsers(cachedFields: Protocol.RowDescription[], typeMap: DataTypeMap, options: QueryOptions): {
|
|
248
|
+
protected _resolveReusedParsers(cachedFields: Protocol.RowDescription[], typeMap: DataTypeMap, options: QueryOptions, columnFormat: Protocol.DataFormat | Protocol.DataFormat[]): {
|
|
249
249
|
parsers: AnyParseFunction[];
|
|
250
250
|
resultFields: FieldInfo[];
|
|
251
251
|
};
|
|
@@ -9,6 +9,7 @@ import { getConnectionConfig } from '../util/connection-config.js';
|
|
|
9
9
|
import { buildCopySql, buildProbeSql, writeCopyBinaryRows, } from '../util/copy-from-rows.js';
|
|
10
10
|
import { escapeLiteral } from '../util/escape-literal.js';
|
|
11
11
|
import { getParsers } from '../util/get-parsers.js';
|
|
12
|
+
import { fetchAsStringEqual, resolveColumnFormats, } from '../util/resolve-column-formats.js';
|
|
12
13
|
import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
|
|
13
14
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
14
15
|
import { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
@@ -27,6 +28,21 @@ function columnFormatsEqual(a, b) {
|
|
|
27
28
|
return false;
|
|
28
29
|
return true;
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Whether a command tag's row count describes rows the statement changed,
|
|
33
|
+
* rather than rows it returned or moved. SELECT, FETCH, MOVE and COPY all
|
|
34
|
+
* carry a count of their own that would be misleading as `rowsAffected`.
|
|
35
|
+
*
|
|
36
|
+
* MERGE has reported its own total (inserted + updated + deleted) since
|
|
37
|
+
* PostgreSQL 15; _handleCommandComplete() has always parsed it, and this
|
|
38
|
+
* is what stops it being dropped on the floor.
|
|
39
|
+
*/
|
|
40
|
+
function reportsRowsAffected(command) {
|
|
41
|
+
return (command === 'INSERT' ||
|
|
42
|
+
command === 'UPDATE' ||
|
|
43
|
+
command === 'DELETE' ||
|
|
44
|
+
command === 'MERGE');
|
|
45
|
+
}
|
|
30
46
|
/**
|
|
31
47
|
* Uses of the same SQL before it is worth preparing.
|
|
32
48
|
*
|
|
@@ -80,7 +96,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
80
96
|
this._config = Object.freeze(getConnectionConfig(config));
|
|
81
97
|
this.socket = new PgSocket(this._config);
|
|
82
98
|
this.socket.on('error', err => this._onError(err));
|
|
83
|
-
|
|
99
|
+
// The reason travels with it: 'close' on its own cannot tell an
|
|
100
|
+
// ordinary shutdown from the backend being terminated under us.
|
|
101
|
+
this.socket.on('close', (reason) => this.emit('close', reason));
|
|
84
102
|
this.socket.on('notification', payload => this.emit('notification', payload));
|
|
85
103
|
this.socket.on('connecting', () => this.emit('connecting'));
|
|
86
104
|
this._onErrorSavePoint = 'SP_' + Math.round(Math.random() * 100000000);
|
|
@@ -147,7 +165,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
147
165
|
// IntlConnection TWICE per close() call - confirmed live: a single
|
|
148
166
|
// close() left two listeners registered on the far side (e.g. two
|
|
149
167
|
// NOTIFY deliveries after Pool's reconnect re-subscribed twice).
|
|
150
|
-
this.socket.once('close', resolve);
|
|
168
|
+
this.socket.once('close', () => resolve());
|
|
151
169
|
this.socket.sendTerminateMessage(() => {
|
|
152
170
|
this.socket.close();
|
|
153
171
|
});
|
|
@@ -267,7 +285,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
267
285
|
* nesting the existing one.
|
|
268
286
|
*/
|
|
269
287
|
async savepoint(name) {
|
|
270
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
288
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
271
289
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
272
290
|
const depth = (this._savepointDepths.get(name) || 0) + 1;
|
|
273
291
|
this._savepointDepths.set(name, depth);
|
|
@@ -299,7 +317,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
299
317
|
* @param immediate - Ignores the tracked depth and releases right away.
|
|
300
318
|
*/
|
|
301
319
|
async releaseSavepoint(name, immediate) {
|
|
302
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
320
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
303
321
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
304
322
|
const depth = this._savepointDepths.get(name) || 0;
|
|
305
323
|
if (!immediate && depth > 1) {
|
|
@@ -359,7 +377,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
359
377
|
* unwind.
|
|
360
378
|
*/
|
|
361
379
|
async rollbackToSavepoint(name) {
|
|
362
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
380
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
363
381
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
364
382
|
this._savepointDepths.delete(name);
|
|
365
383
|
await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
|
|
@@ -523,8 +541,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
523
541
|
break;
|
|
524
542
|
case Protocol.BackendMessageCode.RowDescription:
|
|
525
543
|
fields = msg.fields;
|
|
526
|
-
|
|
527
|
-
|
|
544
|
+
// Simple Query has no Bind at all, so every column is
|
|
545
|
+
// already text - a fetchAsString column simply comes back
|
|
546
|
+
// unparsed.
|
|
547
|
+
parsers = getParsers(typeMap, fields, options);
|
|
548
|
+
current.fields = wrapRowDescription(typeMap, fields, DataFormat.text, options);
|
|
528
549
|
current.rows = [];
|
|
529
550
|
break;
|
|
530
551
|
case Protocol.BackendMessageCode.DataRow:
|
|
@@ -539,11 +560,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
539
560
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
540
561
|
// Ignore BEGIN command that we added to sql
|
|
541
562
|
current.command = msg.command;
|
|
542
|
-
if (current.command
|
|
543
|
-
current.command === 'INSERT' ||
|
|
544
|
-
current.command === 'UPDATE') {
|
|
563
|
+
if (reportsRowsAffected(current.command))
|
|
545
564
|
current.rowsAffected = msg.rowCount;
|
|
546
|
-
}
|
|
547
565
|
if (timingEnabled)
|
|
548
566
|
current.executeTime = performance.now() - currentStart;
|
|
549
567
|
if (current.rows)
|
|
@@ -659,7 +677,13 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
659
677
|
return result;
|
|
660
678
|
}
|
|
661
679
|
}
|
|
662
|
-
|
|
680
|
+
// A fetchAsString query needs its RowDescription before the Bind that
|
|
681
|
+
// asks for those columns as text, and only a named statement has one
|
|
682
|
+
// in hand ahead of time. Preparing on first sight pays a single
|
|
683
|
+
// Parse+Describe round trip per connection instead of leaving every
|
|
684
|
+
// call until the statement earns a name asking for the whole row as
|
|
685
|
+
// text (what queryOnce() falls back to).
|
|
686
|
+
if (!options.fetchAsString?.length && !this._earnsAName(key))
|
|
663
687
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
664
688
|
const name = 'C_' + ++this._preparedCounter;
|
|
665
689
|
// A Parse that fails caches nothing and reports itself; the statement
|
|
@@ -734,7 +758,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
734
758
|
let resultFields;
|
|
735
759
|
let commandTag;
|
|
736
760
|
let error;
|
|
761
|
+
let suspended = false;
|
|
737
762
|
const rowDecoder = resolveRowDecoder(options);
|
|
763
|
+
// fetchAsString names OIDs, but Bind's result format codes are
|
|
764
|
+
// positional - and this path's Describe answers only after the Bind
|
|
765
|
+
// that would have to carry them. With no column types to map the
|
|
766
|
+
// list onto, the whole row is asked for as text instead: the listed
|
|
767
|
+
// columns then arrive exactly as the server renders them, which is
|
|
768
|
+
// what was asked for, and the rest cost a text decode.
|
|
769
|
+
//
|
|
770
|
+
// Only `prepare: false` normally reaches here with a list set -
|
|
771
|
+
// _queryCached() prepares a fetchAsString query on first sight
|
|
772
|
+
// precisely so it can send per-column codes from the second call on
|
|
773
|
+
// (from the first, for every connection that has seen the SQL).
|
|
774
|
+
const columnFormat = options.fetchAsString?.length
|
|
775
|
+
? DataFormat.text
|
|
776
|
+
: options.columnFormat != null
|
|
777
|
+
? options.columnFormat
|
|
778
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
738
779
|
// See executeReused() for what a savepoint riding along does to the
|
|
739
780
|
// CommandComplete stream.
|
|
740
781
|
let leadingCommandTags = savepoint ? 1 : 0;
|
|
@@ -742,9 +783,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
742
783
|
await this.socket
|
|
743
784
|
.sendExtendedQueryMessages({
|
|
744
785
|
parse: { sql, paramTypes },
|
|
745
|
-
bind: {
|
|
786
|
+
bind: {
|
|
787
|
+
typeMap,
|
|
788
|
+
paramTypes,
|
|
789
|
+
params,
|
|
790
|
+
queryOptions: options,
|
|
791
|
+
columnFormat,
|
|
792
|
+
},
|
|
746
793
|
describe: { type: 'P' },
|
|
747
|
-
execute: { fetchCount: options.fetchCount
|
|
794
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
748
795
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
749
796
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
750
797
|
}, (code, msg, done) => {
|
|
@@ -753,11 +800,17 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
753
800
|
case Protocol.BackendMessageCode.BindComplete:
|
|
754
801
|
case Protocol.BackendMessageCode.NoData:
|
|
755
802
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
803
|
+
break;
|
|
804
|
+
// The server stopped at the fetchCount limit rather than
|
|
805
|
+
// running out of rows. The Sync that closes this call
|
|
806
|
+
// discards the portal, so the rest is gone - all that can be
|
|
807
|
+
// done is say the result is a prefix.
|
|
756
808
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
809
|
+
suspended = true;
|
|
757
810
|
break;
|
|
758
811
|
case Protocol.BackendMessageCode.RowDescription:
|
|
759
|
-
parsers = getParsers(typeMap, msg.fields);
|
|
760
|
-
resultFields = wrapRowDescription(typeMap, msg.fields,
|
|
812
|
+
parsers = getParsers(typeMap, msg.fields, options);
|
|
813
|
+
resultFields = wrapRowDescription(typeMap, msg.fields, columnFormat, options);
|
|
761
814
|
result.fields = resultFields;
|
|
762
815
|
result.rowType = resolveRowType(options);
|
|
763
816
|
break;
|
|
@@ -796,11 +849,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
796
849
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
797
850
|
}
|
|
798
851
|
}
|
|
799
|
-
if (
|
|
800
|
-
result.
|
|
801
|
-
|
|
852
|
+
if (suspended)
|
|
853
|
+
result.suspended = true;
|
|
854
|
+
if (reportsRowsAffected(result.command))
|
|
802
855
|
result.rowsAffected = commandTag?.rowCount;
|
|
803
|
-
}
|
|
804
856
|
if (timingEnabled)
|
|
805
857
|
result.executeTime = performance.now() - startTime;
|
|
806
858
|
return result;
|
|
@@ -873,25 +925,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
873
925
|
* so repeated executes of the same statement don't rebuild them.
|
|
874
926
|
* Shared by executeReused() and executeBatchReused().
|
|
875
927
|
*/
|
|
876
|
-
_resolveReusedParsers(cachedFields, typeMap, options) {
|
|
877
|
-
const columnFormat = options.columnFormat != null
|
|
878
|
-
? options.columnFormat
|
|
879
|
-
: DEFAULT_COLUMN_FORMAT;
|
|
928
|
+
_resolveReusedParsers(cachedFields, typeMap, options, columnFormat) {
|
|
880
929
|
const cached = this._executeReusedParserCache.get(cachedFields);
|
|
881
930
|
if (cached &&
|
|
882
931
|
cached.typeMap === typeMap &&
|
|
883
|
-
columnFormatsEqual(cached.columnFormat, columnFormat)
|
|
932
|
+
columnFormatsEqual(cached.columnFormat, columnFormat) &&
|
|
933
|
+
fetchAsStringEqual(cached.fetchAsString, options.fetchAsString)) {
|
|
884
934
|
return { parsers: cached.parsers, resultFields: cached.resultFields };
|
|
885
935
|
}
|
|
886
936
|
const fields = cachedFields.map((f, i) => ({
|
|
887
937
|
...f,
|
|
888
938
|
format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
|
|
889
939
|
}));
|
|
890
|
-
const parsers = getParsers(typeMap, fields);
|
|
891
|
-
const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
|
|
940
|
+
const parsers = getParsers(typeMap, fields, options);
|
|
941
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
892
942
|
this._executeReusedParserCache.set(cachedFields, {
|
|
893
943
|
typeMap,
|
|
894
944
|
columnFormat,
|
|
945
|
+
fetchAsString: options.fetchAsString,
|
|
895
946
|
parsers,
|
|
896
947
|
resultFields,
|
|
897
948
|
});
|
|
@@ -974,6 +1025,16 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
974
1025
|
if (key != null && plan.name)
|
|
975
1026
|
named.set(key, plan);
|
|
976
1027
|
}
|
|
1028
|
+
// Only a statement whose columns are already known can carry
|
|
1029
|
+
// per-column codes; the rest fall back to asking for the whole row
|
|
1030
|
+
// as text, exactly as queryOnce() does and for the same reason.
|
|
1031
|
+
plan.columnFormat = plan.fields
|
|
1032
|
+
? resolveColumnFormats(plan.fields, options)
|
|
1033
|
+
: options.fetchAsString?.length
|
|
1034
|
+
? DataFormat.text
|
|
1035
|
+
: options.columnFormat != null
|
|
1036
|
+
? options.columnFormat
|
|
1037
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
977
1038
|
plans.push(plan);
|
|
978
1039
|
statements.push({
|
|
979
1040
|
// Per statement, not per pipeline: the statements differ, so a
|
|
@@ -988,6 +1049,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
988
1049
|
paramTypes: r.paramTypes,
|
|
989
1050
|
params: r.params,
|
|
990
1051
|
queryOptions: options,
|
|
1052
|
+
columnFormat: plan.columnFormat,
|
|
991
1053
|
},
|
|
992
1054
|
describe: sendDescribe ? { type: 'P' } : undefined,
|
|
993
1055
|
// Unlimited, never options.fetchCount: a suspended portal would
|
|
@@ -1025,7 +1087,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1025
1087
|
const fields = pendingFields || plan.fields;
|
|
1026
1088
|
const result = { command: msg?.command };
|
|
1027
1089
|
if (fields) {
|
|
1028
|
-
const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options);
|
|
1090
|
+
const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options, plan.columnFormat ?? DEFAULT_COLUMN_FORMAT);
|
|
1029
1091
|
result.fields = resultFields;
|
|
1030
1092
|
result.rowType = resolveRowType(options);
|
|
1031
1093
|
if (!result.command)
|
|
@@ -1038,9 +1100,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1038
1100
|
}
|
|
1039
1101
|
result.rows = rows;
|
|
1040
1102
|
}
|
|
1041
|
-
if (result.command
|
|
1042
|
-
result.command === 'INSERT' ||
|
|
1043
|
-
result.command === 'UPDATE')
|
|
1103
|
+
if (reportsRowsAffected(result.command))
|
|
1044
1104
|
result.rowsAffected = msg.rowCount;
|
|
1045
1105
|
pendingFields = undefined;
|
|
1046
1106
|
pendingRows = undefined;
|
|
@@ -1120,8 +1180,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1120
1180
|
const rowDecoder = resolveRowDecoder(options);
|
|
1121
1181
|
let parsers;
|
|
1122
1182
|
let resultFields;
|
|
1183
|
+
const columnFormat = resolveColumnFormats(cachedFields, options);
|
|
1123
1184
|
if (cachedFields) {
|
|
1124
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1185
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1125
1186
|
parsers = resolved.parsers;
|
|
1126
1187
|
resultFields = resolved.resultFields;
|
|
1127
1188
|
}
|
|
@@ -1134,6 +1195,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1134
1195
|
paramTypes,
|
|
1135
1196
|
params,
|
|
1136
1197
|
queryOptions: options,
|
|
1198
|
+
columnFormat,
|
|
1137
1199
|
}));
|
|
1138
1200
|
this.runningQueryCount++;
|
|
1139
1201
|
await this.socket
|
|
@@ -1155,9 +1217,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1155
1217
|
const item = {};
|
|
1156
1218
|
if (msg?.command)
|
|
1157
1219
|
item.command = msg.command;
|
|
1158
|
-
if (item.command
|
|
1159
|
-
item.command === 'INSERT' ||
|
|
1160
|
-
item.command === 'UPDATE')
|
|
1220
|
+
if (reportsRowsAffected(item.command))
|
|
1161
1221
|
item.rowsAffected = msg.rowCount;
|
|
1162
1222
|
if (pendingRows && parsers && resultFields) {
|
|
1163
1223
|
const l = pendingRows.length;
|
|
@@ -1225,9 +1285,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1225
1285
|
let resultFields;
|
|
1226
1286
|
let commandTag;
|
|
1227
1287
|
let error;
|
|
1288
|
+
let suspended = false;
|
|
1228
1289
|
const rowDecoder = resolveRowDecoder(options);
|
|
1290
|
+
// The whole point of reusing a prepared statement here: its
|
|
1291
|
+
// RowDescription is already in hand, so fetchAsString's OID list can
|
|
1292
|
+
// become the per-column result format codes this Bind carries, with
|
|
1293
|
+
// no extra round trip to find out what the columns are.
|
|
1294
|
+
const columnFormat = resolveColumnFormats(cachedFields, options);
|
|
1229
1295
|
if (cachedFields) {
|
|
1230
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1296
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1231
1297
|
parsers = resolved.parsers;
|
|
1232
1298
|
resultFields = resolved.resultFields;
|
|
1233
1299
|
result.fields = resultFields;
|
|
@@ -1247,8 +1313,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1247
1313
|
paramTypes,
|
|
1248
1314
|
params,
|
|
1249
1315
|
queryOptions: options,
|
|
1316
|
+
columnFormat,
|
|
1250
1317
|
},
|
|
1251
|
-
execute: { fetchCount: options.fetchCount
|
|
1318
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
1252
1319
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
1253
1320
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
1254
1321
|
}, (code, msg, done) => {
|
|
@@ -1256,7 +1323,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1256
1323
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
1257
1324
|
case Protocol.BackendMessageCode.BindComplete:
|
|
1258
1325
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1326
|
+
break;
|
|
1327
|
+
// See queryOnce() for what a suspended portal means here.
|
|
1259
1328
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
1329
|
+
suspended = true;
|
|
1260
1330
|
break;
|
|
1261
1331
|
case Protocol.BackendMessageCode.DataRow:
|
|
1262
1332
|
rows.push(msg);
|
|
@@ -1297,11 +1367,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1297
1367
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
1298
1368
|
}
|
|
1299
1369
|
}
|
|
1300
|
-
if (
|
|
1301
|
-
result.
|
|
1302
|
-
|
|
1370
|
+
if (suspended)
|
|
1371
|
+
result.suspended = true;
|
|
1372
|
+
if (reportsRowsAffected(result.command))
|
|
1303
1373
|
result.rowsAffected = commandTag?.rowCount;
|
|
1304
|
-
}
|
|
1305
1374
|
if (timingEnabled)
|
|
1306
1375
|
result.executeTime = performance.now() - startTime;
|
|
1307
1376
|
return result;
|
package/connection/pool.d.ts
CHANGED
|
@@ -37,6 +37,14 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
37
37
|
protected readonly _notificationListeners: SafeEventEmitter;
|
|
38
38
|
protected _notificationConnection?: Connection;
|
|
39
39
|
protected readonly _pipelineSlots: PipelineSlot[];
|
|
40
|
+
/**
|
|
41
|
+
* Why a connection is about to be destroyed, for the ones that were not
|
|
42
|
+
* destroyed on purpose. lightning-pool's own 'destroy' carries the
|
|
43
|
+
* resource and nothing else, so the reason is recorded on the way in -
|
|
44
|
+
* when the connection reports its close - and read back out when the
|
|
45
|
+
* pool reports the removal.
|
|
46
|
+
*/
|
|
47
|
+
protected readonly _destroyReasons: WeakMap<IntlConnection, Error>;
|
|
40
48
|
protected _pipelineMaxQueries: number;
|
|
41
49
|
protected _pipelineMaxConnections: number;
|
|
42
50
|
readonly config: PoolConfiguration;
|
|
@@ -91,6 +99,20 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
91
99
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|
|
92
100
|
unListen(channel: string): Promise<void>;
|
|
93
101
|
unListenAll(): Promise<void>;
|
|
102
|
+
/**
|
|
103
|
+
* A pooled connection reported that its socket closed. `reason` is set
|
|
104
|
+
* only when that was not asked for - see PgSocket._handleClose().
|
|
105
|
+
*/
|
|
106
|
+
protected _onConnectionClosed(intlCon: IntlConnection, reason?: Error): void;
|
|
107
|
+
/**
|
|
108
|
+
* The pool has removed a connection. `destroy` fires the same way for an
|
|
109
|
+
* ordinary eviction (idle timeout, close(), a failed validate) as for a
|
|
110
|
+
* connection that died, so the reason is what tells them apart - an
|
|
111
|
+
* admin kill, a failover and a network fault were otherwise
|
|
112
|
+
* indistinguishable from a timeout, with nothing at all reported when
|
|
113
|
+
* the connection had no query in flight to reject.
|
|
114
|
+
*/
|
|
115
|
+
protected _onConnectionDestroyed(intlCon: IntlConnection): void;
|
|
94
116
|
protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
|
|
95
117
|
/**
|
|
96
118
|
* Picks the pooled connection a pipelined query should ride on, or
|