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
package/CHANGELOG.md CHANGED
@@ -1,6 +1,60 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.3.0](https://github.com/panates/postgrejs/compare/v3.2.0...v3.3.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
21
+
22
+ - feat: add PreparedStatement.executeBatch() for multi-set execution
23
+
24
+ #### 🚀 New Features
25
+
26
+ - feat: add copyFromRows() for binary COPY bulk loading @Eray Hanoğlu
27
+ - feat: add Connection.pipeline() for many statements in one round trip @Eray Hanoğlu
28
+ - feat: reuse prepared statements for repeated queries @Eray Hanoğlu
29
+
30
+ #### 🪲 Fixes
31
+
32
+ - fix: refuse values that have no numeric reading instead of storing zero @Eray Hanoğlu
33
+
34
+ #### 📖 Documentation Changes
35
+
36
+ - docs: regenerate both benchmark reports from an interleaved run @Eray Hanoğlu
37
+ - docs: refresh the Node report and correct a stale asymmetry note @Eray Hanoğlu
38
+ - docs: record statement-level rollback in the README @Eray Hanoğlu
39
+ - docs: record the prepared statement cache in the README @Eray Hanoğlu
40
+ - docs: list pipeline() in the feature list and comparison table @Eray Hanoğlu
41
+ - docs: list binary COPY in the feature list and comparison table @Eray Hanoğlu
42
+ - docs: list batch execution in the feature list and comparison table @Eray Hanoğlu
43
+
44
+ #### ⚡️ Performance Improvments
45
+
46
+ - perf: carry the rollbackOnError savepoint in the statement's own round trip @Eray Hanoğlu
47
+ - perf: let the prepared statement cache serve queries inside a transaction @Eray Hanoğlu
48
+
49
+ #### 💬 General Changes
50
+
51
+ - bench: add Bulk Load scenarios for text and binary COPY @Eray Hanoğlu
52
+ - bench: add Unit of Work scenario for pipeline() @Eray Hanoğlu
53
+ - bench: stop measuring postgres.js on an unprepared path @Eray Hanoğlu
54
+ - bench: interleave repeats instead of running them back to back @Eray Hanoğlu
55
+ - ci: pin GITHUB_TOKEN to read-only in the test and qc workflows @Eray Hanoğlu
56
+
57
+ ### [v3.3.0](https://github.com/panates/postgrejs/compare/v3.2.0...v3.3.0) - 14 September 2026
4
58
 
5
59
  #### 🚀 New Features
6
60
 
package/README.md CHANGED
@@ -23,8 +23,8 @@ onto a client meant for text.
23
23
  The numbers back it up. In PostgreJS's own benchmark suite - run head-to-head against
24
24
  [`pg`](https://github.com/brianc/node-postgres) (node-postgres) and [`postgres`](https://github.com/porsager/postgres)
25
25
  (postgres.js) on identical workloads - PostgreJS opens a connection up to **3x faster** than postgres.js, pushes
26
- pooled queries through up to **6.6x faster** than postgres.js, and fetches large result sets nearly **5x faster**
27
- than pg. It's also the only one of the three Node drivers with a complete binary wire protocol across every data
26
+ pooled queries through up to **6.5x faster** than pg, and fetches large result sets nearly **5x faster** than pg
27
+ too. It's also the only one of the three Node drivers with a complete binary wire protocol across every data
28
28
  type, rather than falling back to text for most of them. See [`doc/BENCHMARKS.md`](doc/BENCHMARKS.md) for the full
29
29
  methodology and every scenario.
30
30
 
@@ -74,8 +74,21 @@ usage.
74
74
  resource management.
75
75
  - **Binary Wire Protocol:** Implements the full binary wire protocol for all PostgreSQL data types, ensuring robust and
76
76
  efficient data handling.
77
- - **Prepared Statements:** Named prepared statements for optimized query execution.
78
- - **Cursors:** Features fast double-link cache cursors for efficient data retrieval.
77
+ - **Prepared Statements:** Named prepared statements for optimized query execution, and a per-connection cache that
78
+ reuses one automatically for SQL the connection has run before - a repeated query costs `Bind`/`Execute` instead of
79
+ parsing again, worth 3.2x on fifty concurrent calls and 2.9x again on a query inside an open transaction. On by
80
+ default, `prepare: false` to opt out.
81
+ - **Statement-Level Rollback:** Inside a transaction each statement runs under a savepoint of its own, so a failed
82
+ statement leaves the transaction usable instead of aborting the whole block. The `SAVEPOINT`/`RELEASE` pair travels
83
+ in the statement's own round trip rather than costing one each - 36µs for the pair, against 507µs sent separately.
84
+ `rollbackOnError: false` to opt out.
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.
90
+ - **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
91
+ reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
79
92
  - **Notifications:** High-level implementation for PostgreSQL notifications (LISTEN/NOTIFY), enabling real-time data
80
93
  updates.
81
94
  - **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements.
@@ -84,8 +97,14 @@ usage.
84
97
  - **Performance Optimization:** Low memory utilization and boosted performance through the use of shared buffers.
85
98
  - **Authorization:** Supports various password algorithms including Clear text, MD5, and SASL, ensuring secure
86
99
  authentication.
87
- - **Bulk Import/Export:** `COPY TO STDOUT` and `COPY FROM STDIN` as Node streams, with backpressure in both directions.
100
+ - **Bulk Import/Export:** `COPY TO STDOUT` and `COPY FROM STDIN` as Node streams, with backpressure in both directions,
101
+ plus `copyFromRows()`, which encodes rows straight into binary `COPY` - around 4x faster than the CSV equivalent and
102
+ no text escaping to get wrong. Takes arrays or objects from anything iterable, so a file larger than memory streams in.
88
103
  - **Query Pipelining:** Pooled queries can share connections so a burst is not capped by pool size - opt-in per call.
104
+ `pipeline()` goes further for a known set of statements: several different ones travel under a single `Sync`, so they
105
+ cost one round trip instead of one each and commit or roll back together - around 2x faster than the same calls
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.
89
108
  - **Dynamic SQL:** A `sql` tag builds statements from composable fragments - values become parameters, names are quoted,
90
109
  and `sql.values()`/`sql.set()` write INSERT and UPDATE clauses from objects.
91
110
  - **Multiple Hosts:** A connection can list several servers and pick one by role
@@ -114,7 +133,7 @@ usage.
114
133
 
115
134
  How PostgreJS compares to [`pg`](https://github.com/brianc/node-postgres) (node-postgres) and
116
135
  [`postgres`](https://github.com/porsager/postgres) (postgres.js). Every row was checked against the libraries' own
117
- source rather than their documentation — versions compared: **PostgreJS 3.1.0, pg 8.23.0, postgres.js 3.4.9**. ✅ built
136
+ source rather than their documentation — versions compared: **PostgreJS 3.4.0, pg 8.23.0, postgres.js 3.4.9**. ✅ built
118
137
  in · 🟡 partial or needs a separate package · ❌ not supported.
119
138
 
120
139
  | Feature | PostgreJS | pg | postgres.js |
@@ -146,10 +165,15 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
146
165
  | ***Querying*** | | | |
147
166
  | Query parameters | ✅ | ✅ | ✅ |
148
167
  | Parameter type casting | ✅ | 🟡 <sup>10</sup> | ✅ |
149
- | Prepared statements | ✅ explicit | ✅ | ✅ automatic |
168
+ | Prepared statements <sup>24</sup> | ✅ automatic | ✅ manual | ✅ automatic |
169
+ | Statement-level rollback <sup>25</sup> | ✅ automatic | ❌ | 🟡 manual |
170
+ | Scoped transaction helper <sup>26</sup> | ✅ nests | ❌ | ✅ |
171
+ | Batch execution <sup>21</sup> | ✅ | ❌ | ❌ |
172
+ | Multi-statement round trip <sup>23</sup> | ✅ | ❌ | ❌ |
150
173
  | Multi-statement scripts | ✅ | ✅ | ✅ |
151
174
  | Server-side cursors | ✅ | 🟡 <sup>11</sup> | ✅ |
152
175
  | `COPY TO` / `COPY FROM` | ✅ | 🟡 <sup>12</sup> | ✅ |
176
+ | Binary COPY encoding <sup>22</sup> | ✅ | ❌ | ❌ |
153
177
  | Row count after a COPY | ✅ | ✅ | ❌ |
154
178
  | ***Transaction management*** | | | |
155
179
  | Transaction API | ✅ | ❌ | ✅ |
@@ -215,6 +239,43 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
215
239
  documentation lists what stops working with it: `pg-cursor`, `pg-query-stream` and
216
240
  `pg-copy-streams` all "operate directly on the binary stream and therefore are
217
241
  incompatible" - so server-side cursors, row streaming and COPY are what it costs.
242
+ - <sup>21</sup> One statement executed over many parameter sets behind a single `Sync`, each set's row count reported
243
+ separately. pg and postgres.js both emit a `Sync` per execution (`syncBuffer` in pg's `connection.js`, the
244
+ concatenated `ExecuteUnnamed` in postgres.js's), so a burst of executions costs a server round of implicit-transaction
245
+ bookkeeping each, pipelined or not. Writing one multi-row statement by hand is a separate approach that all three
246
+ support, and both PostgreJS and postgres.js ship value builders for it - it is faster still where it applies, but it
247
+ is one statement rather than many, and PostgreSQL's 65535-parameter ceiling bounds it.
248
+ - <sup>22</sup> Turning JavaScript rows into the binary `COPY` format, rather than carrying a payload the caller
249
+ formatted first. All three can carry a `COPY` stream, but only as bytes: postgres.js hands back a `Writable` that
250
+ wraps raw chunks in `CopyData`, and pg reaches the same point through `pg-copy-streams`. Producing the format needs a
251
+ binary encoder per type, which neither driver has - see Binary encoders above, where the same gap shows up for query
252
+ parameters. The capability is still reachable with pg through a third package, `pg-copy-streams-binary`, which brings
253
+ its own encoders; postgres.js has no equivalent.
254
+ - <sup>23</sup> Several different statements sent under one `Sync`, so they cost a single round of the server's
255
+ transaction bookkeeping rather than one each, arrive as one implicit transaction, and come back as per-statement
256
+ results. Distinct from the Pipelining row above, which is about not waiting between queries: all three do that, and
257
+ all three still close every statement with its own `Sync` - pg sends one immediately after each Execute
258
+ (`query.js`), and postgres.js concatenates Execute and Sync into a single constant (`ExecuteUnnamed`). Atomicity on
259
+ its own is reachable anywhere with an explicit `BEGIN`/`COMMIT`; what that cannot recover is the round trip, since
260
+ the per-statement Syncs and two extra statements remain.
261
+ - <sup>24</sup> Whether a repeated query is parsed again every time. PostgreJS and postgres.js both keep a per-connection
262
+ cache keyed on the SQL and reuse a server-side statement, so a repeat costs `Bind`/`Execute` rather than
263
+ `Parse`/`Bind`/`Describe`/`Execute`; pg prepares only the statements you name yourself (`query.js`: "named queries
264
+ must always be prepared"), with nothing caching by SQL text. The two automatic ones differ in when they start:
265
+ postgres.js on first sight, PostgreJS on the second use, which leaves a genuinely one-shot query at its unprepared
266
+ cost. Both default to on and both take `prepare: false`, which matters for PgBouncer in transaction pooling mode
267
+ before 1.21, where a named statement does not survive to the next call.
268
+ - <sup>25</sup> Whether one failed statement inside a transaction leaves the rest of the block runnable. PostgreJS puts
269
+ every statement under a savepoint of its own by default (`rollbackOnError`), rolling back to it when the statement
270
+ fails, and carries the `SAVEPOINT`/`RELEASE` pair inside the statement's own round trip rather than paying a round
271
+ trip for each. postgres.js has savepoints, but as a scope the caller opens around a callback (`savepoint(name, fn)`
272
+ in `src/index.js`), so a statement is protected only where someone wrapped it; pg has no savepoint handling in
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.
218
279
 
219
280
 
220
281
  ## Benchmarks
@@ -1,7 +1,9 @@
1
1
  import { ConnectionState } from '../constants.js';
2
+ import type { CopyFromRowsOptions, CopyFromRowsResult } from '../interfaces/copy-from-rows-options.js';
2
3
  import type { ConnectionConfiguration } from '../interfaces/database-connection-params.js';
3
4
  import type { FunctionCallOptions } from '../interfaces/function-call-options.js';
4
5
  import type { FunctionCallResult } from '../interfaces/function-call-result.js';
6
+ import type { PipelineRequest } from '../interfaces/pipeline-request.js';
5
7
  import type { QueryOptions } from '../interfaces/query-options.js';
6
8
  import type { QueryResult } from '../interfaces/query-result.js';
7
9
  import type { ScriptExecuteOptions } from '../interfaces/script-execute-options.js';
@@ -11,6 +13,7 @@ import type { DatabaseError } from '../protocol/database-error.js';
11
13
  import type { Protocol } from '../protocol/protocol.js';
12
14
  import { SafeEventEmitter } from '../safe-event-emitter.js';
13
15
  import type { Maybe, OID } from '../types.js';
16
+ import type { CopyRowSource } from '../util/copy-from-rows.js';
14
17
  import { QueryRequest } from '../util/sql-tag.js';
15
18
  import type { CopyFromStream, CopyToStream } from './copy-stream.js';
16
19
  import { IntlConnection } from './intl-connection.js';
@@ -136,6 +139,100 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
136
139
  *
137
140
  * @param sql {string} - A COPY ... FROM STDIN statement
138
141
  */
142
+ /**
143
+ * Bulk-loads rows into a table with `COPY ... FROM STDIN (FORMAT binary)`,
144
+ * encoding every value with its own type's binary encoder instead of
145
+ * making the caller format a text payload.
146
+ *
147
+ * ```ts
148
+ * const { rowCount } = await connection.copyFromRows('users', [
149
+ * [1, 'John', 10.5],
150
+ * [2, 'Jane', 20.0],
151
+ * ], { columns: ['id', 'name', 'amount'] });
152
+ * ```
153
+ *
154
+ * Rows may be positional arrays or objects read by column name, and the
155
+ * source may be anything iterable - including an async iterable or a
156
+ * Readable, so a file far larger than memory streams straight in:
157
+ *
158
+ * ```ts
159
+ * await connection.copyFromRows('users', (async function* () {
160
+ * for await (const { value } of jsonRows) yield [value.id, value.name];
161
+ * })(), { columns: ['id', 'name'] });
162
+ * ```
163
+ *
164
+ * Rows are pulled rather than pushed, so the source only produces the
165
+ * next row once the previous chunk has reached the socket - backpressure
166
+ * is the loop pausing, not a queue growing.
167
+ *
168
+ * Binary is both faster and cheaper to produce than text: 200,000 rows of
169
+ * int4/text/float8/timestamptz measured 232ms against 483ms for the
170
+ * equivalent CSV `copyFrom()`, client-side encoding included, because
171
+ * writing an int32 costs less than formatting a decimal string.
172
+ *
173
+ * Column types are read from the server (one Describe round trip) unless
174
+ * `columnTypes` supplies them. They have to be exact - binary COPY does
175
+ * no conversion - which is also why a column whose type has no binary
176
+ * encoder is rejected before any row is sent rather than mid-stream.
177
+ *
178
+ * `copyFrom()` remains the way to send an already-formatted text or CSV
179
+ * payload.
180
+ *
181
+ * A value the column's type cannot encode - `'abc'` for an integer
182
+ * column - aborts the copy by default, naming the row and column;
183
+ * `onInvalidValue` can instead null the value or drop the row, and the
184
+ * result says how many that happened to. NaN and Infinity in a float or
185
+ * numeric column are not invalid: PostgreSQL stores them as values
186
+ * distinct from NULL, so they go through untouched.
187
+ *
188
+ * @returns Rows sent, plus what a tolerant `onInvalidValue` swallowed.
189
+ */
190
+ /**
191
+ * Runs several different statements in one round trip: every
192
+ * Parse/Bind/Describe/Execute goes out before any response is waited
193
+ * for, and a single Sync closes the lot.
194
+ *
195
+ * ```ts
196
+ * const [renamed, , total] = await connection.pipeline([
197
+ * sql`update users set name = ${name} where id = ${id}`,
198
+ * sql`insert into audit(msg) values (${msg})`,
199
+ * sql`select count(*)::int as n from users`,
200
+ * ]);
201
+ * renamed.rowsAffected; // 1
202
+ * total.rows?.[0]; // [42]
203
+ * ```
204
+ *
205
+ * The counterpart to `PreparedStatement.executeBatch()`, which runs one
206
+ * statement over many parameter sets; this runs many statements once
207
+ * each. Twenty statements measured 2.3ms against 5.0ms for the same
208
+ * calls through `Promise.all()`, which PostgreJS already pipelines -
209
+ * what the single Sync removes is the server finishing an implicit
210
+ * transaction and answering ReadyForQuery twenty times over, which is
211
+ * why socket reads drop from twenty to one.
212
+ *
213
+ * Three things follow from that framing rather than from choices made
214
+ * above it:
215
+ *
216
+ * - **One transaction.** Unless an explicit transaction is already open,
217
+ * the statements commit or roll back together.
218
+ * - **A failing statement stops the rest.** PostgreSQL discards
219
+ * everything between an error and the Sync, so statements after a
220
+ * rejected one never run. The error carries `failedIndex`.
221
+ * - **No statement can see another's results.** They are all sent before
222
+ * any reply arrives, so anything conditional on an earlier result
223
+ * belongs in a separate call rather than here.
224
+ *
225
+ * Results map to statements by position. Statements that return rows get
226
+ * them decoded as `query()` would, `rowDecoder` included; `fetchCount`
227
+ * does not apply, since a portal suspended mid-pipeline would break that
228
+ * mapping.
229
+ *
230
+ * @param requests Statements to run, as `sql` tag output, plain SQL
231
+ * strings, or `{ sql, params }` objects.
232
+ * @param options Applied to every statement.
233
+ */
234
+ pipeline(requests: (string | QueryRequest | PipelineRequest)[], options?: QueryOptions): Promise<QueryResult[]>;
235
+ copyFromRows(table: string, source: CopyRowSource, options?: CopyFromRowsOptions): Promise<CopyFromRowsResult>;
139
236
  copyFrom(sql: string): Promise<CopyFromStream>;
140
237
  /**
141
238
  * Creates a PreparedStatement instance
@@ -185,6 +282,34 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
185
282
  * Starts a transaction
186
283
  */
187
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>;
188
313
  /**
189
314
  * Commits current transaction.
190
315
  * @param immediate - Commits right away, ignoring how many nested
@@ -286,6 +411,15 @@ export declare class Connection extends SafeEventEmitter implements AsyncDisposa
286
411
  * microtask tick on top of the one `promise`/`.catch()` already goes
287
412
  * through - paid on every single query this wraps, disabled or not.
288
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;
289
423
  protected _captureErrorStack<T>(promise: Promise<T>, entry?: (...args: any[]) => any, asyncErrorHandling?: boolean): Promise<T>;
290
424
  [Symbol.asyncDispose](): Promise<void>;
291
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;
@@ -251,6 +253,139 @@ export class Connection extends SafeEventEmitter {
251
253
  *
252
254
  * @param sql {string} - A COPY ... FROM STDIN statement
253
255
  */
256
+ /**
257
+ * Bulk-loads rows into a table with `COPY ... FROM STDIN (FORMAT binary)`,
258
+ * encoding every value with its own type's binary encoder instead of
259
+ * making the caller format a text payload.
260
+ *
261
+ * ```ts
262
+ * const { rowCount } = await connection.copyFromRows('users', [
263
+ * [1, 'John', 10.5],
264
+ * [2, 'Jane', 20.0],
265
+ * ], { columns: ['id', 'name', 'amount'] });
266
+ * ```
267
+ *
268
+ * Rows may be positional arrays or objects read by column name, and the
269
+ * source may be anything iterable - including an async iterable or a
270
+ * Readable, so a file far larger than memory streams straight in:
271
+ *
272
+ * ```ts
273
+ * await connection.copyFromRows('users', (async function* () {
274
+ * for await (const { value } of jsonRows) yield [value.id, value.name];
275
+ * })(), { columns: ['id', 'name'] });
276
+ * ```
277
+ *
278
+ * Rows are pulled rather than pushed, so the source only produces the
279
+ * next row once the previous chunk has reached the socket - backpressure
280
+ * is the loop pausing, not a queue growing.
281
+ *
282
+ * Binary is both faster and cheaper to produce than text: 200,000 rows of
283
+ * int4/text/float8/timestamptz measured 232ms against 483ms for the
284
+ * equivalent CSV `copyFrom()`, client-side encoding included, because
285
+ * writing an int32 costs less than formatting a decimal string.
286
+ *
287
+ * Column types are read from the server (one Describe round trip) unless
288
+ * `columnTypes` supplies them. They have to be exact - binary COPY does
289
+ * no conversion - which is also why a column whose type has no binary
290
+ * encoder is rejected before any row is sent rather than mid-stream.
291
+ *
292
+ * `copyFrom()` remains the way to send an already-formatted text or CSV
293
+ * payload.
294
+ *
295
+ * A value the column's type cannot encode - `'abc'` for an integer
296
+ * column - aborts the copy by default, naming the row and column;
297
+ * `onInvalidValue` can instead null the value or drop the row, and the
298
+ * result says how many that happened to. NaN and Infinity in a float or
299
+ * numeric column are not invalid: PostgreSQL stores them as values
300
+ * distinct from NULL, so they go through untouched.
301
+ *
302
+ * @returns Rows sent, plus what a tolerant `onInvalidValue` swallowed.
303
+ */
304
+ /**
305
+ * Runs several different statements in one round trip: every
306
+ * Parse/Bind/Describe/Execute goes out before any response is waited
307
+ * for, and a single Sync closes the lot.
308
+ *
309
+ * ```ts
310
+ * const [renamed, , total] = await connection.pipeline([
311
+ * sql`update users set name = ${name} where id = ${id}`,
312
+ * sql`insert into audit(msg) values (${msg})`,
313
+ * sql`select count(*)::int as n from users`,
314
+ * ]);
315
+ * renamed.rowsAffected; // 1
316
+ * total.rows?.[0]; // [42]
317
+ * ```
318
+ *
319
+ * The counterpart to `PreparedStatement.executeBatch()`, which runs one
320
+ * statement over many parameter sets; this runs many statements once
321
+ * each. Twenty statements measured 2.3ms against 5.0ms for the same
322
+ * calls through `Promise.all()`, which PostgreJS already pipelines -
323
+ * what the single Sync removes is the server finishing an implicit
324
+ * transaction and answering ReadyForQuery twenty times over, which is
325
+ * why socket reads drop from twenty to one.
326
+ *
327
+ * Three things follow from that framing rather than from choices made
328
+ * above it:
329
+ *
330
+ * - **One transaction.** Unless an explicit transaction is already open,
331
+ * the statements commit or roll back together.
332
+ * - **A failing statement stops the rest.** PostgreSQL discards
333
+ * everything between an error and the Sync, so statements after a
334
+ * rejected one never run. The error carries `failedIndex`.
335
+ * - **No statement can see another's results.** They are all sent before
336
+ * any reply arrives, so anything conditional on an earlier result
337
+ * belongs in a separate call rather than here.
338
+ *
339
+ * Results map to statements by position. Statements that return rows get
340
+ * them decoded as `query()` would, `rowDecoder` included; `fetchCount`
341
+ * does not apply, since a portal suspended mid-pipeline would break that
342
+ * mapping.
343
+ *
344
+ * @param requests Statements to run, as `sql` tag output, plain SQL
345
+ * strings, or `{ sql, params }` objects.
346
+ * @param options Applied to every statement.
347
+ */
348
+ async pipeline(requests, options = {}) {
349
+ if (!Array.isArray(requests))
350
+ throw new TypeError('pipeline() requires an array of statements');
351
+ if (options.cursor)
352
+ throw new Error('pipeline() cannot return a cursor - every statement runs to ' +
353
+ 'completion under one Sync, so there is no portal left to fetch from');
354
+ if (!requests.length)
355
+ return [];
356
+ const normalized = requests.map(r => typeof r === 'string' ? { sql: r } : { sql: r.sql, params: r.params });
357
+ /* c8 ignore start */
358
+ if (this.listenerCount('debug')) {
359
+ this.emit('debug', {
360
+ location: 'Connection.pipeline',
361
+ connection: this,
362
+ message: `[${this.processID}] pipeline | ${normalized.length} statements`,
363
+ });
364
+ }
365
+ /* c8 ignore stop */
366
+ return await this._captureErrorStack(this._intlCon.executePipeline(normalized, options)).catch((e) => {
367
+ // Name the statement the server actually rejected, not the first
368
+ // one - the error is otherwise attributed to whichever SQL happens
369
+ // to be handy, which in a pipeline is the wrong statement.
370
+ const i = e.failedIndex;
371
+ throw this._handleError(e, i != null && normalized[i] ? normalized[i].sql : normalized[0].sql);
372
+ });
373
+ }
374
+ async copyFromRows(table, source, options) {
375
+ /* c8 ignore start */
376
+ if (this.listenerCount('debug')) {
377
+ this.emit('debug', {
378
+ location: 'Connection.copyFromRows',
379
+ connection: this,
380
+ message: `[${this.processID}] copyFromRows | ${table}`,
381
+ table,
382
+ });
383
+ }
384
+ /* c8 ignore stop */
385
+ return await this._captureErrorStack(this._intlCon.copyFromRows(table, source, options)).catch((e) => {
386
+ throw this._handleError(e, `COPY ${table} FROM STDIN (FORMAT binary)`);
387
+ });
388
+ }
254
389
  async copyFrom(sql) {
255
390
  /* c8 ignore start */
256
391
  if (this.listenerCount('debug')) {
@@ -344,6 +479,50 @@ export class Connection extends SafeEventEmitter {
344
479
  startTransaction() {
345
480
  return this._captureErrorStack(this._intlCon.startTransaction());
346
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
+ }
347
526
  /**
348
527
  * Commits current transaction.
349
528
  * @param immediate - Commits right away, ignoring how many nested
@@ -456,11 +635,17 @@ export class Connection extends SafeEventEmitter {
456
635
  const effectiveAutoCommit = options?.autoCommit != null
457
636
  ? options.autoCommit
458
637
  : this._intlCon.config.autoCommit;
638
+ // An open transaction takes this path too: queryCached() puts the
639
+ // rollbackOnError savepoint in the statement's own round trip rather
640
+ // than leaving it to the prepare()/execute()/close() route below. The
641
+ // one thing it cannot express is an explicit autoCommit:true while a
642
+ // transaction is open - "commit once this statement is done" - which
643
+ // stays with PreparedStatement's own wrapper.
459
644
  if (!options?.cursor &&
460
645
  effectiveAutoCommit !== false &&
461
- !this._intlCon.inTransaction) {
646
+ !(options?.autoCommit === true && this._intlCon.inTransaction)) {
462
647
  const params = options?.params?.map(prm => prm instanceof BindParam ? prm.value : prm);
463
- return await this._captureErrorStack(this._intlCon.queryOnce(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
648
+ return await this._captureErrorStack(this._intlCon.queryCached(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
464
649
  throw this._handleError(e, sql);
465
650
  });
466
651
  }
@@ -556,6 +741,32 @@ export class Connection extends SafeEventEmitter {
556
741
  * microtask tick on top of the one `promise`/`.catch()` already goes
557
742
  * through - paid on every single query this wraps, disabled or not.
558
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
+ }
559
770
  _captureErrorStack(promise, entry, asyncErrorHandling) {
560
771
  const enabled = asyncErrorHandling ?? this._intlCon.config.asyncErrorHandling ?? true;
561
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
  }