postgrejs 3.4.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.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,23 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.4.0](https://github.com/panates/postgrejs/compare/v3.3.0...v3.4.0) -
3
+ ### [v3.5.0](https://github.com/panates/postgrejs/compare/v3.4.0...v3.5.0) -
4
+
5
+ #### 🚀 New Features
6
+
7
+ - feat: add transaction(fn) and let a Cursor be iterated with for await @Eray Hanoğlu
8
+
9
+ #### 📖 Documentation Changes
10
+
11
+ - docs: refresh Unit of Work after pipeline() started reusing statements @Eray Hanoğlu
12
+ - docs: record scoped transactions and cursor iteration in the README @Eray Hanoğlu
13
+ - docs: stop signing commits on the assistant's behalf @Eray Hanoğlu
14
+ - docs: note that pipeline() reuses cached prepared statements @Eray Hanoğlu
15
+
16
+ #### ⚡️ Performance Improvments
17
+
18
+ - perf: let pipeline() bind to the connection's cached statements @Eray Hanoğlu
19
+
20
+ ### [v3.4.0](https://github.com/panates/postgrejs/compare/v3.3.0...v3.4.0) - 16 September 2026
4
21
 
5
22
  - feat: add PreparedStatement.executeBatch() for multi-set execution
6
23
 
package/README.md CHANGED
@@ -82,7 +82,11 @@ usage.
82
82
  statement leaves the transaction usable instead of aborting the whole block. The `SAVEPOINT`/`RELEASE` pair travels
83
83
  in the statement's own round trip rather than costing one each - 36µs for the pair, against 507µs sent separately.
84
84
  `rollbackOnError: false` to opt out.
85
- - **Cursors:** Features fast double-link cache cursors for efficient data retrieval.
85
+ - **Cursors:** Features fast double-link cache cursors for efficient data retrieval, iterable with `for await` a row
86
+ at a time and closed when the loop ends - by exhaustion, a `break`, or a throw.
87
+ - **Scoped Transactions:** `transaction(fn)` commits when the callback returns and rolls back when it throws, on a
88
+ connection or straight from the pool. A call made while a transaction is already open takes a savepoint rather than
89
+ a second `BEGIN`, so a nested scope that fails rolls back its own work and leaves the outer transaction standing.
86
90
  - **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
87
91
  reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
88
92
  - **Notifications:** High-level implementation for PostgreSQL notifications (LISTEN/NOTIFY), enabling real-time data
@@ -99,7 +103,8 @@ usage.
99
103
  - **Query Pipelining:** Pooled queries can share connections so a burst is not capped by pool size - opt-in per call.
100
104
  `pipeline()` goes further for a known set of statements: several different ones travel under a single `Sync`, so they
101
105
  cost one round trip instead of one each and commit or roll back together - around 2x faster than the same calls
102
- through `Promise.all()`, which is already pipelined.
106
+ through `Promise.all()`, which is already pipelined. Statements the connection has run before bind to their cached
107
+ prepared names, so a repeated set sends no `Parse` at all - worth 2.5x on twenty statements.
103
108
  - **Dynamic SQL:** A `sql` tag builds statements from composable fragments - values become parameters, names are quoted,
104
109
  and `sql.values()`/`sql.set()` write INSERT and UPDATE clauses from objects.
105
110
  - **Multiple Hosts:** A connection can list several servers and pick one by role
@@ -162,6 +167,7 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
162
167
  | Parameter type casting | ✅ | 🟡 <sup>10</sup> | ✅ |
163
168
  | Prepared statements <sup>24</sup> | ✅ automatic | ✅ manual | ✅ automatic |
164
169
  | Statement-level rollback <sup>25</sup> | ✅ automatic | ❌ | 🟡 manual |
170
+ | Scoped transaction helper <sup>26</sup> | ✅ nests | ❌ | ✅ |
165
171
  | Batch execution <sup>21</sup> | ✅ | ❌ | ❌ |
166
172
  | Multi-statement round trip <sup>23</sup> | ✅ | ❌ | ❌ |
167
173
  | Multi-statement scripts | ✅ | ✅ | ✅ |
@@ -265,6 +271,11 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
265
271
  trip for each. postgres.js has savepoints, but as a scope the caller opens around a callback (`savepoint(name, fn)`
266
272
  in `src/index.js`), so a statement is protected only where someone wrapped it; pg has no savepoint handling in
267
273
  `lib/` at all, so a failed statement leaves the block aborted until the caller rolls back themselves.
274
+ - <sup>26</sup> Whether the library runs a callback inside a transaction for you, committing or rolling back on the way
275
+ out. PostgreJS has `transaction(fn)` on both a connection and the pool, and a call made inside one already open takes
276
+ a savepoint, so nesting needs nothing from the caller. postgres.js has `sql.begin(fn)` (`src/index.js`), where a
277
+ nested scope is opened explicitly with `sql.savepoint(fn)` instead. pg has no such helper in `lib/` - `BEGIN` and
278
+ `COMMIT` are statements the caller sends, and getting a transaction onto one connection is the caller's problem too.
268
279
 
269
280
 
270
281
  ## Benchmarks
@@ -282,6 +282,34 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
282
282
  * Starts a transaction
283
283
  */
284
284
  startTransaction(): Promise<void>;
285
+ /**
286
+ * Runs `fn` inside a transaction and commits when it returns, or rolls
287
+ * back and rethrows when it throws - the try/catch every caller of
288
+ * startTransaction()/commit()/rollback() ends up writing by hand.
289
+ *
290
+ * ```ts
291
+ * const id = await connection.transaction(async tx => {
292
+ * const r = await tx.query(
293
+ * 'insert into orders (total) values ($1) returning id',
294
+ * { params: [total] },
295
+ * );
296
+ * await tx.query('update stock set n = n - 1 where sku = $1', {
297
+ * params: [sku],
298
+ * });
299
+ * return r.rows![0][0];
300
+ * });
301
+ * ```
302
+ *
303
+ * A call made while a transaction is already open becomes a savepoint
304
+ * rather than a second BEGIN, so an inner scope that fails rolls back
305
+ * its own work and leaves the outer transaction to decide what to do -
306
+ * instead of taking everything down with it, which is what a shared
307
+ * BEGIN/ROLLBACK pair would do.
308
+ *
309
+ * `fn` is handed this same connection: every statement it runs on it is
310
+ * inside the transaction, and one it runs on another connection is not.
311
+ */
312
+ transaction<T>(fn: (connection: this) => Promise<T>): Promise<T>;
285
313
  /**
286
314
  * Commits current transaction.
287
315
  * @param immediate - Commits right away, ignoring how many nested
@@ -383,6 +411,15 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
383
411
  * microtask tick on top of the one `promise`/`.catch()` already goes
384
412
  * through - paid on every single query this wraps, disabled or not.
385
413
  */
414
+ /**
415
+ * The nested half of transaction(): a savepoint of its own, released on
416
+ * success and rolled back to on failure.
417
+ *
418
+ * The `inTransaction` checks are re-read rather than assumed - `fn` is
419
+ * free to commit or roll back the outer transaction itself, and there is
420
+ * no savepoint left to release once it has.
421
+ */
422
+ private _savepointScope;
386
423
  protected _captureErrorStack<T>(promise: Promise<T>, entry?: (...args: any[]) => any, asyncErrorHandling?: boolean): Promise<T>;
387
424
  [Symbol.asyncDispose](): Promise<void>;
388
425
  }
@@ -8,6 +8,8 @@ import { IntlConnection } from './intl-connection.js';
8
8
  import { LargeObject, LargeObjectMode } from './large-object.js';
9
9
  import { PreparedStatement } from './prepared-statement.js';
10
10
  const CAPTURE_STACK_TRACE_LIMIT = 5;
11
+ /** Names the savepoint a nested transaction() scope takes. */
12
+ let transactionScopeCounter = 0;
11
13
  export class Connection extends SafeEventEmitter {
12
14
  _pool;
13
15
  _intlCon;
@@ -477,6 +479,50 @@ export class Connection extends SafeEventEmitter {
477
479
  startTransaction() {
478
480
  return this._captureErrorStack(this._intlCon.startTransaction());
479
481
  }
482
+ /**
483
+ * Runs `fn` inside a transaction and commits when it returns, or rolls
484
+ * back and rethrows when it throws - the try/catch every caller of
485
+ * startTransaction()/commit()/rollback() ends up writing by hand.
486
+ *
487
+ * ```ts
488
+ * const id = await connection.transaction(async tx => {
489
+ * const r = await tx.query(
490
+ * 'insert into orders (total) values ($1) returning id',
491
+ * { params: [total] },
492
+ * );
493
+ * await tx.query('update stock set n = n - 1 where sku = $1', {
494
+ * params: [sku],
495
+ * });
496
+ * return r.rows![0][0];
497
+ * });
498
+ * ```
499
+ *
500
+ * A call made while a transaction is already open becomes a savepoint
501
+ * rather than a second BEGIN, so an inner scope that fails rolls back
502
+ * its own work and leaves the outer transaction to decide what to do -
503
+ * instead of taking everything down with it, which is what a shared
504
+ * BEGIN/ROLLBACK pair would do.
505
+ *
506
+ * `fn` is handed this same connection: every statement it runs on it is
507
+ * inside the transaction, and one it runs on another connection is not.
508
+ */
509
+ async transaction(fn) {
510
+ if (this._intlCon.inTransaction)
511
+ return await this._savepointScope(fn);
512
+ await this.startTransaction();
513
+ try {
514
+ const result = await fn(this);
515
+ await this.commit();
516
+ return result;
517
+ }
518
+ catch (e) {
519
+ // A rollback that fails in turn must not replace the error that
520
+ // caused it: the connection is already in trouble, and the original
521
+ // is what the caller needs to see.
522
+ await this.rollback().catch(() => undefined);
523
+ throw e;
524
+ }
525
+ }
480
526
  /**
481
527
  * Commits current transaction.
482
528
  * @param immediate - Commits right away, ignoring how many nested
@@ -695,6 +741,32 @@ export class Connection extends SafeEventEmitter {
695
741
  * microtask tick on top of the one `promise`/`.catch()` already goes
696
742
  * through - paid on every single query this wraps, disabled or not.
697
743
  */
744
+ /**
745
+ * The nested half of transaction(): a savepoint of its own, released on
746
+ * success and rolled back to on failure.
747
+ *
748
+ * The `inTransaction` checks are re-read rather than assumed - `fn` is
749
+ * free to commit or roll back the outer transaction itself, and there is
750
+ * no savepoint left to release once it has.
751
+ */
752
+ async _savepointScope(fn) {
753
+ const name = 'txscope' + ++transactionScopeCounter;
754
+ await this._captureErrorStack(this._intlCon.execute('SAVEPOINT ' + name));
755
+ try {
756
+ const result = await fn(this);
757
+ if (this._intlCon.inTransaction)
758
+ await this._intlCon.execute('RELEASE SAVEPOINT ' + name);
759
+ return result;
760
+ }
761
+ catch (e) {
762
+ if (this._intlCon.inTransaction) {
763
+ await this._intlCon
764
+ .execute('ROLLBACK TO SAVEPOINT ' + name + '; RELEASE SAVEPOINT ' + name)
765
+ .catch(() => undefined);
766
+ }
767
+ throw e;
768
+ }
769
+ }
698
770
  _captureErrorStack(promise, entry, asyncErrorHandling) {
699
771
  const enabled = asyncErrorHandling ?? this._intlCon.config.asyncErrorHandling ?? true;
700
772
  if (!enabled)
@@ -21,5 +21,13 @@ export declare class Cursor extends SafeEventEmitter implements AsyncDisposable
21
21
  fetch(nRows: number): Promise<Row[]>;
22
22
  close(): Promise<void>;
23
23
  private _fetchRows;
24
+ /**
25
+ * Iterates the remaining rows, fetching a batch at a time, and closes
26
+ * the cursor when the loop ends - whether it ran out of rows, broke
27
+ * early, or the body threw. Nothing is decoded ahead of what is asked
28
+ * for, so this streams a result larger than memory the same way next()
29
+ * does one row at a time.
30
+ */
31
+ [Symbol.asyncIterator](): AsyncIterableIterator<Row>;
24
32
  [Symbol.asyncDispose](): Promise<void>;
25
33
  }
@@ -92,6 +92,23 @@ export class Cursor extends SafeEventEmitter {
92
92
  })
93
93
  .toPromise();
94
94
  }
95
+ /**
96
+ * Iterates the remaining rows, fetching a batch at a time, and closes
97
+ * the cursor when the loop ends - whether it ran out of rows, broke
98
+ * early, or the body threw. Nothing is decoded ahead of what is asked
99
+ * for, so this streams a result larger than memory the same way next()
100
+ * does one row at a time.
101
+ */
102
+ async *[Symbol.asyncIterator]() {
103
+ try {
104
+ let row;
105
+ while ((row = await this.next()) !== undefined)
106
+ yield row;
107
+ }
108
+ finally {
109
+ await this.close();
110
+ }
111
+ }
95
112
  [Symbol.asyncDispose]() {
96
113
  return this.close();
97
114
  }
@@ -207,6 +207,11 @@ export declare class IntlConnection extends SafeEventEmitter {
207
207
  */
208
208
  protected _inlineSavepointFor(sql: string, options: QueryOptions): Maybe<string>;
209
209
  protected _queryCached(sql: string, paramTypes: Maybe<Maybe<OID>[]>, params: Maybe<Maybe<any>[]>, options: QueryOptions, savepoint: Maybe<string>): Promise<QueryResult>;
210
+ /**
211
+ * Counts one more use of this SQL and says whether it has now been seen
212
+ * often enough to be worth preparing under a name.
213
+ */
214
+ protected _earnsAName(key: string): boolean;
210
215
  /** Closes the least recently used statement once the cache is full. */
211
216
  protected _evictPreparedIfFull(): Promise<void>;
212
217
  /**
@@ -41,6 +41,14 @@ const PREPARE_AFTER_USES = 2;
41
41
  * SQL that manages transaction boundaries itself, and so must never be
42
42
  * wrapped in a savepoint or an implicit BEGIN/COMMIT of ours.
43
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
+ }
44
52
  const TRANSACTION_COMMAND_PATTERN = /^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i;
45
53
  export class IntlConnection extends SafeEventEmitter {
46
54
  /**
@@ -627,9 +635,7 @@ export class IntlConnection extends SafeEventEmitter {
627
635
  const enabled = options.prepare ?? this.config.prepare ?? true;
628
636
  if (!enabled)
629
637
  return this.queryOnce(sql, paramTypes, params, options, savepoint);
630
- const key = paramTypes?.length
631
- ? sql + '\u0000' + paramTypes.join(',')
632
- : sql;
638
+ const key = preparedCacheKey(sql, paramTypes);
633
639
  const cached = this._preparedCache.get(key);
634
640
  if (cached) {
635
641
  // Re-insert to mark it most recently used.
@@ -653,12 +659,8 @@ export class IntlConnection extends SafeEventEmitter {
653
659
  return result;
654
660
  }
655
661
  }
656
- const uses = (this._preparedCandidates.get(key) || 0) + 1;
657
- if (uses < PREPARE_AFTER_USES) {
658
- this._preparedCandidates.set(key, uses);
662
+ if (!this._earnsAName(key))
659
663
  return this.queryOnce(sql, paramTypes, params, options, savepoint);
660
- }
661
- this._preparedCandidates.delete(key);
662
664
  const name = 'C_' + ++this._preparedCounter;
663
665
  // A Parse that fails caches nothing and reports itself; the statement
664
666
  // is simply never reused. It runs outside the savepoint, as the
@@ -669,6 +671,19 @@ export class IntlConnection extends SafeEventEmitter {
669
671
  this._preparedCache.set(key, entry);
670
672
  return this.executeReused(name, entry.fields, paramTypes, params, options, savepoint);
671
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
+ }
672
687
  /** Closes the least recently used statement once the cache is full. */
673
688
  async _evictPreparedIfFull() {
674
689
  const max = this.config.preparedStatementCacheSize ?? 64;
@@ -914,94 +929,174 @@ export class IntlConnection extends SafeEventEmitter {
914
929
  const timingEnabled = options.timing ?? this.config.timing ?? false;
915
930
  const startTime = timingEnabled ? performance.now() : 0;
916
931
  const rowDecoder = resolveRowDecoder(options);
917
- const columnFormat = options.columnFormat != null
918
- ? options.columnFormat
919
- : DEFAULT_COLUMN_FORMAT;
920
932
  const results = [];
921
933
  let pendingFields;
922
934
  let pendingRows;
923
935
  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
- }));
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
+ }
940
998
  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);
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;
972
1040
  }
973
- result.rows = rows;
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;
974
1049
  }
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;
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)})`));
983
1059
  }
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
- });
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
+ }
1005
1100
  if (timingEnabled) {
1006
1101
  const elapsed = performance.now() - startTime;
1007
1102
  const l = results.length;
@@ -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>;
@@ -222,6 +222,30 @@ export class Pool extends SafeEventEmitter {
222
222
  slot.load--;
223
223
  }
224
224
  }
225
+ /**
226
+ * Takes a connection out of the pool, runs `fn` inside a transaction on
227
+ * it, and gives the connection back however that ends.
228
+ *
229
+ * ```ts
230
+ * await pool.transaction(async tx => {
231
+ * await tx.query('insert into orders (total) values ($1)', { params: [total] });
232
+ * await tx.query('update stock set n = n - 1 where sku = $1', { params: [sku] });
233
+ * });
234
+ * ```
235
+ *
236
+ * A transaction cannot be spread over pool.query() calls - each of those
237
+ * is free to pick a different connection, and a transaction lives on
238
+ * one. This is the way to get several statements onto the same one.
239
+ */
240
+ async transaction(fn) {
241
+ const connection = await this.acquire();
242
+ try {
243
+ return await connection.transaction(fn);
244
+ }
245
+ finally {
246
+ await this.release(connection);
247
+ }
248
+ }
225
249
  async prepare(sql, options) {
226
250
  const connection = await this.acquire();
227
251
  const statement = await connection.prepare(sql, options);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.4.0",
4
+ "version": "3.5.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -119,9 +119,9 @@ export declare class PgSocket extends SafeEventEmitter {
119
119
  */
120
120
  sendPipelineMessages(args: {
121
121
  statements: {
122
- parse: Frontend.ParseMessageArgs;
122
+ parse?: Frontend.ParseMessageArgs;
123
123
  bind: Frontend.BindMessageArgs;
124
- describe: Frontend.DescribeMessageArgs;
124
+ describe?: Frontend.DescribeMessageArgs;
125
125
  execute: Frontend.ExecuteMessageArgs;
126
126
  }[];
127
127
  }, cb: CaptureCallback): Promise<any>;
@@ -280,17 +280,23 @@ export class PgSocket extends SafeEventEmitter {
280
280
  */
281
281
  sendPipelineMessages(args, cb) {
282
282
  const l = args.statements.length;
283
- const data = new Array(l * 4 + 1);
283
+ const data = [];
284
284
  let i;
285
285
  let st;
286
286
  for (i = 0; i < l; i++) {
287
287
  st = args.statements[i];
288
- data[i * 4] = this._frontend.getParseMessage(st.parse);
289
- data[i * 4 + 1] = this._frontend.getBindMessage(st.bind);
290
- data[i * 4 + 2] = this._frontend.getDescribeMessage(st.describe);
291
- data[i * 4 + 3] = this._frontend.getExecuteMessage(st.execute);
288
+ // Parse and Describe are both optional: a statement bound to one the
289
+ // connection has already prepared needs neither, since the server
290
+ // still holds the plan and IntlConnection still holds the
291
+ // RowDescription that came with it.
292
+ if (st.parse)
293
+ data.push(this._frontend.getParseMessage(st.parse));
294
+ data.push(this._frontend.getBindMessage(st.bind));
295
+ if (st.describe)
296
+ data.push(this._frontend.getDescribeMessage(st.describe));
297
+ data.push(this._frontend.getExecuteMessage(st.execute));
292
298
  }
293
- data[l * 4] = this._frontend.getSyncMessage();
299
+ data.push(this._frontend.getSyncMessage());
294
300
  return this._sendAndCapture(data, cb, 'sendPipelineMessages', args);
295
301
  }
296
302
  /**