postgrejs 3.8.0 → 3.9.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 +38 -1
- package/README.md +50 -0
- package/connection/connection.d.ts +14 -2
- package/connection/connection.js +57 -9
- package/connection/intl-connection.d.ts +146 -5
- package/connection/intl-connection.js +458 -30
- package/connection/pool.d.ts +30 -6
- package/connection/pool.js +22 -3
- package/connection/prepared-statement.js +22 -2
- package/data-type-map.d.ts +16 -0
- package/data-type-map.js +56 -12
- package/data-types/date-type.js +2 -2
- package/data-types/money-type.d.ts +38 -0
- package/data-types/money-type.js +203 -0
- package/data-types/numeric-type.d.ts +2 -0
- package/data-types/numeric-type.js +1 -1
- package/data-types/timestamp-type.js +2 -2
- package/data-types/timestamptz-type.js +2 -2
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/data-mapping-options.d.ts +27 -0
- package/interfaces/database-connection-params.d.ts +32 -5
- package/interfaces/query-options.d.ts +32 -0
- package/interfaces/script-execute-options.d.ts +32 -0
- package/interfaces/transaction-options.d.ts +27 -0
- package/interfaces/transaction-options.js +1 -0
- package/package.json +1 -1
- package/protocol/database-error.d.ts +15 -0
- package/protocol/database-error.js +19 -0
- package/protocol/frontend.js +1 -1
- package/protocol/pg-socket.js +7 -10
- package/util/array-leaf.d.ts +10 -0
- package/util/array-leaf.js +22 -0
- package/util/date-style.d.ts +47 -0
- package/util/date-style.js +123 -0
- package/util/parse-datetime.d.ts +4 -3
- package/util/parse-datetime.js +34 -9
- package/util/resolve-column-formats.js +4 -0
- package/util/row-decoder.d.ts +24 -4
- package/util/row-decoder.js +25 -10
- package/util/sql-tag.js +2 -1
- package/util/transaction-command.d.ts +7 -0
- package/util/transaction-command.js +32 -0
- package/util/unspecified-param.d.ts +0 -5
- package/util/unspecified-param.js +1 -10
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
|
-
import { ConnectionState, DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
2
|
+
import { ConnectionState, DataTypeOIDs, DEFAULT_COLUMN_FORMAT, } from '../constants.js';
|
|
3
3
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
4
|
+
import { parseMoneyFormat } from '../data-types/money-type.js';
|
|
4
5
|
import { DatabaseError } from '../protocol/database-error.js';
|
|
5
6
|
import { PgSocket } from '../protocol/pg-socket.js';
|
|
6
7
|
import { Protocol } from '../protocol/protocol.js';
|
|
7
8
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
8
9
|
import { getConnectionConfig } from '../util/connection-config.js';
|
|
9
10
|
import { buildCopySql, buildProbeSql, writeCopyBinaryRows, } from '../util/copy-from-rows.js';
|
|
11
|
+
import { parseDateStyleSetting } from '../util/date-style.js';
|
|
10
12
|
import { escapeLiteral } from '../util/escape-literal.js';
|
|
11
13
|
import { getParsers } from '../util/get-parsers.js';
|
|
12
14
|
import { fetchAsStringEqual, resolveColumnFormats, } from '../util/resolve-column-formats.js';
|
|
13
|
-
import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
|
|
15
|
+
import { resolveRowDecoder, resolveRowType, } from '../util/row-decoder.js';
|
|
16
|
+
import { isTransactionCommand, refusesSavepoint, } from '../util/transaction-command.js';
|
|
14
17
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
15
18
|
import { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
16
19
|
const DataFormat = Protocol.DataFormat;
|
|
@@ -77,6 +80,72 @@ function reportsRowsAffected(command) {
|
|
|
77
80
|
* it starts saving, which it makes back immediately.
|
|
78
81
|
*/
|
|
79
82
|
const PREPARE_AFTER_USES = 2;
|
|
83
|
+
/**
|
|
84
|
+
* Stands in for the command tag an empty statement never sends, so that
|
|
85
|
+
* the slot is taken and nothing behind it is mistaken for the caller's
|
|
86
|
+
* own command. Its empty name is what leaves `QueryResult.command`
|
|
87
|
+
* unset, which is what `pg` reports for the same statement.
|
|
88
|
+
*/
|
|
89
|
+
/** The connection-configuration keys that are per-statement defaults. */
|
|
90
|
+
const MAPPING_DEFAULT_KEYS = [
|
|
91
|
+
'typeMap',
|
|
92
|
+
'columnFormat',
|
|
93
|
+
'objectRows',
|
|
94
|
+
'rowDecoder',
|
|
95
|
+
'utcDates',
|
|
96
|
+
'fetchAsString',
|
|
97
|
+
'unknownTypesAsString',
|
|
98
|
+
'moneyFormat',
|
|
99
|
+
];
|
|
100
|
+
/**
|
|
101
|
+
* The mapping defaults out of a connection configuration, or undefined
|
|
102
|
+
* when it set none - so the merge can be skipped outright rather than
|
|
103
|
+
* walking an empty object on every statement.
|
|
104
|
+
*/
|
|
105
|
+
function pickMappingDefaults(config) {
|
|
106
|
+
let out;
|
|
107
|
+
let k;
|
|
108
|
+
let i;
|
|
109
|
+
for (i = 0; i < MAPPING_DEFAULT_KEYS.length; i++) {
|
|
110
|
+
k = MAPPING_DEFAULT_KEYS[i];
|
|
111
|
+
if (config[k] === undefined)
|
|
112
|
+
continue;
|
|
113
|
+
out = out || {};
|
|
114
|
+
out[k] = config[k];
|
|
115
|
+
}
|
|
116
|
+
return out && Object.freeze(out);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* The isolation levels PostgreSQL takes, as the SQL that names them.
|
|
120
|
+
*
|
|
121
|
+
* A table rather than the caller's own string: this text is
|
|
122
|
+
* concatenated into a statement, and nothing that reaches it may come
|
|
123
|
+
* from outside this file.
|
|
124
|
+
*/
|
|
125
|
+
const ISOLATION_LEVELS = {
|
|
126
|
+
serializable: 'SERIALIZABLE',
|
|
127
|
+
'repeatable read': 'REPEATABLE READ',
|
|
128
|
+
'read committed': 'READ COMMITTED',
|
|
129
|
+
'read uncommitted': 'READ UNCOMMITTED',
|
|
130
|
+
};
|
|
131
|
+
/** `BEGIN`, carrying whatever modes the caller asked for. */
|
|
132
|
+
function beginStatement(options) {
|
|
133
|
+
if (!options)
|
|
134
|
+
return 'BEGIN';
|
|
135
|
+
let sql = 'BEGIN';
|
|
136
|
+
if (options.isolationLevel != null) {
|
|
137
|
+
const level = ISOLATION_LEVELS[options.isolationLevel.toLowerCase()];
|
|
138
|
+
if (!level)
|
|
139
|
+
throw new TypeError(`Unknown transaction isolation level "${options.isolationLevel}"`);
|
|
140
|
+
sql += ' ISOLATION LEVEL ' + level;
|
|
141
|
+
}
|
|
142
|
+
if (options.readOnly != null)
|
|
143
|
+
sql += options.readOnly ? ' READ ONLY' : ' READ WRITE';
|
|
144
|
+
if (options.deferrable != null)
|
|
145
|
+
sql += options.deferrable ? ' DEFERRABLE' : ' NOT DEFERRABLE';
|
|
146
|
+
return sql;
|
|
147
|
+
}
|
|
148
|
+
const EMPTY_QUERY_TAG = { command: '' };
|
|
80
149
|
/**
|
|
81
150
|
* SQL that manages transaction boundaries itself, and so must never be
|
|
82
151
|
* wrapped in a savepoint or an implicit BEGIN/COMMIT of ours.
|
|
@@ -89,7 +158,6 @@ const PREPARE_AFTER_USES = 2;
|
|
|
89
158
|
function preparedCacheKey(sql, paramTypes) {
|
|
90
159
|
return paramTypes?.length ? sql + '\u0000' + paramTypes.join(',') : sql;
|
|
91
160
|
}
|
|
92
|
-
const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
|
|
93
161
|
export class IntlConnection extends SafeEventEmitter {
|
|
94
162
|
/**
|
|
95
163
|
* Server-side prepared statements this connection has built, keyed by
|
|
@@ -102,6 +170,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
102
170
|
* this is per connection and dies with it.
|
|
103
171
|
*/
|
|
104
172
|
_preparedCache = new Map();
|
|
173
|
+
/**
|
|
174
|
+
* The names being given out right now, one per SQL. A burst of the
|
|
175
|
+
* same statement arrives before any of it is prepared, and without
|
|
176
|
+
* this every call in the burst prepares its own copy: measured at 50
|
|
177
|
+
* concurrent `select $1::int4`, 25 Parse+Describe round trips and 25
|
|
178
|
+
* named statements left on the server that this map's one entry could
|
|
179
|
+
* never close again.
|
|
180
|
+
*/
|
|
181
|
+
_preparing = new Map();
|
|
105
182
|
/** SQL seen once but not yet prepared - see PREPARE_AFTER_USES. */
|
|
106
183
|
_preparedCandidates = new Map();
|
|
107
184
|
_preparedCounter = 0;
|
|
@@ -111,6 +188,33 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
111
188
|
_savepointDepths = new Map();
|
|
112
189
|
_config;
|
|
113
190
|
_onErrorSavePoint;
|
|
191
|
+
/**
|
|
192
|
+
* The data-mapping options this connection was configured with, or
|
|
193
|
+
* undefined when it was given none - which is the common case, and
|
|
194
|
+
* the one that must not pay for this.
|
|
195
|
+
*/
|
|
196
|
+
_mappingDefaults;
|
|
197
|
+
/** The session's DateStyle as last reported, and what it parsed to. */
|
|
198
|
+
_dateStyleRaw;
|
|
199
|
+
_dateStyle;
|
|
200
|
+
/** The server's own money rendering - see ensureMoneyFormat(). */
|
|
201
|
+
_moneyFormat;
|
|
202
|
+
_moneyFormatPromise;
|
|
203
|
+
/** Statements currently holding the wire - see enterWire(). */
|
|
204
|
+
_wireUsers = 0;
|
|
205
|
+
_wireIdleWaiters;
|
|
206
|
+
_wireLock;
|
|
207
|
+
/** Bound once: enterWire()'s fast path hands out this same function. */
|
|
208
|
+
_releaseWire = () => {
|
|
209
|
+
if (--this._wireUsers || !this._wireIdleWaiters)
|
|
210
|
+
return;
|
|
211
|
+
const waiters = this._wireIdleWaiters;
|
|
212
|
+
this._wireIdleWaiters = undefined;
|
|
213
|
+
let i;
|
|
214
|
+
const l = waiters.length;
|
|
215
|
+
for (i = 0; i < l; i++)
|
|
216
|
+
waiters[i]();
|
|
217
|
+
};
|
|
114
218
|
transactionStatus = 'I';
|
|
115
219
|
socket;
|
|
116
220
|
owner;
|
|
@@ -118,6 +222,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
118
222
|
constructor(config) {
|
|
119
223
|
super();
|
|
120
224
|
this._config = Object.freeze(getConnectionConfig(config));
|
|
225
|
+
this._mappingDefaults = pickMappingDefaults(this._config);
|
|
121
226
|
this.socket = new PgSocket(this._config);
|
|
122
227
|
this.socket.on('error', err => this._onError(err));
|
|
123
228
|
// The reason travels with it: 'close' on its own cannot tell an
|
|
@@ -201,7 +306,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
201
306
|
}
|
|
202
307
|
async execute(sql, options = {}, cb) {
|
|
203
308
|
this.assertConnected();
|
|
204
|
-
const transactionCommand =
|
|
309
|
+
const transactionCommand = isTransactionCommand(sql);
|
|
205
310
|
let beginFirst = false;
|
|
206
311
|
let commitLast = false;
|
|
207
312
|
const { autoCommit } = options;
|
|
@@ -225,7 +330,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
225
330
|
// out from under it) - but ordered so the cheap local read still
|
|
226
331
|
// short-circuits the getter whenever rollbackOnError is already
|
|
227
332
|
// false, which it is outside a transaction.
|
|
228
|
-
const rollbackOnError =
|
|
333
|
+
const rollbackOnError =
|
|
334
|
+
// Not `transactionCommand`: `SET TRANSACTION` still wants the
|
|
335
|
+
// implicit BEGIN above, and only the savepoint is fatal to it.
|
|
336
|
+
!refusesSavepoint(sql) &&
|
|
229
337
|
this.inTransaction &&
|
|
230
338
|
(options.rollbackOnError ?? this.config.rollbackOnError ?? true);
|
|
231
339
|
if (rollbackOnError && this.inTransaction)
|
|
@@ -299,10 +407,27 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
299
407
|
* that point). A matching number of commit() calls is then needed to
|
|
300
408
|
* actually commit - see commit().
|
|
301
409
|
*/
|
|
302
|
-
async startTransaction() {
|
|
410
|
+
async startTransaction(options) {
|
|
411
|
+
// A nested call is a depth increment, not a second BEGIN - so modes
|
|
412
|
+
// it carries could only be applied to a transaction someone else
|
|
413
|
+
// opened. PostgreSQL would take them: a nested `BEGIN ISOLATION
|
|
414
|
+
// LEVEL SERIALIZABLE` before any query has run *silently changes the
|
|
415
|
+
// outer transaction's level* (measured, only a warning about the
|
|
416
|
+
// redundant BEGIN), and after one it raises 25001. Neither is what
|
|
417
|
+
// the caller of a nested scope means, so this says so instead.
|
|
418
|
+
// Built before anything is counted: an isolation level this client
|
|
419
|
+
// does not know throws here, and a start that never happened must
|
|
420
|
+
// not leave the depth one higher than the transactions there are.
|
|
421
|
+
const sql = beginStatement(options);
|
|
422
|
+
if (options && this._transactionDepth > 0)
|
|
423
|
+
throw new Error('startTransaction() cannot set transaction modes on a transaction ' +
|
|
424
|
+
'that is already open - they belong to the BEGIN that opens it');
|
|
425
|
+
if (this.inTransaction) {
|
|
426
|
+
this._transactionDepth++;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await this.execute(sql);
|
|
303
430
|
this._transactionDepth++;
|
|
304
|
-
if (!this.inTransaction)
|
|
305
|
-
await this.execute('BEGIN');
|
|
306
431
|
}
|
|
307
432
|
/**
|
|
308
433
|
* Starts a savepoint, or - if one under the same `name` is already open -
|
|
@@ -414,6 +539,92 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
414
539
|
cancel() {
|
|
415
540
|
return this.socket.cancel();
|
|
416
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Claims the wire for one statement, and hands back the function that
|
|
544
|
+
* releases it.
|
|
545
|
+
*
|
|
546
|
+
* Several statements share it by default: PostgreSQL correlates
|
|
547
|
+
* responses to requests by order, so a connection can carry more than
|
|
548
|
+
* one at a time and a caller that does not wait for each reply is
|
|
549
|
+
* measurably faster. `exclusive` is `pipeline: false` - the statement
|
|
550
|
+
* waits until nothing else is running here, and nothing else starts
|
|
551
|
+
* until it releases.
|
|
552
|
+
*
|
|
553
|
+
* Returns the release function itself rather than a promise of one
|
|
554
|
+
* whenever it can be taken without waiting, which is every statement
|
|
555
|
+
* on a connection nobody has asked to serialise - the common case, and
|
|
556
|
+
* the one that must not pay for this.
|
|
557
|
+
*/
|
|
558
|
+
enterWire(exclusive) {
|
|
559
|
+
if (!exclusive && !this._wireLock) {
|
|
560
|
+
this._wireUsers++;
|
|
561
|
+
return this._releaseWire;
|
|
562
|
+
}
|
|
563
|
+
return this._enterWireQueued(!!exclusive);
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Asks the server how it renders money, once per connection.
|
|
567
|
+
*
|
|
568
|
+
* `money` travels as an int64 of the smallest currency unit, and how
|
|
569
|
+
* many of those make one unit is `lc_monetary` - which the server
|
|
570
|
+
* does not report among its startup parameters, so `1234` on the wire
|
|
571
|
+
* is $12.34, ¥1234 or 1.234 KWD depending on a setting only the server
|
|
572
|
+
* knows. Asking it to render a value it already knows is the whole
|
|
573
|
+
* answer, and needs no locale data on this side.
|
|
574
|
+
*
|
|
575
|
+
* `1` is the value asked for because it cannot carry a thousands
|
|
576
|
+
* separator: whatever separator comes back can only be the decimal
|
|
577
|
+
* one, and the digits after it can only be the fraction. `$1.00` is
|
|
578
|
+
* two, `¥1` is none.
|
|
579
|
+
*/
|
|
580
|
+
async ensureMoneyFormat() {
|
|
581
|
+
if (this._moneyFormat)
|
|
582
|
+
return;
|
|
583
|
+
if (!this._moneyFormatPromise) {
|
|
584
|
+
// Assigned before the query starts: the query goes through
|
|
585
|
+
// queryCached like any other, and would ask for the format again
|
|
586
|
+
// on its way out.
|
|
587
|
+
let done;
|
|
588
|
+
this._moneyFormatPromise = new Promise(r => (done = r));
|
|
589
|
+
this.queryCached("select '1'::money::text", undefined, undefined, {
|
|
590
|
+
columnFormat: DataFormat.text,
|
|
591
|
+
})
|
|
592
|
+
.then(r => {
|
|
593
|
+
const text = r.rows?.[0]?.[0];
|
|
594
|
+
if (typeof text === 'string')
|
|
595
|
+
this._moneyFormat = parseMoneyFormat(text);
|
|
596
|
+
})
|
|
597
|
+
.catch(() => undefined)
|
|
598
|
+
.then(done);
|
|
599
|
+
}
|
|
600
|
+
return this._moneyFormatPromise;
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Whether these columns need the server's money format and this
|
|
604
|
+
* connection has not asked for it yet.
|
|
605
|
+
*
|
|
606
|
+
* The scan costs a look at each column's OID, and only until the
|
|
607
|
+
* answer is in - from then on the first test ends it.
|
|
608
|
+
*/
|
|
609
|
+
needsMoneyFormat(fields, options) {
|
|
610
|
+
if (this._moneyFormat || !fields)
|
|
611
|
+
return false;
|
|
612
|
+
const asString = options?.fetchAsString;
|
|
613
|
+
const l = fields.length;
|
|
614
|
+
let i;
|
|
615
|
+
let oid;
|
|
616
|
+
for (i = 0; i < l; i++) {
|
|
617
|
+
oid = fields[i].dataTypeId;
|
|
618
|
+
if (oid !== DataTypeOIDs.money && oid !== DataTypeOIDs._money)
|
|
619
|
+
continue;
|
|
620
|
+
// Named by fetchAsString, so the value goes back exactly as the
|
|
621
|
+
// server rendered it and no decoder asks what a minor unit is.
|
|
622
|
+
if (asString?.includes(oid))
|
|
623
|
+
continue;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
417
628
|
ref() {
|
|
418
629
|
this._refCount++;
|
|
419
630
|
}
|
|
@@ -532,6 +743,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
532
743
|
return stream;
|
|
533
744
|
}
|
|
534
745
|
async _execute(sql, options = {}, cb) {
|
|
746
|
+
options = this.withDefaults(options);
|
|
535
747
|
this.ref();
|
|
536
748
|
try {
|
|
537
749
|
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
@@ -544,11 +756,13 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
544
756
|
let parsers;
|
|
545
757
|
let current = { command: undefined };
|
|
546
758
|
let fields;
|
|
759
|
+
const deferred = [];
|
|
760
|
+
let currentDeferred;
|
|
547
761
|
let error;
|
|
548
762
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
549
763
|
const rowDecoder = resolveRowDecoder(options);
|
|
550
764
|
this.runningQueryCount++;
|
|
551
|
-
|
|
765
|
+
const scriptResult = await this.socket.sendQueryMessage(sql, (code, msg, done) => {
|
|
552
766
|
switch (code) {
|
|
553
767
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
554
768
|
error = msg;
|
|
@@ -578,9 +792,25 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
578
792
|
parsers = getParsers(typeMap, fields, options);
|
|
579
793
|
current.fields = wrapRowDescription(typeMap, fields, DataFormat.text, options);
|
|
580
794
|
current.rows = [];
|
|
795
|
+
currentDeferred = this.needsMoneyFormat(fields, options)
|
|
796
|
+
? {
|
|
797
|
+
target: current,
|
|
798
|
+
raw: [],
|
|
799
|
+
parsers,
|
|
800
|
+
fields: current.fields,
|
|
801
|
+
}
|
|
802
|
+
: undefined;
|
|
803
|
+
if (currentDeferred)
|
|
804
|
+
deferred.push(currentDeferred);
|
|
581
805
|
break;
|
|
582
806
|
case Protocol.BackendMessageCode.DataRow:
|
|
583
807
|
{
|
|
808
|
+
// Set aside rather than read against a scale nobody has
|
|
809
|
+
// confirmed - see _decodeDeferredRows().
|
|
810
|
+
if (currentDeferred) {
|
|
811
|
+
currentDeferred.raw.push(msg);
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
584
814
|
const row = rowDecoder.decode(parsers, msg.data, msg.columnCount, options, current.fields);
|
|
585
815
|
if (cb)
|
|
586
816
|
cb('row', row);
|
|
@@ -589,6 +819,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
589
819
|
}
|
|
590
820
|
break;
|
|
591
821
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
822
|
+
currentDeferred = undefined;
|
|
592
823
|
// Ignore BEGIN command that we added to sql
|
|
593
824
|
current.command = msg.command;
|
|
594
825
|
if (reportsRowsAffected(current.command))
|
|
@@ -620,6 +851,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
620
851
|
break;
|
|
621
852
|
}
|
|
622
853
|
});
|
|
854
|
+
if (deferred.length)
|
|
855
|
+
await this._decodeDeferredRows(deferred, rowDecoder, options);
|
|
856
|
+
return scriptResult;
|
|
623
857
|
}
|
|
624
858
|
finally {
|
|
625
859
|
this.runningQueryCount--;
|
|
@@ -655,9 +889,14 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
655
889
|
* of failing every call from then on.
|
|
656
890
|
*/
|
|
657
891
|
async queryCached(sql, paramTypes, params, options) {
|
|
892
|
+
// Read before the merge below: a connection-wide fetchAsString must
|
|
893
|
+
// not give every statement a name on first sight - see
|
|
894
|
+
// _queryCached().
|
|
895
|
+
const callWantsFields = !!options.fetchAsString?.length || !!options.unknownTypesAsString;
|
|
896
|
+
options = this.withDefaults(options);
|
|
658
897
|
const savepoint = this._inlineSavepointFor(sql, options);
|
|
659
898
|
try {
|
|
660
|
-
return await this._queryCached(sql, paramTypes, params, options, savepoint);
|
|
899
|
+
return await this._queryCached(sql, paramTypes, params, options, savepoint, callWantsFields);
|
|
661
900
|
}
|
|
662
901
|
catch (e) {
|
|
663
902
|
// PostgreSQL throws away everything between a failed statement and
|
|
@@ -673,16 +912,19 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
673
912
|
/**
|
|
674
913
|
* The savepoint rollbackOnError calls for, or undefined when this call
|
|
675
914
|
* needs none: outside a transaction there is nothing to roll back to,
|
|
676
|
-
*
|
|
677
|
-
* boundaries (wrapping COMMIT in a savepoint would be nonsense)
|
|
915
|
+
* a statement that is itself a transaction command manages its own
|
|
916
|
+
* boundaries (wrapping COMMIT in a savepoint would be nonsense), and
|
|
917
|
+
* PostgreSQL refuses `SET TRANSACTION` inside one outright.
|
|
678
918
|
*/
|
|
679
919
|
_inlineSavepointFor(sql, options) {
|
|
680
|
-
if (!this.inTransaction ||
|
|
920
|
+
if (!this.inTransaction || refusesSavepoint(sql))
|
|
681
921
|
return undefined;
|
|
682
922
|
const on = options.rollbackOnError ?? this.config.rollbackOnError ?? true;
|
|
683
923
|
return on ? this._onErrorSavePoint : undefined;
|
|
684
924
|
}
|
|
685
|
-
async _queryCached(sql, paramTypes, params, options, savepoint
|
|
925
|
+
async _queryCached(sql, paramTypes, params, options, savepoint,
|
|
926
|
+
/** Whether the CALL asked for text columns, not the connection. */
|
|
927
|
+
callWantsFields) {
|
|
686
928
|
const enabled = options.prepare ?? this.config.prepare ?? true;
|
|
687
929
|
if (!enabled)
|
|
688
930
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
@@ -718,18 +960,56 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
718
960
|
// text (what queryOnce() falls back to).
|
|
719
961
|
// unknownTypesAsString needs the columns known before the Bind for
|
|
720
962
|
// the same reason fetchAsString does, so it earns a name the same way.
|
|
721
|
-
|
|
963
|
+
// ...unless the caller already asked for the whole row as text, in
|
|
964
|
+
// which case there is nothing for either option to resolve and the
|
|
965
|
+
// Parse+Describe would buy nothing. An array of formats is not the
|
|
966
|
+
// same thing: it can be shorter than the row, and the columns it
|
|
967
|
+
// does not cover are binary.
|
|
968
|
+
//
|
|
969
|
+
// And only for a list the CALL carried. The same list set once on
|
|
970
|
+
// the connection applies to every statement, so naming each one on
|
|
971
|
+
// first sight would double the round trips of a workload full of
|
|
972
|
+
// one-shot SQL and leave a server-side statement behind for each -
|
|
973
|
+
// measured at 5 distinct statements, 5 round trips against 10. A
|
|
974
|
+
// connection-wide list takes queryOnce()'s whole-row-text fallback
|
|
975
|
+
// on a statement's first run instead, and per-column codes from its
|
|
976
|
+
// second, which is where the name arrives anyway.
|
|
977
|
+
const needsFields = callWantsFields && options.columnFormat !== DataFormat.text;
|
|
722
978
|
if (!needsFields && !this._earnsAName(key))
|
|
723
979
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
980
|
+
let preparing = this._preparing.get(key);
|
|
981
|
+
if (!preparing) {
|
|
982
|
+
// Registered before the first await inside, so everything that
|
|
983
|
+
// arrives while this one is in flight finds it rather than
|
|
984
|
+
// starting a second one.
|
|
985
|
+
preparing = this._prepareForCache(key, sql, paramTypes);
|
|
986
|
+
this._preparing.set(key, preparing);
|
|
987
|
+
}
|
|
988
|
+
else if (!needsFields) {
|
|
989
|
+
// Someone else is already naming it. Waiting would cost a round
|
|
990
|
+
// trip the one-shot path does not - the name is for the calls
|
|
991
|
+
// after this burst, not for this one.
|
|
992
|
+
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
993
|
+
}
|
|
994
|
+
const entry = await preparing;
|
|
995
|
+
return this.executeReused(entry.name, entry.fields, paramTypes, params, options, savepoint);
|
|
996
|
+
}
|
|
997
|
+
/**
|
|
998
|
+
* Gives this SQL a name and caches it, once, however many callers are
|
|
999
|
+
* waiting. A Parse that fails caches nothing and reports itself; the
|
|
1000
|
+
* statement is simply never reused. It runs outside the savepoint, as
|
|
1001
|
+
* the prepare() the transaction path used to do ahead of its own
|
|
1002
|
+
* wrapper.
|
|
1003
|
+
*/
|
|
1004
|
+
_prepareForCache(key, sql, paramTypes) {
|
|
724
1005
|
const name = 'C_' + ++this._preparedCounter;
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
this.
|
|
732
|
-
return this.executeReused(name, entry.fields, paramTypes, params, options, savepoint);
|
|
1006
|
+
return (async () => {
|
|
1007
|
+
const { fields } = await this.prepareOnce(sql, paramTypes, name);
|
|
1008
|
+
const entry = { name, fields };
|
|
1009
|
+
await this._evictPreparedIfFull();
|
|
1010
|
+
this._preparedCache.set(key, entry);
|
|
1011
|
+
return entry;
|
|
1012
|
+
})().finally(() => this._preparing.delete(key));
|
|
733
1013
|
}
|
|
734
1014
|
/**
|
|
735
1015
|
* Counts one more use of this SQL and says whether it has now been seen
|
|
@@ -782,6 +1062,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
782
1062
|
await Promise.all([closed, synced]);
|
|
783
1063
|
}
|
|
784
1064
|
async queryOnce(sql, paramTypes, params, options, savepoint) {
|
|
1065
|
+
options = this.withDefaults(options);
|
|
785
1066
|
this.assertConnected();
|
|
786
1067
|
this.ref();
|
|
787
1068
|
try {
|
|
@@ -856,11 +1137,17 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
856
1137
|
case Protocol.BackendMessageCode.DataRow:
|
|
857
1138
|
rows.push(msg);
|
|
858
1139
|
break;
|
|
1140
|
+
// An empty statement - '' or nothing but a comment - answers
|
|
1141
|
+
// with this *instead of* a CommandComplete, so it has to take
|
|
1142
|
+
// the same slot: a RELEASE riding along behind it would
|
|
1143
|
+
// otherwise be read as the caller's own command. The sentinel
|
|
1144
|
+
// carries no command name, which is what `pg` reports too.
|
|
1145
|
+
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
859
1146
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
860
1147
|
if (leadingCommandTags)
|
|
861
1148
|
leadingCommandTags--;
|
|
862
1149
|
else if (!commandTag)
|
|
863
|
-
commandTag = msg;
|
|
1150
|
+
commandTag = msg || EMPTY_QUERY_TAG;
|
|
864
1151
|
break;
|
|
865
1152
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
866
1153
|
error = msg;
|
|
@@ -878,6 +1165,12 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
878
1165
|
});
|
|
879
1166
|
if (commandTag?.command)
|
|
880
1167
|
result.command = commandTag.command;
|
|
1168
|
+
// The rows are still raw here, so the one question money cannot
|
|
1169
|
+
// answer for itself can still be asked before they are read.
|
|
1170
|
+
if (this.needsMoneyFormat(resultFields, options)) {
|
|
1171
|
+
await this.ensureMoneyFormat();
|
|
1172
|
+
options = this.withDefaults(options);
|
|
1173
|
+
}
|
|
881
1174
|
if (resultFields && parsers) {
|
|
882
1175
|
if (!result.command)
|
|
883
1176
|
result.command = 'SELECT';
|
|
@@ -1015,6 +1308,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1015
1308
|
* that arrive before one belong to the statement it closes.
|
|
1016
1309
|
*/
|
|
1017
1310
|
async executePipeline(requests, options) {
|
|
1311
|
+
options = this.withDefaults(options);
|
|
1018
1312
|
this.assertConnected();
|
|
1019
1313
|
this.ref();
|
|
1020
1314
|
try {
|
|
@@ -1025,6 +1319,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1025
1319
|
const results = [];
|
|
1026
1320
|
let pendingFields;
|
|
1027
1321
|
let pendingRows;
|
|
1322
|
+
const deferred = [];
|
|
1028
1323
|
let error;
|
|
1029
1324
|
// Plan every statement against the prepared statement cache before
|
|
1030
1325
|
// writing anything: one that is already prepared binds to its name
|
|
@@ -1138,12 +1433,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1138
1433
|
if (!result.command)
|
|
1139
1434
|
result.command = 'SELECT';
|
|
1140
1435
|
const rows = pendingRows || [];
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1436
|
+
if (this.needsMoneyFormat(fields, options)) {
|
|
1437
|
+
// Set aside: see _decodeDeferredRows().
|
|
1438
|
+
result.rows = [];
|
|
1439
|
+
deferred.push({
|
|
1440
|
+
target: result,
|
|
1441
|
+
raw: rows,
|
|
1442
|
+
parsers,
|
|
1443
|
+
fields: resultFields,
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
else {
|
|
1447
|
+
const l = rows.length;
|
|
1448
|
+
let i;
|
|
1449
|
+
for (i = 0; i < l; i++) {
|
|
1450
|
+
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
1451
|
+
}
|
|
1452
|
+
result.rows = rows;
|
|
1145
1453
|
}
|
|
1146
|
-
result.rows = rows;
|
|
1147
1454
|
}
|
|
1148
1455
|
if (reportsRowsAffected(result.command))
|
|
1149
1456
|
result.rowsAffected = msg.rowCount;
|
|
@@ -1201,6 +1508,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1201
1508
|
fields: promo.plan.fields,
|
|
1202
1509
|
});
|
|
1203
1510
|
}
|
|
1511
|
+
if (deferred.length)
|
|
1512
|
+
await this._decodeDeferredRows(deferred, rowDecoder, options);
|
|
1204
1513
|
if (timingEnabled) {
|
|
1205
1514
|
const elapsed = performance.now() - startTime;
|
|
1206
1515
|
const l = results.length;
|
|
@@ -1215,6 +1524,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1215
1524
|
}
|
|
1216
1525
|
}
|
|
1217
1526
|
async executeBatchReused(statementName, cachedFields, paramTypes, paramSets, options) {
|
|
1527
|
+
options = this.withDefaults(options);
|
|
1218
1528
|
this.assertConnected();
|
|
1219
1529
|
this.ref();
|
|
1220
1530
|
try {
|
|
@@ -1320,6 +1630,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1320
1630
|
}
|
|
1321
1631
|
}
|
|
1322
1632
|
async executeReused(statementName, cachedFields, paramTypes, params, options, savepoint) {
|
|
1633
|
+
options = this.withDefaults(options);
|
|
1323
1634
|
this.assertConnected();
|
|
1324
1635
|
this.ref();
|
|
1325
1636
|
try {
|
|
@@ -1381,6 +1692,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1381
1692
|
case Protocol.BackendMessageCode.DataRow:
|
|
1382
1693
|
rows.push(msg);
|
|
1383
1694
|
break;
|
|
1695
|
+
// See queryOnce() for why an empty statement lands here.
|
|
1696
|
+
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
1384
1697
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
1385
1698
|
// Three statements can answer here when a savepoint rides
|
|
1386
1699
|
// along: SAVEPOINT's tag, then the caller's own, then
|
|
@@ -1389,7 +1702,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1389
1702
|
if (leadingCommandTags)
|
|
1390
1703
|
leadingCommandTags--;
|
|
1391
1704
|
else if (!commandTag)
|
|
1392
|
-
commandTag = msg;
|
|
1705
|
+
commandTag = msg || EMPTY_QUERY_TAG;
|
|
1393
1706
|
break;
|
|
1394
1707
|
case Protocol.BackendMessageCode.ErrorResponse:
|
|
1395
1708
|
error = msg;
|
|
@@ -1407,6 +1720,12 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1407
1720
|
});
|
|
1408
1721
|
if (commandTag?.command)
|
|
1409
1722
|
result.command = commandTag.command;
|
|
1723
|
+
// The rows are still raw here, so the one question money cannot
|
|
1724
|
+
// answer for itself can still be asked before they are read.
|
|
1725
|
+
if (this.needsMoneyFormat(resultFields, options)) {
|
|
1726
|
+
await this.ensureMoneyFormat();
|
|
1727
|
+
options = this.withDefaults(options);
|
|
1728
|
+
}
|
|
1410
1729
|
if (resultFields && parsers) {
|
|
1411
1730
|
if (!result.command)
|
|
1412
1731
|
result.command = 'SELECT';
|
|
@@ -1433,6 +1752,115 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1433
1752
|
const handled = super.emit(event, ...args);
|
|
1434
1753
|
return this.owner ? this.owner.emit(event, ...args) || handled : handled;
|
|
1435
1754
|
}
|
|
1755
|
+
/**
|
|
1756
|
+
* The slow half of enterWire(): either an exclusive statement, or an
|
|
1757
|
+
* ordinary one arriving while an exclusive statement holds the wire.
|
|
1758
|
+
*
|
|
1759
|
+
* The claim itself is synchronous on purpose. Each waiter wakes in its
|
|
1760
|
+
* own microtask and runs to its next `await` without interruption, so
|
|
1761
|
+
* re-reading `_wireLock` and assigning it in the same stretch is what
|
|
1762
|
+
* makes two waiters unable to both believe they took it.
|
|
1763
|
+
*/
|
|
1764
|
+
async _enterWireQueued(exclusive) {
|
|
1765
|
+
while (this._wireLock)
|
|
1766
|
+
await this._wireLock;
|
|
1767
|
+
if (!exclusive) {
|
|
1768
|
+
this._wireUsers++;
|
|
1769
|
+
return this._releaseWire;
|
|
1770
|
+
}
|
|
1771
|
+
let unlock;
|
|
1772
|
+
this._wireLock = new Promise(resolve => (unlock = resolve));
|
|
1773
|
+
// Whatever was already in flight when the lock went up still has to
|
|
1774
|
+
// finish - the point of asking is not to be interleaved with it.
|
|
1775
|
+
if (this._wireUsers)
|
|
1776
|
+
await new Promise(resolve => (this._wireIdleWaiters || (this._wireIdleWaiters = [])).push(resolve));
|
|
1777
|
+
this._wireUsers++;
|
|
1778
|
+
return () => {
|
|
1779
|
+
this._wireLock = undefined;
|
|
1780
|
+
this._releaseWire();
|
|
1781
|
+
unlock();
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* `options` with whatever this connection can answer for it: the
|
|
1786
|
+
* data-mapping defaults it was configured with, and the money format
|
|
1787
|
+
* it has learned.
|
|
1788
|
+
*
|
|
1789
|
+
* At most one shallow copy per statement, and only when there is
|
|
1790
|
+
* something to fill in - a connection configured with nothing and no
|
|
1791
|
+
* money in sight hands the caller's own object straight back.
|
|
1792
|
+
*/
|
|
1793
|
+
/**
|
|
1794
|
+
* Rows left raw because a money column's scale was not in yet.
|
|
1795
|
+
*
|
|
1796
|
+
* Two paths decode inside the message loop, where there is no longer
|
|
1797
|
+
* anywhere to ask the server anything: the Simple Query protocol and
|
|
1798
|
+
* a pipeline. Rather than read those against a scale nobody
|
|
1799
|
+
* confirmed, they set the rows aside and this reads them once the
|
|
1800
|
+
* loop is over and the question can be asked.
|
|
1801
|
+
*/
|
|
1802
|
+
async _decodeDeferredRows(deferred, rowDecoder, options) {
|
|
1803
|
+
await this.ensureMoneyFormat();
|
|
1804
|
+
const opts = this.withDefaults(options);
|
|
1805
|
+
let d;
|
|
1806
|
+
let i;
|
|
1807
|
+
let k;
|
|
1808
|
+
for (i = 0; i < deferred.length; i++) {
|
|
1809
|
+
d = deferred[i];
|
|
1810
|
+
const l = d.raw.length;
|
|
1811
|
+
const out = new Array(l);
|
|
1812
|
+
for (k = 0; k < l; k++)
|
|
1813
|
+
out[k] = rowDecoder.decode(d.parsers, d.raw[k].data, d.raw[k].columnCount, opts, d.fields);
|
|
1814
|
+
d.target.rows = out;
|
|
1815
|
+
}
|
|
1816
|
+
return opts;
|
|
1817
|
+
}
|
|
1818
|
+
withDefaults(options) {
|
|
1819
|
+
const defaults = this._mappingDefaults;
|
|
1820
|
+
const money = this._moneyFormat;
|
|
1821
|
+
const dateStyle = this._currentDateStyle();
|
|
1822
|
+
if (!defaults && !money && !dateStyle)
|
|
1823
|
+
return options;
|
|
1824
|
+
let out;
|
|
1825
|
+
if (defaults) {
|
|
1826
|
+
let k;
|
|
1827
|
+
for (k in defaults) {
|
|
1828
|
+
// The call always wins; the connection only fills a gap.
|
|
1829
|
+
if (options[k] !== undefined)
|
|
1830
|
+
continue;
|
|
1831
|
+
out = out || { ...options };
|
|
1832
|
+
out[k] = defaults[k];
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
if (money && !(out || options).moneyFormat) {
|
|
1836
|
+
out = out || { ...options };
|
|
1837
|
+
out.moneyFormat = money;
|
|
1838
|
+
}
|
|
1839
|
+
if (dateStyle && !(out || options).dateStyle) {
|
|
1840
|
+
out = out || { ...options };
|
|
1841
|
+
out.dateStyle = dateStyle;
|
|
1842
|
+
}
|
|
1843
|
+
return out || options;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* The session's DateStyle, when it is not ISO.
|
|
1847
|
+
*
|
|
1848
|
+
* Read off the reported parameters rather than cached once: `SET
|
|
1849
|
+
* DateStyle` is answered with a fresh ParameterStatus, so the string
|
|
1850
|
+
* changes under us and a connection that changes it mid-session must
|
|
1851
|
+
* not go on decoding by the old one. The comparison is against the
|
|
1852
|
+
* same string the socket holds, so it costs a read and a pointer
|
|
1853
|
+
* compare while nothing changes - which is always, for the ISO
|
|
1854
|
+
* default that needs no parsing at all.
|
|
1855
|
+
*/
|
|
1856
|
+
_currentDateStyle() {
|
|
1857
|
+
const raw = this.socket.sessionParameters.DateStyle;
|
|
1858
|
+
if (raw !== this._dateStyleRaw) {
|
|
1859
|
+
this._dateStyleRaw = raw;
|
|
1860
|
+
this._dateStyle = parseDateStyleSetting(raw);
|
|
1861
|
+
}
|
|
1862
|
+
return this._dateStyle;
|
|
1863
|
+
}
|
|
1436
1864
|
_onError(err) {
|
|
1437
1865
|
if (this.socket.state !== ConnectionState.READY)
|
|
1438
1866
|
return;
|