postgrejs 3.3.0 → 3.5.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 (40) hide show
  1. package/CHANGELOG.md +55 -1
  2. package/README.md +68 -7
  3. package/connection/connection.d.ts +134 -0
  4. package/connection/connection.js +213 -2
  5. package/connection/cursor.d.ts +8 -0
  6. package/connection/cursor.js +17 -0
  7. package/connection/intl-connection.d.ts +118 -3
  8. package/connection/intl-connection.js +615 -31
  9. package/connection/pool.d.ts +16 -0
  10. package/connection/pool.js +24 -0
  11. package/connection/prepared-statement.d.ts +64 -1
  12. package/connection/prepared-statement.js +95 -5
  13. package/data-types/float4-type.js +2 -1
  14. package/data-types/float8-type.js +2 -1
  15. package/data-types/int2-type.js +2 -1
  16. package/data-types/int4-type.js +2 -1
  17. package/data-types/int8-type.js +2 -1
  18. package/data-types/numeric-type.js +7 -0
  19. package/data-types/oid-type.js +2 -1
  20. package/index.d.ts +3 -0
  21. package/index.js +3 -0
  22. package/interfaces/batch-result.d.ts +42 -0
  23. package/interfaces/batch-result.js +1 -0
  24. package/interfaces/copy-from-rows-options.d.ts +58 -0
  25. package/interfaces/copy-from-rows-options.js +1 -0
  26. package/interfaces/database-connection-params.d.ts +25 -0
  27. package/interfaces/pipeline-request.d.ts +16 -0
  28. package/interfaces/pipeline-request.js +1 -0
  29. package/interfaces/query-options.d.ts +6 -0
  30. package/package.json +5 -2
  31. package/protocol/database-error.d.ts +16 -0
  32. package/protocol/database-error.js +15 -0
  33. package/protocol/pg-socket.d.ts +76 -0
  34. package/protocol/pg-socket.js +130 -12
  35. package/util/assert-integer.d.ts +43 -0
  36. package/util/assert-integer.js +58 -0
  37. package/util/copy-from-rows.d.ts +56 -0
  38. package/util/copy-from-rows.js +95 -0
  39. package/util/encode-copy-binary.d.ts +48 -0
  40. package/util/encode-copy-binary.js +112 -0
@@ -1,10 +1,12 @@
1
1
  import { performance } from 'node:perf_hooks';
2
2
  import { ConnectionState, DEFAULT_COLUMN_FORMAT } from '../constants.js';
3
3
  import { GlobalTypeMap } from '../data-type-map.js';
4
+ import { DatabaseError } from '../protocol/database-error.js';
4
5
  import { PgSocket } from '../protocol/pg-socket.js';
5
6
  import { Protocol } from '../protocol/protocol.js';
6
7
  import { SafeEventEmitter } from '../safe-event-emitter.js';
7
8
  import { getConnectionConfig } from '../util/connection-config.js';
9
+ import { buildCopySql, buildProbeSql, writeCopyBinaryRows, } from '../util/copy-from-rows.js';
8
10
  import { escapeLiteral } from '../util/escape-literal.js';
9
11
  import { getParsers } from '../util/get-parsers.js';
10
12
  import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
@@ -25,7 +27,44 @@ function columnFormatsEqual(a, b) {
25
27
  return false;
26
28
  return true;
27
29
  }
30
+ /**
31
+ * Uses of the same SQL before it is worth preparing.
32
+ *
33
+ * Preparing on first sight, as postgres.js does, pays Parse plus Describe
34
+ * and leaves a named statement behind for SQL that may never run again;
35
+ * waiting for the second use keeps a genuinely one-shot query at exactly
36
+ * today's cost. The repeated query pays one extra unprepared call before
37
+ * it starts saving, which it makes back immediately.
38
+ */
39
+ const PREPARE_AFTER_USES = 2;
40
+ /**
41
+ * SQL that manages transaction boundaries itself, and so must never be
42
+ * wrapped in a savepoint or an implicit BEGIN/COMMIT of ours.
43
+ */
44
+ /**
45
+ * What the prepared statement cache is keyed on. The parameter OIDs are
46
+ * part of it because the server fixes them at Parse: the same SQL bound
47
+ * with different types is a different plan.
48
+ */
49
+ function preparedCacheKey(sql, paramTypes) {
50
+ return paramTypes?.length ? sql + '\u0000' + paramTypes.join(',') : sql;
51
+ }
52
+ const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
28
53
  export class IntlConnection extends SafeEventEmitter {
54
+ /**
55
+ * Server-side prepared statements this connection has built, keyed by
56
+ * the SQL text and its parameter OIDs - both, because the OIDs are part
57
+ * of what was parsed, not just how it is bound.
58
+ *
59
+ * A Map, relied on for its insertion order: re-inserting on every hit
60
+ * makes the first entry the least recently used, which is what gets
61
+ * closed when the cache is full. Statements belong to the session, so
62
+ * this is per connection and dies with it.
63
+ */
64
+ _preparedCache = new Map();
65
+ /** SQL seen once but not yet prepared - see PREPARE_AFTER_USES. */
66
+ _preparedCandidates = new Map();
67
+ _preparedCounter = 0;
29
68
  _executeReusedParserCache = new WeakMap();
30
69
  _refCount = 0;
31
70
  _transactionDepth = 0;
@@ -116,7 +155,7 @@ export class IntlConnection extends SafeEventEmitter {
116
155
  }
117
156
  async execute(sql, options = {}, cb) {
118
157
  this.assertConnected();
119
- const transactionCommand = sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
158
+ const transactionCommand = TRANSACTION_COMMAND_PATTERN.test(sql);
120
159
  let beginFirst = false;
121
160
  let commitLast = false;
122
161
  const { autoCommit } = options;
@@ -368,6 +407,69 @@ export class IntlConnection extends SafeEventEmitter {
368
407
  * Runs a COPY ... FROM STDIN and hands back a stream to feed it.
369
408
  * Resolves once the server is ready for data.
370
409
  */
410
+ /**
411
+ * Streams rows into a table with `COPY ... FROM STDIN (FORMAT binary)`,
412
+ * encoding each value with its own type's binary encoder.
413
+ *
414
+ * Binary COPY performs no conversion server-side, so the destination
415
+ * column types have to be known exactly. They are read from the server
416
+ * unless the caller supplies them: a Describe of `select <columns> from
417
+ * <table> where false` against the unnamed statement returns a
418
+ * RowDescription carrying each column's OID, which costs one round trip
419
+ * and leaves nothing to close (the next Parse replaces the unnamed
420
+ * statement). Letting PostgreSQL resolve the name is the reason to do it
421
+ * this way rather than reading the catalog here - schemas, quoting,
422
+ * search_path and views then behave exactly as they will for the COPY.
423
+ */
424
+ async copyFromRows(table, source, options = {}) {
425
+ this.assertConnected();
426
+ let columns = options.columns;
427
+ let dataTypeIds = options.columnTypes;
428
+ if (!dataTypeIds || !columns) {
429
+ const { fields } = await this.prepareOnce(buildProbeSql(table, columns), undefined, '');
430
+ if (!fields?.length)
431
+ throw new Error(`Cannot determine columns of "${table}" - it returned no column descriptions`);
432
+ columns = columns || fields.map(f => f.fieldName);
433
+ dataTypeIds = dataTypeIds || fields.map(f => f.dataTypeId);
434
+ }
435
+ if (columns.length !== dataTypeIds.length)
436
+ throw new Error(`columnTypes has ${dataTypeIds.length} entries but ${columns.length} columns were given`);
437
+ const stream = await this.copyFrom(buildCopySql(table, columns));
438
+ // The CopyFail sent below comes back as an ErrorResponse, which the
439
+ // stream emits as 'error'. Node turns an 'error' with no listener into
440
+ // an uncaught exception and takes the process down - so a value this
441
+ // method already rejected cleanly would kill the caller instead.
442
+ // Recorded rather than ignored, since the same listener covers a
443
+ // socket that dies mid-copy.
444
+ let streamError;
445
+ stream.on('error', e => {
446
+ streamError = streamError || e;
447
+ });
448
+ try {
449
+ const result = await writeCopyBinaryRows(source, {
450
+ columns,
451
+ dataTypeIds,
452
+ options,
453
+ write: chunk => new Promise((resolve, reject) => {
454
+ const accepted = this.socket.sendCopyData(chunk, err => err ? reject(err) : resolve());
455
+ // Taken without buffering - no reason to wait for the write
456
+ // callback, which would add a turn of latency per chunk.
457
+ if (accepted)
458
+ resolve();
459
+ }),
460
+ });
461
+ await new Promise(resolve => stream.end(resolve));
462
+ if (streamError)
463
+ throw streamError;
464
+ return result;
465
+ }
466
+ catch (e) {
467
+ // Tell the server to abandon the copy rather than leaving it waiting
468
+ // for a stream that will never arrive.
469
+ this.socket.sendCopyFail(e instanceof Error ? e.message : 'copyFromRows() failed');
470
+ throw e;
471
+ }
472
+ }
371
473
  async copyFrom(sql) {
372
474
  this.assertConnected();
373
475
  this.ref();
@@ -482,7 +584,144 @@ export class IntlConnection extends SafeEventEmitter {
482
584
  * +close()'s 7 round trips whenever there's no cursor and no explicit/
483
585
  * active transaction to wrap (see Connection.query() for that gating).
484
586
  */
485
- async queryOnce(sql, paramTypes, params, options) {
587
+ /**
588
+ * Runs a one-shot extended query, reusing a server-side prepared
589
+ * statement when this connection has run the same SQL before.
590
+ *
591
+ * A repeated query then costs Bind/Execute instead of
592
+ * Parse/Bind/Describe/Execute, which the server can answer without
593
+ * planning again - fifty concurrent calls of one statement measured
594
+ * 0.97ms against 3.13ms, and socket reads dropped from 38 to 8.
595
+ *
596
+ * Nothing is prepared on first sight: SQL has to be seen
597
+ * PREPARE_AFTER_USES times before it earns a name, so a query that runs
598
+ * once costs exactly what it costs today.
599
+ *
600
+ * A cached statement can be invalidated under us - `alter table` between
601
+ * two calls makes PostgreSQL answer 0A000, "cached plan must not change
602
+ * result type". That is caught here: the entry is dropped and the query
603
+ * runs again unprepared, so a migration against a live connection
604
+ * recovers instead of failing every call from then on.
605
+ */
606
+ async queryCached(sql, paramTypes, params, options) {
607
+ const savepoint = this._inlineSavepointFor(sql, options);
608
+ try {
609
+ return await this._queryCached(sql, paramTypes, params, options, savepoint);
610
+ }
611
+ catch (e) {
612
+ // PostgreSQL throws away everything between a failed statement and
613
+ // the Sync, so an inlined RELEASE never ran and the savepoint is
614
+ // still standing. Rolling back to it leaves the transaction usable,
615
+ // which is the whole point of rollbackOnError - and it is left
616
+ // standing afterwards, as the separate-round-trip wrapper left it.
617
+ if (savepoint)
618
+ await this._execute('ROLLBACK TO ' + savepoint);
619
+ throw e;
620
+ }
621
+ }
622
+ /**
623
+ * The savepoint rollbackOnError calls for, or undefined when this call
624
+ * needs none: outside a transaction there is nothing to roll back to,
625
+ * and a statement that is itself a transaction command manages its own
626
+ * boundaries (wrapping COMMIT in a savepoint would be nonsense).
627
+ */
628
+ _inlineSavepointFor(sql, options) {
629
+ if (!this.inTransaction || TRANSACTION_COMMAND_PATTERN.test(sql))
630
+ return undefined;
631
+ const on = options.rollbackOnError ?? this.config.rollbackOnError ?? true;
632
+ return on ? this._onErrorSavePoint : undefined;
633
+ }
634
+ async _queryCached(sql, paramTypes, params, options, savepoint) {
635
+ const enabled = options.prepare ?? this.config.prepare ?? true;
636
+ if (!enabled)
637
+ return this.queryOnce(sql, paramTypes, params, options, savepoint);
638
+ const key = preparedCacheKey(sql, paramTypes);
639
+ const cached = this._preparedCache.get(key);
640
+ if (cached) {
641
+ // Re-insert to mark it most recently used.
642
+ this._preparedCache.delete(key);
643
+ this._preparedCache.set(key, cached);
644
+ try {
645
+ return await this.executeReused(cached.name, cached.fields, paramTypes, params, options, savepoint);
646
+ }
647
+ catch (e) {
648
+ if (e?.code !== '0A000')
649
+ throw e;
650
+ this._preparedCache.delete(key);
651
+ // ROLLBACK TO leaves the savepoint itself in place, so the retry
652
+ // must not open a second one under the same name - it runs bare
653
+ // and releases the one already held.
654
+ if (savepoint)
655
+ await this._execute('ROLLBACK TO ' + savepoint);
656
+ const result = await this.queryOnce(sql, paramTypes, params, options);
657
+ if (savepoint)
658
+ await this._execute('RELEASE ' + savepoint);
659
+ return result;
660
+ }
661
+ }
662
+ if (!this._earnsAName(key))
663
+ return this.queryOnce(sql, paramTypes, params, options, savepoint);
664
+ const name = 'C_' + ++this._preparedCounter;
665
+ // A Parse that fails caches nothing and reports itself; the statement
666
+ // is simply never reused. It runs outside the savepoint, as the
667
+ // prepare() the transaction path used to do ahead of its own wrapper.
668
+ const { fields } = await this.prepareOnce(sql, paramTypes, name);
669
+ const entry = { name, fields };
670
+ await this._evictPreparedIfFull();
671
+ this._preparedCache.set(key, entry);
672
+ return this.executeReused(name, entry.fields, paramTypes, params, options, savepoint);
673
+ }
674
+ /**
675
+ * Counts one more use of this SQL and says whether it has now been seen
676
+ * often enough to be worth preparing under a name.
677
+ */
678
+ _earnsAName(key) {
679
+ const uses = (this._preparedCandidates.get(key) || 0) + 1;
680
+ if (uses < PREPARE_AFTER_USES) {
681
+ this._preparedCandidates.set(key, uses);
682
+ return false;
683
+ }
684
+ this._preparedCandidates.delete(key);
685
+ return true;
686
+ }
687
+ /** Closes the least recently used statement once the cache is full. */
688
+ async _evictPreparedIfFull() {
689
+ const max = this.config.preparedStatementCacheSize ?? 64;
690
+ while (this._preparedCache.size >= max) {
691
+ const oldest = this._preparedCache.keys().next();
692
+ if (oldest.done)
693
+ return;
694
+ const entry = this._preparedCache.get(oldest.value);
695
+ this._preparedCache.delete(oldest.value);
696
+ // A statement that cannot be closed is already gone as far as this
697
+ // connection is concerned; losing the cache entry is the part that
698
+ // matters, and failing the caller's query over it would be worse.
699
+ await this._closePreparedStatement(entry.name).catch(() => undefined);
700
+ }
701
+ }
702
+ /**
703
+ * Closes an evicted statement server-side. Close answers CloseComplete
704
+ * but no ReadyForQuery, so the Sync goes separately and both are awaited
705
+ * together - the same shape PreparedStatement._close() uses.
706
+ */
707
+ async _closePreparedStatement(name) {
708
+ const closed = this.socket.sendCloseMessage({ type: 'S', name }, (code, msg, done) => {
709
+ if (code === Protocol.BackendMessageCode.CloseComplete)
710
+ done(undefined);
711
+ else if (code === Protocol.BackendMessageCode.ErrorResponse)
712
+ done(msg);
713
+ });
714
+ const synced = this.socket.sendSyncMessage((code, msg, done) => {
715
+ if (code === Protocol.BackendMessageCode.ReadyForQuery) {
716
+ this.transactionStatus = msg.status;
717
+ done(undefined);
718
+ }
719
+ else if (code === Protocol.BackendMessageCode.ErrorResponse)
720
+ done(msg);
721
+ });
722
+ await Promise.all([closed, synced]);
723
+ }
724
+ async queryOnce(sql, paramTypes, params, options, savepoint) {
486
725
  this.assertConnected();
487
726
  this.ref();
488
727
  try {
@@ -496,6 +735,9 @@ export class IntlConnection extends SafeEventEmitter {
496
735
  let commandTag;
497
736
  let error;
498
737
  const rowDecoder = resolveRowDecoder(options);
738
+ // See executeReused() for what a savepoint riding along does to the
739
+ // CommandComplete stream.
740
+ let leadingCommandTags = savepoint ? 1 : 0;
499
741
  this.runningQueryCount++;
500
742
  await this.socket
501
743
  .sendExtendedQueryMessages({
@@ -503,6 +745,8 @@ export class IntlConnection extends SafeEventEmitter {
503
745
  bind: { typeMap, paramTypes, params, queryOptions: options },
504
746
  describe: { type: 'P' },
505
747
  execute: { fetchCount: options.fetchCount || 100 },
748
+ before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
749
+ after: savepoint ? 'RELEASE ' + savepoint : undefined,
506
750
  }, (code, msg, done) => {
507
751
  switch (code) {
508
752
  case Protocol.BackendMessageCode.ParseComplete:
@@ -521,7 +765,10 @@ export class IntlConnection extends SafeEventEmitter {
521
765
  rows.push(msg);
522
766
  break;
523
767
  case Protocol.BackendMessageCode.CommandComplete:
524
- commandTag = msg;
768
+ if (leadingCommandTags)
769
+ leadingCommandTags--;
770
+ else if (!commandTag)
771
+ commandTag = msg;
525
772
  break;
526
773
  case Protocol.BackendMessageCode.ErrorResponse:
527
774
  error = msg;
@@ -620,7 +867,352 @@ export class IntlConnection extends SafeEventEmitter {
620
867
  * the RowDescription fields prepareOnce() already fetched instead of
621
868
  * re-Describing a fresh portal on every call.
622
869
  */
623
- async executeReused(statementName, cachedFields, paramTypes, params, options) {
870
+ /**
871
+ * Parsers and wrapped field descriptions for a statement whose
872
+ * RowDescription prepare() already fetched, memoised per RowDescription
873
+ * so repeated executes of the same statement don't rebuild them.
874
+ * Shared by executeReused() and executeBatchReused().
875
+ */
876
+ _resolveReusedParsers(cachedFields, typeMap, options) {
877
+ const columnFormat = options.columnFormat != null
878
+ ? options.columnFormat
879
+ : DEFAULT_COLUMN_FORMAT;
880
+ const cached = this._executeReusedParserCache.get(cachedFields);
881
+ if (cached &&
882
+ cached.typeMap === typeMap &&
883
+ columnFormatsEqual(cached.columnFormat, columnFormat)) {
884
+ return { parsers: cached.parsers, resultFields: cached.resultFields };
885
+ }
886
+ const fields = cachedFields.map((f, i) => ({
887
+ ...f,
888
+ format: Array.isArray(columnFormat) ? columnFormat[i] : columnFormat,
889
+ }));
890
+ const parsers = getParsers(typeMap, fields);
891
+ const resultFields = wrapRowDescription(typeMap, fields, columnFormat);
892
+ this._executeReusedParserCache.set(cachedFields, {
893
+ typeMap,
894
+ columnFormat,
895
+ parsers,
896
+ resultFields,
897
+ });
898
+ return { parsers, resultFields };
899
+ }
900
+ /**
901
+ * Executes one prepared statement once per parameter set, as a single
902
+ * Bind/Execute stream closed by one Sync (see
903
+ * PgSocket.sendBatchBindExecuteMessages() for the wire shape and what
904
+ * the single Sync implies).
905
+ *
906
+ * Sets map to results positionally: the server answers each Execute with
907
+ * its own CommandComplete, and rows that arrive before one belong to the
908
+ * set it closes. When the server rejects a set, everything behind it is
909
+ * discarded unexecuted - the completed prefix is attached to the thrown
910
+ * error as `batchResults`, and the rejected set's index as `failedIndex`,
911
+ * so a caller can tell how far the batch actually got.
912
+ */
913
+ /**
914
+ * Runs several different statements as one Parse/Bind/Describe/Execute
915
+ * stream closed by a single Sync (see PgSocket.sendPipelineMessages() for
916
+ * the wire shape and what that framing implies).
917
+ *
918
+ * The counterpart to executeBatchReused(): that one runs a single
919
+ * prepared statement over many parameter sets, this one runs many
920
+ * statements once each. Results map to statements positionally - the
921
+ * server answers each Execute with its own CommandComplete, and any rows
922
+ * that arrive before one belong to the statement it closes.
923
+ */
924
+ async executePipeline(requests, options) {
925
+ this.assertConnected();
926
+ this.ref();
927
+ try {
928
+ const typeMap = options.typeMap || GlobalTypeMap;
929
+ const timingEnabled = options.timing ?? this.config.timing ?? false;
930
+ const startTime = timingEnabled ? performance.now() : 0;
931
+ const rowDecoder = resolveRowDecoder(options);
932
+ const results = [];
933
+ let pendingFields;
934
+ let pendingRows;
935
+ let error;
936
+ // Plan every statement against the prepared statement cache before
937
+ // writing anything: one that is already prepared binds to its name
938
+ // and skips both Parse and Describe, one seen often enough earns a
939
+ // name here, and the rest go out unnamed exactly as before.
940
+ const prepareEnabled = options.prepare ?? this.config.prepare ?? true;
941
+ const plans = [];
942
+ const promotions = [];
943
+ const cachedKeys = [];
944
+ const named = new Map();
945
+ const statements = [];
946
+ for (const r of requests) {
947
+ const key = prepareEnabled
948
+ ? preparedCacheKey(r.sql, r.paramTypes)
949
+ : undefined;
950
+ // A repeat can only skip its own Parse if the statement it binds
951
+ // to has a name. An unnamed one would be gone by now: any later
952
+ // statement's Parse replaces it.
953
+ let plan = key != null ? named.get(key) : undefined;
954
+ let sendParse = !plan;
955
+ let sendDescribe = !plan;
956
+ if (!plan) {
957
+ const cached = key != null ? this._preparedCache.get(key) : undefined;
958
+ if (cached && key != null) {
959
+ // Re-insert to mark it most recently used.
960
+ this._preparedCache.delete(key);
961
+ this._preparedCache.set(key, cached);
962
+ cachedKeys.push(key);
963
+ plan = { name: cached.name, fields: cached.fields };
964
+ sendParse = false;
965
+ sendDescribe = false;
966
+ }
967
+ else if (key != null && this._earnsAName(key)) {
968
+ plan = { name: 'C_' + ++this._preparedCounter };
969
+ promotions.push({ key, plan });
970
+ }
971
+ else {
972
+ plan = {};
973
+ }
974
+ if (key != null && plan.name)
975
+ named.set(key, plan);
976
+ }
977
+ plans.push(plan);
978
+ statements.push({
979
+ // Per statement, not per pipeline: the statements differ, so a
980
+ // single shared list of parameter OIDs could only ever be right
981
+ // for one of them.
982
+ parse: sendParse
983
+ ? { sql: r.sql, paramTypes: r.paramTypes, statement: plan.name }
984
+ : undefined,
985
+ bind: {
986
+ typeMap,
987
+ statement: plan.name,
988
+ paramTypes: r.paramTypes,
989
+ params: r.params,
990
+ queryOptions: options,
991
+ },
992
+ describe: sendDescribe ? { type: 'P' } : undefined,
993
+ // Unlimited, never options.fetchCount: a suspended portal would
994
+ // break the positional statement-to-result mapping.
995
+ execute: { fetchCount: 0 },
996
+ });
997
+ }
998
+ this.runningQueryCount++;
999
+ try {
1000
+ await this.socket
1001
+ .sendPipelineMessages({ statements }, (code, msg, done) => {
1002
+ switch (code) {
1003
+ case Protocol.BackendMessageCode.ParseComplete:
1004
+ case Protocol.BackendMessageCode.BindComplete:
1005
+ case Protocol.BackendMessageCode.NoticeResponse:
1006
+ break;
1007
+ case Protocol.BackendMessageCode.NoData:
1008
+ pendingFields = undefined;
1009
+ break;
1010
+ case Protocol.BackendMessageCode.RowDescription:
1011
+ pendingFields = msg.fields;
1012
+ break;
1013
+ case Protocol.BackendMessageCode.DataRow:
1014
+ (pendingRows || (pendingRows = [])).push(msg);
1015
+ break;
1016
+ case Protocol.BackendMessageCode.EmptyQueryResponse:
1017
+ case Protocol.BackendMessageCode.CommandComplete: {
1018
+ const plan = plans[results.length];
1019
+ // A statement bound to a cached name sent no Describe, so
1020
+ // its columns come from the cache entry instead; one being
1021
+ // prepared here records its RowDescription for the entry it
1022
+ // is about to become, and for any repeat behind it.
1023
+ if (pendingFields)
1024
+ plan.fields = pendingFields;
1025
+ const fields = pendingFields || plan.fields;
1026
+ const result = { command: msg?.command };
1027
+ if (fields) {
1028
+ const { parsers, resultFields } = this._resolveReusedParsers(fields, typeMap, options);
1029
+ result.fields = resultFields;
1030
+ result.rowType = resolveRowType(options);
1031
+ if (!result.command)
1032
+ result.command = 'SELECT';
1033
+ const rows = pendingRows || [];
1034
+ const l = rows.length;
1035
+ let i;
1036
+ for (i = 0; i < l; i++) {
1037
+ rows[i] = rowDecoder.decode(parsers, rows[i].data, rows[i].columnCount, options, resultFields);
1038
+ }
1039
+ result.rows = rows;
1040
+ }
1041
+ if (result.command === 'DELETE' ||
1042
+ result.command === 'INSERT' ||
1043
+ result.command === 'UPDATE')
1044
+ result.rowsAffected = msg.rowCount;
1045
+ pendingFields = undefined;
1046
+ pendingRows = undefined;
1047
+ results.push(result);
1048
+ break;
1049
+ }
1050
+ case Protocol.BackendMessageCode.ErrorResponse:
1051
+ error = msg;
1052
+ break;
1053
+ case Protocol.BackendMessageCode.ReadyForQuery:
1054
+ this.transactionStatus = msg.status;
1055
+ done(error);
1056
+ break;
1057
+ default:
1058
+ done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
1059
+ }
1060
+ })
1061
+ .catch((e) => {
1062
+ // The rejected statement is the one right after everything
1063
+ // that completed, since results only grow on CommandComplete.
1064
+ if (e instanceof DatabaseError && error === e)
1065
+ e.failedIndex = results.length;
1066
+ throw e;
1067
+ })
1068
+ .finally(() => {
1069
+ this.runningQueryCount--;
1070
+ });
1071
+ }
1072
+ catch (e) {
1073
+ // A cached statement can be invalidated between two pipelines the
1074
+ // same way a cached single query can (0A000, "cached plan must not
1075
+ // change result type"). Drop what this pipeline reused and run it
1076
+ // again unprepared - once, since the second attempt has nothing
1077
+ // cached left to reuse.
1078
+ if (e?.code !== '0A000' || !cachedKeys.length)
1079
+ throw e;
1080
+ for (const k of cachedKeys)
1081
+ this._preparedCache.delete(k);
1082
+ // Inside an explicit transaction there is nothing to retry into:
1083
+ // the failed statement has already aborted the block.
1084
+ if (this.inTransaction)
1085
+ throw e;
1086
+ return await this.executePipeline(requests, options);
1087
+ }
1088
+ // Only now, with every Parse answered, are the new names real.
1089
+ let promo;
1090
+ let p;
1091
+ const promotionCount = promotions.length;
1092
+ for (p = 0; p < promotionCount; p++) {
1093
+ promo = promotions[p];
1094
+ await this._evictPreparedIfFull();
1095
+ this._preparedCache.set(promo.key, {
1096
+ name: promo.plan.name,
1097
+ fields: promo.plan.fields,
1098
+ });
1099
+ }
1100
+ if (timingEnabled) {
1101
+ const elapsed = performance.now() - startTime;
1102
+ const l = results.length;
1103
+ let i;
1104
+ for (i = 0; i < l; i++)
1105
+ results[i].executeTime = elapsed;
1106
+ }
1107
+ return results;
1108
+ }
1109
+ finally {
1110
+ this.unref();
1111
+ }
1112
+ }
1113
+ async executeBatchReused(statementName, cachedFields, paramTypes, paramSets, options) {
1114
+ this.assertConnected();
1115
+ this.ref();
1116
+ try {
1117
+ const typeMap = options.typeMap || GlobalTypeMap;
1118
+ const timingEnabled = options.timing ?? this.config.timing ?? false;
1119
+ const startTime = timingEnabled ? performance.now() : 0;
1120
+ const rowDecoder = resolveRowDecoder(options);
1121
+ let parsers;
1122
+ let resultFields;
1123
+ if (cachedFields) {
1124
+ const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
1125
+ parsers = resolved.parsers;
1126
+ resultFields = resolved.resultFields;
1127
+ }
1128
+ const results = [];
1129
+ let pendingRows;
1130
+ let error;
1131
+ const binds = paramSets.map(params => ({
1132
+ typeMap,
1133
+ statement: statementName,
1134
+ paramTypes,
1135
+ params,
1136
+ queryOptions: options,
1137
+ }));
1138
+ this.runningQueryCount++;
1139
+ await this.socket
1140
+ .sendBatchBindExecuteMessages({
1141
+ binds,
1142
+ // Unlimited, never options.fetchCount: a suspended portal would
1143
+ // break the positional set-to-result mapping above.
1144
+ execute: { fetchCount: 0 },
1145
+ }, (code, msg, done) => {
1146
+ switch (code) {
1147
+ case Protocol.BackendMessageCode.BindComplete:
1148
+ case Protocol.BackendMessageCode.NoticeResponse:
1149
+ break;
1150
+ case Protocol.BackendMessageCode.DataRow:
1151
+ (pendingRows || (pendingRows = [])).push(msg);
1152
+ break;
1153
+ case Protocol.BackendMessageCode.EmptyQueryResponse:
1154
+ case Protocol.BackendMessageCode.CommandComplete: {
1155
+ const item = {};
1156
+ if (msg?.command)
1157
+ item.command = msg.command;
1158
+ if (item.command === 'DELETE' ||
1159
+ item.command === 'INSERT' ||
1160
+ item.command === 'UPDATE')
1161
+ item.rowsAffected = msg.rowCount;
1162
+ if (pendingRows && parsers && resultFields) {
1163
+ const l = pendingRows.length;
1164
+ let i;
1165
+ for (i = 0; i < l; i++) {
1166
+ pendingRows[i] = rowDecoder.decode(parsers, pendingRows[i].data, pendingRows[i].columnCount, options, resultFields);
1167
+ }
1168
+ item.rows = pendingRows;
1169
+ if (!item.command)
1170
+ item.command = 'SELECT';
1171
+ }
1172
+ pendingRows = undefined;
1173
+ results.push(item);
1174
+ break;
1175
+ }
1176
+ case Protocol.BackendMessageCode.ErrorResponse:
1177
+ error = msg;
1178
+ break;
1179
+ case Protocol.BackendMessageCode.ReadyForQuery:
1180
+ this.transactionStatus = msg.status;
1181
+ done(error);
1182
+ break;
1183
+ default:
1184
+ done(new Error(`Server returned unexpected response message (${String.fromCharCode(code)})`));
1185
+ }
1186
+ })
1187
+ .catch((e) => {
1188
+ // The rejected set is the one right after everything that
1189
+ // completed, since results only grow on CommandComplete.
1190
+ if (e instanceof DatabaseError && error === e) {
1191
+ e.failedIndex = results.length;
1192
+ e.batchResults = results;
1193
+ }
1194
+ throw e;
1195
+ })
1196
+ .finally(() => {
1197
+ this.runningQueryCount--;
1198
+ });
1199
+ let totalRowsAffected = 0;
1200
+ const l = results.length;
1201
+ let i;
1202
+ for (i = 0; i < l; i++)
1203
+ totalRowsAffected += results[i].rowsAffected || 0;
1204
+ const out = { results, totalRowsAffected };
1205
+ if (resultFields)
1206
+ out.fields = resultFields;
1207
+ if (timingEnabled)
1208
+ out.executeTime = performance.now() - startTime;
1209
+ return out;
1210
+ }
1211
+ finally {
1212
+ this.unref();
1213
+ }
1214
+ }
1215
+ async executeReused(statementName, cachedFields, paramTypes, params, options, savepoint) {
624
1216
  this.assertConnected();
625
1217
  this.ref();
626
1218
  try {
@@ -635,35 +1227,17 @@ export class IntlConnection extends SafeEventEmitter {
635
1227
  let error;
636
1228
  const rowDecoder = resolveRowDecoder(options);
637
1229
  if (cachedFields) {
638
- const columnFormat = options.columnFormat != null
639
- ? options.columnFormat
640
- : DEFAULT_COLUMN_FORMAT;
641
- const cached = this._executeReusedParserCache.get(cachedFields);
642
- if (cached &&
643
- cached.typeMap === typeMap &&
644
- columnFormatsEqual(cached.columnFormat, columnFormat)) {
645
- parsers = cached.parsers;
646
- resultFields = cached.resultFields;
647
- }
648
- else {
649
- const fields = cachedFields.map((f, i) => ({
650
- ...f,
651
- format: Array.isArray(columnFormat)
652
- ? columnFormat[i]
653
- : columnFormat,
654
- }));
655
- parsers = getParsers(typeMap, fields);
656
- resultFields = wrapRowDescription(typeMap, fields, columnFormat);
657
- this._executeReusedParserCache.set(cachedFields, {
658
- typeMap,
659
- columnFormat,
660
- parsers,
661
- resultFields,
662
- });
663
- }
1230
+ const resolved = this._resolveReusedParsers(cachedFields, typeMap, options);
1231
+ parsers = resolved.parsers;
1232
+ resultFields = resolved.resultFields;
664
1233
  result.fields = resultFields;
665
1234
  result.rowType = resolveRowType(options);
666
1235
  }
1236
+ // The SAVEPOINT/RELEASE pair rollbackOnError needs travels with the
1237
+ // statement instead of costing a round trip each - see
1238
+ // PreparedStatement._withTransaction(), which hands the name down
1239
+ // rather than sending them itself.
1240
+ let leadingCommandTags = savepoint ? 1 : 0;
667
1241
  this.runningQueryCount++;
668
1242
  await this.socket
669
1243
  .sendBindExecuteMessages({
@@ -675,8 +1249,11 @@ export class IntlConnection extends SafeEventEmitter {
675
1249
  queryOptions: options,
676
1250
  },
677
1251
  execute: { fetchCount: options.fetchCount || 100 },
1252
+ before: savepoint ? 'SAVEPOINT ' + savepoint : undefined,
1253
+ after: savepoint ? 'RELEASE ' + savepoint : undefined,
678
1254
  }, (code, msg, done) => {
679
1255
  switch (code) {
1256
+ case Protocol.BackendMessageCode.ParseComplete:
680
1257
  case Protocol.BackendMessageCode.BindComplete:
681
1258
  case Protocol.BackendMessageCode.NoticeResponse:
682
1259
  case Protocol.BackendMessageCode.PortalSuspended:
@@ -685,7 +1262,14 @@ export class IntlConnection extends SafeEventEmitter {
685
1262
  rows.push(msg);
686
1263
  break;
687
1264
  case Protocol.BackendMessageCode.CommandComplete:
688
- commandTag = msg;
1265
+ // Three statements can answer here when a savepoint rides
1266
+ // along: SAVEPOINT's tag, then the caller's own, then
1267
+ // RELEASE's. Only the middle one describes what the caller
1268
+ // asked for.
1269
+ if (leadingCommandTags)
1270
+ leadingCommandTags--;
1271
+ else if (!commandTag)
1272
+ commandTag = msg;
689
1273
  break;
690
1274
  case Protocol.BackendMessageCode.ErrorResponse:
691
1275
  error = msg;