postgrejs 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/README.md +17 -3
- 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 +106 -39
- package/connection/pool.js +28 -6
- 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/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/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- 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,31 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.
|
|
3
|
+
### [v3.6.0](https://github.com/panates/postgrejs/compare/v3.5.0...v3.6.0) -
|
|
4
|
+
|
|
5
|
+
#### 🚀 New Features
|
|
6
|
+
|
|
7
|
+
- feat: make fetchAsString request text from the server @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### 🪲 Fixes
|
|
10
|
+
|
|
11
|
+
- fix: decode float4 as the number the server would print @Eray Hanoğlu
|
|
12
|
+
- fix: report rowsAffected for MERGE @Eray Hanoğlu
|
|
13
|
+
- fix: quote LISTEN channel names instead of restricting them @Eray Hanoğlu
|
|
14
|
+
- fix: stop a pooled cursor's connection going back into the pool @Eray Hanoğlu
|
|
15
|
+
- fix: stop inferring "char" for one-character strings @Eray Hanoğlu
|
|
16
|
+
- fix: copy both indexes when a DataTypeMap is built from another @Eray Hanoğlu
|
|
17
|
+
- fix: emit 'notification' once per NOTIFY @Eray Hanoğlu
|
|
18
|
+
- fix: accept single-character savepoint names @Eray Hanoğlu
|
|
19
|
+
|
|
20
|
+
#### 📖 Documentation Changes
|
|
21
|
+
|
|
22
|
+
- docs: record what 3.6 changes for existing code @Eray Hanoğlu
|
|
23
|
+
|
|
24
|
+
#### 💬 General Changes
|
|
25
|
+
|
|
26
|
+
- feat!: fetch every row by default instead of the first 100 @Eray Hanoğlu
|
|
27
|
+
|
|
28
|
+
### [v3.5.0](https://github.com/panates/postgrejs/compare/v3.4.0...v3.5.0) - 16 September 2026
|
|
4
29
|
|
|
5
30
|
#### 🚀 New Features
|
|
6
31
|
|
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.
|
|
@@ -82,16 +85,23 @@ usage.
|
|
|
82
85
|
statement leaves the transaction usable instead of aborting the whole block. The `SAVEPOINT`/`RELEASE` pair travels
|
|
83
86
|
in the statement's own round trip rather than costing one each - 36µs for the pair, against 507µs sent separately.
|
|
84
87
|
`rollbackOnError: false` to opt out.
|
|
88
|
+
- **Complete Results:** `query()` returns every row the statement produced. A `fetchCount` limit is a limit rather
|
|
89
|
+
than a silent cap: the result carries `suspended: true` when the server stopped at it, so a prefix can never be
|
|
90
|
+
mistaken for the whole answer. `0` means unlimited, as it does in the protocol.
|
|
85
91
|
- **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.
|
|
92
|
+
at a time and closed when the loop ends - by exhaustion, a `break`, or a throw. A cursor reads through a portal, and
|
|
93
|
+
a portal lives as long as the transaction that created it - `Pool.query()` keeps its connection out of the pool
|
|
94
|
+
until the cursor closes, and on a bare `Connection` open the cursor inside a transaction if anything else will run
|
|
95
|
+
on that connection meanwhile.
|
|
87
96
|
- **Scoped Transactions:** `transaction(fn)` commits when the callback returns and rolls back when it throws, on a
|
|
88
97
|
connection or straight from the pool. A call made while a transaction is already open takes a savepoint rather than
|
|
89
98
|
a second `BEGIN`, so a nested scope that fails rolls back its own work and leaves the outer transaction standing.
|
|
90
99
|
- **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
|
|
91
100
|
reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
|
|
92
101
|
- **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.
|
|
102
|
+
updates. Channel names are quoted rather than pattern-matched, so any name PostgreSQL accepts works.
|
|
103
|
+
- **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements. A type can set
|
|
104
|
+
`inferrable: false` to stay out of parameter type inference while still decoding its own columns.
|
|
95
105
|
- **Parameter Binding:** Bind parameters with OID mappings for precise and efficient query execution.
|
|
96
106
|
- **Array Handling:** Supports multidimensional arrays with fast binary encoding/decoding.
|
|
97
107
|
- **Performance Optimization:** Low memory utilization and boosted performance through the use of shared buffers.
|
|
@@ -119,6 +129,10 @@ usage.
|
|
|
119
129
|
`rollbackPrepared()`, from any connection.
|
|
120
130
|
- **Cancellation:** Any call takes an `AbortSignal`, which also gives per-query timeouts via `AbortSignal.timeout()`.
|
|
121
131
|
- **Flexible Data Retrieval:** Can return both array and object rows to suit different data processing needs.
|
|
132
|
+
- **Values as Text:** `fetchAsString: [DataTypeOIDs.int8]` asks the server for those columns in its own text format
|
|
133
|
+
and hands the bytes back unparsed - so `count(*)` arrives as `'3'`, one consistent type where a decoded `int8` is a
|
|
134
|
+
`number` or a `BigInt` depending on magnitude. Any OID, arrays included. The string is PostgreSQL's own rendering
|
|
135
|
+
rather than one reconstructed from the binary value, so it cannot drift from what the server would print.
|
|
122
136
|
- **Resource Management:** Auto disposal of resources with the "using" syntax
|
|
123
137
|
([TC39 Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management)), ensuring
|
|
124
138
|
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
|
*
|
|
@@ -267,7 +283,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
267
283
|
* nesting the existing one.
|
|
268
284
|
*/
|
|
269
285
|
async savepoint(name) {
|
|
270
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
286
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
271
287
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
272
288
|
const depth = (this._savepointDepths.get(name) || 0) + 1;
|
|
273
289
|
this._savepointDepths.set(name, depth);
|
|
@@ -299,7 +315,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
299
315
|
* @param immediate - Ignores the tracked depth and releases right away.
|
|
300
316
|
*/
|
|
301
317
|
async releaseSavepoint(name, immediate) {
|
|
302
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
318
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
303
319
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
304
320
|
const depth = this._savepointDepths.get(name) || 0;
|
|
305
321
|
if (!immediate && depth > 1) {
|
|
@@ -359,7 +375,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
359
375
|
* unwind.
|
|
360
376
|
*/
|
|
361
377
|
async rollbackToSavepoint(name) {
|
|
362
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
378
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
363
379
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
364
380
|
this._savepointDepths.delete(name);
|
|
365
381
|
await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
|
|
@@ -523,8 +539,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
523
539
|
break;
|
|
524
540
|
case Protocol.BackendMessageCode.RowDescription:
|
|
525
541
|
fields = msg.fields;
|
|
526
|
-
|
|
527
|
-
|
|
542
|
+
// Simple Query has no Bind at all, so every column is
|
|
543
|
+
// already text - a fetchAsString column simply comes back
|
|
544
|
+
// unparsed.
|
|
545
|
+
parsers = getParsers(typeMap, fields, options);
|
|
546
|
+
current.fields = wrapRowDescription(typeMap, fields, DataFormat.text, options);
|
|
528
547
|
current.rows = [];
|
|
529
548
|
break;
|
|
530
549
|
case Protocol.BackendMessageCode.DataRow:
|
|
@@ -539,11 +558,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
539
558
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
540
559
|
// Ignore BEGIN command that we added to sql
|
|
541
560
|
current.command = msg.command;
|
|
542
|
-
if (current.command
|
|
543
|
-
current.command === 'INSERT' ||
|
|
544
|
-
current.command === 'UPDATE') {
|
|
561
|
+
if (reportsRowsAffected(current.command))
|
|
545
562
|
current.rowsAffected = msg.rowCount;
|
|
546
|
-
}
|
|
547
563
|
if (timingEnabled)
|
|
548
564
|
current.executeTime = performance.now() - currentStart;
|
|
549
565
|
if (current.rows)
|
|
@@ -659,7 +675,13 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
659
675
|
return result;
|
|
660
676
|
}
|
|
661
677
|
}
|
|
662
|
-
|
|
678
|
+
// A fetchAsString query needs its RowDescription before the Bind that
|
|
679
|
+
// asks for those columns as text, and only a named statement has one
|
|
680
|
+
// in hand ahead of time. Preparing on first sight pays a single
|
|
681
|
+
// Parse+Describe round trip per connection instead of leaving every
|
|
682
|
+
// call until the statement earns a name asking for the whole row as
|
|
683
|
+
// text (what queryOnce() falls back to).
|
|
684
|
+
if (!options.fetchAsString?.length && !this._earnsAName(key))
|
|
663
685
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
664
686
|
const name = 'C_' + ++this._preparedCounter;
|
|
665
687
|
// A Parse that fails caches nothing and reports itself; the statement
|
|
@@ -734,7 +756,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
734
756
|
let resultFields;
|
|
735
757
|
let commandTag;
|
|
736
758
|
let error;
|
|
759
|
+
let suspended = false;
|
|
737
760
|
const rowDecoder = resolveRowDecoder(options);
|
|
761
|
+
// fetchAsString names OIDs, but Bind's result format codes are
|
|
762
|
+
// positional - and this path's Describe answers only after the Bind
|
|
763
|
+
// that would have to carry them. With no column types to map the
|
|
764
|
+
// list onto, the whole row is asked for as text instead: the listed
|
|
765
|
+
// columns then arrive exactly as the server renders them, which is
|
|
766
|
+
// what was asked for, and the rest cost a text decode.
|
|
767
|
+
//
|
|
768
|
+
// Only `prepare: false` normally reaches here with a list set -
|
|
769
|
+
// _queryCached() prepares a fetchAsString query on first sight
|
|
770
|
+
// precisely so it can send per-column codes from the second call on
|
|
771
|
+
// (from the first, for every connection that has seen the SQL).
|
|
772
|
+
const columnFormat = options.fetchAsString?.length
|
|
773
|
+
? DataFormat.text
|
|
774
|
+
: options.columnFormat != null
|
|
775
|
+
? options.columnFormat
|
|
776
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
738
777
|
// See executeReused() for what a savepoint riding along does to the
|
|
739
778
|
// CommandComplete stream.
|
|
740
779
|
let leadingCommandTags = savepoint ? 1 : 0;
|
|
@@ -742,9 +781,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
742
781
|
await this.socket
|
|
743
782
|
.sendExtendedQueryMessages({
|
|
744
783
|
parse: { sql, paramTypes },
|
|
745
|
-
bind: {
|
|
784
|
+
bind: {
|
|
785
|
+
typeMap,
|
|
786
|
+
paramTypes,
|
|
787
|
+
params,
|
|
788
|
+
queryOptions: options,
|
|
789
|
+
columnFormat,
|
|
790
|
+
},
|
|
746
791
|
describe: { type: 'P' },
|
|
747
|
-
execute: { fetchCount: options.fetchCount
|
|
792
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
748
793
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
749
794
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
750
795
|
}, (code, msg, done) => {
|
|
@@ -753,11 +798,17 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
753
798
|
case Protocol.BackendMessageCode.BindComplete:
|
|
754
799
|
case Protocol.BackendMessageCode.NoData:
|
|
755
800
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
801
|
+
break;
|
|
802
|
+
// The server stopped at the fetchCount limit rather than
|
|
803
|
+
// running out of rows. The Sync that closes this call
|
|
804
|
+
// discards the portal, so the rest is gone - all that can be
|
|
805
|
+
// done is say the result is a prefix.
|
|
756
806
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
807
|
+
suspended = true;
|
|
757
808
|
break;
|
|
758
809
|
case Protocol.BackendMessageCode.RowDescription:
|
|
759
|
-
parsers = getParsers(typeMap, msg.fields);
|
|
760
|
-
resultFields = wrapRowDescription(typeMap, msg.fields,
|
|
810
|
+
parsers = getParsers(typeMap, msg.fields, options);
|
|
811
|
+
resultFields = wrapRowDescription(typeMap, msg.fields, columnFormat, options);
|
|
761
812
|
result.fields = resultFields;
|
|
762
813
|
result.rowType = resolveRowType(options);
|
|
763
814
|
break;
|
|
@@ -796,11 +847,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
796
847
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
797
848
|
}
|
|
798
849
|
}
|
|
799
|
-
if (
|
|
800
|
-
result.
|
|
801
|
-
|
|
850
|
+
if (suspended)
|
|
851
|
+
result.suspended = true;
|
|
852
|
+
if (reportsRowsAffected(result.command))
|
|
802
853
|
result.rowsAffected = commandTag?.rowCount;
|
|
803
|
-
}
|
|
804
854
|
if (timingEnabled)
|
|
805
855
|
result.executeTime = performance.now() - startTime;
|
|
806
856
|
return result;
|
|
@@ -873,25 +923,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
873
923
|
* so repeated executes of the same statement don't rebuild them.
|
|
874
924
|
* Shared by executeReused() and executeBatchReused().
|
|
875
925
|
*/
|
|
876
|
-
_resolveReusedParsers(cachedFields, typeMap, options) {
|
|
877
|
-
const columnFormat = options.columnFormat != null
|
|
878
|
-
? options.columnFormat
|
|
879
|
-
: DEFAULT_COLUMN_FORMAT;
|
|
926
|
+
_resolveReusedParsers(cachedFields, typeMap, options, columnFormat) {
|
|
880
927
|
const cached = this._executeReusedParserCache.get(cachedFields);
|
|
881
928
|
if (cached &&
|
|
882
929
|
cached.typeMap === typeMap &&
|
|
883
|
-
columnFormatsEqual(cached.columnFormat, columnFormat)
|
|
930
|
+
columnFormatsEqual(cached.columnFormat, columnFormat) &&
|
|
931
|
+
fetchAsStringEqual(cached.fetchAsString, options.fetchAsString)) {
|
|
884
932
|
return { parsers: cached.parsers, resultFields: cached.resultFields };
|
|
885
933
|
}
|
|
886
934
|
const fields = cachedFields.map((f, i) => ({
|
|
887
935
|
...f,
|
|
888
936
|
format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
|
|
889
937
|
}));
|
|
890
|
-
const parsers = getParsers(typeMap, fields);
|
|
891
|
-
const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
|
|
938
|
+
const parsers = getParsers(typeMap, fields, options);
|
|
939
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
892
940
|
this._executeReusedParserCache.set(cachedFields, {
|
|
893
941
|
typeMap,
|
|
894
942
|
columnFormat,
|
|
943
|
+
fetchAsString: options.fetchAsString,
|
|
895
944
|
parsers,
|
|
896
945
|
resultFields,
|
|
897
946
|
});
|
|
@@ -974,6 +1023,16 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
974
1023
|
if (key != null && plan.name)
|
|
975
1024
|
named.set(key, plan);
|
|
976
1025
|
}
|
|
1026
|
+
// Only a statement whose columns are already known can carry
|
|
1027
|
+
// per-column codes; the rest fall back to asking for the whole row
|
|
1028
|
+
// as text, exactly as queryOnce() does and for the same reason.
|
|
1029
|
+
plan.columnFormat = plan.fields
|
|
1030
|
+
? resolveColumnFormats(plan.fields, options)
|
|
1031
|
+
: options.fetchAsString?.length
|
|
1032
|
+
? DataFormat.text
|
|
1033
|
+
: options.columnFormat != null
|
|
1034
|
+
? options.columnFormat
|
|
1035
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
977
1036
|
plans.push(plan);
|
|
978
1037
|
statements.push({
|
|
979
1038
|
// Per statement, not per pipeline: the statements differ, so a
|
|
@@ -988,6 +1047,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
988
1047
|
paramTypes: r.paramTypes,
|
|
989
1048
|
params: r.params,
|
|
990
1049
|
queryOptions: options,
|
|
1050
|
+
columnFormat: plan.columnFormat,
|
|
991
1051
|
},
|
|
992
1052
|
describe: sendDescribe ? { type: 'P' } : undefined,
|
|
993
1053
|
// Unlimited, never options.fetchCount: a suspended portal would
|
|
@@ -1025,7 +1085,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1025
1085
|
const fields = pendingFields || plan.fields;
|
|
1026
1086
|
const result = { command: msg?.command };
|
|
1027
1087
|
if (fields) {
|
|
1028
|
-
const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options);
|
|
1088
|
+
const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options, plan.columnFormat ?? DEFAULT_COLUMN_FORMAT);
|
|
1029
1089
|
result.fields = resultFields;
|
|
1030
1090
|
result.rowType = resolveRowType(options);
|
|
1031
1091
|
if (!result.command)
|
|
@@ -1038,9 +1098,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1038
1098
|
}
|
|
1039
1099
|
result.rows = rows;
|
|
1040
1100
|
}
|
|
1041
|
-
if (result.command
|
|
1042
|
-
result.command === 'INSERT' ||
|
|
1043
|
-
result.command === 'UPDATE')
|
|
1101
|
+
if (reportsRowsAffected(result.command))
|
|
1044
1102
|
result.rowsAffected = msg.rowCount;
|
|
1045
1103
|
pendingFields = undefined;
|
|
1046
1104
|
pendingRows = undefined;
|
|
@@ -1120,8 +1178,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1120
1178
|
const rowDecoder = resolveRowDecoder(options);
|
|
1121
1179
|
let parsers;
|
|
1122
1180
|
let resultFields;
|
|
1181
|
+
const columnFormat = resolveColumnFormats(cachedFields, options);
|
|
1123
1182
|
if (cachedFields) {
|
|
1124
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1183
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1125
1184
|
parsers = resolved.parsers;
|
|
1126
1185
|
resultFields = resolved.resultFields;
|
|
1127
1186
|
}
|
|
@@ -1134,6 +1193,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1134
1193
|
paramTypes,
|
|
1135
1194
|
params,
|
|
1136
1195
|
queryOptions: options,
|
|
1196
|
+
columnFormat,
|
|
1137
1197
|
}));
|
|
1138
1198
|
this.runningQueryCount++;
|
|
1139
1199
|
await this.socket
|
|
@@ -1155,9 +1215,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1155
1215
|
const item = {};
|
|
1156
1216
|
if (msg?.command)
|
|
1157
1217
|
item.command = msg.command;
|
|
1158
|
-
if (item.command
|
|
1159
|
-
item.command === 'INSERT' ||
|
|
1160
|
-
item.command === 'UPDATE')
|
|
1218
|
+
if (reportsRowsAffected(item.command))
|
|
1161
1219
|
item.rowsAffected = msg.rowCount;
|
|
1162
1220
|
if (pendingRows && parsers && resultFields) {
|
|
1163
1221
|
const l = pendingRows.length;
|
|
@@ -1225,9 +1283,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1225
1283
|
let resultFields;
|
|
1226
1284
|
let commandTag;
|
|
1227
1285
|
let error;
|
|
1286
|
+
let suspended = false;
|
|
1228
1287
|
const rowDecoder = resolveRowDecoder(options);
|
|
1288
|
+
// The whole point of reusing a prepared statement here: its
|
|
1289
|
+
// RowDescription is already in hand, so fetchAsString's OID list can
|
|
1290
|
+
// become the per-column result format codes this Bind carries, with
|
|
1291
|
+
// no extra round trip to find out what the columns are.
|
|
1292
|
+
const columnFormat = resolveColumnFormats(cachedFields, options);
|
|
1229
1293
|
if (cachedFields) {
|
|
1230
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1294
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1231
1295
|
parsers = resolved.parsers;
|
|
1232
1296
|
resultFields = resolved.resultFields;
|
|
1233
1297
|
result.fields = resultFields;
|
|
@@ -1247,8 +1311,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1247
1311
|
paramTypes,
|
|
1248
1312
|
params,
|
|
1249
1313
|
queryOptions: options,
|
|
1314
|
+
columnFormat,
|
|
1250
1315
|
},
|
|
1251
|
-
execute: { fetchCount: options.fetchCount
|
|
1316
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
1252
1317
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
1253
1318
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
1254
1319
|
}, (code, msg, done) => {
|
|
@@ -1256,7 +1321,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1256
1321
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
1257
1322
|
case Protocol.BackendMessageCode.BindComplete:
|
|
1258
1323
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1324
|
+
break;
|
|
1325
|
+
// See queryOnce() for what a suspended portal means here.
|
|
1259
1326
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
1327
|
+
suspended = true;
|
|
1260
1328
|
break;
|
|
1261
1329
|
case Protocol.BackendMessageCode.DataRow:
|
|
1262
1330
|
rows.push(msg);
|
|
@@ -1297,11 +1365,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1297
1365
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
1298
1366
|
}
|
|
1299
1367
|
}
|
|
1300
|
-
if (
|
|
1301
|
-
result.
|
|
1302
|
-
|
|
1368
|
+
if (suspended)
|
|
1369
|
+
result.suspended = true;
|
|
1370
|
+
if (reportsRowsAffected(result.command))
|
|
1303
1371
|
result.rowsAffected = commandTag?.rowCount;
|
|
1304
|
-
}
|
|
1305
1372
|
if (timingEnabled)
|
|
1306
1373
|
result.executeTime = performance.now() - startTime;
|
|
1307
1374
|
return result;
|
package/connection/pool.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Pool as LightningPool, } from 'lightning-pool';
|
|
|
2
2
|
import { coerceToBoolean, coerceToInt } from 'putil-varhelpers';
|
|
3
3
|
import { ConnectionState } from '../constants.js';
|
|
4
4
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
5
|
+
import { normalizeChannelName } from '../util/channel-name.js';
|
|
5
6
|
import { getConnectionConfig } from '../util/connection-config.js';
|
|
6
7
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
7
8
|
import { startsCopy, startsTransaction } from '../util/starts-transaction.js';
|
|
@@ -205,12 +206,32 @@ export class Pool extends SafeEventEmitter {
|
|
|
205
206
|
: undefined;
|
|
206
207
|
if (!slot) {
|
|
207
208
|
const connection = await this.acquire();
|
|
209
|
+
let result;
|
|
208
210
|
try {
|
|
209
|
-
|
|
211
|
+
result = await connection.query(sql, options);
|
|
210
212
|
}
|
|
211
|
-
|
|
213
|
+
catch (e) {
|
|
212
214
|
await this.release(connection);
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
// A portal lives only as long as the transaction it was created in,
|
|
218
|
+
// and outside an explicit one that is the implicit transaction any
|
|
219
|
+
// other statement's Sync ends. So a connection carrying an open
|
|
220
|
+
// cursor cannot go back in the pool when this call returns - handing
|
|
221
|
+
// it to the next caller would destroy the cursor's portal, and did
|
|
222
|
+
// ("portal ... does not exist", nondeterministically, depending on
|
|
223
|
+
// which connection that caller happened to get). It goes back when
|
|
224
|
+
// the cursor closes instead, which for-await and `await using` both
|
|
225
|
+
// do on their own; a cursor that is never closed holds its
|
|
226
|
+
// connection until the pool itself is closed.
|
|
227
|
+
if (result.cursor) {
|
|
228
|
+
result.cursor.once('close', () => {
|
|
229
|
+
this.release(connection).catch(e => this.emit('error', e));
|
|
230
|
+
});
|
|
231
|
+
return result;
|
|
213
232
|
}
|
|
233
|
+
await this.release(connection);
|
|
234
|
+
return result;
|
|
214
235
|
}
|
|
215
236
|
try {
|
|
216
237
|
const shared = slot.connection || (await slot.promise);
|
|
@@ -256,8 +277,10 @@ export class Pool extends SafeEventEmitter {
|
|
|
256
277
|
return this._pool.releaseAsync(getIntlConnection(connection));
|
|
257
278
|
}
|
|
258
279
|
async listen(channel, callback) {
|
|
259
|
-
|
|
260
|
-
|
|
280
|
+
// Folded here as well as in Connection.listen(): this emitter's keys
|
|
281
|
+
// have to be the same strings the inner connection registers, or
|
|
282
|
+
// unListen() would miss them.
|
|
283
|
+
channel = normalizeChannelName(channel);
|
|
261
284
|
// Bug: _initNotificationConnection() only ever bootstraps the shared
|
|
262
285
|
// connection and registers every channel known at that moment - it
|
|
263
286
|
// returns immediately once that connection already exists, so a second,
|
|
@@ -274,8 +297,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
274
297
|
}
|
|
275
298
|
}
|
|
276
299
|
async unListen(channel) {
|
|
277
|
-
|
|
278
|
-
throw new TypeError(`Invalid channel name`);
|
|
300
|
+
channel = normalizeChannelName(channel);
|
|
279
301
|
this._notificationListeners.removeAllListeners(channel);
|
|
280
302
|
if (!this._notificationListeners.eventNames().length) {
|
|
281
303
|
await this.unListenAll();
|
package/connection/portal.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class Portal {
|
|
|
23
23
|
* no explicit BEGIN left transactionStatus 'I' and a later Execute on the
|
|
24
24
|
* same named portal failed with "portal ... does not exist").
|
|
25
25
|
*/
|
|
26
|
-
bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions): Promise<Protocol.RowDescription[]>;
|
|
26
|
+
bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions, columnFormat?: Protocol.DataFormat | Protocol.DataFormat[]): Promise<Protocol.RowDescription[]>;
|
|
27
27
|
execute(fetchCount?: number): Promise<PortalExecuteResult>;
|
|
28
28
|
/**
|
|
29
29
|
* Close + Sync as one request (see PgSocket.sendCloseAndSyncMessages()
|
package/connection/portal.js
CHANGED
|
@@ -22,7 +22,7 @@ export class Portal {
|
|
|
22
22
|
* no explicit BEGIN left transactionStatus 'I' and a later Execute on the
|
|
23
23
|
* same named portal failed with "portal ... does not exist").
|
|
24
24
|
*/
|
|
25
|
-
async bindAndRetrieveFields(params, queryOptions) {
|
|
25
|
+
async bindAndRetrieveFields(params, queryOptions, columnFormat) {
|
|
26
26
|
const intoCon = getIntlConnection(this.connection);
|
|
27
27
|
intoCon.ref();
|
|
28
28
|
try {
|
|
@@ -35,6 +35,7 @@ export class Portal {
|
|
|
35
35
|
paramTypes: this._statement.paramTypes,
|
|
36
36
|
params,
|
|
37
37
|
queryOptions,
|
|
38
|
+
columnFormat,
|
|
38
39
|
},
|
|
39
40
|
describe: { type: 'P', name: this.name },
|
|
40
41
|
}, (code, msg, done) => {
|
|
@@ -68,7 +69,7 @@ export class Portal {
|
|
|
68
69
|
const rows = [];
|
|
69
70
|
const executePromise = socket.sendExecuteMessage({
|
|
70
71
|
portal: this.name,
|
|
71
|
-
fetchCount: fetchCount
|
|
72
|
+
fetchCount: fetchCount ?? 100,
|
|
72
73
|
}, (code, msg, done) => {
|
|
73
74
|
switch (code) {
|
|
74
75
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
@@ -1,9 +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 { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
5
4
|
import { withAbortSignal } from '../util/abort-signal.js';
|
|
6
5
|
import { getParsers } from '../util/get-parsers.js';
|
|
6
|
+
import { resolveColumnFormats } from '../util/resolve-column-formats.js';
|
|
7
7
|
import { resolveRowType } from '../util/row-decoder.js';
|
|
8
8
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
9
9
|
import { Cursor } from './cursor.js';
|
|
@@ -208,10 +208,14 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
208
208
|
intlCon.ref();
|
|
209
209
|
let portal = new Portal(this, 'P_' + ++portalCounter);
|
|
210
210
|
try {
|
|
211
|
-
|
|
211
|
+
// prepare() already fetched this statement's RowDescription, so
|
|
212
|
+
// fetchAsString's OID list can become per-column result format
|
|
213
|
+
// codes on the Bind below with no round trip of its own.
|
|
214
|
+
const columnFormat = resolveColumnFormats(this._fields, options);
|
|
215
|
+
const fields = await portal.bindAndRetrieveFields(options.params, options, columnFormat);
|
|
212
216
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
213
|
-
const parsers = getParsers(typeMap, fields);
|
|
214
|
-
const resultFields = wrapRowDescription(typeMap, fields,
|
|
217
|
+
const parsers = getParsers(typeMap, fields, options);
|
|
218
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
215
219
|
const result = {
|
|
216
220
|
command: undefined,
|
|
217
221
|
fields: resultFields,
|