postgrejs 3.4.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 +43 -1
- package/README.md +29 -4
- package/connection/connection.d.ts +37 -0
- package/connection/connection.js +85 -7
- package/connection/cursor.d.ts +8 -0
- package/connection/cursor.js +39 -1
- package/connection/intl-connection.d.ts +6 -1
- package/connection/intl-connection.js +284 -122
- package/connection/pool.d.ts +16 -0
- package/connection/pool.js +52 -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/protocol/pg-socket.d.ts +2 -2
- package/protocol/pg-socket.js +12 -6
- 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
|
@@ -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
|
*
|
|
@@ -41,6 +57,14 @@ const PREPARE_AFTER_USES = 2;
|
|
|
41
57
|
* SQL that manages transaction boundaries itself, and so must never be
|
|
42
58
|
* wrapped in a savepoint or an implicit BEGIN/COMMIT of ours.
|
|
43
59
|
*/
|
|
60
|
+
/**
|
|
61
|
+
* What the prepared statement cache is keyed on. The parameter OIDs are
|
|
62
|
+
* part of it because the server fixes them at Parse: the same SQL bound
|
|
63
|
+
* with different types is a different plan.
|
|
64
|
+
*/
|
|
65
|
+
function preparedCacheKey(sql, paramTypes) {
|
|
66
|
+
return paramTypes?.length ? sql + '\u0000' + paramTypes.join(',') : sql;
|
|
67
|
+
}
|
|
44
68
|
const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
|
|
45
69
|
export class IntlConnection extends SafeEventEmitter {
|
|
46
70
|
/**
|
|
@@ -259,7 +283,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
259
283
|
* nesting the existing one.
|
|
260
284
|
*/
|
|
261
285
|
async savepoint(name) {
|
|
262
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
286
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
263
287
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
264
288
|
const depth = (this._savepointDepths.get(name) || 0) + 1;
|
|
265
289
|
this._savepointDepths.set(name, depth);
|
|
@@ -291,7 +315,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
291
315
|
* @param immediate - Ignores the tracked depth and releases right away.
|
|
292
316
|
*/
|
|
293
317
|
async releaseSavepoint(name, immediate) {
|
|
294
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
318
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
295
319
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
296
320
|
const depth = this._savepointDepths.get(name) || 0;
|
|
297
321
|
if (!immediate && depth > 1) {
|
|
@@ -351,7 +375,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
351
375
|
* unwind.
|
|
352
376
|
*/
|
|
353
377
|
async rollbackToSavepoint(name) {
|
|
354
|
-
if (!(name && name.match(/^[a-zA-Z]\w
|
|
378
|
+
if (!(name && name.match(/^[a-zA-Z]\w*$/)))
|
|
355
379
|
throw new Error(`Invalid savepoint "${name}"`);
|
|
356
380
|
this._savepointDepths.delete(name);
|
|
357
381
|
await this.execute('ROLLBACK TO SAVEPOINT ' + name, { autoCommit: false });
|
|
@@ -515,8 +539,11 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
515
539
|
break;
|
|
516
540
|
case Protocol.BackendMessageCode.RowDescription:
|
|
517
541
|
fields = msg.fields;
|
|
518
|
-
|
|
519
|
-
|
|
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);
|
|
520
547
|
current.rows = [];
|
|
521
548
|
break;
|
|
522
549
|
case Protocol.BackendMessageCode.DataRow:
|
|
@@ -531,11 +558,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
531
558
|
case Protocol.BackendMessageCode.CommandComplete:
|
|
532
559
|
// Ignore BEGIN command that we added to sql
|
|
533
560
|
current.command = msg.command;
|
|
534
|
-
if (current.command
|
|
535
|
-
current.command === 'INSERT' ||
|
|
536
|
-
current.command === 'UPDATE') {
|
|
561
|
+
if (reportsRowsAffected(current.command))
|
|
537
562
|
current.rowsAffected = msg.rowCount;
|
|
538
|
-
}
|
|
539
563
|
if (timingEnabled)
|
|
540
564
|
current.executeTime = performance.now() - currentStart;
|
|
541
565
|
if (current.rows)
|
|
@@ -627,9 +651,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
627
651
|
const enabled = options.prepare ?? this.config.prepare ?? true;
|
|
628
652
|
if (!enabled)
|
|
629
653
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
630
|
-
const key = paramTypes
|
|
631
|
-
? sql + '\u0000' + paramTypes.join(',')
|
|
632
|
-
: sql;
|
|
654
|
+
const key = preparedCacheKey(sql, paramTypes);
|
|
633
655
|
const cached = this._preparedCache.get(key);
|
|
634
656
|
if (cached) {
|
|
635
657
|
// Re-insert to mark it most recently used.
|
|
@@ -653,12 +675,14 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
653
675
|
return result;
|
|
654
676
|
}
|
|
655
677
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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))
|
|
659
685
|
return this.queryOnce(sql, paramTypes, params, options, savepoint);
|
|
660
|
-
}
|
|
661
|
-
this._preparedCandidates.delete(key);
|
|
662
686
|
const name = 'C_' + ++this._preparedCounter;
|
|
663
687
|
// A Parse that fails caches nothing and reports itself; the statement
|
|
664
688
|
// is simply never reused. It runs outside the savepoint, as the
|
|
@@ -669,6 +693,19 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
669
693
|
this._preparedCache.set(key, entry);
|
|
670
694
|
return this.executeReused(name, entry.fields, paramTypes, params, options, savepoint);
|
|
671
695
|
}
|
|
696
|
+
/**
|
|
697
|
+
* Counts one more use of this SQL and says whether it has now been seen
|
|
698
|
+
* often enough to be worth preparing under a name.
|
|
699
|
+
*/
|
|
700
|
+
_earnsAName(key) {
|
|
701
|
+
const uses = (this._preparedCandidates.get(key) || 0) + 1;
|
|
702
|
+
if (uses < PREPARE_AFTER_USES) {
|
|
703
|
+
this._preparedCandidates.set(key, uses);
|
|
704
|
+
return false;
|
|
705
|
+
}
|
|
706
|
+
this._preparedCandidates.delete(key);
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
672
709
|
/** Closes the least recently used statement once the cache is full. */
|
|
673
710
|
async _evictPreparedIfFull() {
|
|
674
711
|
const max = this.config.preparedStatementCacheSize ?? 64;
|
|
@@ -719,7 +756,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
719
756
|
let resultFields;
|
|
720
757
|
let commandTag;
|
|
721
758
|
let error;
|
|
759
|
+
let suspended = false;
|
|
722
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;
|
|
723
777
|
// See executeReused() for what a savepoint riding along does to the
|
|
724
778
|
// CommandComplete stream.
|
|
725
779
|
let leadingCommandTags = savepoint ? 1 : 0;
|
|
@@ -727,9 +781,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
727
781
|
await this.socket
|
|
728
782
|
.sendExtendedQueryMessages({
|
|
729
783
|
parse: { sql, paramTypes },
|
|
730
|
-
bind: {
|
|
784
|
+
bind: {
|
|
785
|
+
typeMap,
|
|
786
|
+
paramTypes,
|
|
787
|
+
params,
|
|
788
|
+
queryOptions: options,
|
|
789
|
+
columnFormat,
|
|
790
|
+
},
|
|
731
791
|
describe: { type: 'P' },
|
|
732
|
-
execute: { fetchCount: options.fetchCount
|
|
792
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
733
793
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
734
794
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
735
795
|
}, (code, msg, done) => {
|
|
@@ -738,11 +798,17 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
738
798
|
case Protocol.BackendMessageCode.BindComplete:
|
|
739
799
|
case Protocol.BackendMessageCode.NoData:
|
|
740
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.
|
|
741
806
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
807
|
+
suspended = true;
|
|
742
808
|
break;
|
|
743
809
|
case Protocol.BackendMessageCode.RowDescription:
|
|
744
|
-
parsers = getParsers(typeMap, msg.fields);
|
|
745
|
-
resultFields = wrapRowDescription(typeMap, msg.fields,
|
|
810
|
+
parsers = getParsers(typeMap, msg.fields, options);
|
|
811
|
+
resultFields = wrapRowDescription(typeMap, msg.fields, columnFormat, options);
|
|
746
812
|
result.fields = resultFields;
|
|
747
813
|
result.rowType = resolveRowType(options);
|
|
748
814
|
break;
|
|
@@ -781,11 +847,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
781
847
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
782
848
|
}
|
|
783
849
|
}
|
|
784
|
-
if (
|
|
785
|
-
result.
|
|
786
|
-
|
|
850
|
+
if (suspended)
|
|
851
|
+
result.suspended = true;
|
|
852
|
+
if (reportsRowsAffected(result.command))
|
|
787
853
|
result.rowsAffected = commandTag?.rowCount;
|
|
788
|
-
}
|
|
789
854
|
if (timingEnabled)
|
|
790
855
|
result.executeTime = performance.now() - startTime;
|
|
791
856
|
return result;
|
|
@@ -858,25 +923,24 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
858
923
|
* so repeated executes of the same statement don't rebuild them.
|
|
859
924
|
* Shared by executeReused() and executeBatchReused().
|
|
860
925
|
*/
|
|
861
|
-
_resolveReusedParsers(cachedFields, typeMap, options) {
|
|
862
|
-
const columnFormat = options.columnFormat != null
|
|
863
|
-
? options.columnFormat
|
|
864
|
-
: DEFAULT_COLUMN_FORMAT;
|
|
926
|
+
_resolveReusedParsers(cachedFields, typeMap, options, columnFormat) {
|
|
865
927
|
const cached = this._executeReusedParserCache.get(cachedFields);
|
|
866
928
|
if (cached &&
|
|
867
929
|
cached.typeMap === typeMap &&
|
|
868
|
-
columnFormatsEqual(cached.columnFormat, columnFormat)
|
|
930
|
+
columnFormatsEqual(cached.columnFormat, columnFormat) &&
|
|
931
|
+
fetchAsStringEqual(cached.fetchAsString, options.fetchAsString)) {
|
|
869
932
|
return { parsers: cached.parsers, resultFields: cached.resultFields };
|
|
870
933
|
}
|
|
871
934
|
const fields = cachedFields.map((f, i) => ({
|
|
872
935
|
...f,
|
|
873
936
|
format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
|
|
874
937
|
}));
|
|
875
|
-
const parsers = getParsers(typeMap, fields);
|
|
876
|
-
const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
|
|
938
|
+
const parsers = getParsers(typeMap, fields, options);
|
|
939
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
877
940
|
this._executeReusedParserCache.set(cachedFields, {
|
|
878
941
|
typeMap,
|
|
879
942
|
columnFormat,
|
|
943
|
+
fetchAsString: options.fetchAsString,
|
|
880
944
|
parsers,
|
|
881
945
|
resultFields,
|
|
882
946
|
});
|
|
@@ -914,94 +978,183 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
914
978
|
const timingEnabled = options.timing ?? this.config.timing ?? false;
|
|
915
979
|
const startTime = timingEnabled ? performance.now() : 0;
|
|
916
980
|
const rowDecoder = resolveRowDecoder(options);
|
|
917
|
-
const columnFormat = options.columnFormat != null
|
|
918
|
-
? options.columnFormat
|
|
919
|
-
: DEFAULT_COLUMN_FORMAT;
|
|
920
981
|
const results = [];
|
|
921
982
|
let pendingFields;
|
|
922
983
|
let pendingRows;
|
|
923
984
|
let error;
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
985
|
+
// Plan every statement against the prepared statement cache before
|
|
986
|
+
// writing anything: one that is already prepared binds to its name
|
|
987
|
+
// and skips both Parse and Describe, one seen often enough earns a
|
|
988
|
+
// name here, and the rest go out unnamed exactly as before.
|
|
989
|
+
const prepareEnabled = options.prepare ?? this.config.prepare ?? true;
|
|
990
|
+
const plans = [];
|
|
991
|
+
const promotions = [];
|
|
992
|
+
const cachedKeys = [];
|
|
993
|
+
const named = new Map();
|
|
994
|
+
const statements = [];
|
|
995
|
+
for (const r of requests) {
|
|
996
|
+
const key = prepareEnabled
|
|
997
|
+
? preparedCacheKey(r.sql, r.paramTypes)
|
|
998
|
+
: undefined;
|
|
999
|
+
// A repeat can only skip its own Parse if the statement it binds
|
|
1000
|
+
// to has a name. An unnamed one would be gone by now: any later
|
|
1001
|
+
// statement's Parse replaces it.
|
|
1002
|
+
let plan = key != null ? named.get(key) : undefined;
|
|
1003
|
+
let sendParse = !plan;
|
|
1004
|
+
let sendDescribe = !plan;
|
|
1005
|
+
if (!plan) {
|
|
1006
|
+
const cached = key != null ? this._preparedCache.get(key) : undefined;
|
|
1007
|
+
if (cached && key != null) {
|
|
1008
|
+
// Re-insert to mark it most recently used.
|
|
1009
|
+
this._preparedCache.delete(key);
|
|
1010
|
+
this._preparedCache.set(key, cached);
|
|
1011
|
+
cachedKeys.push(key);
|
|
1012
|
+
plan = { name: cached.name, fields: cached.fields };
|
|
1013
|
+
sendParse = false;
|
|
1014
|
+
sendDescribe = false;
|
|
1015
|
+
}
|
|
1016
|
+
else if (key != null && this._earnsAName(key)) {
|
|
1017
|
+
plan = { name: 'C_' + ++this._preparedCounter };
|
|
1018
|
+
promotions.push({ key, plan });
|
|
1019
|
+
}
|
|
1020
|
+
else {
|
|
1021
|
+
plan = {};
|
|
1022
|
+
}
|
|
1023
|
+
if (key != null && plan.name)
|
|
1024
|
+
named.set(key, plan);
|
|
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;
|
|
1036
|
+
plans.push(plan);
|
|
1037
|
+
statements.push({
|
|
1038
|
+
// Per statement, not per pipeline: the statements differ, so a
|
|
1039
|
+
// single shared list of parameter OIDs could only ever be right
|
|
1040
|
+
// for one of them.
|
|
1041
|
+
parse: sendParse
|
|
1042
|
+
? { sql: r.sql, paramTypes: r.paramTypes, statement: plan.name }
|
|
1043
|
+
: undefined,
|
|
1044
|
+
bind: {
|
|
1045
|
+
typeMap,
|
|
1046
|
+
statement: plan.name,
|
|
1047
|
+
paramTypes: r.paramTypes,
|
|
1048
|
+
params: r.params,
|
|
1049
|
+
queryOptions: options,
|
|
1050
|
+
columnFormat: plan.columnFormat,
|
|
1051
|
+
},
|
|
1052
|
+
describe: sendDescribe ? { type: 'P' } : undefined,
|
|
1053
|
+
// Unlimited, never options.fetchCount: a suspended portal would
|
|
1054
|
+
// break the positional statement-to-result mapping.
|
|
1055
|
+
execute: { fetchCount: 0 },
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
940
1058
|
this.runningQueryCount++;
|
|
941
|
-
|
|
942
|
-
.
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1059
|
+
try {
|
|
1060
|
+
await this.socket
|
|
1061
|
+
.sendPipelineMessages({ statements }, (code, msg, done) => {
|
|
1062
|
+
switch (code) {
|
|
1063
|
+
case Protocol.BackendMessageCode.ParseComplete:
|
|
1064
|
+
case Protocol.BackendMessageCode.BindComplete:
|
|
1065
|
+
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1066
|
+
break;
|
|
1067
|
+
case Protocol.BackendMessageCode.NoData:
|
|
1068
|
+
pendingFields = undefined;
|
|
1069
|
+
break;
|
|
1070
|
+
case Protocol.BackendMessageCode.RowDescription:
|
|
1071
|
+
pendingFields = msg.fields;
|
|
1072
|
+
break;
|
|
1073
|
+
case Protocol.BackendMessageCode.DataRow:
|
|
1074
|
+
(pendingRows || (pendingRows = [])).push(msg);
|
|
1075
|
+
break;
|
|
1076
|
+
case Protocol.BackendMessageCode.EmptyQueryResponse:
|
|
1077
|
+
case Protocol.BackendMessageCode.CommandComplete: {
|
|
1078
|
+
const plan = plans[results.length];
|
|
1079
|
+
// A statement bound to a cached name sent no Describe, so
|
|
1080
|
+
// its columns come from the cache entry instead; one being
|
|
1081
|
+
// prepared here records its RowDescription for the entry it
|
|
1082
|
+
// is about to become, and for any repeat behind it.
|
|
1083
|
+
if (pendingFields)
|
|
1084
|
+
plan.fields = pendingFields;
|
|
1085
|
+
const fields = pendingFields || plan.fields;
|
|
1086
|
+
const result = { command: msg?.command };
|
|
1087
|
+
if (fields) {
|
|
1088
|
+
const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options, plan.columnFormat ?? DEFAULT_COLUMN_FORMAT);
|
|
1089
|
+
result.fields = resultFields;
|
|
1090
|
+
result.rowType = resolveRowType(options);
|
|
1091
|
+
if (!result.command)
|
|
1092
|
+
result.command = 'SELECT';
|
|
1093
|
+
const rows = pendingRows || [];
|
|
1094
|
+
const l = rows.length;
|
|
1095
|
+
let i;
|
|
1096
|
+
for (i = 0; i < l; i++) {
|
|
1097
|
+
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
1098
|
+
}
|
|
1099
|
+
result.rows = rows;
|
|
972
1100
|
}
|
|
973
|
-
result.
|
|
1101
|
+
if (reportsRowsAffected(result.command))
|
|
1102
|
+
result.rowsAffected = msg.rowCount;
|
|
1103
|
+
pendingFields = undefined;
|
|
1104
|
+
pendingRows = undefined;
|
|
1105
|
+
results.push(result);
|
|
1106
|
+
break;
|
|
974
1107
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1108
|
+
case Protocol.BackendMessageCode.ErrorResponse:
|
|
1109
|
+
error = msg;
|
|
1110
|
+
break;
|
|
1111
|
+
case Protocol.BackendMessageCode.ReadyForQuery:
|
|
1112
|
+
this.transactionStatus = msg.status;
|
|
1113
|
+
done(error);
|
|
1114
|
+
break;
|
|
1115
|
+
default:
|
|
1116
|
+
done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
|
|
983
1117
|
}
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1118
|
+
})
|
|
1119
|
+
.catch((e) => {
|
|
1120
|
+
// The rejected statement is the one right after everything
|
|
1121
|
+
// that completed, since results only grow on CommandComplete.
|
|
1122
|
+
if (e instanceof DatabaseError && error === e)
|
|
1123
|
+
e.failedIndex = results.length;
|
|
1124
|
+
throw e;
|
|
1125
|
+
})
|
|
1126
|
+
.finally(() => {
|
|
1127
|
+
this.runningQueryCount--;
|
|
1128
|
+
});
|
|
1129
|
+
}
|
|
1130
|
+
catch (e) {
|
|
1131
|
+
// A cached statement can be invalidated between two pipelines the
|
|
1132
|
+
// same way a cached single query can (0A000, "cached plan must not
|
|
1133
|
+
// change result type"). Drop what this pipeline reused and run it
|
|
1134
|
+
// again unprepared - once, since the second attempt has nothing
|
|
1135
|
+
// cached left to reuse.
|
|
1136
|
+
if (e?.code !== '0A000' || !cachedKeys.length)
|
|
1137
|
+
throw e;
|
|
1138
|
+
for (const k of cachedKeys)
|
|
1139
|
+
this._preparedCache.delete(k);
|
|
1140
|
+
// Inside an explicit transaction there is nothing to retry into:
|
|
1141
|
+
// the failed statement has already aborted the block.
|
|
1142
|
+
if (this.inTransaction)
|
|
1143
|
+
throw e;
|
|
1144
|
+
return await this.executePipeline(requests, options);
|
|
1145
|
+
}
|
|
1146
|
+
// Only now, with every Parse answered, are the new names real.
|
|
1147
|
+
let promo;
|
|
1148
|
+
let p;
|
|
1149
|
+
const promotionCount = promotions.length;
|
|
1150
|
+
for (p = 0; p < promotionCount; p++) {
|
|
1151
|
+
promo = promotions[p];
|
|
1152
|
+
await this._evictPreparedIfFull();
|
|
1153
|
+
this._preparedCache.set(promo.key, {
|
|
1154
|
+
name: promo.plan.name,
|
|
1155
|
+
fields: promo.plan.fields,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1005
1158
|
if (timingEnabled) {
|
|
1006
1159
|
const elapsed = performance.now() - startTime;
|
|
1007
1160
|
const l = results.length;
|
|
@@ -1025,8 +1178,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1025
1178
|
const rowDecoder = resolveRowDecoder(options);
|
|
1026
1179
|
let parsers;
|
|
1027
1180
|
let resultFields;
|
|
1181
|
+
const columnFormat = resolveColumnFormats(cachedFields, options);
|
|
1028
1182
|
if (cachedFields) {
|
|
1029
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1183
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1030
1184
|
parsers = resolved.parsers;
|
|
1031
1185
|
resultFields = resolved.resultFields;
|
|
1032
1186
|
}
|
|
@@ -1039,6 +1193,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1039
1193
|
paramTypes,
|
|
1040
1194
|
params,
|
|
1041
1195
|
queryOptions: options,
|
|
1196
|
+
columnFormat,
|
|
1042
1197
|
}));
|
|
1043
1198
|
this.runningQueryCount++;
|
|
1044
1199
|
await this.socket
|
|
@@ -1060,9 +1215,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1060
1215
|
const item = {};
|
|
1061
1216
|
if (msg?.command)
|
|
1062
1217
|
item.command = msg.command;
|
|
1063
|
-
if (item.command
|
|
1064
|
-
item.command === 'INSERT' ||
|
|
1065
|
-
item.command === 'UPDATE')
|
|
1218
|
+
if (reportsRowsAffected(item.command))
|
|
1066
1219
|
item.rowsAffected = msg.rowCount;
|
|
1067
1220
|
if (pendingRows && parsers && resultFields) {
|
|
1068
1221
|
const l = pendingRows.length;
|
|
@@ -1130,9 +1283,15 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1130
1283
|
let resultFields;
|
|
1131
1284
|
let commandTag;
|
|
1132
1285
|
let error;
|
|
1286
|
+
let suspended = false;
|
|
1133
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);
|
|
1134
1293
|
if (cachedFields) {
|
|
1135
|
-
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
|
|
1294
|
+
const resolved = this._resolveReusedParsers(cachedFields, typeMap, options, columnFormat);
|
|
1136
1295
|
parsers = resolved.parsers;
|
|
1137
1296
|
resultFields = resolved.resultFields;
|
|
1138
1297
|
result.fields = resultFields;
|
|
@@ -1152,8 +1311,9 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1152
1311
|
paramTypes,
|
|
1153
1312
|
params,
|
|
1154
1313
|
queryOptions: options,
|
|
1314
|
+
columnFormat,
|
|
1155
1315
|
},
|
|
1156
|
-
execute: { fetchCount: options.fetchCount
|
|
1316
|
+
execute: { fetchCount: options.fetchCount ?? 0 },
|
|
1157
1317
|
before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
|
|
1158
1318
|
after: savepoint ? 'RELEASE ' + savepoint : undefined,
|
|
1159
1319
|
}, (code, msg, done) => {
|
|
@@ -1161,7 +1321,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1161
1321
|
case Protocol.BackendMessageCode.ParseComplete:
|
|
1162
1322
|
case Protocol.BackendMessageCode.BindComplete:
|
|
1163
1323
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
1324
|
+
break;
|
|
1325
|
+
// See queryOnce() for what a suspended portal means here.
|
|
1164
1326
|
case Protocol.BackendMessageCode.PortalSuspended:
|
|
1327
|
+
suspended = true;
|
|
1165
1328
|
break;
|
|
1166
1329
|
case Protocol.BackendMessageCode.DataRow:
|
|
1167
1330
|
rows.push(msg);
|
|
@@ -1202,11 +1365,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1202
1365
|
rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
|
|
1203
1366
|
}
|
|
1204
1367
|
}
|
|
1205
|
-
if (
|
|
1206
|
-
result.
|
|
1207
|
-
|
|
1368
|
+
if (suspended)
|
|
1369
|
+
result.suspended = true;
|
|
1370
|
+
if (reportsRowsAffected(result.command))
|
|
1208
1371
|
result.rowsAffected = commandTag?.rowCount;
|
|
1209
|
-
}
|
|
1210
1372
|
if (timingEnabled)
|
|
1211
1373
|
result.executeTime = performance.now() - startTime;
|
|
1212
1374
|
return result;
|
package/connection/pool.d.ts
CHANGED
|
@@ -70,6 +70,22 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
70
70
|
* Executes a query
|
|
71
71
|
*/
|
|
72
72
|
query(sql: string | QueryRequest, options?: PoolQueryOptions): Promise<QueryResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Takes a connection out of the pool, runs `fn` inside a transaction on
|
|
75
|
+
* it, and gives the connection back however that ends.
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* await pool.transaction(async tx => {
|
|
79
|
+
* await tx.query('insert into orders (total) values ($1)', { params: [total] });
|
|
80
|
+
* await tx.query('update stock set n = n - 1 where sku = $1', { params: [sku] });
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* A transaction cannot be spread over pool.query() calls - each of those
|
|
85
|
+
* is free to pick a different connection, and a transaction lives on
|
|
86
|
+
* one. This is the way to get several statements onto the same one.
|
|
87
|
+
*/
|
|
88
|
+
transaction<T>(fn: (connection: Connection) => Promise<T>): Promise<T>;
|
|
73
89
|
prepare(sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
|
|
74
90
|
release(connection: Connection): Promise<void>;
|
|
75
91
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|