@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 +232 -16
- package/cf/src/connection.js +41 -14
- package/cf/src/index.js +31 -7
- package/cf/src/query.js +3 -10
- package/cf/src/types.js +5 -0
- package/cjs/src/connection.js +41 -14
- package/cjs/src/index.js +31 -7
- package/cjs/src/query.js +3 -10
- package/cjs/src/types.js +5 -0
- package/package.json +3 -2
- package/src/connection.js +41 -14
- package/src/index.js +31 -7
- package/src/query.js +3 -10
- package/src/types.js +5 -0
- package/types/index.d.ts +30 -0
- package/types/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Postgres.js by Steady Brew
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
20
|
+
## Compatibility
|
|
21
|
+
|
|
22
|
+
[](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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
35
|
+
## Switching from Postgres.js
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
npm install postgres@npm:@steadybrew/postgresjs@^4
|
|
39
|
+
```
|
|
29
40
|
|
|
30
|
-
|
|
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
|
|
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
|
|
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
|
package/cf/src/connection.js
CHANGED
|
@@ -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
|
|
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
|
|
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 || (
|
|
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(
|
|
1224
|
+
function Bind(q) {
|
|
1225
|
+
const differences = []
|
|
1219
1226
|
let prev
|
|
1220
1227
|
, type
|
|
1221
1228
|
|
|
1222
|
-
b().B().str(
|
|
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 =
|
|
1230
|
-
?
|
|
1231
|
-
: ''
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =>
|
|
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(
|
|
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
|
|
434
|
+
function track(q) {
|
|
427
435
|
q.catch(e => uncaughtError || (uncaughtError = e))
|
|
428
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
package/cjs/src/connection.js
CHANGED
|
@@ -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
|
|
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
|
|
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 || (
|
|
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(
|
|
1222
|
+
function Bind(q) {
|
|
1223
|
+
const differences = []
|
|
1217
1224
|
let prev
|
|
1218
1225
|
, type
|
|
1219
1226
|
|
|
1220
|
-
b().B().str(
|
|
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 =
|
|
1228
|
-
?
|
|
1229
|
-
: ''
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =>
|
|
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(
|
|
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
|
|
433
|
+
function track(q) {
|
|
426
434
|
q.catch(e => uncaughtError || (uncaughtError = e))
|
|
427
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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.
|
|
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": ">=
|
|
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
|
|
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
|
|
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 || (
|
|
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(
|
|
1222
|
+
function Bind(q) {
|
|
1223
|
+
const differences = []
|
|
1217
1224
|
let prev
|
|
1218
1225
|
, type
|
|
1219
1226
|
|
|
1220
|
-
b().B().str(
|
|
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 =
|
|
1228
|
-
?
|
|
1229
|
-
: ''
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =>
|
|
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(
|
|
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
|
|
433
|
+
function track(q) {
|
|
426
434
|
q.catch(e => uncaughtError || (uncaughtError = e))
|
|
427
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
package/types/package.json
CHANGED