postgrejs 3.7.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +50 -0
  3. package/connection/connection.d.ts +14 -2
  4. package/connection/connection.js +70 -10
  5. package/connection/intl-connection.d.ts +146 -5
  6. package/connection/intl-connection.js +485 -35
  7. package/connection/pool.d.ts +30 -6
  8. package/connection/pool.js +28 -3
  9. package/connection/prepared-statement.d.ts +11 -0
  10. package/connection/prepared-statement.js +39 -5
  11. package/data-type-map.d.ts +16 -0
  12. package/data-type-map.js +56 -12
  13. package/data-types/classes/numeric.d.ts +8 -4
  14. package/data-types/classes/numeric.js +8 -4
  15. package/data-types/date-type.js +2 -2
  16. package/data-types/int2-vector-type.js +4 -1
  17. package/data-types/money-type.d.ts +38 -0
  18. package/data-types/money-type.js +203 -0
  19. package/data-types/numeric-type.d.ts +2 -0
  20. package/data-types/numeric-type.js +1 -1
  21. package/data-types/oid-vector-type.js +4 -1
  22. package/data-types/timestamp-type.js +2 -2
  23. package/data-types/timestamptz-type.js +11 -2
  24. package/index.d.ts +1 -0
  25. package/index.js +1 -0
  26. package/interfaces/data-mapping-options.d.ts +27 -0
  27. package/interfaces/database-connection-params.d.ts +32 -5
  28. package/interfaces/query-options.d.ts +32 -0
  29. package/interfaces/script-execute-options.d.ts +32 -0
  30. package/interfaces/transaction-options.d.ts +27 -0
  31. package/interfaces/transaction-options.js +1 -0
  32. package/package.json +1 -1
  33. package/protocol/database-error.d.ts +15 -0
  34. package/protocol/database-error.js +19 -0
  35. package/protocol/frontend.js +17 -0
  36. package/protocol/pg-socket.js +7 -10
  37. package/util/array-leaf.d.ts +10 -0
  38. package/util/array-leaf.js +22 -0
  39. package/util/date-style.d.ts +47 -0
  40. package/util/date-style.js +123 -0
  41. package/util/decode-binaryarray.js +8 -1
  42. package/util/encode-binaryarray.d.ts +12 -1
  43. package/util/encode-binaryarray.js +13 -2
  44. package/util/format-datetime.d.ts +18 -0
  45. package/util/format-datetime.js +33 -0
  46. package/util/parse-datetime.d.ts +4 -3
  47. package/util/parse-datetime.js +34 -9
  48. package/util/resolve-column-formats.js +4 -0
  49. package/util/row-decoder.d.ts +24 -4
  50. package/util/row-decoder.js +25 -10
  51. package/util/sql-tag.js +16 -0
  52. package/util/stringify-for-sql.js +7 -0
  53. package/util/transaction-command.d.ts +7 -0
  54. package/util/transaction-command.js +32 -0
  55. package/util/unspecified-param.d.ts +27 -0
  56. package/util/unspecified-param.js +31 -0
@@ -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,12 +222,17 @@ 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
124
229
  // ordinary shutdown from the backend being terminated under us.
125
230
  this.socket.on('close', (reason) => this.emit('close', reason));
126
231
  this.socket.on('notification', payload => this.emit('notification', payload));
232
+ // Covers a notice that arrives while no statement is running. One
233
+ // raised *by* a statement is dispatched to that statement's own
234
+ // message loop instead, which is why each of those forwards it too.
235
+ this.socket.on('notice', payload => this.emit('notice', payload));
127
236
  this.socket.on('connecting', () => this.emit('connecting'));
128
237
  this._onErrorSavePoint = 'SP_' + Math.round(Math.random() * 100000000);
129
238
  }
@@ -197,7 +306,7 @@ export class IntlConnection extends SafeEventEmitter {
197
306
  }
198
307
  async execute(sql, options = {}, cb) {
199
308
  this.assertConnected();
200
- const transactionCommand = TRANSACTION_COMMAND_PATTERN.test(sql);
309
+ const transactionCommand = isTransactionCommand(sql);
201
310
  let beginFirst = false;
202
311
  let commitLast = false;
203
312
  const { autoCommit } = options;
@@ -221,7 +330,10 @@ export class IntlConnection extends SafeEventEmitter {
221
330
  // out from under it) - but ordered so the cheap local read still
222
331
  // short-circuits the getter whenever rollbackOnError is already
223
332
  // false, which it is outside a transaction.
224
- const rollbackOnError = !transactionCommand &&
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) &&
225
337
  this.inTransaction &&
226
338
  (options.rollbackOnError ?? this.config.rollbackOnError ?? true);
227
339
  if (rollbackOnError && this.inTransaction)
@@ -295,10 +407,27 @@ export class IntlConnection extends SafeEventEmitter {
295
407
  * that point). A matching number of commit() calls is then needed to
296
408
  * actually commit - see commit().
297
409
  */
298
- 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);
299
430
  this._transactionDepth++;
300
- if (!this.inTransaction)
301
- await this.execute('BEGIN');
302
431
  }
303
432
  /**
304
433
  * Starts a savepoint, or - if one under the same `name` is already open -
@@ -410,6 +539,92 @@ export class IntlConnection extends SafeEventEmitter {
410
539
  cancel() {
411
540
  return this.socket.cancel();
412
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
+ }
413
628
  ref() {
414
629
  this._refCount++;
415
630
  }
@@ -528,6 +743,7 @@ export class IntlConnection extends SafeEventEmitter {
528
743
  return stream;
529
744
  }
530
745
  async _execute(sql, options = {}, cb) {
746
+ options = this.withDefaults(options);
531
747
  this.ref();
532
748
  try {
533
749
  const timingEnabled = options.timing ?? this.config.timing ?? false;
@@ -540,11 +756,13 @@ export class IntlConnection extends SafeEventEmitter {
540
756
  let parsers;
541
757
  let current = { command: undefined };
542
758
  let fields;
759
+ const deferred = [];
760
+ let currentDeferred;
543
761
  let error;
544
762
  const typeMap = options.typeMap || GlobalTypeMap;
545
763
  const rowDecoder = resolveRowDecoder(options);
546
764
  this.runningQueryCount++;
547
- return await this.socket.sendQueryMessage(sql, (code, msg, done) => {
765
+ const scriptResult = await this.socket.sendQueryMessage(sql, (code, msg, done) => {
548
766
  switch (code) {
549
767
  case Protocol.BackendMessageCode.ErrorResponse:
550
768
  error = msg;
@@ -561,6 +779,9 @@ export class IntlConnection extends SafeEventEmitter {
561
779
  new Error('COPY TO STDOUT is not supported by execute() - use copyTo() instead');
562
780
  break;
563
781
  case Protocol.BackendMessageCode.NoticeResponse:
782
+ // Not an error: handed over, and the loop carries on.
783
+ this.emit('notice', msg);
784
+ break;
564
785
  case Protocol.BackendMessageCode.EmptyQueryResponse:
565
786
  break;
566
787
  case Protocol.BackendMessageCode.RowDescription:
@@ -571,9 +792,25 @@ export class IntlConnection extends SafeEventEmitter {
571
792
  parsers = getParsers(typeMap, fields, options);
572
793
  current.fields = wrapRowDescription(typeMap, fields, DataFormat.text, options);
573
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);
574
805
  break;
575
806
  case Protocol.BackendMessageCode.DataRow:
576
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
+ }
577
814
  const row = rowDecoder.decode(parsers, msg.data, msg.columnCount, options, current.fields);
578
815
  if (cb)
579
816
  cb('row', row);
@@ -582,6 +819,7 @@ export class IntlConnection extends SafeEventEmitter {
582
819
  }
583
820
  break;
584
821
  case Protocol.BackendMessageCode.CommandComplete:
822
+ currentDeferred = undefined;
585
823
  // Ignore BEGIN command that we added to sql
586
824
  current.command = msg.command;
587
825
  if (reportsRowsAffected(current.command))
@@ -613,6 +851,9 @@ export class IntlConnection extends SafeEventEmitter {
613
851
  break;
614
852
  }
615
853
  });
854
+ if (deferred.length)
855
+ await this._decodeDeferredRows(deferred, rowDecoder, options);
856
+ return scriptResult;
616
857
  }
617
858
  finally {
618
859
  this.runningQueryCount--;
@@ -648,9 +889,14 @@ export class IntlConnection extends SafeEventEmitter {
648
889
  * of failing every call from then on.
649
890
  */
650
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);
651
897
  const savepoint = this._inlineSavepointFor(sql, options);
652
898
  try {
653
- return await this._queryCached(sql, paramTypes, params, options, savepoint);
899
+ return await this._queryCached(sql, paramTypes, params, options, savepoint, callWantsFields);
654
900
  }
655
901
  catch (e) {
656
902
  // PostgreSQL throws away everything between a failed statement and
@@ -666,16 +912,19 @@ export class IntlConnection extends SafeEventEmitter {
666
912
  /**
667
913
  * The savepoint rollbackOnError calls for, or undefined when this call
668
914
  * needs none: outside a transaction there is nothing to roll back to,
669
- * and a statement that is itself a transaction command manages its own
670
- * 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.
671
918
  */
672
919
  _inlineSavepointFor(sql, options) {
673
- if (!this.inTransaction || TRANSACTION_COMMAND_PATTERN.test(sql))
920
+ if (!this.inTransaction || refusesSavepoint(sql))
674
921
  return undefined;
675
922
  const on = options.rollbackOnError ?? this.config.rollbackOnError ?? true;
676
923
  return on ? this._onErrorSavePoint : undefined;
677
924
  }
678
- 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) {
679
928
  const enabled = options.prepare ?? this.config.prepare ?? true;
680
929
  if (!enabled)
681
930
  return this.queryOnce(sql, paramTypes, params, options, savepoint);
@@ -711,18 +960,56 @@ export class IntlConnection extends SafeEventEmitter {
711
960
  // text (what queryOnce() falls back to).
712
961
  // unknownTypesAsString needs the columns known before the Bind for
713
962
  // the same reason fetchAsString does, so it earns a name the same way.
714
- const needsFields = !!options.fetchAsString?.length || !!options.unknownTypesAsString;
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;
715
978
  if (!needsFields && !this._earnsAName(key))
716
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) {
717
1005
  const name = 'C_' + ++this._preparedCounter;
718
- // A Parse that fails caches nothing and reports itself; the statement
719
- // is simply never reused. It runs outside the savepoint, as the
720
- // prepare() the transaction path used to do ahead of its own wrapper.
721
- const { fields } = await this.prepareOnce(sql, paramTypes, name);
722
- const entry = { name, fields };
723
- await this._evictPreparedIfFull();
724
- this._preparedCache.set(key, entry);
725
- 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));
726
1013
  }
727
1014
  /**
728
1015
  * Counts one more use of this SQL and says whether it has now been seen
@@ -775,6 +1062,7 @@ export class IntlConnection extends SafeEventEmitter {
775
1062
  await Promise.all([closed, synced]);
776
1063
  }
777
1064
  async queryOnce(sql, paramTypes, params, options, savepoint) {
1065
+ options = this.withDefaults(options);
778
1066
  this.assertConnected();
779
1067
  this.ref();
780
1068
  try {
@@ -825,10 +1113,13 @@ export class IntlConnection extends SafeEventEmitter {
825
1113
  after: savepoint ? 'RELEASE ' + savepoint : undefined,
826
1114
  }, (code, msg, done) => {
827
1115
  switch (code) {
1116
+ case Protocol.BackendMessageCode.NoticeResponse:
1117
+ // Not an error: handed over, and the loop carries on.
1118
+ this.emit('notice', msg);
1119
+ break;
828
1120
  case Protocol.BackendMessageCode.ParseComplete:
829
1121
  case Protocol.BackendMessageCode.BindComplete:
830
1122
  case Protocol.BackendMessageCode.NoData:
831
- case Protocol.BackendMessageCode.NoticeResponse:
832
1123
  break;
833
1124
  // The server stopped at the fetchCount limit rather than
834
1125
  // running out of rows. The Sync that closes this call
@@ -846,11 +1137,17 @@ export class IntlConnection extends SafeEventEmitter {
846
1137
  case Protocol.BackendMessageCode.DataRow:
847
1138
  rows.push(msg);
848
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:
849
1146
  case Protocol.BackendMessageCode.CommandComplete:
850
1147
  if (leadingCommandTags)
851
1148
  leadingCommandTags--;
852
1149
  else if (!commandTag)
853
- commandTag = msg;
1150
+ commandTag = msg || EMPTY_QUERY_TAG;
854
1151
  break;
855
1152
  case Protocol.BackendMessageCode.ErrorResponse:
856
1153
  error = msg;
@@ -868,6 +1165,12 @@ export class IntlConnection extends SafeEventEmitter {
868
1165
  });
869
1166
  if (commandTag?.command)
870
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
+ }
871
1174
  if (resultFields && parsers) {
872
1175
  if (!result.command)
873
1176
  result.command = 'SELECT';
@@ -910,10 +1213,13 @@ export class IntlConnection extends SafeEventEmitter {
910
1213
  describe: { type: 'S', name: statementName },
911
1214
  }, (code, msg, done) => {
912
1215
  switch (code) {
1216
+ case Protocol.BackendMessageCode.NoticeResponse:
1217
+ // Not an error: handed over, and the loop carries on.
1218
+ this.emit('notice', msg);
1219
+ break;
913
1220
  case Protocol.BackendMessageCode.ParseComplete:
914
1221
  case Protocol.BackendMessageCode.ParameterDescription:
915
1222
  case Protocol.BackendMessageCode.NoData:
916
- case Protocol.BackendMessageCode.NoticeResponse:
917
1223
  break;
918
1224
  case Protocol.BackendMessageCode.RowDescription:
919
1225
  fields = msg.fields;
@@ -1002,6 +1308,7 @@ export class IntlConnection extends SafeEventEmitter {
1002
1308
  * that arrive before one belong to the statement it closes.
1003
1309
  */
1004
1310
  async executePipeline(requests, options) {
1311
+ options = this.withDefaults(options);
1005
1312
  this.assertConnected();
1006
1313
  this.ref();
1007
1314
  try {
@@ -1012,6 +1319,7 @@ export class IntlConnection extends SafeEventEmitter {
1012
1319
  const results = [];
1013
1320
  let pendingFields;
1014
1321
  let pendingRows;
1322
+ const deferred = [];
1015
1323
  let error;
1016
1324
  // Plan every statement against the prepared statement cache before
1017
1325
  // writing anything: one that is already prepared binds to its name
@@ -1091,9 +1399,12 @@ export class IntlConnection extends SafeEventEmitter {
1091
1399
  await this.socket
1092
1400
  .sendPipelineMessages({ statements }, (code, msg, done) => {
1093
1401
  switch (code) {
1402
+ case Protocol.BackendMessageCode.NoticeResponse:
1403
+ // Not an error: handed over, and the loop carries on.
1404
+ this.emit('notice', msg);
1405
+ break;
1094
1406
  case Protocol.BackendMessageCode.ParseComplete:
1095
1407
  case Protocol.BackendMessageCode.BindComplete:
1096
- case Protocol.BackendMessageCode.NoticeResponse:
1097
1408
  break;
1098
1409
  case Protocol.BackendMessageCode.NoData:
1099
1410
  pendingFields = undefined;
@@ -1122,12 +1433,24 @@ export class IntlConnection extends SafeEventEmitter {
1122
1433
  if (!result.command)
1123
1434
  result.command = 'SELECT';
1124
1435
  const rows = pendingRows || [];
1125
- const l = rows.length;
1126
- let i;
1127
- for (i = 0; i < l; i++) {
1128
- rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
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;
1129
1453
  }
1130
- result.rows = rows;
1131
1454
  }
1132
1455
  if (reportsRowsAffected(result.command))
1133
1456
  result.rowsAffected = msg.rowCount;
@@ -1185,6 +1508,8 @@ export class IntlConnection extends SafeEventEmitter {
1185
1508
  fields: promo.plan.fields,
1186
1509
  });
1187
1510
  }
1511
+ if (deferred.length)
1512
+ await this._decodeDeferredRows(deferred, rowDecoder, options);
1188
1513
  if (timingEnabled) {
1189
1514
  const elapsed = performance.now() - startTime;
1190
1515
  const l = results.length;
@@ -1199,6 +1524,7 @@ export class IntlConnection extends SafeEventEmitter {
1199
1524
  }
1200
1525
  }
1201
1526
  async executeBatchReused(statementName, cachedFields, paramTypes, paramSets, options) {
1527
+ options = this.withDefaults(options);
1202
1528
  this.assertConnected();
1203
1529
  this.ref();
1204
1530
  try {
@@ -1234,8 +1560,11 @@ export class IntlConnection extends SafeEventEmitter {
1234
1560
  execute: { fetchCount: 0 },
1235
1561
  }, (code, msg, done) => {
1236
1562
  switch (code) {
1237
- case Protocol.BackendMessageCode.BindComplete:
1238
1563
  case Protocol.BackendMessageCode.NoticeResponse:
1564
+ // Not an error: handed over, and the loop carries on.
1565
+ this.emit('notice', msg);
1566
+ break;
1567
+ case Protocol.BackendMessageCode.BindComplete:
1239
1568
  break;
1240
1569
  case Protocol.BackendMessageCode.DataRow:
1241
1570
  (pendingRows || (pendingRows = [])).push(msg);
@@ -1301,6 +1630,7 @@ export class IntlConnection extends SafeEventEmitter {
1301
1630
  }
1302
1631
  }
1303
1632
  async executeReused(statementName, cachedFields, paramTypes, params, options, savepoint) {
1633
+ options = this.withDefaults(options);
1304
1634
  this.assertConnected();
1305
1635
  this.ref();
1306
1636
  try {
@@ -1348,9 +1678,12 @@ export class IntlConnection extends SafeEventEmitter {
1348
1678
  after: savepoint ? 'RELEASE ' + savepoint : undefined,
1349
1679
  }, (code, msg, done) => {
1350
1680
  switch (code) {
1681
+ case Protocol.BackendMessageCode.NoticeResponse:
1682
+ // Not an error: handed over, and the loop carries on.
1683
+ this.emit('notice', msg);
1684
+ break;
1351
1685
  case Protocol.BackendMessageCode.ParseComplete:
1352
1686
  case Protocol.BackendMessageCode.BindComplete:
1353
- case Protocol.BackendMessageCode.NoticeResponse:
1354
1687
  break;
1355
1688
  // See queryOnce() for what a suspended portal means here.
1356
1689
  case Protocol.BackendMessageCode.PortalSuspended:
@@ -1359,6 +1692,8 @@ export class IntlConnection extends SafeEventEmitter {
1359
1692
  case Protocol.BackendMessageCode.DataRow:
1360
1693
  rows.push(msg);
1361
1694
  break;
1695
+ // See queryOnce() for why an empty statement lands here.
1696
+ case Protocol.BackendMessageCode.EmptyQueryResponse:
1362
1697
  case Protocol.BackendMessageCode.CommandComplete:
1363
1698
  // Three statements can answer here when a savepoint rides
1364
1699
  // along: SAVEPOINT's tag, then the caller's own, then
@@ -1367,7 +1702,7 @@ export class IntlConnection extends SafeEventEmitter {
1367
1702
  if (leadingCommandTags)
1368
1703
  leadingCommandTags--;
1369
1704
  else if (!commandTag)
1370
- commandTag = msg;
1705
+ commandTag = msg || EMPTY_QUERY_TAG;
1371
1706
  break;
1372
1707
  case Protocol.BackendMessageCode.ErrorResponse:
1373
1708
  error = msg;
@@ -1385,6 +1720,12 @@ export class IntlConnection extends SafeEventEmitter {
1385
1720
  });
1386
1721
  if (commandTag?.command)
1387
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
+ }
1388
1729
  if (resultFields && parsers) {
1389
1730
  if (!result.command)
1390
1731
  result.command = 'SELECT';
@@ -1411,6 +1752,115 @@ export class IntlConnection extends SafeEventEmitter {
1411
1752
  const handled = super.emit(event, ...args);
1412
1753
  return this.owner ? this.owner.emit(event, ...args) || handled : handled;
1413
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
+ }
1414
1864
  _onError(err) {
1415
1865
  if (this.socket.state !== ConnectionState.READY)
1416
1866
  return;