postgrejs 2.23.1 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +171 -1
- package/LICENSE +26 -17
- package/README.md +195 -175
- package/connection/connection.d.ts +169 -16
- package/connection/connection.js +318 -56
- package/connection/copy-stream.d.ts +88 -0
- package/connection/copy-stream.js +244 -0
- package/connection/cursor.js +20 -13
- package/connection/intl-connection.d.ts +113 -5
- package/connection/intl-connection.js +500 -70
- package/connection/large-object.d.ts +65 -0
- package/connection/large-object.js +151 -0
- package/connection/logical-replication.d.ts +121 -0
- package/connection/logical-replication.js +279 -0
- package/connection/pool.d.ts +61 -4
- package/connection/pool.js +177 -29
- package/connection/portal.d.ts +18 -4
- package/connection/portal.js +53 -59
- package/connection/prepared-statement.d.ts +22 -1
- package/connection/prepared-statement.js +129 -109
- package/data-type-map.js +4 -2
- package/data-types/bool-type.js +14 -10
- package/data-types/box-type.js +15 -8
- package/data-types/bytea-type.js +9 -5
- package/data-types/char-type.js +11 -5
- package/data-types/circle-type.js +15 -7
- package/data-types/date-type.js +25 -19
- package/data-types/float4-type.js +10 -3
- package/data-types/float8-type.js +12 -3
- package/data-types/int2-type.js +9 -4
- package/data-types/int2-vector-type.js +9 -4
- package/data-types/int4-type.js +11 -5
- package/data-types/int8-type.js +22 -7
- package/data-types/json-type.js +17 -8
- package/data-types/jsonb-type.js +20 -14
- package/data-types/lseg-type.js +16 -8
- package/data-types/numeric-type.d.ts +8 -0
- package/data-types/numeric-type.js +118 -9
- package/data-types/oid-type.d.ts +0 -1
- package/data-types/oid-type.js +9 -10
- package/data-types/oid-vector-type.d.ts +9 -0
- package/data-types/oid-vector-type.js +51 -0
- package/data-types/point-type.js +14 -6
- package/data-types/time-type.js +20 -13
- package/data-types/timestamp-type.js +32 -25
- package/data-types/timestamptz-type.js +33 -21
- package/data-types/uuid-type.js +21 -11
- package/data-types/varchar-type.js +16 -4
- package/index.d.ts +5 -0
- package/index.js +5 -0
- package/interfaces/command-result.d.ts +2 -1
- package/interfaces/data-type.d.ts +5 -3
- package/interfaces/database-connection-params.d.ts +108 -0
- package/interfaces/query-options.d.ts +23 -0
- package/interfaces/script-execute-options.d.ts +23 -0
- package/interfaces/script-result.d.ts +3 -2
- package/package.json +2 -3
- package/protocol/backend.d.ts +6 -1
- package/protocol/backend.js +70 -45
- package/protocol/buffer-reader.d.ts +0 -2
- package/protocol/buffer-reader.js +3 -10
- package/protocol/cert-signature.d.ts +12 -0
- package/protocol/cert-signature.js +114 -0
- package/protocol/frontend.d.ts +16 -0
- package/protocol/frontend.js +70 -11
- package/protocol/pg-socket.d.ts +220 -9
- package/protocol/pg-socket.js +539 -102
- package/protocol/pgoutput.d.ts +76 -0
- package/protocol/pgoutput.js +155 -0
- package/protocol/protocol.d.ts +3 -4
- package/protocol/protocol.js +1 -1
- package/protocol/sasl.d.ts +11 -1
- package/protocol/sasl.js +36 -5
- package/protocol/smart-buffer.d.ts +0 -2
- package/protocol/smart-buffer.js +11 -16
- package/types.d.ts +5 -3
- package/util/abort-signal.d.ts +34 -0
- package/util/abort-signal.js +76 -0
- package/util/array-calculatedim.js +3 -1
- package/util/connection-config.js +77 -3
- package/util/decode-binaryarray.d.ts +1 -1
- package/util/decode-binaryarray.js +15 -6
- package/util/encode-binaryarray.js +7 -3
- package/util/escape-identifier.d.ts +14 -0
- package/util/escape-identifier.js +32 -0
- package/util/escape-literal.js +3 -0
- package/util/fast-parseint.d.ts +1 -0
- package/util/fast-parseint.js +12 -17
- package/util/format-datetime.d.ts +10 -0
- package/util/format-datetime.js +71 -0
- package/util/get-parsers.js +31 -10
- package/util/parse-array.js +2 -2
- package/util/parse-bytea.d.ts +2 -0
- package/util/parse-bytea.js +83 -0
- package/util/parse-datetime.d.ts +3 -1
- package/util/parse-datetime.js +86 -17
- package/util/parse-row.d.ts +3 -1
- package/util/parse-row.js +60 -5
- package/util/parse-time.js +8 -1
- package/util/sql-tag.d.ts +60 -0
- package/util/sql-tag.js +202 -0
- package/util/starts-transaction.d.ts +38 -0
- package/util/starts-transaction.js +148 -0
- package/util/stringify-arrayliteral.js +13 -1
- package/util/wrap-row-description.js +13 -4
- package/util/convert-row-to-object.d.ts +0 -2
- package/util/convert-row-to-object.js +0 -9
package/connection/pool.d.ts
CHANGED
|
@@ -6,27 +6,54 @@ import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.
|
|
|
6
6
|
import type { ScriptResult } from '../interfaces/script-result.js';
|
|
7
7
|
import type { StatementPrepareOptions } from '../interfaces/statement-prepare-options.js';
|
|
8
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
9
|
+
import { QueryRequest } from '../util/sql-tag.js';
|
|
9
10
|
import { Connection, type NotificationCallback } from './connection.js';
|
|
10
11
|
import { IntlConnection } from './intl-connection.js';
|
|
11
12
|
import type { PreparedStatement } from './prepared-statement.js';
|
|
13
|
+
export interface PoolPipelineOptions {
|
|
14
|
+
pipeline?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export type PoolQueryOptions = QueryOptions & PoolPipelineOptions;
|
|
17
|
+
export type PoolScriptExecuteOptions = ScriptExecuteOptions & PoolPipelineOptions;
|
|
18
|
+
/**
|
|
19
|
+
* One pooled connection the pipelined path is currently borrowing.
|
|
20
|
+
*
|
|
21
|
+
* A slot joins the list the moment its acquire starts rather than when it
|
|
22
|
+
* finishes, so callers arriving while a connection is still being opened
|
|
23
|
+
* queue onto `promise` instead of starting a second acquire of their own.
|
|
24
|
+
* `load` counts queries handed to this slot that have not settled yet - it
|
|
25
|
+
* is incremented synchronously, at selection time, which `runningQueryCount`
|
|
26
|
+
* cannot be: everything in a Promise.all() burst picks its connection before
|
|
27
|
+
* any of them reaches execute(), so at that point every connection still
|
|
28
|
+
* reports zero running queries.
|
|
29
|
+
*/
|
|
30
|
+
interface PipelineSlot {
|
|
31
|
+
connection?: Connection;
|
|
32
|
+
promise: Promise<Connection>;
|
|
33
|
+
load: number;
|
|
34
|
+
}
|
|
12
35
|
export declare class Pool extends SafeEventEmitter {
|
|
13
36
|
protected readonly _pool: LightningPool<IntlConnection>;
|
|
14
37
|
protected readonly _notificationListeners: SafeEventEmitter;
|
|
15
38
|
protected _notificationConnection?: Connection;
|
|
39
|
+
protected readonly _pipelineSlots: PipelineSlot[];
|
|
40
|
+
protected _pipelineMaxQueries: number;
|
|
41
|
+
protected _pipelineMaxConnections: number;
|
|
16
42
|
readonly config: PoolConfiguration;
|
|
17
43
|
constructor(config?: PoolConfiguration | string);
|
|
18
44
|
/**
|
|
19
|
-
* Returns number of connections that are currently acquired
|
|
45
|
+
* Returns the number of connections that are currently acquired
|
|
20
46
|
*/
|
|
21
47
|
get acquiredConnections(): number;
|
|
22
48
|
/**
|
|
23
|
-
* Returns number of unused connections in the pool
|
|
49
|
+
* Returns the number of unused connections in the pool
|
|
24
50
|
*/
|
|
25
51
|
get idleConnections(): number;
|
|
26
52
|
/**
|
|
27
53
|
* Returns total number of connections in the pool regardless of whether they are idle or in use
|
|
28
54
|
*/
|
|
29
55
|
get totalConnections(): number;
|
|
56
|
+
start(): void;
|
|
30
57
|
/**
|
|
31
58
|
* Obtains a connection from the connection pool
|
|
32
59
|
*/
|
|
@@ -38,15 +65,45 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
38
65
|
/**
|
|
39
66
|
* Executes a script
|
|
40
67
|
*/
|
|
41
|
-
execute(sql: string, options?:
|
|
68
|
+
execute(sql: string | QueryRequest, options?: PoolScriptExecuteOptions): Promise<ScriptResult>;
|
|
42
69
|
/**
|
|
43
70
|
* Executes a query
|
|
44
71
|
*/
|
|
45
|
-
query(sql: string, options?:
|
|
72
|
+
query(sql: string | QueryRequest, options?: PoolQueryOptions): Promise<QueryResult>;
|
|
46
73
|
prepare(sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
|
|
47
74
|
release(connection: Connection): Promise<void>;
|
|
48
75
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|
|
49
76
|
unListen(channel: string): Promise<void>;
|
|
50
77
|
unListenAll(): Promise<void>;
|
|
78
|
+
protected _canPipeline(pipeline: boolean | undefined, sql: string, autoCommit: boolean | undefined, signal: AbortSignal | undefined): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Picks the pooled connection a pipelined query should ride on, or
|
|
81
|
+
* undefined when the caller should take a connection of its own.
|
|
82
|
+
*
|
|
83
|
+
* Deliberately synchronous: it must never await, because everything in a
|
|
84
|
+
* Promise.all() burst has to pick its connection and reach execute()
|
|
85
|
+
* within a single event loop tick for PgSocket to coalesce the whole
|
|
86
|
+
* burst into one write per connection. Opening another connection is
|
|
87
|
+
* started here but never awaited by the caller that triggered it.
|
|
88
|
+
*/
|
|
89
|
+
protected _acquireShared(): PipelineSlot | undefined;
|
|
90
|
+
/**
|
|
91
|
+
* Starts borrowing one more connection and returns its slot right away,
|
|
92
|
+
* already carrying the caller that triggered the growth.
|
|
93
|
+
*/
|
|
94
|
+
protected _growPipeline(): PipelineSlot;
|
|
95
|
+
/**
|
|
96
|
+
* Stops new queries joining a slot. Queries already on it are unaffected.
|
|
97
|
+
*/
|
|
98
|
+
protected _dropPipelineSlot(slot: PipelineSlot): void;
|
|
99
|
+
/**
|
|
100
|
+
* Backstop for a transaction startsTransaction() could not see coming -
|
|
101
|
+
* one opened inside a stored procedure, for instance. The connection is
|
|
102
|
+
* still sound, but nothing else may join it while it is in a
|
|
103
|
+
* transaction, so the slot goes away and the connection returns to the
|
|
104
|
+
* pool once its own queries finish.
|
|
105
|
+
*/
|
|
106
|
+
protected _checkSharedTransaction(slot: PipelineSlot, connection: Connection): void;
|
|
51
107
|
protected _initNotificationConnection(): Promise<void>;
|
|
52
108
|
}
|
|
109
|
+
export {};
|
package/connection/pool.js
CHANGED
|
@@ -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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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:
|
|
63
|
-
/*
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
187
|
+
slot.load--;
|
|
163
188
|
}
|
|
164
189
|
}
|
|
165
190
|
/**
|
|
166
191
|
* Executes a query
|
|
167
192
|
*/
|
|
168
193
|
async query(sql, options) {
|
|
169
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|
package/connection/portal.d.ts
CHANGED
|
@@ -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?:
|
|
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
|
-
|
|
20
|
-
|
|
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
|
}
|
package/connection/portal.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
135
|
-
socket.
|
|
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.
|
|
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)})`));
|