@steadybrew/postgresjs 4.4.1 → 4.5.0-rc1

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,122 @@ 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.
17
25
 
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.
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 |
19
32
 
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 |
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.
27
34
 
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.
35
+ ## Switching from Postgres.js
36
+
37
+ ```sh
38
+ npm install postgres@npm:@steadybrew/postgresjs@^4
39
+ ```
29
40
 
30
- Supabase PostgreSQL and Supabase Edge are separate environments. Deno 1.46.3 coverage does not imply Deno 2 or hosted Supabase Edge certification.
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.
42
+
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:
44
+
45
+ ```js
46
+ // sentry-postgres.mjs
47
+ import { SENTRY_RUNTIME_INSTRUMENTATIONS } from '@sentry/server-utils/orchestrion/config'
48
+
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
+ ```
58
+
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
+ Upstream issues on [porsager/postgres](https://github.com/porsager/postgres/issues) that this fork fixes, and the version that fixed them. Each fix has a test that reproduces the report. The [changelog](./CHANGELOG.md) has the details.
75
+
76
+ <details>
77
+ <summary>40 bugs and 4 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 can't run on a reconnected session, so it can't read another user's rows through RLS | 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 never handed to another transaction | 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
+
91
+ **Hangs**
92
+
93
+ | Issue | What happens now | Since |
94
+ | --- | --- | --- |
95
+ | [#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 |
96
+ | [#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 |
97
+ | [#1186](https://github.com/porsager/postgres/issues/1186) | Queries queued on a reserved or transaction handle settle when its connection closes | 4.0.0 |
98
+ | [#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 |
99
+ | [#1246](https://github.com/porsager/postgres/issues/1246) | A query sent between a socket error and its close settles | 4.1.0 |
100
+ | [#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 |
101
+ | [#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 |
102
+
103
+ **Crashes**
104
+
105
+ | Issue | What happens now | Since |
106
+ | --- | --- | --- |
107
+ | [#1066](https://github.com/porsager/postgres/issues/1066), [#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 handle whose backend died rejects its statements instead of throwing an uncaught exception | 4.0.0 |
108
+ | [#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 |
109
+ | [#1088](https://github.com/porsager/postgres/issues/1088) | Timers no longer fail on Cloudflare Workers | 4.0.0 |
110
+
111
+ **Reconnects and failover**
112
+
113
+ | Issue | What happens now | Since |
114
+ | --- | --- | --- |
115
+ | [#1179](https://github.com/porsager/postgres/issues/1179) | Failed connection attempts back off instead of the whole pool retrying at once | 4.0.0 |
116
+ | [#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 |
117
+
118
+ **Transactions, notices and subscriptions**
119
+
120
+ | Issue | What happens now | Since |
121
+ | --- | --- | --- |
122
+ | [#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` under write backpressure or with `max_pipeline: 0` | 4.0.0 |
123
+ | [#1063](https://github.com/porsager/postgres/issues/1063) | `onnotice: false` silences notices | 4.4.0 |
124
+ | [#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 |
125
+
126
+ **Feature requests**
127
+
128
+ | Issue | What you get | Since |
129
+ | --- | --- | --- |
130
+ | [#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 |
131
+ | [#1100](https://github.com/porsager/postgres/issues/1100) | An `onidle` callback and the `@steadybrew/postgresjs/vercel` adapter | 4.2.0 |
132
+ | [#1164](https://github.com/porsager/postgres/issues/1164) | Built-in array types are decoded with `fetch_types: false` | 4.0.0 |
133
+
134
+ </details>
135
+
43
136
  ## Getting started
44
137
 
45
138
  <br>
@@ -113,6 +206,7 @@ const { default: postgres } = await import('@steadybrew/postgresjs')
113
206
  * [Listen & notify](#listen--notify)
114
207
  * [Realtime subscribe](#realtime-subscribe)
115
208
  * [Numbers, bigint, numeric](#numbers-bigint-numeric)
209
+ * [Parameter types](#parameter-types)
116
210
  * [Result Array](#result-array)
117
211
  * [Connection details](#connection-details)
118
212
  * [Custom Types](#custom-types)
@@ -151,7 +245,7 @@ Postgres.js utilizes [Tagged template functions](https://developer.mozilla.org/e
151
245
  1. **Enforcing** safe query generation
152
246
  2. Giving the ` sql`` ` function powerful [utility](#dynamic-inserts) and [query building](#building-queries) features.
153
247
 
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.
248
+ 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
249
 
156
250
  All queries will return a `Result` array, with objects mapping column names to each row.
157
251
 
@@ -977,6 +1071,103 @@ const sql = postgres({
977
1071
 
978
1072
  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
1073
 
1074
+ ## Parameter types
1075
+
1076
+ 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.
1077
+
1078
+ 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`.
1079
+
1080
+ ### `parameter_types: 'server'` (default)
1081
+
1082
+ 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:
1083
+
1084
+ - 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
1085
+ - 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`)
1086
+ - anything other than `true` sent to a `boolean` placeholder is stored as `false`, including the strings `'true'` and `'t'` and the number `1`
1087
+ - a string sent to a `bytea` placeholder is sent as hex of its UTF-8 bytes
1088
+ - a custom type registered in `types` with a `from` list runs its `serialize` function
1089
+
1090
+ ### `parameter_types: 'client'`
1091
+
1092
+ The JavaScript value decides how it is serialized and PostgreSQL parses the text, so a query with values is one round trip:
1093
+
1094
+ - a string is sent unchanged as text
1095
+ - a number is sent as its JavaScript text (`'' + n`)
1096
+ - a plain object (prototype `Object.prototype` or `null`) is sent as `JSON.stringify(x)`
1097
+ - any other object (Decimal, Luxon, moment, URL, class instances) is sent as `'' + x`
1098
+ - `null` is SQL `NULL`; `undefined` is handled as before
1099
+ - a value with a type Postgres.js can tell from the JavaScript value is serialized for that type, as with `'server'`
1100
+ - 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
1101
+
1102
+ What this changes compared to `'server'`:
1103
+
1104
+ | Value | Type PostgreSQL infers for the placeholder | `'server'` | `'client'` |
1105
+ | --- | --- | --- | --- |
1106
+ | string | json, jsonb | sent as JSON: stores a JSON string | parsed as JSON; text that is not JSON is rejected |
1107
+ | string | boolean | always `false` | PostgreSQL boolean input (`'t'`, `'yes'`, `'on'`, `'1'` are true); invalid text is rejected |
1108
+ | 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 |
1109
+ | string | bytea | hex of the UTF-8 bytes | bytea text input; differs only for strings containing backslashes |
1110
+ | string, number | a custom type registered in `types` with a `from` list | the type's `serialize` | sent unchanged as text |
1111
+ | number | json, jsonb | `JSON.stringify` (`NaN` and `Infinity` become `null`) | the number's text (`NaN` and `Infinity` are rejected) |
1112
+ | number | boolean | `false` | `1` is true, `0` is false, anything else is rejected |
1113
+ | number | date, timestamp, timestamptz | `new Date(ms)` | rejected, except `Infinity`; a 6- or 8-digit number is read as a `YYMMDD` or `YYYYMMDD` date |
1114
+ | plain object | json, jsonb | `JSON.stringify` or your json `serialize` | `JSON.stringify` |
1115
+ | plain object | text | `[object Object]` | JSON text |
1116
+ | plain object | a custom type registered in `types` with a `from` list | the type's `serialize` | JSON text, normally rejected |
1117
+ | class instance | json, jsonb | `JSON.stringify`, so `toJSON` applies | `'' + x` |
1118
+ | class instance | date, timestamp, timestamptz | `new Date(x)` | `'' + x`, normally rejected |
1119
+
1120
+ `time`, `timetz` and `interval` placeholders have no serializer in either mode: strings and numbers are sent unchanged as text.
1121
+
1122
+ In practice:
1123
+
1124
+ - pass `new Date(ms)` for epoch numbers, and set `connection: { timezone }` so date strings are read in the time zone you expect
1125
+ - 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
1126
+ - `NaN`, `Infinity` and `-0` are sent as `NaN`, `Infinity` and `0`, and PostgreSQL's input rules for the placeholder's type decide
1127
+
1128
+ Run `'warn'` first to see which of your queries are affected.
1129
+
1130
+ **Planned for 5.0:** `'client'` becomes the default. Pass `parameter_types: 'server'` to keep the 4.x behaviour.
1131
+
1132
+ ### `parameter_types: 'warn'`
1133
+
1134
+ `'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:
1135
+
1136
+ ```js
1137
+ const sql = postgres({
1138
+ parameter_types: 'warn',
1139
+ ontypemismatch: ({ query, parameters }) => {
1140
+ // parameters: [{ position: 2, oid: 3802, typeof: 'string' }]
1141
+ // position is the $n number, oid the type PostgreSQL inferred for the placeholder
1142
+ }
1143
+ })
1144
+ ```
1145
+
1146
+ 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`.
1147
+
1148
+ `'warn'` compares the serialized text, not the stored value, so a report does not always mean the stored value would change.
1149
+
1150
+ These reported cases store the same value under both modes:
1151
+
1152
+ - `0`, `'0'`, `'false'`, `'no'` and `'off'` sent to a `boolean` placeholder (the texts differ, both store `false`)
1153
+ - 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'`)
1154
+ - a string without backslashes sent to a `bytea` placeholder
1155
+
1156
+ These reported cases store a different value, or are rejected, under `'client'`:
1157
+
1158
+ - `1`, `'t'`, `'true'`, `'yes'`, `'on'` and `'1'` sent to a `boolean` placeholder (stored `false` today, `true` under `'client'`)
1159
+ - a date string with an explicit offset sent to a `timestamp` or `date` placeholder (`'server'` converts to UTC first, `'client'` drops the offset)
1160
+ - 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
1161
+ - a string or number sent to a custom type registered in `types` with a `from` list, which skips its `serialize`
1162
+
1163
+ Date strings without an explicit offset:
1164
+
1165
+ | String | Placeholder | Stored the same under both modes when |
1166
+ | --- | --- | --- |
1167
+ | date and time, such as `'2020-01-02T10:00'` | `timestamptz` | the Node.js process time zone and the PostgreSQL session's `TimeZone` setting match |
1168
+ | date only, such as `'2020-01-02'` | `timestamptz` | the PostgreSQL session's `TimeZone` setting is UTC |
1169
+ | 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) |
1170
+
980
1171
  ## Result Array
981
1172
 
982
1173
  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.
@@ -1057,10 +1248,12 @@ const sql = postgres('postgres://username:password@host:port/database', {
1057
1248
  // ensure only connecting to primary
1058
1249
  fetch_types : true, // Automatically fetches types on connect
1059
1250
  // on initial connection.
1251
+ parameter_types : 'server', // 'client' or 'warn', see Parameter types (PGPARAMETER_TYPES)
1252
+ ontypemismatch : fn, // Called by parameter_types: 'warn', default console.warn
1060
1253
  })
1061
1254
  ```
1062
1255
 
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.
1256
+ 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
1257
 
1065
1258
  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
1259
 
@@ -1276,11 +1469,14 @@ const sql = postgres()
1276
1469
  | `application_name` | `PGAPPNAME` |
1277
1470
  | `idle_timeout` | `PGIDLE_TIMEOUT` |
1278
1471
  | `connect_timeout` | `PGCONNECT_TIMEOUT` |
1472
+ | `parameter_types` | `PGPARAMETER_TYPES` |
1279
1473
 
1280
1474
  ### Prepared statements
1281
1475
 
1282
1476
  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
1477
 
1478
+ 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).
1479
+
1284
1480
  **update**: [since 1.21.0](https://www.pgbouncer.org/2023/10/pgbouncer-1-21-0)
1285
1481
  PGBouncer supports protocol-level named prepared statements when [configured
1286
1482
  properly](https://www.pgbouncer.org/config.html#max_prepared_statements)
@@ -1326,6 +1522,8 @@ const [custom] = await sql`
1326
1522
 
1327
1523
  ```
1328
1524
 
1525
+ 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).
1526
+
1329
1527
  ### Custom socket
1330
1528
 
1331
1529
  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 +1555,17 @@ To ensure proper teardown and cleanup on server restarts use `await sql.end()` b
1357
1555
 
1358
1556
  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
1557
 
1558
+ With explicit resource management the pool ends itself at the end of a scope:
1559
+
1560
+ ```js
1561
+ async function main() {
1562
+ await using sql = postgres()
1563
+ await sql`select 1`
1564
+ }
1565
+ ```
1566
+
1567
+ 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+.
1568
+
1360
1569
  #### Sample shutdown using [Prexit](https://github.com/porsager/prexit)
1361
1570
 
1362
1571
  ```js
@@ -1384,6 +1593,13 @@ await reserved.release()
1384
1593
 
1385
1594
  Once you have finished with the reserved connection, call `release` to add it back to the pool.
1386
1595
 
1596
+ ```js
1597
+ using reserved = await sql.reserve()
1598
+ await reserved`select * from users`
1599
+ ```
1600
+
1601
+ A `using` or `await using` declaration calls `release()` when the scope exits.
1602
+
1387
1603
  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
1604
 
1389
1605
  ## 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'
@@ -216,7 +216,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
216
216
  const connection = {
217
217
  queue: queues.closed,
218
218
  idleTimer,
219
- connect: request => dispatch('acquire', request),
219
+ // Sentry's hook wraps connect and reads the connection from this, so it must stay a method
220
+ connect(request) {
221
+ dispatch('acquire', request)
222
+ },
220
223
  terminate: () => dispatch('terminate'),
221
224
  execute,
222
225
  release: () => dispatch('release'),
@@ -540,7 +543,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
540
543
 
541
544
  build(q)
542
545
  const written = write(toBuffer(q))
543
- if (q.describeFirst || q.cursorFn || mayCopy(q))
546
+ if (q.describeFirst || q.cursorFn || mayCopy(q) || (q.prepare && !q.prepared && !q.options.simple && q.parameters.length))
544
547
  barrier = q
545
548
  return written && accepting()
546
549
  } catch (error) {
@@ -578,7 +581,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
578
581
 
579
582
  function prepared(q) {
580
583
  return Buffer.concat([
581
- Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName),
584
+ Bind(q),
582
585
  q.cursorFn
583
586
  ? Execute('', q.cursorRows)
584
587
  : ExecuteUnnamed
@@ -606,8 +609,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
606
609
  q.signature = q.prepare && types + string
607
610
  q.onlyDescribe && (delete statements[q.signature])
608
611
  q.parameters = q.parameters || parameters
612
+ q.inferred = options.parameter_types === 'server' ? null : types.slice()
609
613
  q.prepared = q.prepare && q.signature in statements
610
- q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared)
614
+ q.describeFirst = q.onlyDescribe || (!q.prepared && types.some((x, i) =>
615
+ !x && (options.parameter_types !== 'client' || Array.isArray(q.parameters[i]))
616
+ ))
611
617
  q.statement = q.prepared
612
618
  ? statements[q.signature]
613
619
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
@@ -691,7 +697,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
691
697
  err = new Error(err)
692
698
 
693
699
  'query' in err || 'parameters' in err || Object.defineProperties(err, {
694
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
700
+ stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug, writable: true, configurable: true },
695
701
  query: { value: query.string, enumerable: options.debug },
696
702
  parameters: { value: query.parameters, enumerable: options.debug },
697
703
  args: { value: query.args, enumerable: options.debug },
@@ -1215,20 +1221,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1215
1221
  }
1216
1222
 
1217
1223
  /* Messages */
1218
- function Bind(parameters, types, statement = '', portal = '') {
1224
+ function Bind(q) {
1225
+ const differences = []
1219
1226
  let prev
1220
1227
  , type
1221
1228
 
1222
- b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length)
1229
+ b().B().str((q.cursorName || '') + b.N).str(q.statement.name + b.N).i16(0).i16(q.parameters.length)
1223
1230
 
1224
- parameters.forEach((x, i) => {
1231
+ q.parameters.forEach((x, i) => {
1225
1232
  if (x === null)
1226
1233
  return b.i32(0xFFFFFFFF)
1227
1234
 
1228
- type = types[i]
1229
- x = type in options.serializers
1230
- ? options.serializers[type](x)
1231
- : '' + x
1235
+ type = q.statement.types[i]
1236
+ x = !q.inferred || q.inferred[i] || Array.isArray(x)
1237
+ ? serialize(x, type)
1238
+ : options.parameter_types === 'client' ? clientSerialize(x) : compare(x, type, i, differences)
1232
1239
 
1233
1240
  prev = b.i
1234
1241
  b.inc(4).str(x).i32(b.i - prev - 4, prev)
@@ -1236,7 +1243,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1236
1243
 
1237
1244
  b.i16(0)
1238
1245
 
1239
- return b.end()
1246
+ const message = b.end()
1247
+ differences.length && callback(options.ontypemismatch || console.warn, { query: q.string, parameters: differences }) // eslint-disable-line
1248
+ return message
1249
+ }
1250
+
1251
+ function serialize(x, type) {
1252
+ return type in options.serializers
1253
+ ? options.serializers[type](x)
1254
+ : '' + x
1255
+ }
1256
+
1257
+ function compare(x, type, i, differences) {
1258
+ const serverText = serialize(x, type)
1259
+ let clientText
1260
+ try {
1261
+ clientText = clientSerialize(x)
1262
+ } catch (error) {
1263
+ clientText = error
1264
+ }
1265
+ serverText === clientText || differences.push({ position: i + 1, oid: type, typeof: typeof x, ...options.debug && { serverText, clientText } })
1266
+ return serverText
1240
1267
  }
1241
1268
 
1242
1269
  function Parse(str, parameters, types, name = '') {
package/cf/src/index.js CHANGED
@@ -87,7 +87,8 @@ function Postgres(a, b) {
87
87
  listen,
88
88
  begin,
89
89
  close,
90
- end
90
+ end,
91
+ ...Symbol.asyncDispose && { [Symbol.asyncDispose]: () => end() }
91
92
  })
92
93
 
93
94
  return sql
@@ -328,10 +329,17 @@ function Postgres(a, b) {
328
329
 
329
330
  async function reserve() {
330
331
  const l = await lease(false)
331
- const sql = Sql(q => send(l, q))
332
+ const sql = Sql(handler)
332
333
  sql.release = () => settle(l, Lease.Released)
334
+ Symbol.dispose && (sql[Symbol.dispose] = () => sql.release())
335
+ Symbol.asyncDispose && (sql[Symbol.asyncDispose] = async() => sql.release())
333
336
  sql.stats = stats
334
337
  return sql
338
+
339
+ // dd-trace starts query spans only inside function declarations named handler
340
+ function handler(q) {
341
+ send(l, q)
342
+ }
335
343
  }
336
344
 
337
345
  async function begin(options, fn) {
@@ -348,7 +356,7 @@ function Postgres(a, b) {
348
356
  }
349
357
 
350
358
  async function transaction() {
351
- await Sql(q => send(l, q)).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
359
+ await Sql(handler).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
352
360
  e instanceof PostgresError && settle(l, Lease.Released)
353
361
  throw e
354
362
  })
@@ -359,7 +367,7 @@ function Postgres(a, b) {
359
367
  let result
360
368
  l.state = Lease.Settling
361
369
  try {
362
- result = await statement(Sql(q => send(l, q, Lease.Settling)))
370
+ result = await statement(Sql(q => handler(q, Lease.Settling)))
363
371
  } catch (e) {
364
372
  settle(l, Lease.Discarded)
365
373
  throw e
@@ -369,7 +377,7 @@ function Postgres(a, b) {
369
377
  }
370
378
 
371
379
  async function scope(fn, name) {
372
- const sql = Sql(handler)
380
+ const sql = Sql(track)
373
381
  sql.savepoint = savepoint
374
382
  sql.prepare = x => {
375
383
  if (typeof x !== 'string' || !x || x.includes('\0'))
@@ -423,11 +431,16 @@ function Postgres(a, b) {
423
431
  return scope(fn, 's' + savepoints++ + (name ? '_' + name : ''))
424
432
  }
425
433
 
426
- function handler(q) {
434
+ function track(q) {
427
435
  q.catch(e => uncaughtError || (uncaughtError = e))
428
- send(l, q)
436
+ handler(q)
429
437
  }
430
438
  }
439
+
440
+ // dd-trace starts query spans only inside function declarations named handler
441
+ function handler(q, expected) {
442
+ send(l, q, expected)
443
+ }
431
444
  }
432
445
 
433
446
  function move(c, queue) {
@@ -675,6 +688,7 @@ function parseOptions(a, b) {
675
688
  prepare : true,
676
689
  debug : false,
677
690
  fetch_types : true,
691
+ parameter_types : 'server',
678
692
  publications : 'alltables',
679
693
  target_session_attrs: null
680
694
  }
@@ -705,11 +719,13 @@ function parseOptions(a, b) {
705
719
  },
706
720
  types : o.types || {},
707
721
  target_session_attrs: tsa(o, url, env),
722
+ parameter_types : parameterTypes(o, url, env),
708
723
  onnotice : o.onnotice,
709
724
  onnotify : o.onnotify,
710
725
  onclose : o.onclose,
711
726
  onidle : o.onidle,
712
727
  onparameter : o.onparameter,
728
+ ontypemismatch : o.ontypemismatch,
713
729
  socket : o.socket,
714
730
  transform : parseTransform(o.transform || { undefined: undefined }),
715
731
  parameters : {},
@@ -729,6 +745,14 @@ function tsa(o, url, env) {
729
745
  throw new Error('target_session_attrs ' + x + ' is not supported')
730
746
  }
731
747
 
748
+ function parameterTypes(o, url, env) {
749
+ const x = o.parameter_types || url.searchParams.get('parameter_types') || env.PGPARAMETER_TYPES || 'server'
750
+ if (['server', 'client', 'warn'].includes(x))
751
+ return x
752
+
753
+ throw new Error('parameter_types ' + x + ' is not supported')
754
+ }
755
+
732
756
  function max_lifetime() {
733
757
  return 60 * (30 + Math.random() * 30)
734
758
  }
package/cf/src/query.js CHANGED
@@ -23,16 +23,9 @@ export class Query extends Promise {
23
23
  this.state = null
24
24
  this.statement = null
25
25
 
26
- this.resolve = x => {
27
- this.active = false
28
- this.settled = true
29
- resolve(x)
30
- }
31
- this.reject = x => {
32
- this.active = false
33
- this.settled = true
34
- reject(x)
35
- }
26
+ // dd-trace's load-time rewrite only accepts single-expression arrows here
27
+ this.resolve = x => (this.active = false, this.settled = true, resolve(x))
28
+ this.reject = x => (this.active = false, this.settled = true, reject(x))
36
29
 
37
30
  this.active = false
38
31
  this.settled = false
package/cf/src/types.js CHANGED
@@ -249,6 +249,11 @@ export const inferType = function inferType(x) {
249
249
  )
250
250
  }
251
251
 
252
+ export function clientSerialize(x) {
253
+ const proto = typeof x === 'object' && Object.getPrototypeOf(x)
254
+ return proto === Object.prototype || proto === null ? JSON.stringify(x) : '' + x
255
+ }
256
+
252
257
  const escapeBackslash = /\\/g
253
258
  const escapeQuote = /"/g
254
259
 
@@ -4,7 +4,7 @@ const crypto = require('crypto')
4
4
  const Stream = require('stream')
5
5
  const { performance } = require('perf_hooks')
6
6
 
7
- const { stringify, handleValue, addArrayType } = require('./types.js')
7
+ const { stringify, handleValue, addArrayType, clientSerialize } = require('./types.js')
8
8
  const { Errors, callback } = require('./errors.js')
9
9
  const Result = require('./result.js')
10
10
  const Queue = require('./queue.js')
@@ -214,7 +214,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
214
214
  const connection = {
215
215
  queue: queues.closed,
216
216
  idleTimer,
217
- connect: request => dispatch('acquire', request),
217
+ // Sentry's hook wraps connect and reads the connection from this, so it must stay a method
218
+ connect(request) {
219
+ dispatch('acquire', request)
220
+ },
218
221
  terminate: () => dispatch('terminate'),
219
222
  execute,
220
223
  release: () => dispatch('release'),
@@ -538,7 +541,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
538
541
 
539
542
  build(q)
540
543
  const written = write(toBuffer(q))
541
- if (q.describeFirst || q.cursorFn || mayCopy(q))
544
+ if (q.describeFirst || q.cursorFn || mayCopy(q) || (q.prepare && !q.prepared && !q.options.simple && q.parameters.length))
542
545
  barrier = q
543
546
  return written && accepting()
544
547
  } catch (error) {
@@ -576,7 +579,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
576
579
 
577
580
  function prepared(q) {
578
581
  return Buffer.concat([
579
- Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName),
582
+ Bind(q),
580
583
  q.cursorFn
581
584
  ? Execute('', q.cursorRows)
582
585
  : ExecuteUnnamed
@@ -604,8 +607,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
604
607
  q.signature = q.prepare && types + string
605
608
  q.onlyDescribe && (delete statements[q.signature])
606
609
  q.parameters = q.parameters || parameters
610
+ q.inferred = options.parameter_types === 'server' ? null : types.slice()
607
611
  q.prepared = q.prepare && q.signature in statements
608
- q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared)
612
+ q.describeFirst = q.onlyDescribe || (!q.prepared && types.some((x, i) =>
613
+ !x && (options.parameter_types !== 'client' || Array.isArray(q.parameters[i]))
614
+ ))
609
615
  q.statement = q.prepared
610
616
  ? statements[q.signature]
611
617
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
@@ -689,7 +695,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
689
695
  err = new Error(err)
690
696
 
691
697
  'query' in err || 'parameters' in err || Object.defineProperties(err, {
692
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
698
+ stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug, writable: true, configurable: true },
693
699
  query: { value: query.string, enumerable: options.debug },
694
700
  parameters: { value: query.parameters, enumerable: options.debug },
695
701
  args: { value: query.args, enumerable: options.debug },
@@ -1213,20 +1219,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1213
1219
  }
1214
1220
 
1215
1221
  /* Messages */
1216
- function Bind(parameters, types, statement = '', portal = '') {
1222
+ function Bind(q) {
1223
+ const differences = []
1217
1224
  let prev
1218
1225
  , type
1219
1226
 
1220
- b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length)
1227
+ b().B().str((q.cursorName || '') + b.N).str(q.statement.name + b.N).i16(0).i16(q.parameters.length)
1221
1228
 
1222
- parameters.forEach((x, i) => {
1229
+ q.parameters.forEach((x, i) => {
1223
1230
  if (x === null)
1224
1231
  return b.i32(0xFFFFFFFF)
1225
1232
 
1226
- type = types[i]
1227
- x = type in options.serializers
1228
- ? options.serializers[type](x)
1229
- : '' + x
1233
+ type = q.statement.types[i]
1234
+ x = !q.inferred || q.inferred[i] || Array.isArray(x)
1235
+ ? serialize(x, type)
1236
+ : options.parameter_types === 'client' ? clientSerialize(x) : compare(x, type, i, differences)
1230
1237
 
1231
1238
  prev = b.i
1232
1239
  b.inc(4).str(x).i32(b.i - prev - 4, prev)
@@ -1234,7 +1241,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1234
1241
 
1235
1242
  b.i16(0)
1236
1243
 
1237
- return b.end()
1244
+ const message = b.end()
1245
+ differences.length && callback(options.ontypemismatch || console.warn, { query: q.string, parameters: differences }) // eslint-disable-line
1246
+ return message
1247
+ }
1248
+
1249
+ function serialize(x, type) {
1250
+ return type in options.serializers
1251
+ ? options.serializers[type](x)
1252
+ : '' + x
1253
+ }
1254
+
1255
+ function compare(x, type, i, differences) {
1256
+ const serverText = serialize(x, type)
1257
+ let clientText
1258
+ try {
1259
+ clientText = clientSerialize(x)
1260
+ } catch (error) {
1261
+ clientText = error
1262
+ }
1263
+ serverText === clientText || differences.push({ position: i + 1, oid: type, typeof: typeof x, ...options.debug && { serverText, clientText } })
1264
+ return serverText
1238
1265
  }
1239
1266
 
1240
1267
  function Parse(str, parameters, types, name = '') {
package/cjs/src/index.js CHANGED
@@ -86,7 +86,8 @@ function Postgres(a, b) {
86
86
  listen,
87
87
  begin,
88
88
  close,
89
- end
89
+ end,
90
+ ...Symbol.asyncDispose && { [Symbol.asyncDispose]: () => end() }
90
91
  })
91
92
 
92
93
  return sql
@@ -327,10 +328,17 @@ function Postgres(a, b) {
327
328
 
328
329
  async function reserve() {
329
330
  const l = await lease(false)
330
- const sql = Sql(q => send(l, q))
331
+ const sql = Sql(handler)
331
332
  sql.release = () => settle(l, Lease.Released)
333
+ Symbol.dispose && (sql[Symbol.dispose] = () => sql.release())
334
+ Symbol.asyncDispose && (sql[Symbol.asyncDispose] = async() => sql.release())
332
335
  sql.stats = stats
333
336
  return sql
337
+
338
+ // dd-trace starts query spans only inside function declarations named handler
339
+ function handler(q) {
340
+ send(l, q)
341
+ }
334
342
  }
335
343
 
336
344
  async function begin(options, fn) {
@@ -347,7 +355,7 @@ function Postgres(a, b) {
347
355
  }
348
356
 
349
357
  async function transaction() {
350
- await Sql(q => send(l, q)).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
358
+ await Sql(handler).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
351
359
  e instanceof PostgresError && settle(l, Lease.Released)
352
360
  throw e
353
361
  })
@@ -358,7 +366,7 @@ function Postgres(a, b) {
358
366
  let result
359
367
  l.state = Lease.Settling
360
368
  try {
361
- result = await statement(Sql(q => send(l, q, Lease.Settling)))
369
+ result = await statement(Sql(q => handler(q, Lease.Settling)))
362
370
  } catch (e) {
363
371
  settle(l, Lease.Discarded)
364
372
  throw e
@@ -368,7 +376,7 @@ function Postgres(a, b) {
368
376
  }
369
377
 
370
378
  async function scope(fn, name) {
371
- const sql = Sql(handler)
379
+ const sql = Sql(track)
372
380
  sql.savepoint = savepoint
373
381
  sql.prepare = x => {
374
382
  if (typeof x !== 'string' || !x || x.includes('\0'))
@@ -422,11 +430,16 @@ function Postgres(a, b) {
422
430
  return scope(fn, 's' + savepoints++ + (name ? '_' + name : ''))
423
431
  }
424
432
 
425
- function handler(q) {
433
+ function track(q) {
426
434
  q.catch(e => uncaughtError || (uncaughtError = e))
427
- send(l, q)
435
+ handler(q)
428
436
  }
429
437
  }
438
+
439
+ // dd-trace starts query spans only inside function declarations named handler
440
+ function handler(q, expected) {
441
+ send(l, q, expected)
442
+ }
430
443
  }
431
444
 
432
445
  function move(c, queue) {
@@ -674,6 +687,7 @@ function parseOptions(a, b) {
674
687
  prepare : true,
675
688
  debug : false,
676
689
  fetch_types : true,
690
+ parameter_types : 'server',
677
691
  publications : 'alltables',
678
692
  target_session_attrs: null
679
693
  }
@@ -704,11 +718,13 @@ function parseOptions(a, b) {
704
718
  },
705
719
  types : o.types || {},
706
720
  target_session_attrs: tsa(o, url, env),
721
+ parameter_types : parameterTypes(o, url, env),
707
722
  onnotice : o.onnotice,
708
723
  onnotify : o.onnotify,
709
724
  onclose : o.onclose,
710
725
  onidle : o.onidle,
711
726
  onparameter : o.onparameter,
727
+ ontypemismatch : o.ontypemismatch,
712
728
  socket : o.socket,
713
729
  transform : parseTransform(o.transform || { undefined: undefined }),
714
730
  parameters : {},
@@ -728,6 +744,14 @@ function tsa(o, url, env) {
728
744
  throw new Error('target_session_attrs ' + x + ' is not supported')
729
745
  }
730
746
 
747
+ function parameterTypes(o, url, env) {
748
+ const x = o.parameter_types || url.searchParams.get('parameter_types') || env.PGPARAMETER_TYPES || 'server'
749
+ if (['server', 'client', 'warn'].includes(x))
750
+ return x
751
+
752
+ throw new Error('parameter_types ' + x + ' is not supported')
753
+ }
754
+
731
755
  function max_lifetime() {
732
756
  return 60 * (30 + Math.random() * 30)
733
757
  }
package/cjs/src/query.js CHANGED
@@ -23,16 +23,9 @@ const Query = module.exports.Query = class Query extends Promise {
23
23
  this.state = null
24
24
  this.statement = null
25
25
 
26
- this.resolve = x => {
27
- this.active = false
28
- this.settled = true
29
- resolve(x)
30
- }
31
- this.reject = x => {
32
- this.active = false
33
- this.settled = true
34
- reject(x)
35
- }
26
+ // dd-trace's load-time rewrite only accepts single-expression arrows here
27
+ this.resolve = x => (this.active = false, this.settled = true, resolve(x))
28
+ this.reject = x => (this.active = false, this.settled = true, reject(x))
36
29
 
37
30
  this.active = false
38
31
  this.settled = false
package/cjs/src/types.js CHANGED
@@ -248,6 +248,11 @@ const inferType = module.exports.inferType = function inferType(x) {
248
248
  )
249
249
  }
250
250
 
251
+ module.exports.clientSerialize = clientSerialize;function clientSerialize(x) {
252
+ const proto = typeof x === 'object' && Object.getPrototypeOf(x)
253
+ return proto === Object.prototype || proto === null ? JSON.stringify(x) : '' + x
254
+ }
255
+
251
256
  const escapeBackslash = /\\/g
252
257
  const escapeQuote = /"/g
253
258
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadybrew/postgresjs",
3
- "version": "4.4.1",
3
+ "version": "4.5.0-rc1",
4
4
  "description": "PostgreSQL client for Node.js and Deno, independently maintained by Steady Brew",
5
5
  "type": "module",
6
6
  "module": "src/index.js",
@@ -24,7 +24,7 @@
24
24
  "types": "types/index.d.ts",
25
25
  "typings": "types/index.d.ts",
26
26
  "engines": {
27
- "node": ">=24"
27
+ "node": ">=22"
28
28
  },
29
29
  "scripts": {
30
30
  "build": "npm run build:cjs && npm run build:deno && npm run build:cf",
@@ -34,6 +34,7 @@
34
34
  "test": "npm run test:startup && npm run test:esm && npm run test:cjs && npm run test:deno",
35
35
  "test:esm": "node tests-startup/run.js --integration && node tests/index.js",
36
36
  "test:startup": "node tests-startup/run.js",
37
+ "test:tracing": "node tests-tracing/run.js",
37
38
  "test:cjs": "npm run build:cjs && node tests-startup/run.js --integration --cjs && cd cjs/tests && node index.js && cd ../../",
38
39
  "test:deno": "npm run test:deno-socket && npm run build:deno && cd deno/tests && deno run --no-lock --allow-all --unsafely-ignore-certificate-errors index.js && cd ../../",
39
40
  "lint": "eslint src && eslint tests && eslint tests-startup --ext .js,.mjs",
package/src/connection.js CHANGED
@@ -4,7 +4,7 @@ import crypto from 'crypto'
4
4
  import Stream from 'stream'
5
5
  import { performance } from 'perf_hooks'
6
6
 
7
- import { stringify, handleValue, addArrayType } from './types.js'
7
+ import { stringify, handleValue, addArrayType, clientSerialize } from './types.js'
8
8
  import { Errors, callback } from './errors.js'
9
9
  import Result from './result.js'
10
10
  import Queue from './queue.js'
@@ -214,7 +214,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
214
214
  const connection = {
215
215
  queue: queues.closed,
216
216
  idleTimer,
217
- connect: request => dispatch('acquire', request),
217
+ // Sentry's hook wraps connect and reads the connection from this, so it must stay a method
218
+ connect(request) {
219
+ dispatch('acquire', request)
220
+ },
218
221
  terminate: () => dispatch('terminate'),
219
222
  execute,
220
223
  release: () => dispatch('release'),
@@ -538,7 +541,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
538
541
 
539
542
  build(q)
540
543
  const written = write(toBuffer(q))
541
- if (q.describeFirst || q.cursorFn || mayCopy(q))
544
+ if (q.describeFirst || q.cursorFn || mayCopy(q) || (q.prepare && !q.prepared && !q.options.simple && q.parameters.length))
542
545
  barrier = q
543
546
  return written && accepting()
544
547
  } catch (error) {
@@ -576,7 +579,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
576
579
 
577
580
  function prepared(q) {
578
581
  return Buffer.concat([
579
- Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName),
582
+ Bind(q),
580
583
  q.cursorFn
581
584
  ? Execute('', q.cursorRows)
582
585
  : ExecuteUnnamed
@@ -604,8 +607,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
604
607
  q.signature = q.prepare && types + string
605
608
  q.onlyDescribe && (delete statements[q.signature])
606
609
  q.parameters = q.parameters || parameters
610
+ q.inferred = options.parameter_types === 'server' ? null : types.slice()
607
611
  q.prepared = q.prepare && q.signature in statements
608
- q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared)
612
+ q.describeFirst = q.onlyDescribe || (!q.prepared && types.some((x, i) =>
613
+ !x && (options.parameter_types !== 'client' || Array.isArray(q.parameters[i]))
614
+ ))
609
615
  q.statement = q.prepared
610
616
  ? statements[q.signature]
611
617
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
@@ -689,7 +695,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
689
695
  err = new Error(err)
690
696
 
691
697
  'query' in err || 'parameters' in err || Object.defineProperties(err, {
692
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
698
+ stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug, writable: true, configurable: true },
693
699
  query: { value: query.string, enumerable: options.debug },
694
700
  parameters: { value: query.parameters, enumerable: options.debug },
695
701
  args: { value: query.args, enumerable: options.debug },
@@ -1213,20 +1219,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1213
1219
  }
1214
1220
 
1215
1221
  /* Messages */
1216
- function Bind(parameters, types, statement = '', portal = '') {
1222
+ function Bind(q) {
1223
+ const differences = []
1217
1224
  let prev
1218
1225
  , type
1219
1226
 
1220
- b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length)
1227
+ b().B().str((q.cursorName || '') + b.N).str(q.statement.name + b.N).i16(0).i16(q.parameters.length)
1221
1228
 
1222
- parameters.forEach((x, i) => {
1229
+ q.parameters.forEach((x, i) => {
1223
1230
  if (x === null)
1224
1231
  return b.i32(0xFFFFFFFF)
1225
1232
 
1226
- type = types[i]
1227
- x = type in options.serializers
1228
- ? options.serializers[type](x)
1229
- : '' + x
1233
+ type = q.statement.types[i]
1234
+ x = !q.inferred || q.inferred[i] || Array.isArray(x)
1235
+ ? serialize(x, type)
1236
+ : options.parameter_types === 'client' ? clientSerialize(x) : compare(x, type, i, differences)
1230
1237
 
1231
1238
  prev = b.i
1232
1239
  b.inc(4).str(x).i32(b.i - prev - 4, prev)
@@ -1234,7 +1241,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1234
1241
 
1235
1242
  b.i16(0)
1236
1243
 
1237
- return b.end()
1244
+ const message = b.end()
1245
+ differences.length && callback(options.ontypemismatch || console.warn, { query: q.string, parameters: differences }) // eslint-disable-line
1246
+ return message
1247
+ }
1248
+
1249
+ function serialize(x, type) {
1250
+ return type in options.serializers
1251
+ ? options.serializers[type](x)
1252
+ : '' + x
1253
+ }
1254
+
1255
+ function compare(x, type, i, differences) {
1256
+ const serverText = serialize(x, type)
1257
+ let clientText
1258
+ try {
1259
+ clientText = clientSerialize(x)
1260
+ } catch (error) {
1261
+ clientText = error
1262
+ }
1263
+ serverText === clientText || differences.push({ position: i + 1, oid: type, typeof: typeof x, ...options.debug && { serverText, clientText } })
1264
+ return serverText
1238
1265
  }
1239
1266
 
1240
1267
  function Parse(str, parameters, types, name = '') {
package/src/index.js CHANGED
@@ -86,7 +86,8 @@ function Postgres(a, b) {
86
86
  listen,
87
87
  begin,
88
88
  close,
89
- end
89
+ end,
90
+ ...Symbol.asyncDispose && { [Symbol.asyncDispose]: () => end() }
90
91
  })
91
92
 
92
93
  return sql
@@ -327,10 +328,17 @@ function Postgres(a, b) {
327
328
 
328
329
  async function reserve() {
329
330
  const l = await lease(false)
330
- const sql = Sql(q => send(l, q))
331
+ const sql = Sql(handler)
331
332
  sql.release = () => settle(l, Lease.Released)
333
+ Symbol.dispose && (sql[Symbol.dispose] = () => sql.release())
334
+ Symbol.asyncDispose && (sql[Symbol.asyncDispose] = async() => sql.release())
332
335
  sql.stats = stats
333
336
  return sql
337
+
338
+ // dd-trace starts query spans only inside function declarations named handler
339
+ function handler(q) {
340
+ send(l, q)
341
+ }
334
342
  }
335
343
 
336
344
  async function begin(options, fn) {
@@ -347,7 +355,7 @@ function Postgres(a, b) {
347
355
  }
348
356
 
349
357
  async function transaction() {
350
- await Sql(q => send(l, q)).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
358
+ await Sql(handler).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
351
359
  e instanceof PostgresError && settle(l, Lease.Released)
352
360
  throw e
353
361
  })
@@ -358,7 +366,7 @@ function Postgres(a, b) {
358
366
  let result
359
367
  l.state = Lease.Settling
360
368
  try {
361
- result = await statement(Sql(q => send(l, q, Lease.Settling)))
369
+ result = await statement(Sql(q => handler(q, Lease.Settling)))
362
370
  } catch (e) {
363
371
  settle(l, Lease.Discarded)
364
372
  throw e
@@ -368,7 +376,7 @@ function Postgres(a, b) {
368
376
  }
369
377
 
370
378
  async function scope(fn, name) {
371
- const sql = Sql(handler)
379
+ const sql = Sql(track)
372
380
  sql.savepoint = savepoint
373
381
  sql.prepare = x => {
374
382
  if (typeof x !== 'string' || !x || x.includes('\0'))
@@ -422,11 +430,16 @@ function Postgres(a, b) {
422
430
  return scope(fn, 's' + savepoints++ + (name ? '_' + name : ''))
423
431
  }
424
432
 
425
- function handler(q) {
433
+ function track(q) {
426
434
  q.catch(e => uncaughtError || (uncaughtError = e))
427
- send(l, q)
435
+ handler(q)
428
436
  }
429
437
  }
438
+
439
+ // dd-trace starts query spans only inside function declarations named handler
440
+ function handler(q, expected) {
441
+ send(l, q, expected)
442
+ }
430
443
  }
431
444
 
432
445
  function move(c, queue) {
@@ -674,6 +687,7 @@ function parseOptions(a, b) {
674
687
  prepare : true,
675
688
  debug : false,
676
689
  fetch_types : true,
690
+ parameter_types : 'server',
677
691
  publications : 'alltables',
678
692
  target_session_attrs: null
679
693
  }
@@ -704,11 +718,13 @@ function parseOptions(a, b) {
704
718
  },
705
719
  types : o.types || {},
706
720
  target_session_attrs: tsa(o, url, env),
721
+ parameter_types : parameterTypes(o, url, env),
707
722
  onnotice : o.onnotice,
708
723
  onnotify : o.onnotify,
709
724
  onclose : o.onclose,
710
725
  onidle : o.onidle,
711
726
  onparameter : o.onparameter,
727
+ ontypemismatch : o.ontypemismatch,
712
728
  socket : o.socket,
713
729
  transform : parseTransform(o.transform || { undefined: undefined }),
714
730
  parameters : {},
@@ -728,6 +744,14 @@ function tsa(o, url, env) {
728
744
  throw new Error('target_session_attrs ' + x + ' is not supported')
729
745
  }
730
746
 
747
+ function parameterTypes(o, url, env) {
748
+ const x = o.parameter_types || url.searchParams.get('parameter_types') || env.PGPARAMETER_TYPES || 'server'
749
+ if (['server', 'client', 'warn'].includes(x))
750
+ return x
751
+
752
+ throw new Error('parameter_types ' + x + ' is not supported')
753
+ }
754
+
731
755
  function max_lifetime() {
732
756
  return 60 * (30 + Math.random() * 30)
733
757
  }
package/src/query.js CHANGED
@@ -23,16 +23,9 @@ export class Query extends Promise {
23
23
  this.state = null
24
24
  this.statement = null
25
25
 
26
- this.resolve = x => {
27
- this.active = false
28
- this.settled = true
29
- resolve(x)
30
- }
31
- this.reject = x => {
32
- this.active = false
33
- this.settled = true
34
- reject(x)
35
- }
26
+ // dd-trace's load-time rewrite only accepts single-expression arrows here
27
+ this.resolve = x => (this.active = false, this.settled = true, resolve(x))
28
+ this.reject = x => (this.active = false, this.settled = true, reject(x))
36
29
 
37
30
  this.active = false
38
31
  this.settled = false
package/src/types.js CHANGED
@@ -248,6 +248,11 @@ export const inferType = function inferType(x) {
248
248
  )
249
249
  }
250
250
 
251
+ export function clientSerialize(x) {
252
+ const proto = typeof x === 'object' && Object.getPrototypeOf(x)
253
+ return proto === Object.prototype || proto === null ? JSON.stringify(x) : '' + x
254
+ }
255
+
251
256
  const escapeBackslash = /\\/g
252
257
  const escapeQuote = /"/g
253
258
 
package/types/index.d.ts CHANGED
@@ -110,6 +110,10 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
110
110
  * @default true
111
111
  */
112
112
  fetch_types: boolean;
113
+ /** How values whose type cannot be told from the JavaScript value are serialized: 'server' (default) asks PostgreSQL for the placeholder's type first, 'client' sends their text for PostgreSQL to parse, 'warn' does the same as 'server' and reports values 'client' would send differently */
114
+ parameter_types: 'server' | 'client' | 'warn';
115
+ /** Called in parameter_types: 'warn' mode with each query whose values would be sent differently, console.warn by default; if it throws, the error is rethrown as an uncaught exception */
116
+ ontypemismatch: (report: postgres.TypeMismatchReport) => void;
113
117
  /**
114
118
  * Publications to subscribe to (only relevant when calling `sql.subscribe()`)
115
119
  * @default 'alltables'
@@ -411,6 +415,23 @@ declare namespace postgres {
411
415
  };
412
416
  }
413
417
 
418
+ interface TypeMismatchReport {
419
+ query: string;
420
+ parameters: TypeMismatch[];
421
+ }
422
+
423
+ interface TypeMismatch {
424
+ /** The `$n` number of the placeholder */
425
+ position: number;
426
+ /** The type OID PostgreSQL inferred for the placeholder */
427
+ oid: number;
428
+ typeof: string;
429
+ /** Only set when debug is enabled */
430
+ serverText?: string;
431
+ /** Only set when debug is enabled; the error is what the 'client' mode would throw */
432
+ clientText?: string | globalThis.Error;
433
+ }
434
+
414
435
  interface Notice {
415
436
  [field: string]: string;
416
437
  }
@@ -742,6 +763,9 @@ declare namespace postgres {
742
763
 
743
764
  end(options?: { timeout?: number | undefined } | undefined): Promise<void>;
744
765
 
766
+ /** Calls `end()` with no timeout when an `await using` scope exits */
767
+ [Symbol.asyncDispose](): Promise<void>;
768
+
745
769
  listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest;
746
770
 
747
771
  /** `onerror` receives replication connection errors and errors thrown while a change is decoded; such a change is skipped. */
@@ -767,6 +791,12 @@ declare namespace postgres {
767
791
 
768
792
  interface ReservedSql<TTypes extends Record<string, unknown> = {}> extends Sql<TTypes> {
769
793
  release(): void;
794
+
795
+ /** Calls `release()` when a `using` scope exits */
796
+ [Symbol.dispose](): void;
797
+
798
+ /** Calls `release()` when an `await using` scope exits */
799
+ [Symbol.asyncDispose](): Promise<void>;
770
800
  }
771
801
  }
772
802
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "devDependencies": {
3
- "@types/node": "^16"
3
+ "@types/node": "^22"
4
4
  }
5
5
  }