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 +18 -1
- package/README.md +13 -2
- package/connection/connection.d.ts +37 -0
- package/connection/connection.js +72 -0
- package/connection/cursor.d.ts +8 -0
- package/connection/cursor.js +17 -0
- package/connection/intl-connection.d.ts +5 -0
- package/connection/intl-connection.js +183 -88
- package/connection/pool.d.ts +16 -0
- package/connection/pool.js +24 -0
- package/package.json +1 -1
- package/protocol/pg-socket.d.ts +2 -2
- package/protocol/pg-socket.js +12 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.
|
|
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
|
}
|
package/connection/connection.js
CHANGED
|
@@ -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)
|
package/connection/cursor.d.ts
CHANGED
|
@@ -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
|
}
|
package/connection/cursor.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
-
|
|
942
|
-
.
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
const
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
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.
|
|
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
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
//
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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;
|
package/connection/pool.d.ts
CHANGED
|
@@ -70,6 +70,22 @@ export declare class Pool extends SafeEventEmitter {
|
|
|
70
70
|
* Executes a query
|
|
71
71
|
*/
|
|
72
72
|
query(sql: string | QueryRequest, options?: PoolQueryOptions): Promise<QueryResult>;
|
|
73
|
+
/**
|
|
74
|
+
* Takes a connection out of the pool, runs `fn` inside a transaction on
|
|
75
|
+
* it, and gives the connection back however that ends.
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* await pool.transaction(async tx => {
|
|
79
|
+
* await tx.query('insert into orders (total) values ($1)', { params: [total] });
|
|
80
|
+
* await tx.query('update stock set n = n - 1 where sku = $1', { params: [sku] });
|
|
81
|
+
* });
|
|
82
|
+
* ```
|
|
83
|
+
*
|
|
84
|
+
* A transaction cannot be spread over pool.query() calls - each of those
|
|
85
|
+
* is free to pick a different connection, and a transaction lives on
|
|
86
|
+
* one. This is the way to get several statements onto the same one.
|
|
87
|
+
*/
|
|
88
|
+
transaction<T>(fn: (connection: Connection) => Promise<T>): Promise<T>;
|
|
73
89
|
prepare(sql: string, options?: StatementPrepareOptions): Promise<PreparedStatement>;
|
|
74
90
|
release(connection: Connection): Promise<void>;
|
|
75
91
|
listen(channel: string, callback: NotificationCallback): Promise<void>;
|
package/connection/pool.js
CHANGED
|
@@ -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
package/protocol/pg-socket.d.ts
CHANGED
|
@@ -119,9 +119,9 @@ export declare class PgSocket extends SafeEventEmitter {
|
|
|
119
119
|
*/
|
|
120
120
|
sendPipelineMessages(args: {
|
|
121
121
|
statements: {
|
|
122
|
-
parse
|
|
122
|
+
parse?: Frontend.ParseMessageArgs;
|
|
123
123
|
bind: Frontend.BindMessageArgs;
|
|
124
|
-
describe
|
|
124
|
+
describe?: Frontend.DescribeMessageArgs;
|
|
125
125
|
execute: Frontend.ExecuteMessageArgs;
|
|
126
126
|
}[];
|
|
127
127
|
}, cb: CaptureCallback): Promise<any>;
|
package/protocol/pg-socket.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
|
299
|
+
data.push(this._frontend.getSyncMessage());
|
|
294
300
|
return this._sendAndCapture(data, cb, 'sendPipelineMessages', args);
|
|
295
301
|
}
|
|
296
302
|
/**
|