postgrejs 3.1.3 → 3.4.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 (80) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +79 -13
  3. package/connection/connection.d.ts +97 -0
  4. package/connection/connection.js +141 -2
  5. package/connection/cursor.d.ts +2 -1
  6. package/connection/cursor.js +6 -6
  7. package/connection/intl-connection.d.ts +113 -3
  8. package/connection/intl-connection.js +530 -45
  9. package/connection/prepared-statement.d.ts +64 -1
  10. package/connection/prepared-statement.js +97 -6
  11. package/data-types/bool-type.js +0 -1
  12. package/data-types/box-type.js +0 -1
  13. package/data-types/bytea-type.js +5 -3
  14. package/data-types/char-type.js +2 -2
  15. package/data-types/circle-type.js +0 -1
  16. package/data-types/date-type.js +1 -2
  17. package/data-types/float4-type.js +4 -5
  18. package/data-types/float8-type.js +4 -6
  19. package/data-types/int2-type.js +2 -2
  20. package/data-types/int2-vector-type.js +3 -2
  21. package/data-types/int4-type.js +2 -2
  22. package/data-types/int8-type.js +2 -2
  23. package/data-types/json-type.js +2 -2
  24. package/data-types/jsonb-type.js +6 -5
  25. package/data-types/lseg-type.js +0 -1
  26. package/data-types/numeric-type.js +9 -4
  27. package/data-types/oid-type.js +2 -2
  28. package/data-types/oid-vector-type.js +3 -2
  29. package/data-types/point-type.js +0 -1
  30. package/data-types/time-type.js +1 -2
  31. package/data-types/timestamp-type.js +13 -3
  32. package/data-types/timestamptz-type.js +27 -10
  33. package/data-types/uuid-type.js +1 -2
  34. package/data-types/varchar-type.js +2 -2
  35. package/index.d.ts +4 -0
  36. package/index.js +4 -0
  37. package/interfaces/batch-result.d.ts +42 -0
  38. package/interfaces/batch-result.js +1 -0
  39. package/interfaces/command-result.d.ts +3 -2
  40. package/interfaces/copy-from-rows-options.d.ts +58 -0
  41. package/interfaces/copy-from-rows-options.js +1 -0
  42. package/interfaces/data-type.d.ts +0 -1
  43. package/interfaces/database-connection-params.d.ts +25 -0
  44. package/interfaces/pipeline-request.d.ts +16 -0
  45. package/interfaces/pipeline-request.js +1 -0
  46. package/interfaces/query-options.d.ts +18 -0
  47. package/interfaces/script-execute-options.d.ts +12 -0
  48. package/package.json +18 -3
  49. package/protocol/backend.js +6 -6
  50. package/protocol/buffer-reader.d.ts +7 -12
  51. package/protocol/buffer-reader.js +11 -58
  52. package/protocol/database-error.d.ts +16 -0
  53. package/protocol/database-error.js +15 -0
  54. package/protocol/frontend.js +86 -96
  55. package/protocol/pg-socket.d.ts +82 -33
  56. package/protocol/pg-socket.js +143 -86
  57. package/protocol/pgoutput.js +2 -2
  58. package/protocol/smart-buffer.d.ts +11 -29
  59. package/protocol/smart-buffer.js +19 -162
  60. package/types.d.ts +13 -1
  61. package/util/assert-integer.d.ts +43 -0
  62. package/util/assert-integer.js +58 -0
  63. package/util/bigint-methods.d.ts +0 -1
  64. package/util/bigint-methods.js +0 -22
  65. package/util/copy-from-rows.d.ts +56 -0
  66. package/util/copy-from-rows.js +95 -0
  67. package/util/decode-binaryarray.d.ts +1 -1
  68. package/util/decode-binaryarray.js +7 -7
  69. package/util/encode-binaryarray.js +6 -6
  70. package/util/encode-copy-binary.d.ts +48 -0
  71. package/util/encode-copy-binary.js +112 -0
  72. package/util/fast-parsefloat.d.ts +15 -0
  73. package/util/fast-parsefloat.js +141 -0
  74. package/util/get-parsers.js +4 -8
  75. package/util/parse-datetime.js +7 -0
  76. package/util/parse-pg-timestamp.d.ts +31 -0
  77. package/util/parse-pg-timestamp.js +199 -0
  78. package/util/parse-row.js +120 -0
  79. package/util/row-decoder.d.ts +49 -0
  80. package/util/row-decoder.js +63 -0
package/CHANGELOG.md CHANGED
@@ -1,6 +1,84 @@
1
1
  ## Changelog
2
2
 
3
- ### [v3.1.3](https://github.com/panates/postgrejs/compare/v3.1.2...v3.1.3) -
3
+ ### [v3.4.0](https://github.com/panates/postgrejs/compare/v3.3.0...v3.4.0) -
4
+
5
+ - feat: add PreparedStatement.executeBatch() for multi-set execution
6
+
7
+ #### 🚀 New Features
8
+
9
+ - feat: add copyFromRows() for binary COPY bulk loading @Eray Hanoğlu
10
+ - feat: add Connection.pipeline() for many statements in one round trip @Eray Hanoğlu
11
+ - feat: reuse prepared statements for repeated queries @Eray Hanoğlu
12
+
13
+ #### 🪲 Fixes
14
+
15
+ - fix: refuse values that have no numeric reading instead of storing zero @Eray Hanoğlu
16
+
17
+ #### 📖 Documentation Changes
18
+
19
+ - docs: regenerate both benchmark reports from an interleaved run @Eray Hanoğlu
20
+ - docs: refresh the Node report and correct a stale asymmetry note @Eray Hanoğlu
21
+ - docs: record statement-level rollback in the README @Eray Hanoğlu
22
+ - docs: record the prepared statement cache in the README @Eray Hanoğlu
23
+ - docs: list pipeline() in the feature list and comparison table @Eray Hanoğlu
24
+ - docs: list binary COPY in the feature list and comparison table @Eray Hanoğlu
25
+ - docs: list batch execution in the feature list and comparison table @Eray Hanoğlu
26
+
27
+ #### ⚡️ Performance Improvments
28
+
29
+ - perf: carry the rollbackOnError savepoint in the statement's own round trip @Eray Hanoğlu
30
+ - perf: let the prepared statement cache serve queries inside a transaction @Eray Hanoğlu
31
+
32
+ #### 💬 General Changes
33
+
34
+ - bench: add Bulk Load scenarios for text and binary COPY @Eray Hanoğlu
35
+ - bench: add Unit of Work scenario for pipeline() @Eray Hanoğlu
36
+ - bench: stop measuring postgres.js on an unprepared path @Eray Hanoğlu
37
+ - bench: interleave repeats instead of running them back to back @Eray Hanoğlu
38
+ - ci: pin GITHUB_TOKEN to read-only in the test and qc workflows @Eray Hanoğlu
39
+
40
+ ### [v3.3.0](https://github.com/panates/postgrejs/compare/v3.2.0...v3.3.0) - 14 September 2026
41
+
42
+ #### 🚀 New Features
43
+
44
+ - feat: add first-class Bun support with dedicated benchmark suite @Eray Hanoğlu
45
+ - feat: add pluggable RowDecoder for custom row decoding @Eray Hanoğlu
46
+
47
+ #### 🪲 Fixes
48
+
49
+ - fix: return the actual instant when decoding timestamptz from binary @Eray Hanoğlu
50
+ - fix: point coverage badge at dev instead of the abandoned master branch @Eray Hanoğlu
51
+
52
+ #### 📖 Documentation Changes
53
+
54
+ - docs: refresh both benchmark reports on 3.3.0 @Eray Hanoğlu
55
+ - docs: document Bun support and the Bun benchmark report in the README @Eray Hanoğlu
56
+ - docs: cover the Bun benchmarks in the README's speed section too @Eray Hanoğlu
57
+ - docs: document RowDecoder's data-immutability contract @Eray Hanoğlu
58
+
59
+ #### ⚡️ Performance Improvments
60
+
61
+ - perf: decode timestamps from the wire bytes instead of a string @Eray Hanoğlu
62
+ - perf: build object rows from a compiled literal instead of per-column assignment @Eray Hanoğlu
63
+ - perf: decode float and numeric text without materialising a string @Eray Hanoğlu
64
+ - perf: decode PostgreSQL's own timestamp text shape without the engine parser @Eray Hanoğlu
65
+ - perf: write protocol messages immediately instead of deferring a tick @Eray Hanoğlu
66
+
67
+ #### 💬 General Changes
68
+
69
+ - bench: report near-ties as ties instead of ranking them @Eray Hanoğlu
70
+ - feat!: give decodeBinary the value's length, not a slice of it @Eray Hanoğlu
71
+ - bench: stop measuring type-declaration policy in the Extended Query scenarios @Eray Hanoğlu
72
+ - bench: stop one stalled repeat from making every result a tie @Eray Hanoğlu
73
+ - bench: document why concurrent Simple Query varies its literal @Eray Hanoğlu
74
+
75
+ ### [v3.2.0](https://github.com/panates/postgrejs/compare/v3.1.3...v3.2.0) - 12 September 2026
76
+
77
+ #### 🛠 Refactoring and Updates
78
+
79
+ - refactor: build SmartBuffer/BufferReader on top of flexy-buffer @Eray Hanoğlu
80
+
81
+ ### [v3.1.3](https://github.com/panates/postgrejs/compare/v3.1.2...v3.1.3) - 11 September 2026
4
82
 
5
83
  #### 🪲 Fixes
6
84
 
package/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  ## Why PostgreJS?
14
14
 
15
- **PostgreJS** is a PostgreSQL driver for Node.js built from the wire protocol up - no `libpq`, no native bindings,
15
+ **PostgreJS** is a PostgreSQL driver for Node.js and Bun built from the wire protocol up - no `libpq`, no native bindings,
16
16
  just TypeScript talking directly to PostgreSQL. That from-scratch design is also what makes it fast and light: every
17
17
  byte on the wire is handled by code written for exactly that purpose, with a binary-first protocol, shared buffers,
18
18
  and row/column decoding pipelines built to avoid unnecessary allocation, instead of generic string plumbing bolted
@@ -22,11 +22,14 @@ onto a client meant for text.
22
22
 
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
- (postgres.js) on identical workloads - PostgreJS opens a connection up to **3x faster** than postgres.js and
26
- **2x faster** than pg, pushes pooled queries through up to **6.6x faster** than postgres.js, and fetches large
27
- result sets nearly **5x faster** than pg. It's also the only one of the three with a complete binary wire protocol
28
- across every data type, rather than falling back to text for most of them. See
29
- [`doc/BENCHMARKS.md`](doc/BENCHMARKS.md) for the full methodology and every scenario.
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.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
+ type, rather than falling back to text for most of them. See [`doc/BENCHMARKS.md`](doc/BENCHMARKS.md) for the full
29
+ methodology and every scenario.
30
+
31
+ The same suite runs under Bun, where Bun's own built-in `Bun.sql` joins the comparison as a fourth client - see
32
+ [`doc/BENCHMARKS-bun.md`](doc/BENCHMARKS-bun.md).
30
33
 
31
34
  ### 🪶 Small Footprint
32
35
 
@@ -71,8 +74,17 @@ usage.
71
74
  resource management.
72
75
  - **Binary Wire Protocol:** Implements the full binary wire protocol for all PostgreSQL data types, ensuring robust and
73
76
  efficient data handling.
74
- - **Prepared Statements:** Named prepared statements for optimized query execution.
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.
75
85
  - **Cursors:** Features fast double-link cache cursors for efficient data retrieval.
86
+ - **Batch Execution:** `executeBatch()` runs one prepared statement over many parameter sets under a single `Sync`,
87
+ reporting each set's row count - 1000 updates in 20ms where the same calls pipelined individually take 188ms.
76
88
  - **Notifications:** High-level implementation for PostgreSQL notifications (LISTEN/NOTIFY), enabling real-time data
77
89
  updates.
78
90
  - **Extensibility:** Extensible data-types and type mapping to accommodate custom requirements.
@@ -81,8 +93,13 @@ usage.
81
93
  - **Performance Optimization:** Low memory utilization and boosted performance through the use of shared buffers.
82
94
  - **Authorization:** Supports various password algorithms including Clear text, MD5, and SASL, ensuring secure
83
95
  authentication.
84
- - **Bulk Import/Export:** `COPY TO STDOUT` and `COPY FROM STDIN` as Node streams, with backpressure in both directions.
96
+ - **Bulk Import/Export:** `COPY TO STDOUT` and `COPY FROM STDIN` as Node streams, with backpressure in both directions,
97
+ plus `copyFromRows()`, which encodes rows straight into binary `COPY` - around 4x faster than the CSV equivalent and
98
+ no text escaping to get wrong. Takes arrays or objects from anything iterable, so a file larger than memory streams in.
85
99
  - **Query Pipelining:** Pooled queries can share connections so a burst is not capped by pool size - opt-in per call.
100
+ `pipeline()` goes further for a known set of statements: several different ones travel under a single `Sync`, so they
101
+ 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.
86
103
  - **Dynamic SQL:** A `sql` tag builds statements from composable fragments - values become parameters, names are quoted,
87
104
  and `sql.values()`/`sql.set()` write INSERT and UPDATE clauses from objects.
88
105
  - **Multiple Hosts:** A connection can list several servers and pick one by role
@@ -111,7 +128,7 @@ usage.
111
128
 
112
129
  How PostgreJS compares to [`pg`](https://github.com/brianc/node-postgres) (node-postgres) and
113
130
  [`postgres`](https://github.com/porsager/postgres) (postgres.js). Every row was checked against the libraries' own
114
- source rather than their documentation — versions compared: **PostgreJS 3.1.0, pg 8.23.0, postgres.js 3.4.9**. ✅ built
131
+ source rather than their documentation — versions compared: **PostgreJS 3.4.0, pg 8.23.0, postgres.js 3.4.9**. ✅ built
115
132
  in · 🟡 partial or needs a separate package · ❌ not supported.
116
133
 
117
134
  | Feature | PostgreJS | pg | postgres.js |
@@ -143,10 +160,14 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
143
160
  | ***Querying*** | | | |
144
161
  | Query parameters | ✅ | ✅ | ✅ |
145
162
  | Parameter type casting | ✅ | 🟡 <sup>10</sup> | ✅ |
146
- | Prepared statements | ✅ explicit | ✅ | ✅ automatic |
163
+ | Prepared statements <sup>24</sup> | ✅ automatic | ✅ manual | ✅ automatic |
164
+ | Statement-level rollback <sup>25</sup> | ✅ automatic | ❌ | 🟡 manual |
165
+ | Batch execution <sup>21</sup> | ✅ | ❌ | ❌ |
166
+ | Multi-statement round trip <sup>23</sup> | ✅ | ❌ | ❌ |
147
167
  | Multi-statement scripts | ✅ | ✅ | ✅ |
148
168
  | Server-side cursors | ✅ | 🟡 <sup>11</sup> | ✅ |
149
169
  | `COPY TO` / `COPY FROM` | ✅ | 🟡 <sup>12</sup> | ✅ |
170
+ | Binary COPY encoding <sup>22</sup> | ✅ | ❌ | ❌ |
150
171
  | Row count after a COPY | ✅ | ✅ | ❌ |
151
172
  | ***Transaction management*** | | | |
152
173
  | Transaction API | ✅ | ❌ | ✅ |
@@ -212,6 +233,38 @@ in · 🟡 partial or needs a separate package · ❌ not supported.
212
233
  documentation lists what stops working with it: `pg-cursor`, `pg-query-stream` and
213
234
  `pg-copy-streams` all "operate directly on the binary stream and therefore are
214
235
  incompatible" - so server-side cursors, row streaming and COPY are what it costs.
236
+ - <sup>21</sup> One statement executed over many parameter sets behind a single `Sync`, each set's row count reported
237
+ separately. pg and postgres.js both emit a `Sync` per execution (`syncBuffer` in pg's `connection.js`, the
238
+ concatenated `ExecuteUnnamed` in postgres.js's), so a burst of executions costs a server round of implicit-transaction
239
+ bookkeeping each, pipelined or not. Writing one multi-row statement by hand is a separate approach that all three
240
+ support, and both PostgreJS and postgres.js ship value builders for it - it is faster still where it applies, but it
241
+ is one statement rather than many, and PostgreSQL's 65535-parameter ceiling bounds it.
242
+ - <sup>22</sup> Turning JavaScript rows into the binary `COPY` format, rather than carrying a payload the caller
243
+ formatted first. All three can carry a `COPY` stream, but only as bytes: postgres.js hands back a `Writable` that
244
+ wraps raw chunks in `CopyData`, and pg reaches the same point through `pg-copy-streams`. Producing the format needs a
245
+ binary encoder per type, which neither driver has - see Binary encoders above, where the same gap shows up for query
246
+ parameters. The capability is still reachable with pg through a third package, `pg-copy-streams-binary`, which brings
247
+ its own encoders; postgres.js has no equivalent.
248
+ - <sup>23</sup> Several different statements sent under one `Sync`, so they cost a single round of the server's
249
+ transaction bookkeeping rather than one each, arrive as one implicit transaction, and come back as per-statement
250
+ results. Distinct from the Pipelining row above, which is about not waiting between queries: all three do that, and
251
+ all three still close every statement with its own `Sync` - pg sends one immediately after each Execute
252
+ (`query.js`), and postgres.js concatenates Execute and Sync into a single constant (`ExecuteUnnamed`). Atomicity on
253
+ its own is reachable anywhere with an explicit `BEGIN`/`COMMIT`; what that cannot recover is the round trip, since
254
+ the per-statement Syncs and two extra statements remain.
255
+ - <sup>24</sup> Whether a repeated query is parsed again every time. PostgreJS and postgres.js both keep a per-connection
256
+ cache keyed on the SQL and reuse a server-side statement, so a repeat costs `Bind`/`Execute` rather than
257
+ `Parse`/`Bind`/`Describe`/`Execute`; pg prepares only the statements you name yourself (`query.js`: "named queries
258
+ must always be prepared"), with nothing caching by SQL text. The two automatic ones differ in when they start:
259
+ postgres.js on first sight, PostgreJS on the second use, which leaves a genuinely one-shot query at its unprepared
260
+ cost. Both default to on and both take `prepare: false`, which matters for PgBouncer in transaction pooling mode
261
+ before 1.21, where a named statement does not survive to the next call.
262
+ - <sup>25</sup> Whether one failed statement inside a transaction leaves the rest of the block runnable. PostgreJS puts
263
+ every statement under a savepoint of its own by default (`rollbackOnError`), rolling back to it when the statement
264
+ fails, and carries the `SAVEPOINT`/`RELEASE` pair inside the statement's own round trip rather than paying a round
265
+ trip for each. postgres.js has savepoints, but as a scope the caller opens around a callback (`savepoint(name, fn)`
266
+ in `src/index.js`), so a statement is protected only where someone wrapped it; pg has no savepoint handling in
267
+ `lib/` at all, so a failed statement leaves the block aborted until the caller rolls back themselves.
215
268
 
216
269
 
217
270
  ## Benchmarks
@@ -223,14 +276,27 @@ concurrency, each library run through its own idiomatic fast path. The numbers t
223
276
  machine via `npm run bench` against the repo's own
224
277
  `docker/docker-compose.yml` Postgres instance; see [`benchmark/README.md`](./benchmark/README.md) for details.
225
278
 
279
+ The same suite runs under Bun via `npm run bench:bun`, where it also measures Bun's own built-in `Bun.sql` client as
280
+ a fourth library. Those results live in [`doc/BENCHMARKS-bun.md`](doc/BENCHMARKS-bun.md) and are deliberately kept
281
+ in a separate report rather than merged into the Node one: GC instrumentation, cursor streaming and binary-format
282
+ control all differ enough between the two runtimes that a single table would conflate a library difference with a
283
+ runtime difference.
284
+
285
+ Both reports list libraries in a fixed order rather than fastest-first, and mark every result statistically tied
286
+ with the leader rather than bolding a lone winner - several scenarios separate the leading drivers by about a
287
+ percent, which is less than the run-to-run spread of the measurement itself.
288
+
226
289
  ## Support
227
290
 
228
291
  You can report bugs and discuss features on the [GitHub issues](https://github.com/panates/postgrejs/issues) page When
229
292
  you open an issue please provide version of NodeJS and PostgreSQL server.
230
293
 
231
- ## Node Compatibility
294
+ ## Runtime Compatibility
232
295
 
233
- - node >= 20.x
296
+ - Node.js >= 20.x
297
+ - Bun - the same test suite runs under Bun in CI against PostgreSQL 18 on every push, so Bun is a supported target
298
+ rather than an untested coincidence. (Coverage is collected on the Node matrix only: `c8` instruments V8's
299
+ coverage APIs, which JavaScriptCore doesn't have.)
234
300
 
235
301
  ## License
236
302
 
@@ -242,5 +308,5 @@ PostgreJS is available under the [BSD 3-Clause](LICENSE) license.
242
308
  [downloads-url]: https://npmjs.org/package/postgrejs
243
309
  [ci-test-image]: https://github.com/panates/postgrejs/actions/workflows/test.yml/badge.svg
244
310
  [ci-test-url]: https://github.com/panates/postgrejs/actions/workflows/test.yml
245
- [coveralls-image]: https://img.shields.io/coveralls/panates/postgrejs/master.svg
311
+ [coveralls-image]: https://img.shields.io/coveralls/panates/postgrejs/dev.svg
246
312
  [coveralls-url]: https://coveralls.io/r/panates/postgrejs
@@ -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
@@ -251,6 +251,139 @@ export class Connection extends SafeEventEmitter {
251
251
  *
252
252
  * @param sql {string} - A COPY ... FROM STDIN statement
253
253
  */
254
+ /**
255
+ * Bulk-loads rows into a table with `COPY ... FROM STDIN (FORMAT binary)`,
256
+ * encoding every value with its own type's binary encoder instead of
257
+ * making the caller format a text payload.
258
+ *
259
+ * ```ts
260
+ * const { rowCount } = await connection.copyFromRows('users', [
261
+ * [1, 'John', 10.5],
262
+ * [2, 'Jane', 20.0],
263
+ * ], { columns: ['id', 'name', 'amount'] });
264
+ * ```
265
+ *
266
+ * Rows may be positional arrays or objects read by column name, and the
267
+ * source may be anything iterable - including an async iterable or a
268
+ * Readable, so a file far larger than memory streams straight in:
269
+ *
270
+ * ```ts
271
+ * await connection.copyFromRows('users', (async function* () {
272
+ * for await (const { value } of jsonRows) yield [value.id, value.name];
273
+ * })(), { columns: ['id', 'name'] });
274
+ * ```
275
+ *
276
+ * Rows are pulled rather than pushed, so the source only produces the
277
+ * next row once the previous chunk has reached the socket - backpressure
278
+ * is the loop pausing, not a queue growing.
279
+ *
280
+ * Binary is both faster and cheaper to produce than text: 200,000 rows of
281
+ * int4/text/float8/timestamptz measured 232ms against 483ms for the
282
+ * equivalent CSV `copyFrom()`, client-side encoding included, because
283
+ * writing an int32 costs less than formatting a decimal string.
284
+ *
285
+ * Column types are read from the server (one Describe round trip) unless
286
+ * `columnTypes` supplies them. They have to be exact - binary COPY does
287
+ * no conversion - which is also why a column whose type has no binary
288
+ * encoder is rejected before any row is sent rather than mid-stream.
289
+ *
290
+ * `copyFrom()` remains the way to send an already-formatted text or CSV
291
+ * payload.
292
+ *
293
+ * A value the column's type cannot encode - `'abc'` for an integer
294
+ * column - aborts the copy by default, naming the row and column;
295
+ * `onInvalidValue` can instead null the value or drop the row, and the
296
+ * result says how many that happened to. NaN and Infinity in a float or
297
+ * numeric column are not invalid: PostgreSQL stores them as values
298
+ * distinct from NULL, so they go through untouched.
299
+ *
300
+ * @returns Rows sent, plus what a tolerant `onInvalidValue` swallowed.
301
+ */
302
+ /**
303
+ * Runs several different statements in one round trip: every
304
+ * Parse/Bind/Describe/Execute goes out before any response is waited
305
+ * for, and a single Sync closes the lot.
306
+ *
307
+ * ```ts
308
+ * const [renamed, , total] = await connection.pipeline([
309
+ * sql`update users set name = ${name} where id = ${id}`,
310
+ * sql`insert into audit(msg) values (${msg})`,
311
+ * sql`select count(*)::int as n from users`,
312
+ * ]);
313
+ * renamed.rowsAffected; // 1
314
+ * total.rows?.[0]; // [42]
315
+ * ```
316
+ *
317
+ * The counterpart to `PreparedStatement.executeBatch()`, which runs one
318
+ * statement over many parameter sets; this runs many statements once
319
+ * each. Twenty statements measured 2.3ms against 5.0ms for the same
320
+ * calls through `Promise.all()`, which PostgreJS already pipelines -
321
+ * what the single Sync removes is the server finishing an implicit
322
+ * transaction and answering ReadyForQuery twenty times over, which is
323
+ * why socket reads drop from twenty to one.
324
+ *
325
+ * Three things follow from that framing rather than from choices made
326
+ * above it:
327
+ *
328
+ * - **One transaction.** Unless an explicit transaction is already open,
329
+ * the statements commit or roll back together.
330
+ * - **A failing statement stops the rest.** PostgreSQL discards
331
+ * everything between an error and the Sync, so statements after a
332
+ * rejected one never run. The error carries `failedIndex`.
333
+ * - **No statement can see another's results.** They are all sent before
334
+ * any reply arrives, so anything conditional on an earlier result
335
+ * belongs in a separate call rather than here.
336
+ *
337
+ * Results map to statements by position. Statements that return rows get
338
+ * them decoded as `query()` would, `rowDecoder` included; `fetchCount`
339
+ * does not apply, since a portal suspended mid-pipeline would break that
340
+ * mapping.
341
+ *
342
+ * @param requests Statements to run, as `sql` tag output, plain SQL
343
+ * strings, or `{ sql, params }` objects.
344
+ * @param options Applied to every statement.
345
+ */
346
+ async pipeline(requests, options = {}) {
347
+ if (!Array.isArray(requests))
348
+ throw new TypeError('pipeline() requires an array of statements');
349
+ if (options.cursor)
350
+ throw new Error('pipeline() cannot return a cursor - every statement runs to ' +
351
+ 'completion under one Sync, so there is no portal left to fetch from');
352
+ if (!requests.length)
353
+ return [];
354
+ const normalized = requests.map(r => typeof r === 'string' ? { sql: r } : { sql: r.sql, params: r.params });
355
+ /* c8 ignore start */
356
+ if (this.listenerCount('debug')) {
357
+ this.emit('debug', {
358
+ location: 'Connection.pipeline',
359
+ connection: this,
360
+ message: `[${this.processID}] pipeline | ${normalized.length} statements`,
361
+ });
362
+ }
363
+ /* c8 ignore stop */
364
+ return await this._captureErrorStack(this._intlCon.executePipeline(normalized, options)).catch((e) => {
365
+ // Name the statement the server actually rejected, not the first
366
+ // one - the error is otherwise attributed to whichever SQL happens
367
+ // to be handy, which in a pipeline is the wrong statement.
368
+ const i = e.failedIndex;
369
+ throw this._handleError(e, i != null && normalized[i] ? normalized[i].sql : normalized[0].sql);
370
+ });
371
+ }
372
+ async copyFromRows(table, source, options) {
373
+ /* c8 ignore start */
374
+ if (this.listenerCount('debug')) {
375
+ this.emit('debug', {
376
+ location: 'Connection.copyFromRows',
377
+ connection: this,
378
+ message: `[${this.processID}] copyFromRows | ${table}`,
379
+ table,
380
+ });
381
+ }
382
+ /* c8 ignore stop */
383
+ return await this._captureErrorStack(this._intlCon.copyFromRows(table, source, options)).catch((e) => {
384
+ throw this._handleError(e, `COPY ${table} FROM STDIN (FORMAT binary)`);
385
+ });
386
+ }
254
387
  async copyFrom(sql) {
255
388
  /* c8 ignore start */
256
389
  if (this.listenerCount('debug')) {
@@ -456,11 +589,17 @@ export class Connection extends SafeEventEmitter {
456
589
  const effectiveAutoCommit = options?.autoCommit != null
457
590
  ? options.autoCommit
458
591
  : this._intlCon.config.autoCommit;
592
+ // An open transaction takes this path too: queryCached() puts the
593
+ // rollbackOnError savepoint in the statement's own round trip rather
594
+ // than leaving it to the prepare()/execute()/close() route below. The
595
+ // one thing it cannot express is an explicit autoCommit:true while a
596
+ // transaction is open - "commit once this statement is done" - which
597
+ // stays with PreparedStatement's own wrapper.
459
598
  if (!options?.cursor &&
460
599
  effectiveAutoCommit !== false &&
461
- !this._intlCon.inTransaction) {
600
+ !(options?.autoCommit === true && this._intlCon.inTransaction)) {
462
601
  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) => {
602
+ return await this._captureErrorStack(this._intlCon.queryCached(sql, paramTypes, params, options || {}), this.query, options?.asyncErrorHandling).catch((e) => {
464
603
  throw this._handleError(e, sql);
465
604
  });
466
605
  }
@@ -9,12 +9,13 @@ export declare class Cursor extends SafeEventEmitter implements AsyncDisposable
9
9
  private readonly _portal;
10
10
  private readonly _parsers;
11
11
  private readonly _queryOptions;
12
+ private readonly _rowDecoder;
12
13
  private _taskQueue;
13
14
  private _rows;
14
15
  private _closed;
15
16
  readonly fields: FieldInfo[];
16
17
  constructor(statement: PreparedStatement, portal: Portal, fields: FieldInfo[], parsers: AnyParseFunction[], queryOptions: QueryOptions);
17
- get rowType(): 'array' | 'object';
18
+ get rowType(): 'array' | 'object' | 'custom';
18
19
  get isClosed(): boolean;
19
20
  next(): Promise<Maybe<Row>>;
20
21
  fetch(nRows: number): Promise<Row[]>;
@@ -1,12 +1,13 @@
1
1
  import DoublyLinked from 'doublylinked';
2
2
  import { TaskQueue } from 'power-tasks';
3
3
  import { SafeEventEmitter } from '../safe-event-emitter.js';
4
- import { parseObjectRow, parseRow } from '../util/parse-row.js';
4
+ import { resolveRowDecoder, resolveRowType } from '../util/row-decoder.js';
5
5
  export class Cursor extends SafeEventEmitter {
6
6
  _statement;
7
7
  _portal;
8
8
  _parsers;
9
9
  _queryOptions;
10
+ _rowDecoder;
10
11
  _taskQueue = new TaskQueue({ concurrency: 1 });
11
12
  _rows = new DoublyLinked();
12
13
  _closed = false;
@@ -18,9 +19,10 @@ export class Cursor extends SafeEventEmitter {
18
19
  this._parsers = parsers;
19
20
  this._queryOptions = queryOptions;
20
21
  this.fields = fields;
22
+ this._rowDecoder = resolveRowDecoder(queryOptions);
21
23
  }
22
24
  get rowType() {
23
- return this._queryOptions.objectRows ? 'object' : 'array';
25
+ return resolveRowType(this._queryOptions);
24
26
  }
25
27
  get isClosed() {
26
28
  return this._closed;
@@ -71,16 +73,14 @@ export class Cursor extends SafeEventEmitter {
71
73
  if (r && r.rows && r.rows.length) {
72
74
  const rows = r.rows;
73
75
  if (this._parsers) {
74
- const objectRows = queryOptions.objectRows;
75
76
  const fields = this.fields;
76
77
  const parsers = this._parsers;
78
+ const rowDecoder = this._rowDecoder;
77
79
  const rowLen = rows.length;
78
80
  let i;
79
81
  for (i = 0; i < rowLen; i++) {
80
82
  const { data, columnCount } = rows[i];
81
- rows[i] = objectRows
82
- ? parseObjectRow(parsers, data, columnCount, this._queryOptions, fields)
83
- : parseRow(parsers, data, columnCount, this._queryOptions);
83
+ rows[i] = rowDecoder.decode(parsers, data, columnCount, this._queryOptions, fields);
84
84
  }
85
85
  }
86
86
  this._rows.push(...rows);