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.
Files changed (41) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +29 -4
  3. package/connection/connection.d.ts +37 -0
  4. package/connection/connection.js +85 -7
  5. package/connection/cursor.d.ts +8 -0
  6. package/connection/cursor.js +39 -1
  7. package/connection/intl-connection.d.ts +6 -1
  8. package/connection/intl-connection.js +284 -122
  9. package/connection/pool.d.ts +16 -0
  10. package/connection/pool.js +52 -6
  11. package/connection/portal.d.ts +1 -1
  12. package/connection/portal.js +3 -2
  13. package/connection/prepared-statement.js +8 -4
  14. package/data-type-map.d.ts +11 -0
  15. package/data-type-map.js +20 -2
  16. package/data-types/char-type.js +15 -0
  17. package/data-types/date-type.js +4 -18
  18. package/data-types/float4-type.js +79 -1
  19. package/data-types/json-type.js +7 -12
  20. package/data-types/jsonb-type.js +7 -12
  21. package/data-types/time-type.js +2 -15
  22. package/data-types/timestamp-type.js +8 -29
  23. package/data-types/timestamptz-type.js +7 -31
  24. package/interfaces/batch-result.d.ts +1 -1
  25. package/interfaces/command-result.d.ts +21 -1
  26. package/interfaces/data-mapping-options.d.ts +16 -0
  27. package/interfaces/data-type.d.ts +11 -0
  28. package/interfaces/query-options.d.ts +25 -3
  29. package/package.json +1 -1
  30. package/protocol/frontend.d.ts +7 -0
  31. package/protocol/frontend.js +5 -3
  32. package/protocol/pg-socket.d.ts +2 -2
  33. package/protocol/pg-socket.js +12 -6
  34. package/util/channel-name.d.ts +24 -0
  35. package/util/channel-name.js +44 -0
  36. package/util/get-parsers.d.ts +2 -1
  37. package/util/get-parsers.js +16 -1
  38. package/util/resolve-column-formats.d.ts +33 -0
  39. package/util/resolve-column-formats.js +67 -0
  40. package/util/wrap-row-description.d.ts +2 -1
  41. 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
- parsers = getParsers(typeMap, fields);
519
- current.fields = wrapRowDescription(typeMap, fields, DataFormat.text);
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 === 'DELETE' ||
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?.length
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
- const uses = (this._preparedCandidates.get(key) || 0) + 1;
657
- if (uses < PREPARE_AFTER_USES) {
658
- this._preparedCandidates.set(key, uses);
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: { typeMap, paramTypes, params, queryOptions: options },
784
+ bind: {
785
+ typeMap,
786
+ paramTypes,
787
+ params,
788
+ queryOptions: options,
789
+ columnFormat,
790
+ },
731
791
  describe: { type: 'P' },
732
- execute: { fetchCount: options.fetchCount || 100 },
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, options.columnFormat || DEFAULT_COLUMN_FORMAT);
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 (result.command === 'DELETE' ||
785
- result.command === 'INSERT' ||
786
- result.command === 'UPDATE') {
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
- const statements = requests.map(r => ({
925
- // Per statement, not per pipeline: the statements differ, so a
926
- // single shared list of parameter OIDs could only ever be right
927
- // for one of them.
928
- parse: { sql: r.sql, paramTypes: r.paramTypes },
929
- bind: {
930
- typeMap,
931
- paramTypes: r.paramTypes,
932
- params: r.params,
933
- queryOptions: options,
934
- },
935
- describe: { type: 'P' },
936
- // Unlimited, never options.fetchCount: a suspended portal would
937
- // break the positional statement-to-result mapping.
938
- execute: { fetchCount: 0 },
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
- await this.socket
942
- .sendPipelineMessages({ statements }, (code, msg, done) => {
943
- switch (code) {
944
- case Protocol.BackendMessageCode.ParseComplete:
945
- case Protocol.BackendMessageCode.BindComplete:
946
- case Protocol.BackendMessageCode.NoticeResponse:
947
- break;
948
- case Protocol.BackendMessageCode.NoData:
949
- pendingFields = undefined;
950
- break;
951
- case Protocol.BackendMessageCode.RowDescription:
952
- pendingFields = msg.fields;
953
- break;
954
- case Protocol.BackendMessageCode.DataRow:
955
- (pendingRows || (pendingRows = [])).push(msg);
956
- break;
957
- case Protocol.BackendMessageCode.EmptyQueryResponse:
958
- case Protocol.BackendMessageCode.CommandComplete: {
959
- const result = { command: msg?.command };
960
- if (pendingFields) {
961
- const parsers = getParsers(typeMap, pendingFields);
962
- const resultFields = wrapRowDescription(typeMap, pendingFields, columnFormat);
963
- result.fields = resultFields;
964
- result.rowType = resolveRowType(options);
965
- if (!result.command)
966
- result.command = 'SELECT';
967
- const rows = pendingRows || [];
968
- const l = rows.length;
969
- let i;
970
- for (i = 0; i < l; i++) {
971
- rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
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.rows = rows;
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
- if (result.command === 'DELETE' ||
976
- result.command === 'INSERT' ||
977
- result.command === 'UPDATE')
978
- result.rowsAffected = msg.rowCount;
979
- pendingFields = undefined;
980
- pendingRows = undefined;
981
- results.push(result);
982
- break;
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
- case Protocol.BackendMessageCode.ErrorResponse:
985
- error = msg;
986
- break;
987
- case Protocol.BackendMessageCode.ReadyForQuery:
988
- this.transactionStatus = msg.status;
989
- done(error);
990
- break;
991
- default:
992
- done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
993
- }
994
- })
995
- .catch((e) => {
996
- // The rejected statement is the one right after everything that
997
- // completed, since results only grow on CommandComplete.
998
- if (e instanceof DatabaseError && error === e)
999
- e.failedIndex = results.length;
1000
- throw e;
1001
- })
1002
- .finally(() => {
1003
- this.runningQueryCount--;
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 === 'DELETE' ||
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 || 100 },
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 (result.command === 'DELETE' ||
1206
- result.command === 'INSERT' ||
1207
- result.command === 'UPDATE') {
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;
@@ -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>;