@steadybrew/postgresjs 4.4.1 → 4.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/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # Postgres.js by Steady Brew
2
2
 
3
- This is an independently maintained fork of [Postgres.js](https://github.com/porsager/postgres), the PostgreSQL client for Node.js and Deno created by [Rasmus Porsager](https://github.com/porsager). [Steady Brew](https://github.com/steadybrew) aims to fix bugs and improve the library through community contributions, regression tests, and careful architectural review, while preserving its simple API.
3
+ A maintained fork of [Postgres.js](https://github.com/porsager/postgres), the PostgreSQL client created by [Rasmus Porsager](https://github.com/porsager), with the same API.
4
+
5
+ - **Fixes reported Postgres.js bugs:** 40 open upstream issues, from queries receiving another query's rows to hung pools and crashed processes, each covered by a regression test. See [the list](#fixed-postgresjs-issues).
6
+ - **Built for serverless and connection poolers:** bounded connection retries with backoff, idle connections replaced when a frozen instance resumes, pool statistics from `sql.stats()`, and an adapter for Vercel Fluid. Query timeouts and cancellable pool waits are next on the [roadmap](ROADMAP.md).
7
+ - **Switch with one line:** your code and the libraries that import `postgres` keep working. See [Switching from Postgres.js](#switching-from-postgresjs).
4
8
 
5
9
  ---
6
10
 
@@ -13,33 +17,142 @@ This is an independently maintained fork of [Postgres.js](https://github.com/por
13
17
 
14
18
  <br clear="all">
15
19
 
16
- ## Compatibility policy
20
+ ## Compatibility
21
+
22
+ [![test](https://github.com/steadybrew/postgresjs/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/steadybrew/postgresjs/actions/workflows/test.yml?query=branch%3Amain)
23
+
24
+ **Version 4 runs on Node.js 22 or newer** and supports PostgreSQL 15–18. Every release passes the full test matrix before it is published.
25
+
26
+ | Runtime | PostgreSQL | Notes |
27
+ | --- | --- | --- |
28
+ | Node 22, 24 and 26, ESM and CJS | 15–18 | |
29
+ | Deno 1.46.3 | 17 | No Deno 2 claim |
30
+ | Cloudflare Workers (workerd) | 17 | A "Stream was cancelled" rejection on socket close remains ([#1196](https://github.com/porsager/postgres/issues/1196), [#1202](https://github.com/porsager/postgres/issues/1202)) |
31
+ | Bun | | Best effort, not tested in CI |
32
+
33
+ [The test workflow](https://github.com/steadybrew/postgresjs/blob/main/.github/workflows/test.yml) defines which pairs run on each push. Supabase PostgreSQL and Supabase Edge are separate environments; Deno 1.46.3 coverage does not imply Deno 2 or hosted Supabase Edge.
34
+
35
+ ## Switching from Postgres.js
36
+
37
+ ```sh
38
+ npm install postgres@npm:@steadybrew/postgresjs@^4
39
+ ```
40
+
41
+ This installs the fork under the name `postgres`, so `import postgres from 'postgres'` stays as it is, and so do libraries that import it, such as `drizzle-orm/postgres-js`, `drizzle-kit` and `kysely-postgres-js`. Then read [Upgrading to 4.0.0](#upgrading-to-400) for the behaviour changes.
17
42
 
18
- **Version 4 requires Node.js 24 or newer** and supports PostgreSQL 15–18. Version 4.0.0 raised the minimum Node.js version from 12. The minimum stays at Node 24 until an explicit policy change; newly maintained Node majors are tested before compatibility is claimed.
43
+ Tracing: from 4.5.0, dd-trace records the same database spans as with Postgres.js when the fork is installed under the `postgres` name. Sentry's automatic integration instruments `postgres` versions below 4 only, so widen its range before Sentry loads:
19
44
 
20
- | Runtime | PostgreSQL target | CI jobs | Validation status |
21
- | --- | --- | ---: | --- |
22
- | Node 24, ESM and CJS | 15, 16, 17, 18 | 4 | Passed release-candidate CI |
23
- | Node 26, ESM and CJS | 15, 16, 17, 18 | 4 | Passed release-candidate CI |
24
- | Deno 1.46.3 | 17 | 1 | Passed release-candidate CI; no Deno 2 claim |
25
- | Cloudflare workerd, Wrangler 4.123.0 | 17 | 1 | Passed release-candidate CI; known "Stream was cancelled" rejection remains |
26
- | Bun export | No dedicated target | 0 | Retained as best effort; unverified, without dedicated CI |
45
+ ```js
46
+ // sentry-postgres.mjs
47
+ import { SENTRY_RUNTIME_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config'
27
48
 
28
- All ten [CI jobs](https://github.com/steadybrew/postgresjs/actions/runs/37943516565) passed for 4.1.0. The core Node matrix has eight jobs; Deno and Cloudflare run independently. PostgreSQL versions below 15 are outside the supported matrix. See the [release notes](./CHANGELOG.md#v410---2026-10-09) for validation details and [known limitations](./CHANGELOG.md#known-limitations), including the Cloudflare cancellation rejection tolerated by its CI gate.
49
+ for (const instrumentation of SENTRY_RUNTIME_INSTRUMENTATIONS) {
50
+ if (instrumentation.module.name === 'postgres')
51
+ instrumentation.module.versionRange = '>=3.0.0 <5'
52
+ }
53
+ ```
54
+
55
+ ```sh
56
+ node --import ./sentry-postgres.mjs --import @sentry/node/import app.js
57
+ ```
29
58
 
30
- Supabase PostgreSQL and Supabase Edge are separate environments. Deno 1.46.3 coverage does not imply Deno 2 or hosted Supabase Edge certification.
59
+ This covers Sentry's runtime hook, not its bundler plugins. With pnpm, add `@sentry/server-utils` at the same version as `@sentry/node` so the file can import it.
31
60
 
32
61
  ## Upgrading to 4.0.0
33
62
 
34
- Version 4.0.0 fixes connection startup, reservations, reconnects, shutdown, transaction ownership, and built-in array handling. When upgrading from `@steadybrew/postgresjs` 3.x:
63
+ Version 4.0.0 fixes connection startup, reservations, reconnects, shutdown, transaction ownership, and built-in array handling. When upgrading from Postgres.js 3.4 or `@steadybrew/postgresjs` 3.x:
35
64
 
36
- - Upgrade to **Node.js 24 or newer** before installing.
65
+ - Upgrade to **Node.js 22 or newer** before installing.
37
66
  - Keep queries within the reservation or transaction that owns them. Reserved handles used after `release()`, and transaction handles used after completion, reject with `CONNECTION_ENDED`. A handle whose connection has closed cannot run queries on a reconnected session.
38
67
  - Pass a non-empty string without NUL characters to transaction `sql.prepare(name)`. Invalid names throw `INVALID_TRANSACTION_NAME`.
39
68
  - If you use `fetch_types: false`, review code that expects built-in arrays as raw strings: supported built-in arrays are now decoded as JavaScript arrays. SQL `NULL` elements become JavaScript `null`; quoted `"NULL"` text remains a string.
40
69
 
41
70
  See the [changelog](./CHANGELOG.md#breaking-changes-and-migration) for the full migration notes and fixes.
42
71
 
72
+ ## Fixed Postgres.js issues
73
+
74
+ Bugs and feature requests from [porsager/postgres](https://github.com/porsager/postgres) that this fork fixes, whether reported in an issue or in a pull request suggesting a fix, and the version that fixed them. Each fix has a test that reproduces the report, except [#1100](https://github.com/porsager/postgres/issues/1100), which rests on a production measurement, and [#1154](https://github.com/porsager/postgres/issues/1154), whose test reproduces the reported stack without a known trigger. The [changelog](./CHANGELOG.md) has the details.
75
+
76
+ <details>
77
+ <summary>54 bugs and 7 feature requests</summary>
78
+
79
+ **Wrong results and data safety**
80
+
81
+ | Issue | What happens now | Since |
82
+ | --- | --- | --- |
83
+ | [#1082](https://github.com/porsager/postgres/issues/1082), [#729](https://github.com/porsager/postgres/issues/729) | A query whose values fail to serialize rejects on its own; the query running alongside no longer gets its error, and the next query no longer gets another query's rows | 4.3.0 |
84
+ | [#1216](https://github.com/porsager/postgres/issues/1216) | A transaction handle whose connection closed rejects with `CONNECTION_CLOSED` instead of running inside the next user's transaction, so it can't read their rows through row-level security. | 4.0.0 |
85
+ | [#1248](https://github.com/porsager/postgres/issues/1248) | Statements in a `sql.begin()` callback after its connection closes reject instead of running outside the transaction | 4.0.0 |
86
+ | [#1204](https://github.com/porsager/postgres/issues/1204) | A transaction's connection is not handed to another `sql.begin()` when its socket drains late, so one transaction's `COMMIT` can't commit another's unfinished writes. | 4.0.0 |
87
+ | [#1217](https://github.com/porsager/postgres/issues/1217) | `sql.begin()` rejects with `TRANSACTION_ROLLED_BACK` when the server rolled back the COMMIT | 4.4.0 |
88
+ | [#1249](https://github.com/porsager/postgres/issues/1249), [#1226](https://github.com/porsager/postgres/issues/1226), [#1223](https://github.com/porsager/postgres/issues/1223) | The first query after a reconnect no longer rejects with an error from the closed session | 4.0.0 |
89
+ | [#789](https://github.com/porsager/postgres/issues/789) | `sql.array()` infers the right array type on first use | 4.0.0 |
90
+ | [#1049](https://github.com/porsager/postgres/issues/1049), [#1124](https://github.com/porsager/postgres/issues/1124) | `NULL` elements in arrays parse as `null`, not `NaN` or the string `"NULL"` | 4.0.0 |
91
+ | [#1252](https://github.com/porsager/postgres/issues/1252) | Cancelling a pipelined query that hasn't started no longer gives the next query its result | 4.1.0 |
92
+ | [#1253](https://github.com/porsager/postgres/issues/1253), [#1181](https://github.com/porsager/postgres/issues/1181), [#1119](https://github.com/porsager/postgres/issues/1119) | A query no longer gets empty slots at the start of its result after the previous query on the connection failed or was cancelled partway through its rows | 4.5.0 |
93
+
94
+ **Hangs**
95
+
96
+ | Issue | What happens now | Since |
97
+ | --- | --- | --- |
98
+ | [#751](https://github.com/porsager/postgres/issues/751), [#1219](https://github.com/porsager/postgres/issues/1219), [#1203](https://github.com/porsager/postgres/issues/1203) | `sql.reserve()` on a pool that hasn't opened a connection yet resolves, with or without `fetch_types` and on Cloudflare Workers | 4.0.0 |
99
+ | [#1195](https://github.com/porsager/postgres/issues/1195), [#1199](https://github.com/porsager/postgres/issues/1199) | A reservation survives its connection being terminated by the server, and the pool recovers after it is released | 4.0.0 |
100
+ | [#1186](https://github.com/porsager/postgres/issues/1186) | Queries queued on a reserved or transaction handle settle when its connection closes | 4.0.0 |
101
+ | [#1097](https://github.com/porsager/postgres/issues/1097), [#1130](https://github.com/porsager/postgres/issues/1130), [#1242](https://github.com/porsager/postgres/issues/1242), [#861](https://github.com/porsager/postgres/issues/861) | `sql.end()` resolves after a disconnect, after a failed query and after a connection closes inside `sql.begin()`, and waits for queries queued in the pool | 4.0.0 |
102
+ | [#1246](https://github.com/porsager/postgres/issues/1246) | A query sent between a socket error and its close settles, and a transaction reset mid-statement no longer loses a pool connection | 4.1.0 |
103
+ | [#1193](https://github.com/porsager/postgres/issues/1193) | A connection closed during startup settles its first query instead of reconnecting in a tight loop | 4.0.0 |
104
+ | [#1234](https://github.com/porsager/postgres/issues/1234) | Concurrent queries in `sql.begin()` after a column type change roll the transaction back and return the connection to the pool | 4.4.0 |
105
+ | [#1255](https://github.com/porsager/postgres/issues/1255), [#1261](https://github.com/porsager/postgres/issues/1261) | A reserved connection whose `max_lifetime` passed is closed when released, and `sql.end()` called before the release resolves | 4.0.0 |
106
+ | [#1016](https://github.com/porsager/postgres/pull/1016) | Queries no longer hang after a `COPY ... TO STDOUT` whose reader fell behind | 4.5.0 |
107
+
108
+ **Crashes**
109
+
110
+ | Issue | What happens now | Since |
111
+ | --- | --- | --- |
112
+ | [#1154](https://github.com/porsager/postgres/issues/1154), [#1208](https://github.com/porsager/postgres/issues/1208), [#1133](https://github.com/porsager/postgres/issues/1133) | A reserved or transaction connection that the server or network closes rejects its later statements, and `sql.begin()`'s own `ROLLBACK` or `COMMIT`, with `CONNECTION_CLOSED` instead of crashing the process with `Cannot read properties of null (reading 'write')`. | 4.0.0 |
113
+ | [#1192](https://github.com/porsager/postgres/issues/1192), [#1205](https://github.com/porsager/postgres/issues/1205), [#1086](https://github.com/porsager/postgres/issues/1086) | A failed or cancelled type query at startup rejects the query instead of crashing the process | 4.0.0 |
114
+ | [#1088](https://github.com/porsager/postgres/issues/1088) | Timers no longer fail on Cloudflare Workers | 4.0.0 |
115
+ | [#1227](https://github.com/porsager/postgres/issues/1227) | Assigning `err.stack` on a query error no longer throws on Node 22 and newer | 4.5.0 |
116
+ | [#1009](https://github.com/porsager/postgres/pull/1009) | The `camel` and `pascal` transforms no longer throw on a column name ending with an underscore | 4.5.0 |
117
+
118
+ **Reconnects and failover**
119
+
120
+ | Issue | What happens now | Since |
121
+ | --- | --- | --- |
122
+ | [#1179](https://github.com/porsager/postgres/issues/1179) | Failed connection attempts back off instead of the whole pool retrying at once | 4.0.0 |
123
+ | [#1250](https://github.com/porsager/postgres/issues/1250) | A connection closed with a statement still buffered reconnects, instead of failing every later statement with `CONNECT_TIMEOUT` | 4.0.0 |
124
+ | [#1174](https://github.com/porsager/postgres/issues/1174), [#815](https://github.com/porsager/postgres/issues/815), [#988](https://github.com/porsager/postgres/issues/988) | With several hosts, retries move to the next host, a connect timeout fails over, and `prefer-standby` falls back to a primary | 4.0.0 |
125
+
126
+ **Transactions, notices and subscriptions**
127
+
128
+ | Issue | What happens now | Since |
129
+ | --- | --- | --- |
130
+ | [#823](https://github.com/porsager/postgres/issues/823), [#1189](https://github.com/porsager/postgres/issues/1189), [#1210](https://github.com/porsager/postgres/issues/1210) | `sql.begin()` no longer fails with `UNSAFE_TRANSACTION` or sends a second `BEGIN` on a busy connection when transactions and queries run concurrently, at the pipeline limit, under write backpressure or with `max_pipeline: 0` | 4.0.0 |
131
+ | [#1063](https://github.com/porsager/postgres/issues/1063) | `onnotice: false` silences notices | 4.4.0 |
132
+ | [#1087](https://github.com/porsager/postgres/issues/1087) | `sql.subscribe()` no longer adds a listener on each subscribe or loses changes after a transform throws | 4.4.0 |
133
+
134
+ **TypeScript**
135
+
136
+ | Issue | What happens now | Since |
137
+ | --- | --- | --- |
138
+ | [#1158](https://github.com/porsager/postgres/issues/1158), [#1177](https://github.com/porsager/postgres/pull/1177) | `sslnegotiation` is in the option types | 4.5.0 |
139
+ | [#968](https://github.com/porsager/postgres/pull/968) | `socket` is in the option types | 4.5.0 |
140
+ | [#703](https://github.com/porsager/postgres/issues/703), [#1194](https://github.com/porsager/postgres/pull/1194) | `new PostgresError('message')` sets the message, as with `Error`, and the types declare both the message and the fields forms | 4.5.0 |
141
+
142
+ **Feature requests**
143
+
144
+ | Issue | What you get | Since |
145
+ | --- | --- | --- |
146
+ | [#443](https://github.com/porsager/postgres/issues/443), [#908](https://github.com/porsager/postgres/issues/908) | `sql.stats()` reports connections in use and idle, and queries waiting | 4.3.0 |
147
+ | [#1100](https://github.com/porsager/postgres/issues/1100) | An `onidle` callback and the `@steadybrew/postgresjs/vercel` adapter | 4.2.0 |
148
+ | [#1164](https://github.com/porsager/postgres/issues/1164) | Built-in array types are decoded, and plain JavaScript arrays are sent, with `fetch_types: false` | 4.0.0 |
149
+ | [#886](https://github.com/porsager/postgres/issues/886), [#1094](https://github.com/porsager/postgres/issues/1094) | `await using` ends a pool and releases a reserved connection when it goes out of scope | 4.5.0 |
150
+ | [#1225](https://github.com/porsager/postgres/issues/1225) | Queries with values in one round trip instead of two, opt-in with `parameter_types: 'client'` | 4.5.0 |
151
+
152
+ Bun isn't tested here, so Bun-only reports such as [#1066](https://github.com/porsager/postgres/issues/1066) aren't listed; the same crash on Node is [#1208](https://github.com/porsager/postgres/issues/1208).
153
+
154
+ </details>
155
+
43
156
  ## Getting started
44
157
 
45
158
  <br>
@@ -113,6 +226,7 @@ const { default: postgres } = await import('@steadybrew/postgresjs')
113
226
  * [Listen & notify](#listen--notify)
114
227
  * [Realtime subscribe](#realtime-subscribe)
115
228
  * [Numbers, bigint, numeric](#numbers-bigint-numeric)
229
+ * [Parameter types](#parameter-types)
116
230
  * [Result Array](#result-array)
117
231
  * [Connection details](#connection-details)
118
232
  * [Custom Types](#custom-types)
@@ -151,7 +265,7 @@ Postgres.js utilizes [Tagged template functions](https://developer.mozilla.org/e
151
265
  1. **Enforcing** safe query generation
152
266
  2. Giving the ` sql`` ` function powerful [utility](#dynamic-inserts) and [query building](#building-queries) features.
153
267
 
154
- Any generic value will be serialized according to an inferred type, and replaced by a PostgreSQL protocol placeholder `$1, $2, ...`. The parameters are then sent separately to the database which handles escaping & casting.
268
+ Any generic value will be serialized as described in [Parameter types](#parameter-types), and replaced by a PostgreSQL protocol placeholder `$1, $2, ...`. The values are then sent separately to PostgreSQL, which handles escaping & casting.
155
269
 
156
270
  All queries will return a `Result` array, with objects mapping column names to each row.
157
271
 
@@ -599,11 +713,13 @@ A `COPY` holds its connection until it finishes. Postgres.js cannot tell that a
599
713
  Postgres.js supports, [canceling queries in progress](https://www.postgresql.org/docs/7.1/protocol-protocol.html#AEN39000). It works by opening a new connection with a protocol level startup message to cancel the current query running on a specific connection. That means there is no guarantee that the query will be canceled, and due to the possible race conditions it might even result in canceling another query. This is fine for long running queries, but in the case of high load and fast queries it might be better to simply ignore results instead of canceling.
600
714
 
601
715
  ```js
602
- const query = sql`select pg_sleep 100`.execute()
716
+ const query = sql`select pg_sleep(100)`.execute()
603
717
  setTimeout(() => query.cancel(), 100)
604
718
  const result = await query
605
719
  ```
606
720
 
721
+ A cancelled query rejects with code `57014`. A cancel can arrive too late, in which case the query may still complete, or fail with its own error. `query.cancel()` resolves once the cancel request has been sent. A cancelled query you don't await does not cause an unhandled rejection.
722
+
607
723
  ### Execute
608
724
 
609
725
  #### ```await sql``.execute()```
@@ -977,6 +1093,103 @@ const sql = postgres({
977
1093
 
978
1094
  There is currently no guaranteed way to handle `numeric` / `decimal` types in native Javascript. **These [and similar] types will be returned as a `string`**. The best way in this case is to use [custom types](#custom-types).
979
1095
 
1096
+ ## Parameter types
1097
+
1098
+ A `$1` placeholder has a type that PostgreSQL infers from the SQL around it (the column it is compared to or inserted into, a function argument, a cast). Postgres.js has to turn each JavaScript value into text for that type, and `parameter_types` chooses how.
1099
+
1100
+ Some values carry a type Postgres.js can tell from the JavaScript value: `Date`, `Buffer` / `Uint8Array`, booleans, `bigint`, and anything wrapped in `sql.typed(value, oid)`, `sql.types.*`, `sql.json` or `sql.array` with elements. They are serialized for that type and sent in a single round trip, in every mode. Everything else (strings, numbers, plain objects, `sql.typed(value)` without an OID, an empty `sql.array([])`, arrays) has no such type. `null` is always SQL `NULL`.
1101
+
1102
+ ### `parameter_types: 'server'` (default)
1103
+
1104
+ For a value whose type Postgres.js cannot tell, it first asks PostgreSQL for the type of the placeholder, which costs an extra round trip on every query with `prepare: false` and the first time each connection runs that SQL with `prepare: true`, and then serializes the value for that type. This has consequences worth knowing:
1105
+
1106
+ - a string sent to a `json` or `jsonb` placeholder is sent as JSON, so `'{"a":1}'` is stored as a JSON string, not an object
1107
+ - a string or number sent to a `date`, `timestamp` or `timestamptz` placeholder goes through `new Date(x).toISOString()`, where a date-only string such as `'2020-01-02'` is read as UTC and a date-and-time string without an offset such as `'2020-01-02T10:00'` is read in the Node.js process time zone (`TZ`)
1108
+ - anything other than `true` sent to a `boolean` placeholder is stored as `false`, including the strings `'true'` and `'t'` and the number `1`
1109
+ - a string sent to a `bytea` placeholder is sent as hex of its UTF-8 bytes
1110
+ - a custom type registered in `types` with a `from` list runs its `serialize` function
1111
+
1112
+ ### `parameter_types: 'client'`
1113
+
1114
+ The JavaScript value decides how it is serialized and PostgreSQL parses the text, so a query with values is one round trip:
1115
+
1116
+ - a string is sent unchanged as text
1117
+ - a number is sent as its JavaScript text (`'' + n`)
1118
+ - a plain object (prototype `Object.prototype` or `null`) is sent as `JSON.stringify(x)`
1119
+ - any other object (Decimal, Luxon, moment, URL, class instances) is sent as `'' + x`
1120
+ - `null` is SQL `NULL`; `undefined` is handled as before
1121
+ - a value with a type Postgres.js can tell from the JavaScript value is serialized for that type, as with `'server'`
1122
+ - an array whose elements have no such type still makes Postgres.js ask PostgreSQL for the placeholder's type first, because only PostgreSQL knows whether the placeholder is an array, JSON or a custom type; that query takes the extra round trip
1123
+
1124
+ What this changes compared to `'server'`:
1125
+
1126
+ | Value | Type PostgreSQL infers for the placeholder | `'server'` | `'client'` |
1127
+ | --- | --- | --- | --- |
1128
+ | string | json, jsonb | sent as JSON: stores a JSON string | parsed as JSON; text that is not JSON is rejected |
1129
+ | string | boolean | always `false` | PostgreSQL boolean input (`'t'`, `'yes'`, `'on'`, `'1'` are true); invalid text is rejected |
1130
+ | string | date, timestamp, timestamptz | `new Date(s).toISOString()`: date-only strings are read as UTC, date-and-time strings without an offset in the Node.js process time zone (`TZ`) | parsed by PostgreSQL in the PostgreSQL session's `TimeZone` setting; `date` and `timestamp` ignore an explicit offset |
1131
+ | string | bytea | hex of the UTF-8 bytes | bytea text input; differs only for strings containing backslashes |
1132
+ | string, number | a custom type registered in `types` with a `from` list | the type's `serialize` | sent unchanged as text |
1133
+ | number | json, jsonb | `JSON.stringify` (`NaN` and `Infinity` become `null`) | the number's text (`NaN` and `Infinity` are rejected) |
1134
+ | number | boolean | `false` | `1` is true, `0` is false, anything else is rejected |
1135
+ | number | date, timestamp, timestamptz | `new Date(ms)` | rejected, except `Infinity`; a 6- or 8-digit number is read as a `YYMMDD` or `YYYYMMDD` date |
1136
+ | plain object | json, jsonb | `JSON.stringify` or your json `serialize` | `JSON.stringify` |
1137
+ | plain object | text | `[object Object]` | JSON text |
1138
+ | plain object | a custom type registered in `types` with a `from` list | the type's `serialize` | JSON text, normally rejected |
1139
+ | class instance | json, jsonb | `JSON.stringify`, so `toJSON` applies | `'' + x` |
1140
+ | class instance | date, timestamp, timestamptz | `new Date(x)` | `'' + x`, normally rejected |
1141
+
1142
+ `time`, `timetz` and `interval` placeholders have no serializer in either mode: strings and numbers are sent unchanged as text.
1143
+
1144
+ In practice:
1145
+
1146
+ - pass `new Date(ms)` for epoch numbers, and set `connection: { timezone }` so date strings are read in the time zone you expect
1147
+ - pass `sql.json(s)` for a JSON string value, and `sql.types.name(v)` or `sql.typed(v, oid)` for a custom type; a custom type's `serialize` also runs for a whole array bound to that type, never for a plain value
1148
+ - `NaN`, `Infinity` and `-0` are sent as `NaN`, `Infinity` and `0`, and PostgreSQL's input rules for the placeholder's type decide
1149
+
1150
+ Run `'warn'` first to see which of your queries are affected.
1151
+
1152
+ **Planned for 5.0:** `'client'` becomes the default. Pass `parameter_types: 'server'` to keep the 4.x behaviour.
1153
+
1154
+ ### `parameter_types: 'warn'`
1155
+
1156
+ `'warn'` behaves exactly like `'server'`, and also reports every query in which a value would be sent differently by `'client'`. The report goes to `ontypemismatch`, or to `console.warn` when it is not set:
1157
+
1158
+ ```js
1159
+ const sql = postgres({
1160
+ parameter_types: 'warn',
1161
+ ontypemismatch: ({ query, parameters }) => {
1162
+ // parameters: [{ position: 2, oid: 3802, typeof: 'string' }]
1163
+ // position is the $n number, oid the type PostgreSQL inferred for the placeholder
1164
+ }
1165
+ })
1166
+ ```
1167
+
1168
+ With `debug: true` each entry also carries `serverText` and `clientText`, the two texts that were compared; `clientText` is the error when the `'client'` mode would throw. A query is reported on every execution that has a difference; sample in `ontypemismatch` if that is too much. `'warn'` serializes each untyped value twice and compares the strings. The option can also be set with `?parameter_types=warn` in the connection URL or `PGPARAMETER_TYPES`.
1169
+
1170
+ `'warn'` compares the serialized text, not the stored value, so a report does not always mean the stored value would change.
1171
+
1172
+ These reported cases store the same value under both modes:
1173
+
1174
+ - `0`, `'0'`, `'false'`, `'no'` and `'off'` sent to a `boolean` placeholder (the texts differ, both store `false`)
1175
+ - a date string with an explicit offset sent to a `timestamptz` placeholder, as long as it has no more than millisecond precision (`toISOString()` truncates microseconds, so a string with microseconds stores a different value under `'server'`)
1176
+ - a string without backslashes sent to a `bytea` placeholder
1177
+
1178
+ These reported cases store a different value, or are rejected, under `'client'`:
1179
+
1180
+ - `1`, `'t'`, `'true'`, `'yes'`, `'on'` and `'1'` sent to a `boolean` placeholder (stored `false` today, `true` under `'client'`)
1181
+ - a date string with an explicit offset sent to a `timestamp` or `date` placeholder (`'server'` converts to UTC first, `'client'` drops the offset)
1182
+ - a string sent to a `json` or `jsonb` placeholder, a plain object sent to a `text` placeholder, and a number sent to a date, timestamp or boolean placeholder
1183
+ - a string or number sent to a custom type registered in `types` with a `from` list, which skips its `serialize`
1184
+
1185
+ Date strings without an explicit offset:
1186
+
1187
+ | String | Placeholder | Stored the same under both modes when |
1188
+ | --- | --- | --- |
1189
+ | date and time, such as `'2020-01-02T10:00'` | `timestamptz` | the Node.js process time zone and the PostgreSQL session's `TimeZone` setting match |
1190
+ | date only, such as `'2020-01-02'` | `timestamptz` | the PostgreSQL session's `TimeZone` setting is UTC |
1191
+ | date and time | `timestamp` or `date` | the Node.js process runs in UTC (`'server'` stores the UTC wall time, `'client'` stores the text as written) |
1192
+
980
1193
  ## Result Array
981
1194
 
982
1195
  The `Result` Array returned from queries is a custom array allowing for easy destructuring or passing on directly to JSON.stringify or general Array usage. It includes the following properties.
@@ -1053,14 +1266,16 @@ const sql = postgres('postgres://username:password@host:port/database', {
1053
1266
  application_name : 'postgres.js', // Default application_name
1054
1267
  ... // Other connection parameters, see https://www.postgresql.org/docs/current/runtime-config-client.html
1055
1268
  },
1056
- target_session_attrs : null, // Use 'read-write' with multiple hosts to
1057
- // ensure only connecting to primary
1269
+ target_session_attrs : null, // Which kind of server to connect to, see
1270
+ // Multi-host connections below
1058
1271
  fetch_types : true, // Automatically fetches types on connect
1059
1272
  // on initial connection.
1273
+ parameter_types : 'server', // 'client' or 'warn', see Parameter types (PGPARAMETER_TYPES)
1274
+ ontypemismatch : fn, // Called by parameter_types: 'warn', default console.warn
1060
1275
  })
1061
1276
  ```
1062
1277
 
1063
- The `onnotice`, `onparameter`, `onnotify`, `onclose`, `onidle` and `debug` callbacks passed in the options are called synchronously from inside the driver. If one throws, the error is rethrown as an uncaught exception and the connection and its queries are not affected; under Node's default handling that ends the process.
1278
+ The `onnotice`, `onparameter`, `ontypemismatch`, `onnotify`, `onclose`, `onidle` and `debug` callbacks passed in the options are called synchronously from inside the driver. If one throws, the error is rethrown as an uncaught exception and the connection and its queries are not affected; under Node's default handling that ends the process.
1064
1279
 
1065
1280
  Note that `max_lifetime = 60 * (30 + Math.random() * 30)` by default. This resolves to an interval between 30 and 60 minutes to optimize for the benefits of prepared statements **and** working nicely with Linux's OOM killer.
1066
1281
 
@@ -1099,7 +1314,20 @@ Multiple connection strings can be passed to `postgres()` in the form of `postgr
1099
1314
 
1100
1315
  Connections will be attempted in order of the specified hosts/ports. On a successful connection, all retries will be reset. This ensures that hosts can come up and down seamlessly.
1101
1316
 
1102
- If you specify `target_session_attrs: 'primary'` or `PGTARGETSESSIONATTRS=primary` Postgres.js will only connect to the primary host, allowing for zero downtime failovers.
1317
+ `target_session_attrs` (or `PGTARGETSESSIONATTRS`) picks which kind of server to connect to. The values and their meaning are libpq's:
1318
+
1319
+ | Value | Connects to |
1320
+ |---|---|
1321
+ | `null` (default) | any server |
1322
+ | `'read-write'` | a server that accepts writes: not a hot standby, and `default_transaction_read_only` off |
1323
+ | `'primary'` | a server that is not a hot standby, even if `default_transaction_read_only` is on |
1324
+ | `'read-only'` | a hot standby, or a server with `default_transaction_read_only` on |
1325
+ | `'standby'` | a hot standby |
1326
+ | `'prefer-standby'` | a hot standby if any host is one, otherwise any server |
1327
+
1328
+ During a failover or an upgrade, `'read-write'` and `'primary'` wait for the promoted server instead of connecting to a standby whose writes would fail. Read traffic that should keep using standbys during the switch belongs on `'prefer-standby'` or no `target_session_attrs` at all.
1329
+
1330
+ With several hosts, a host that answers but is the wrong kind is skipped and the next one is tried at once; if every host answers and none matches, the connection fails immediately with `TARGET_SESSION_ATTRS`. With a single host, or when some host fails to connect, it keeps retrying, bounded by `connect_timeout` per host, because a failover may be in progress.
1103
1331
 
1104
1332
  ### The Connection Pool
1105
1333
 
@@ -1276,11 +1504,14 @@ const sql = postgres()
1276
1504
  | `application_name` | `PGAPPNAME` |
1277
1505
  | `idle_timeout` | `PGIDLE_TIMEOUT` |
1278
1506
  | `connect_timeout` | `PGCONNECT_TIMEOUT` |
1507
+ | `parameter_types` | `PGPARAMETER_TYPES` |
1279
1508
 
1280
1509
  ### Prepared statements
1281
1510
 
1282
1511
  Prepared statements will automatically be created for any queries where it can be inferred that the query is static. This can be disabled by using the `prepare: false` option. For instance — this is useful when [using PGBouncer in `transaction mode`](https://github.com/porsager/postgres/issues/93#issuecomment-656290493).
1283
1512
 
1513
+ With `prepare: false`, `parameter_types: 'client'` sends queries with values in one round trip, unless a value is an untyped array; see [Parameter types](#parameter-types).
1514
+
1284
1515
  **update**: [since 1.21.0](https://www.pgbouncer.org/2023/10/pgbouncer-1-21-0)
1285
1516
  PGBouncer supports protocol-level named prepared statements when [configured
1286
1517
  properly](https://www.pgbouncer.org/config.html#max_prepared_statements)
@@ -1326,6 +1557,8 @@ const [custom] = await sql`
1326
1557
 
1327
1558
  ```
1328
1559
 
1560
+ With `parameter_types: 'client'` a type's `serialize` runs for `sql.typed.name(v)` and for a whole array bound to that type, not for a plain value; see [Parameter types](#parameter-types).
1561
+
1329
1562
  ### Custom socket
1330
1563
 
1331
1564
  Easily do in-process ssh tunneling to your database by providing a custom socket for Postgres.js to use. The function (optionally async) must return a socket-like duplex stream.
@@ -1357,6 +1590,17 @@ To ensure proper teardown and cleanup on server restarts use `await sql.end()` b
1357
1590
 
1358
1591
  Calling `sql.end()` will reject new queries and return a Promise which resolves when all queries are finished and the underlying connections are closed. If a `{ timeout }` option is provided any pending queries will be rejected once the timeout (in seconds) is reached and the connections will be destroyed.
1359
1592
 
1593
+ With explicit resource management the pool ends itself at the end of a scope:
1594
+
1595
+ ```js
1596
+ async function main() {
1597
+ await using sql = postgres()
1598
+ await sql`select 1`
1599
+ }
1600
+ ```
1601
+
1602
+ This calls `sql.end()` when the scope exits, so it suits scripts and jobs, not a shared client at a module's top level. The syntax needs Node.js 24+ or TypeScript 5.2+.
1603
+
1360
1604
  #### Sample shutdown using [Prexit](https://github.com/porsager/prexit)
1361
1605
 
1362
1606
  ```js
@@ -1384,6 +1628,13 @@ await reserved.release()
1384
1628
 
1385
1629
  Once you have finished with the reserved connection, call `release` to add it back to the pool.
1386
1630
 
1631
+ ```js
1632
+ using reserved = await sql.reserve()
1633
+ await reserved`select * from users`
1634
+ ```
1635
+
1636
+ A `using` or `await using` declaration calls `release()` when the scope exits.
1637
+
1387
1638
  After release, queries through that reserved handle reject with `CONNECTION_ENDED`. Call `sql.reserve()` again to acquire a new handle. A reserved handle whose connection has closed cannot be reused after reconnect.
1388
1639
 
1389
1640
  ## Error handling
@@ -6,7 +6,7 @@ import { crypto } from '../polyfills.js'
6
6
  import Stream from 'node:stream'
7
7
  import { performance } from '../polyfills.js'
8
8
 
9
- import { stringify, handleValue, addArrayType } from './types.js'
9
+ import { stringify, handleValue, addArrayType, clientSerialize } from './types.js'
10
10
  import { Errors, callback } from './errors.js'
11
11
  import Result from './result.js'
12
12
  import Queue from './queue.js'
@@ -106,6 +106,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
106
106
  , hostIndex = 0
107
107
  , length = 0
108
108
  , rows = 0
109
+ , copyPaused = false
109
110
  , serverSignature = null
110
111
  , nextWriteTimer = null
111
112
  , incomings = null
@@ -216,7 +217,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
216
217
  const connection = {
217
218
  queue: queues.closed,
218
219
  idleTimer,
219
- connect: request => dispatch('acquire', request),
220
+ // Sentry's hook wraps connect and reads the connection from this, so it must stay a method
221
+ connect(request) {
222
+ dispatch('acquire', request)
223
+ },
220
224
  terminate: () => dispatch('terminate'),
221
225
  execute,
222
226
  release: () => dispatch('release'),
@@ -461,6 +465,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
461
465
  query = results = errorResponse = chunk = nextWriteTimer = barrier = null
462
466
  result = new Result()
463
467
  rows = 0
468
+ copyPaused = false
464
469
  nonce = serverSignature = null
465
470
  }
466
471
 
@@ -540,7 +545,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
540
545
 
541
546
  build(q)
542
547
  const written = write(toBuffer(q))
543
- if (q.describeFirst || q.cursorFn || mayCopy(q))
548
+ if (q.describeFirst || q.cursorFn || mayCopy(q) || (q.prepare && !q.prepared && !q.options.simple && q.parameters.length))
544
549
  barrier = q
545
550
  return written && accepting()
546
551
  } catch (error) {
@@ -578,7 +583,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
578
583
 
579
584
  function prepared(q) {
580
585
  return Buffer.concat([
581
- Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName),
586
+ Bind(q),
582
587
  q.cursorFn
583
588
  ? Execute('', q.cursorRows)
584
589
  : ExecuteUnnamed
@@ -606,8 +611,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
606
611
  q.signature = q.prepare && types + string
607
612
  q.onlyDescribe && (delete statements[q.signature])
608
613
  q.parameters = q.parameters || parameters
614
+ q.inferred = options.parameter_types === 'server' ? null : types.slice()
609
615
  q.prepared = q.prepare && q.signature in statements
610
- q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared)
616
+ q.describeFirst = q.onlyDescribe || (!q.prepared && types.some((x, i) =>
617
+ !x && (options.parameter_types !== 'client' || Array.isArray(q.parameters[i]))
618
+ ))
611
619
  q.statement = q.prepared
612
620
  ? statements[q.signature]
613
621
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
@@ -680,6 +688,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
680
688
 
681
689
  function errored(err) {
682
690
  stream && (stream.destroy(err), stream = null)
691
+ resumeCopy()
683
692
  query && queryError(query, err)
684
693
  }
685
694
 
@@ -691,7 +700,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
691
700
  err = new Error(err)
692
701
 
693
702
  'query' in err || 'parameters' in err || Object.defineProperties(err, {
694
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
703
+ stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug, writable: true, configurable: true },
695
704
  query: { value: query.string, enumerable: options.debug },
696
705
  parameters: { value: query.parameters, enumerable: options.debug },
697
706
  args: { value: query.args, enumerable: options.debug },
@@ -796,7 +805,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
796
805
  query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
797
806
  query.retried
798
807
  ? errored(query.retried)
799
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
808
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier && !query.cancelled
800
809
  && (x[5] !== 69 || endsAbortedState.test(query.string))
801
810
  ? retry(query, errorResponse)
802
811
  : errored(errorResponse)
@@ -810,6 +819,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
810
819
 
811
820
  query = results = errorResponse = null
812
821
  result = new Result()
822
+ rows = 0
813
823
 
814
824
  dispatch('ready')
815
825
  }
@@ -1042,8 +1052,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1042
1052
 
1043
1053
  function mismatchReason(x, xs) {
1044
1054
  return (
1055
+ (x === 'read-write' && xs.in_hot_standby === 'on' && 'is a standby') ||
1045
1056
  (x === 'read-write' && xs.default_transaction_read_only === 'on' && 'is read-only') ||
1046
- (x === 'read-only' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1057
+ (x === 'read-only' && xs.in_hot_standby === 'off' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1047
1058
  (x === 'primary' && xs.in_hot_standby === 'on' && 'is a standby') ||
1048
1059
  (x === 'standby' && xs.in_hot_standby === 'off' && 'is a primary') ||
1049
1060
  (x === 'prefer-standby' && xs.in_hot_standby === 'off' && standbyPass() && 'is a primary') ||
@@ -1142,7 +1153,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1142
1153
 
1143
1154
  function CopyOutResponse() {
1144
1155
  stream = new Stream.Readable({
1145
- read() { socket.resume() }
1156
+ read() { copyPaused = false; socket.resume() }
1146
1157
  })
1147
1158
  query.resolve(stream)
1148
1159
  connection.owner && connection.owner.next()
@@ -1152,7 +1163,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1152
1163
  function CopyBothResponse() {
1153
1164
  stream = new Stream.Duplex({
1154
1165
  autoDestroy: true,
1155
- read() { socket.resume() },
1166
+ read() { copyPaused = false; socket.resume() },
1156
1167
  /* c8 ignore next 11 */
1157
1168
  write(chunk, encoding, callback) {
1158
1169
  socket.write(b().d().raw(chunk).end(), callback)
@@ -1172,12 +1183,23 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1172
1183
  }
1173
1184
 
1174
1185
  function CopyData(x) {
1175
- stream && (stream.push(x.subarray(5)) || socket.pause())
1186
+ if (stream && !stream.push(x.subarray(5))) {
1187
+ copyPaused = true
1188
+ socket.pause()
1189
+ }
1176
1190
  }
1177
1191
 
1178
1192
  function CopyDone() {
1179
1193
  stream && stream.push(null)
1180
1194
  stream = null
1195
+ resumeCopy()
1196
+ }
1197
+
1198
+ function resumeCopy() {
1199
+ if (copyPaused) {
1200
+ copyPaused = false
1201
+ socket.resume()
1202
+ }
1181
1203
  }
1182
1204
 
1183
1205
  function NoticeResponse(x) {
@@ -1215,20 +1237,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1215
1237
  }
1216
1238
 
1217
1239
  /* Messages */
1218
- function Bind(parameters, types, statement = '', portal = '') {
1240
+ function Bind(q) {
1241
+ const differences = []
1219
1242
  let prev
1220
1243
  , type
1221
1244
 
1222
- b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length)
1245
+ b().B().str((q.cursorName || '') + b.N).str(q.statement.name + b.N).i16(0).i16(q.parameters.length)
1223
1246
 
1224
- parameters.forEach((x, i) => {
1247
+ q.parameters.forEach((x, i) => {
1225
1248
  if (x === null)
1226
1249
  return b.i32(0xFFFFFFFF)
1227
1250
 
1228
- type = types[i]
1229
- x = type in options.serializers
1230
- ? options.serializers[type](x)
1231
- : '' + x
1251
+ type = q.statement.types[i]
1252
+ x = !q.inferred || q.inferred[i] || Array.isArray(x)
1253
+ ? serialize(x, type)
1254
+ : options.parameter_types === 'client' ? clientSerialize(x) : compare(x, type, i, differences)
1232
1255
 
1233
1256
  prev = b.i
1234
1257
  b.inc(4).str(x).i32(b.i - prev - 4, prev)
@@ -1236,7 +1259,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1236
1259
 
1237
1260
  b.i16(0)
1238
1261
 
1239
- return b.end()
1262
+ const message = b.end()
1263
+ differences.length && callback(options.ontypemismatch || console.warn, { query: q.string, parameters: differences }) // eslint-disable-line
1264
+ return message
1265
+ }
1266
+
1267
+ function serialize(x, type) {
1268
+ return type in options.serializers
1269
+ ? options.serializers[type](x)
1270
+ : '' + x
1271
+ }
1272
+
1273
+ function compare(x, type, i, differences) {
1274
+ const serverText = serialize(x, type)
1275
+ let clientText
1276
+ try {
1277
+ clientText = clientSerialize(x)
1278
+ } catch (error) {
1279
+ clientText = error
1280
+ }
1281
+ serverText === clientText || differences.push({ position: i + 1, oid: type, typeof: typeof x, ...options.debug && { serverText, clientText } })
1282
+ return serverText
1240
1283
  }
1241
1284
 
1242
1285
  function Parse(str, parameters, types, name = '') {
package/cf/src/errors.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export class PostgresError extends Error {
2
2
  constructor(x) {
3
- super(x.message)
3
+ super(typeof x === 'string' ? x : x.message)
4
4
  this.name = this.constructor.name
5
- Object.assign(this, x)
5
+ typeof x === 'string' || Object.assign(this, x)
6
6
  }
7
7
  }
8
8