@steadybrew/postgresjs 4.5.0-rc1 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -71,22 +71,25 @@ See the [changelog](./CHANGELOG.md#breaking-changes-and-migration) for the full
71
71
 
72
72
  ## Fixed Postgres.js issues
73
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.
74
+ Bugs and feature requests from [porsager/postgres](https://github.com/porsager/postgres) that this fork fixes, whether reported in an issue or in a pull request suggesting a fix, and the version that fixed them. Each fix has a test that reproduces the report, except [#1100](https://github.com/porsager/postgres/issues/1100), which rests on a production measurement, and [#1154](https://github.com/porsager/postgres/issues/1154), whose test reproduces the reported stack without a known trigger. The [changelog](./CHANGELOG.md) has the details.
75
75
 
76
76
  <details>
77
- <summary>40 bugs and 4 feature requests</summary>
77
+ <summary>54 bugs and 7 feature requests</summary>
78
78
 
79
79
  **Wrong results and data safety**
80
80
 
81
81
  | Issue | What happens now | Since |
82
82
  | --- | --- | --- |
83
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 |
84
+ | [#1216](https://github.com/porsager/postgres/issues/1216) | A transaction handle whose connection closed rejects with `CONNECTION_CLOSED` instead of running inside the next user's transaction, so it can't read their rows through row-level security. | 4.0.0 |
85
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 |
86
+ | [#1204](https://github.com/porsager/postgres/issues/1204) | A transaction's connection is not handed to another `sql.begin()` when its socket drains late, so one transaction's `COMMIT` can't commit another's unfinished writes. | 4.0.0 |
87
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
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
89
  | [#789](https://github.com/porsager/postgres/issues/789) | `sql.array()` infers the right array type on first use | 4.0.0 |
90
+ | [#1049](https://github.com/porsager/postgres/issues/1049), [#1124](https://github.com/porsager/postgres/issues/1124) | `NULL` elements in arrays parse as `null`, not `NaN` or the string `"NULL"` | 4.0.0 |
91
+ | [#1252](https://github.com/porsager/postgres/issues/1252) | Cancelling a pipelined query that hasn't started no longer gives the next query its result | 4.1.0 |
92
+ | [#1253](https://github.com/porsager/postgres/issues/1253), [#1181](https://github.com/porsager/postgres/issues/1181), [#1119](https://github.com/porsager/postgres/issues/1119) | A query no longer gets empty slots at the start of its result after the previous query on the connection failed or was cancelled partway through its rows | 4.5.0 |
90
93
 
91
94
  **Hangs**
92
95
 
@@ -96,40 +99,57 @@ Upstream issues on [porsager/postgres](https://github.com/porsager/postgres/issu
96
99
  | [#1195](https://github.com/porsager/postgres/issues/1195), [#1199](https://github.com/porsager/postgres/issues/1199) | A reservation survives its connection being terminated by the server, and the pool recovers after it is released | 4.0.0 |
97
100
  | [#1186](https://github.com/porsager/postgres/issues/1186) | Queries queued on a reserved or transaction handle settle when its connection closes | 4.0.0 |
98
101
  | [#1097](https://github.com/porsager/postgres/issues/1097), [#1130](https://github.com/porsager/postgres/issues/1130), [#1242](https://github.com/porsager/postgres/issues/1242), [#861](https://github.com/porsager/postgres/issues/861) | `sql.end()` resolves after a disconnect, after a failed query and after a connection closes inside `sql.begin()`, and waits for queries queued in the pool | 4.0.0 |
99
- | [#1246](https://github.com/porsager/postgres/issues/1246) | A query sent between a socket error and its close settles | 4.1.0 |
102
+ | [#1246](https://github.com/porsager/postgres/issues/1246) | A query sent between a socket error and its close settles, and a transaction reset mid-statement no longer loses a pool connection | 4.1.0 |
100
103
  | [#1193](https://github.com/porsager/postgres/issues/1193) | A connection closed during startup settles its first query instead of reconnecting in a tight loop | 4.0.0 |
101
104
  | [#1234](https://github.com/porsager/postgres/issues/1234) | Concurrent queries in `sql.begin()` after a column type change roll the transaction back and return the connection to the pool | 4.4.0 |
105
+ | [#1255](https://github.com/porsager/postgres/issues/1255), [#1261](https://github.com/porsager/postgres/issues/1261) | A reserved connection whose `max_lifetime` passed is closed when released, and `sql.end()` called before the release resolves | 4.0.0 |
106
+ | [#1016](https://github.com/porsager/postgres/pull/1016) | Queries no longer hang after a `COPY ... TO STDOUT` whose reader fell behind | 4.5.0 |
102
107
 
103
108
  **Crashes**
104
109
 
105
110
  | Issue | What happens now | Since |
106
111
  | --- | --- | --- |
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 |
112
+ | [#1154](https://github.com/porsager/postgres/issues/1154), [#1208](https://github.com/porsager/postgres/issues/1208), [#1133](https://github.com/porsager/postgres/issues/1133) | A reserved or transaction connection that the server or network closes rejects its later statements, and `sql.begin()`'s own `ROLLBACK` or `COMMIT`, with `CONNECTION_CLOSED` instead of crashing the process with `Cannot read properties of null (reading 'write')`. | 4.0.0 |
108
113
  | [#1192](https://github.com/porsager/postgres/issues/1192), [#1205](https://github.com/porsager/postgres/issues/1205), [#1086](https://github.com/porsager/postgres/issues/1086) | A failed or cancelled type query at startup rejects the query instead of crashing the process | 4.0.0 |
109
114
  | [#1088](https://github.com/porsager/postgres/issues/1088) | Timers no longer fail on Cloudflare Workers | 4.0.0 |
115
+ | [#1227](https://github.com/porsager/postgres/issues/1227) | Assigning `err.stack` on a query error no longer throws on Node 22 and newer | 4.5.0 |
116
+ | [#1009](https://github.com/porsager/postgres/pull/1009) | The `camel` and `pascal` transforms no longer throw on a column name ending with an underscore | 4.5.0 |
110
117
 
111
118
  **Reconnects and failover**
112
119
 
113
120
  | Issue | What happens now | Since |
114
121
  | --- | --- | --- |
115
122
  | [#1179](https://github.com/porsager/postgres/issues/1179) | Failed connection attempts back off instead of the whole pool retrying at once | 4.0.0 |
123
+ | [#1250](https://github.com/porsager/postgres/issues/1250) | A connection closed with a statement still buffered reconnects, instead of failing every later statement with `CONNECT_TIMEOUT` | 4.0.0 |
116
124
  | [#1174](https://github.com/porsager/postgres/issues/1174), [#815](https://github.com/porsager/postgres/issues/815), [#988](https://github.com/porsager/postgres/issues/988) | With several hosts, retries move to the next host, a connect timeout fails over, and `prefer-standby` falls back to a primary | 4.0.0 |
117
125
 
118
126
  **Transactions, notices and subscriptions**
119
127
 
120
128
  | Issue | What happens now | Since |
121
129
  | --- | --- | --- |
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 |
130
+ | [#823](https://github.com/porsager/postgres/issues/823), [#1189](https://github.com/porsager/postgres/issues/1189), [#1210](https://github.com/porsager/postgres/issues/1210) | `sql.begin()` no longer fails with `UNSAFE_TRANSACTION` or sends a second `BEGIN` on a busy connection when transactions and queries run concurrently, at the pipeline limit, under write backpressure or with `max_pipeline: 0` | 4.0.0 |
123
131
  | [#1063](https://github.com/porsager/postgres/issues/1063) | `onnotice: false` silences notices | 4.4.0 |
124
132
  | [#1087](https://github.com/porsager/postgres/issues/1087) | `sql.subscribe()` no longer adds a listener on each subscribe or loses changes after a transform throws | 4.4.0 |
125
133
 
134
+ **TypeScript**
135
+
136
+ | Issue | What happens now | Since |
137
+ | --- | --- | --- |
138
+ | [#1158](https://github.com/porsager/postgres/issues/1158), [#1177](https://github.com/porsager/postgres/pull/1177) | `sslnegotiation` is in the option types | 4.5.0 |
139
+ | [#968](https://github.com/porsager/postgres/pull/968) | `socket` is in the option types | 4.5.0 |
140
+ | [#703](https://github.com/porsager/postgres/issues/703), [#1194](https://github.com/porsager/postgres/pull/1194) | `new PostgresError('message')` sets the message, as with `Error`, and the types declare both the message and the fields forms | 4.5.0 |
141
+
126
142
  **Feature requests**
127
143
 
128
144
  | Issue | What you get | Since |
129
145
  | --- | --- | --- |
130
146
  | [#443](https://github.com/porsager/postgres/issues/443), [#908](https://github.com/porsager/postgres/issues/908) | `sql.stats()` reports connections in use and idle, and queries waiting | 4.3.0 |
131
147
  | [#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 |
148
+ | [#1164](https://github.com/porsager/postgres/issues/1164) | Built-in array types are decoded, and plain JavaScript arrays are sent, with `fetch_types: false` | 4.0.0 |
149
+ | [#886](https://github.com/porsager/postgres/issues/886), [#1094](https://github.com/porsager/postgres/issues/1094) | `await using` ends a pool and releases a reserved connection when it goes out of scope | 4.5.0 |
150
+ | [#1225](https://github.com/porsager/postgres/issues/1225) | Queries with values in one round trip instead of two, opt-in with `parameter_types: 'client'` | 4.5.0 |
151
+
152
+ Bun isn't tested here, so Bun-only reports such as [#1066](https://github.com/porsager/postgres/issues/1066) aren't listed; the same crash on Node is [#1208](https://github.com/porsager/postgres/issues/1208).
133
153
 
134
154
  </details>
135
155
 
@@ -693,11 +713,13 @@ A `COPY` holds its connection until it finishes. Postgres.js cannot tell that a
693
713
  Postgres.js supports, [canceling queries in progress](https://www.postgresql.org/docs/7.1/protocol-protocol.html#AEN39000). It works by opening a new connection with a protocol level startup message to cancel the current query running on a specific connection. That means there is no guarantee that the query will be canceled, and due to the possible race conditions it might even result in canceling another query. This is fine for long running queries, but in the case of high load and fast queries it might be better to simply ignore results instead of canceling.
694
714
 
695
715
  ```js
696
- const query = sql`select pg_sleep 100`.execute()
716
+ const query = sql`select pg_sleep(100)`.execute()
697
717
  setTimeout(() => query.cancel(), 100)
698
718
  const result = await query
699
719
  ```
700
720
 
721
+ A cancelled query rejects with code `57014`. A cancel can arrive too late, in which case the query may still complete, or fail with its own error. `query.cancel()` resolves once the cancel request has been sent. A cancelled query you don't await does not cause an unhandled rejection.
722
+
701
723
  ### Execute
702
724
 
703
725
  #### ```await sql``.execute()```
@@ -1244,8 +1266,8 @@ const sql = postgres('postgres://username:password@host:port/database', {
1244
1266
  application_name : 'postgres.js', // Default application_name
1245
1267
  ... // Other connection parameters, see https://www.postgresql.org/docs/current/runtime-config-client.html
1246
1268
  },
1247
- target_session_attrs : null, // Use 'read-write' with multiple hosts to
1248
- // ensure only connecting to primary
1269
+ target_session_attrs : null, // Which kind of server to connect to, see
1270
+ // Multi-host connections below
1249
1271
  fetch_types : true, // Automatically fetches types on connect
1250
1272
  // on initial connection.
1251
1273
  parameter_types : 'server', // 'client' or 'warn', see Parameter types (PGPARAMETER_TYPES)
@@ -1292,7 +1314,20 @@ Multiple connection strings can be passed to `postgres()` in the form of `postgr
1292
1314
 
1293
1315
  Connections will be attempted in order of the specified hosts/ports. On a successful connection, all retries will be reset. This ensures that hosts can come up and down seamlessly.
1294
1316
 
1295
- If you specify `target_session_attrs: 'primary'` or `PGTARGETSESSIONATTRS=primary` Postgres.js will only connect to the primary host, allowing for zero downtime failovers.
1317
+ `target_session_attrs` (or `PGTARGETSESSIONATTRS`) picks which kind of server to connect to. The values and their meaning are libpq's:
1318
+
1319
+ | Value | Connects to |
1320
+ |---|---|
1321
+ | `null` (default) | any server |
1322
+ | `'read-write'` | a server that accepts writes: not a hot standby, and `default_transaction_read_only` off |
1323
+ | `'primary'` | a server that is not a hot standby, even if `default_transaction_read_only` is on |
1324
+ | `'read-only'` | a hot standby, or a server with `default_transaction_read_only` on |
1325
+ | `'standby'` | a hot standby |
1326
+ | `'prefer-standby'` | a hot standby if any host is one, otherwise any server |
1327
+
1328
+ During a failover or an upgrade, `'read-write'` and `'primary'` wait for the promoted server instead of connecting to a standby whose writes would fail. Read traffic that should keep using standbys during the switch belongs on `'prefer-standby'` or no `target_session_attrs` at all.
1329
+
1330
+ With several hosts, a host that answers but is the wrong kind is skipped and the next one is tried at once; if every host answers and none matches, the connection fails immediately with `TARGET_SESSION_ATTRS`. With a single host, or when some host fails to connect, it keeps retrying, bounded by `connect_timeout` per host, because a failover may be in progress.
1296
1331
 
1297
1332
  ### The Connection Pool
1298
1333
 
@@ -106,6 +106,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
106
106
  , hostIndex = 0
107
107
  , length = 0
108
108
  , rows = 0
109
+ , copyPaused = false
109
110
  , serverSignature = null
110
111
  , nextWriteTimer = null
111
112
  , incomings = null
@@ -464,6 +465,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
464
465
  query = results = errorResponse = chunk = nextWriteTimer = barrier = null
465
466
  result = new Result()
466
467
  rows = 0
468
+ copyPaused = false
467
469
  nonce = serverSignature = null
468
470
  }
469
471
 
@@ -686,6 +688,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
686
688
 
687
689
  function errored(err) {
688
690
  stream && (stream.destroy(err), stream = null)
691
+ resumeCopy()
689
692
  query && queryError(query, err)
690
693
  }
691
694
 
@@ -802,7 +805,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
802
805
  query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
803
806
  query.retried
804
807
  ? errored(query.retried)
805
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
808
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier && !query.cancelled
806
809
  && (x[5] !== 69 || endsAbortedState.test(query.string))
807
810
  ? retry(query, errorResponse)
808
811
  : errored(errorResponse)
@@ -816,6 +819,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
816
819
 
817
820
  query = results = errorResponse = null
818
821
  result = new Result()
822
+ rows = 0
819
823
 
820
824
  dispatch('ready')
821
825
  }
@@ -1048,8 +1052,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1048
1052
 
1049
1053
  function mismatchReason(x, xs) {
1050
1054
  return (
1055
+ (x === 'read-write' && xs.in_hot_standby === 'on' && 'is a standby') ||
1051
1056
  (x === 'read-write' && xs.default_transaction_read_only === 'on' && 'is read-only') ||
1052
- (x === 'read-only' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1057
+ (x === 'read-only' && xs.in_hot_standby === 'off' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1053
1058
  (x === 'primary' && xs.in_hot_standby === 'on' && 'is a standby') ||
1054
1059
  (x === 'standby' && xs.in_hot_standby === 'off' && 'is a primary') ||
1055
1060
  (x === 'prefer-standby' && xs.in_hot_standby === 'off' && standbyPass() && 'is a primary') ||
@@ -1148,7 +1153,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1148
1153
 
1149
1154
  function CopyOutResponse() {
1150
1155
  stream = new Stream.Readable({
1151
- read() { socket.resume() }
1156
+ read() { copyPaused = false; socket.resume() }
1152
1157
  })
1153
1158
  query.resolve(stream)
1154
1159
  connection.owner && connection.owner.next()
@@ -1158,7 +1163,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1158
1163
  function CopyBothResponse() {
1159
1164
  stream = new Stream.Duplex({
1160
1165
  autoDestroy: true,
1161
- read() { socket.resume() },
1166
+ read() { copyPaused = false; socket.resume() },
1162
1167
  /* c8 ignore next 11 */
1163
1168
  write(chunk, encoding, callback) {
1164
1169
  socket.write(b().d().raw(chunk).end(), callback)
@@ -1178,12 +1183,23 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1178
1183
  }
1179
1184
 
1180
1185
  function CopyData(x) {
1181
- stream && (stream.push(x.subarray(5)) || socket.pause())
1186
+ if (stream && !stream.push(x.subarray(5))) {
1187
+ copyPaused = true
1188
+ socket.pause()
1189
+ }
1182
1190
  }
1183
1191
 
1184
1192
  function CopyDone() {
1185
1193
  stream && stream.push(null)
1186
1194
  stream = null
1195
+ resumeCopy()
1196
+ }
1197
+
1198
+ function resumeCopy() {
1199
+ if (copyPaused) {
1200
+ copyPaused = false
1201
+ socket.resume()
1202
+ }
1187
1203
  }
1188
1204
 
1189
1205
  function NoticeResponse(x) {
package/cf/src/errors.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export class PostgresError extends Error {
2
2
  constructor(x) {
3
- super(x.message)
3
+ super(typeof x === 'string' ? x : x.message)
4
4
  this.name = this.constructor.name
5
- Object.assign(this, x)
5
+ typeof x === 'string' || Object.assign(this, x)
6
6
  }
7
7
  }
8
8
 
package/cf/src/index.js CHANGED
@@ -703,8 +703,8 @@ function parseOptions(a, b) {
703
703
  ...Object.entries(defaults).reduce(
704
704
  (acc, [k, d]) => {
705
705
  const value = k in o ? o[k] : k in query
706
- ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k])
707
- : env['PG' + k.toUpperCase()] || d
706
+ ? off(query[k])
707
+ : env['PG' + k.toUpperCase()] ? off(env['PG' + k.toUpperCase()]) : d
708
708
  acc[k] = typeof value === 'string' && ints.includes(k)
709
709
  ? +value
710
710
  : value
@@ -737,6 +737,10 @@ function parseOptions(a, b) {
737
737
  return options
738
738
  }
739
739
 
740
+ function off(x) {
741
+ return x === 'disable' || x === 'false' ? false : x
742
+ }
743
+
740
744
  function tsa(o, url, env) {
741
745
  const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS
742
746
  if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x))
package/cf/src/query.js CHANGED
@@ -55,6 +55,7 @@ export class Query extends Promise {
55
55
  if (!this.canceller || this.settled)
56
56
  return Promise.resolve()
57
57
 
58
+ Promise.prototype.then.call(this, undefined, () => undefined)
58
59
  const cancelled = this.canceller(this)
59
60
  this.canceller = null
60
61
  cancelled.catch(() => undefined)
package/cf/src/types.js CHANGED
@@ -347,14 +347,14 @@ function arrayParserLoop(s, x, parser, typarray) {
347
347
  export const toCamel = x => {
348
348
  let str = x[0]
349
349
  for (let i = 1; i < x.length; i++)
350
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
350
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
351
351
  return str
352
352
  }
353
353
 
354
354
  export const toPascal = x => {
355
355
  let str = x[0].toUpperCase()
356
356
  for (let i = 1; i < x.length; i++)
357
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
357
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
358
358
  return str
359
359
  }
360
360
 
@@ -104,6 +104,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
104
104
  , hostIndex = 0
105
105
  , length = 0
106
106
  , rows = 0
107
+ , copyPaused = false
107
108
  , serverSignature = null
108
109
  , nextWriteTimer = null
109
110
  , incomings = null
@@ -462,6 +463,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
462
463
  query = results = errorResponse = chunk = nextWriteTimer = barrier = null
463
464
  result = new Result()
464
465
  rows = 0
466
+ copyPaused = false
465
467
  nonce = serverSignature = null
466
468
  }
467
469
 
@@ -684,6 +686,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
684
686
 
685
687
  function errored(err) {
686
688
  stream && (stream.destroy(err), stream = null)
689
+ resumeCopy()
687
690
  query && queryError(query, err)
688
691
  }
689
692
 
@@ -800,7 +803,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
800
803
  query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
801
804
  query.retried
802
805
  ? errored(query.retried)
803
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
806
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier && !query.cancelled
804
807
  && (x[5] !== 69 || endsAbortedState.test(query.string))
805
808
  ? retry(query, errorResponse)
806
809
  : errored(errorResponse)
@@ -814,6 +817,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
814
817
 
815
818
  query = results = errorResponse = null
816
819
  result = new Result()
820
+ rows = 0
817
821
 
818
822
  dispatch('ready')
819
823
  }
@@ -1046,8 +1050,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1046
1050
 
1047
1051
  function mismatchReason(x, xs) {
1048
1052
  return (
1053
+ (x === 'read-write' && xs.in_hot_standby === 'on' && 'is a standby') ||
1049
1054
  (x === 'read-write' && xs.default_transaction_read_only === 'on' && 'is read-only') ||
1050
- (x === 'read-only' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1055
+ (x === 'read-only' && xs.in_hot_standby === 'off' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1051
1056
  (x === 'primary' && xs.in_hot_standby === 'on' && 'is a standby') ||
1052
1057
  (x === 'standby' && xs.in_hot_standby === 'off' && 'is a primary') ||
1053
1058
  (x === 'prefer-standby' && xs.in_hot_standby === 'off' && standbyPass() && 'is a primary') ||
@@ -1146,7 +1151,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1146
1151
 
1147
1152
  function CopyOutResponse() {
1148
1153
  stream = new Stream.Readable({
1149
- read() { socket.resume() }
1154
+ read() { copyPaused = false; socket.resume() }
1150
1155
  })
1151
1156
  query.resolve(stream)
1152
1157
  connection.owner && connection.owner.next()
@@ -1156,7 +1161,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1156
1161
  function CopyBothResponse() {
1157
1162
  stream = new Stream.Duplex({
1158
1163
  autoDestroy: true,
1159
- read() { socket.resume() },
1164
+ read() { copyPaused = false; socket.resume() },
1160
1165
  /* c8 ignore next 11 */
1161
1166
  write(chunk, encoding, callback) {
1162
1167
  socket.write(b().d().raw(chunk).end(), callback)
@@ -1176,12 +1181,23 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1176
1181
  }
1177
1182
 
1178
1183
  function CopyData(x) {
1179
- stream && (stream.push(x.subarray(5)) || socket.pause())
1184
+ if (stream && !stream.push(x.subarray(5))) {
1185
+ copyPaused = true
1186
+ socket.pause()
1187
+ }
1180
1188
  }
1181
1189
 
1182
1190
  function CopyDone() {
1183
1191
  stream && stream.push(null)
1184
1192
  stream = null
1193
+ resumeCopy()
1194
+ }
1195
+
1196
+ function resumeCopy() {
1197
+ if (copyPaused) {
1198
+ copyPaused = false
1199
+ socket.resume()
1200
+ }
1185
1201
  }
1186
1202
 
1187
1203
  function NoticeResponse(x) {
package/cjs/src/errors.js CHANGED
@@ -1,8 +1,8 @@
1
1
  const PostgresError = module.exports.PostgresError = class PostgresError extends Error {
2
2
  constructor(x) {
3
- super(x.message)
3
+ super(typeof x === 'string' ? x : x.message)
4
4
  this.name = this.constructor.name
5
- Object.assign(this, x)
5
+ typeof x === 'string' || Object.assign(this, x)
6
6
  }
7
7
  }
8
8
 
package/cjs/src/index.js CHANGED
@@ -702,8 +702,8 @@ function parseOptions(a, b) {
702
702
  ...Object.entries(defaults).reduce(
703
703
  (acc, [k, d]) => {
704
704
  const value = k in o ? o[k] : k in query
705
- ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k])
706
- : env['PG' + k.toUpperCase()] || d
705
+ ? off(query[k])
706
+ : env['PG' + k.toUpperCase()] ? off(env['PG' + k.toUpperCase()]) : d
707
707
  acc[k] = typeof value === 'string' && ints.includes(k)
708
708
  ? +value
709
709
  : value
@@ -736,6 +736,10 @@ function parseOptions(a, b) {
736
736
  return options
737
737
  }
738
738
 
739
+ function off(x) {
740
+ return x === 'disable' || x === 'false' ? false : x
741
+ }
742
+
739
743
  function tsa(o, url, env) {
740
744
  const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS
741
745
  if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x))
package/cjs/src/query.js CHANGED
@@ -55,6 +55,7 @@ const Query = module.exports.Query = class Query extends Promise {
55
55
  if (!this.canceller || this.settled)
56
56
  return Promise.resolve()
57
57
 
58
+ Promise.prototype.then.call(this, undefined, () => undefined)
58
59
  const cancelled = this.canceller(this)
59
60
  this.canceller = null
60
61
  cancelled.catch(() => undefined)
package/cjs/src/types.js CHANGED
@@ -346,14 +346,14 @@ function arrayParserLoop(s, x, parser, typarray) {
346
346
  const toCamel = module.exports.toCamel = x => {
347
347
  let str = x[0]
348
348
  for (let i = 1; i < x.length; i++)
349
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
349
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
350
350
  return str
351
351
  }
352
352
 
353
353
  const toPascal = module.exports.toPascal = x => {
354
354
  let str = x[0].toUpperCase()
355
355
  for (let i = 1; i < x.length; i++)
356
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
356
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
357
357
  return str
358
358
  }
359
359
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steadybrew/postgresjs",
3
- "version": "4.5.0-rc1",
3
+ "version": "4.5.0",
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",
@@ -32,10 +32,10 @@
32
32
  "build:deno": "node transpile.deno.js",
33
33
  "build:cf": "node transpile.cf.js",
34
34
  "test": "npm run test:startup && npm run test:esm && npm run test:cjs && npm run test:deno",
35
- "test:esm": "node tests-startup/run.js --integration && node tests/index.js",
35
+ "test:esm": "node tests-startup/run.js --integration && node tests/index.js && node tests-startup/run.js --reports",
36
36
  "test:startup": "node tests-startup/run.js",
37
37
  "test:tracing": "node tests-tracing/run.js",
38
- "test:cjs": "npm run build:cjs && node tests-startup/run.js --integration --cjs && cd cjs/tests && node index.js && cd ../../",
38
+ "test:cjs": "npm run build:cjs && node tests-startup/run.js --integration --cjs && cd cjs/tests && node index.js && cd ../../ && node tests-startup/run.js --reports --cjs",
39
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 ../../",
40
40
  "lint": "eslint src && eslint tests && eslint tests-startup --ext .js,.mjs",
41
41
  "prepare": "npm run build",
package/src/connection.js CHANGED
@@ -104,6 +104,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
104
104
  , hostIndex = 0
105
105
  , length = 0
106
106
  , rows = 0
107
+ , copyPaused = false
107
108
  , serverSignature = null
108
109
  , nextWriteTimer = null
109
110
  , incomings = null
@@ -462,6 +463,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
462
463
  query = results = errorResponse = chunk = nextWriteTimer = barrier = null
463
464
  result = new Result()
464
465
  rows = 0
466
+ copyPaused = false
465
467
  nonce = serverSignature = null
466
468
  }
467
469
 
@@ -684,6 +686,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
684
686
 
685
687
  function errored(err) {
686
688
  stream && (stream.destroy(err), stream = null)
689
+ resumeCopy()
687
690
  query && queryError(query, err)
688
691
  }
689
692
 
@@ -800,7 +803,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
800
803
  query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
801
804
  query.retried
802
805
  ? errored(query.retried)
803
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
806
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier && !query.cancelled
804
807
  && (x[5] !== 69 || endsAbortedState.test(query.string))
805
808
  ? retry(query, errorResponse)
806
809
  : errored(errorResponse)
@@ -814,6 +817,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
814
817
 
815
818
  query = results = errorResponse = null
816
819
  result = new Result()
820
+ rows = 0
817
821
 
818
822
  dispatch('ready')
819
823
  }
@@ -1046,8 +1050,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1046
1050
 
1047
1051
  function mismatchReason(x, xs) {
1048
1052
  return (
1053
+ (x === 'read-write' && xs.in_hot_standby === 'on' && 'is a standby') ||
1049
1054
  (x === 'read-write' && xs.default_transaction_read_only === 'on' && 'is read-only') ||
1050
- (x === 'read-only' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1055
+ (x === 'read-only' && xs.in_hot_standby === 'off' && xs.default_transaction_read_only === 'off' && 'is read-write') ||
1051
1056
  (x === 'primary' && xs.in_hot_standby === 'on' && 'is a standby') ||
1052
1057
  (x === 'standby' && xs.in_hot_standby === 'off' && 'is a primary') ||
1053
1058
  (x === 'prefer-standby' && xs.in_hot_standby === 'off' && standbyPass() && 'is a primary') ||
@@ -1146,7 +1151,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1146
1151
 
1147
1152
  function CopyOutResponse() {
1148
1153
  stream = new Stream.Readable({
1149
- read() { socket.resume() }
1154
+ read() { copyPaused = false; socket.resume() }
1150
1155
  })
1151
1156
  query.resolve(stream)
1152
1157
  connection.owner && connection.owner.next()
@@ -1156,7 +1161,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1156
1161
  function CopyBothResponse() {
1157
1162
  stream = new Stream.Duplex({
1158
1163
  autoDestroy: true,
1159
- read() { socket.resume() },
1164
+ read() { copyPaused = false; socket.resume() },
1160
1165
  /* c8 ignore next 11 */
1161
1166
  write(chunk, encoding, callback) {
1162
1167
  socket.write(b().d().raw(chunk).end(), callback)
@@ -1176,12 +1181,23 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1176
1181
  }
1177
1182
 
1178
1183
  function CopyData(x) {
1179
- stream && (stream.push(x.subarray(5)) || socket.pause())
1184
+ if (stream && !stream.push(x.subarray(5))) {
1185
+ copyPaused = true
1186
+ socket.pause()
1187
+ }
1180
1188
  }
1181
1189
 
1182
1190
  function CopyDone() {
1183
1191
  stream && stream.push(null)
1184
1192
  stream = null
1193
+ resumeCopy()
1194
+ }
1195
+
1196
+ function resumeCopy() {
1197
+ if (copyPaused) {
1198
+ copyPaused = false
1199
+ socket.resume()
1200
+ }
1185
1201
  }
1186
1202
 
1187
1203
  function NoticeResponse(x) {
package/src/errors.js CHANGED
@@ -1,8 +1,8 @@
1
1
  export class PostgresError extends Error {
2
2
  constructor(x) {
3
- super(x.message)
3
+ super(typeof x === 'string' ? x : x.message)
4
4
  this.name = this.constructor.name
5
- Object.assign(this, x)
5
+ typeof x === 'string' || Object.assign(this, x)
6
6
  }
7
7
  }
8
8
 
package/src/index.js CHANGED
@@ -702,8 +702,8 @@ function parseOptions(a, b) {
702
702
  ...Object.entries(defaults).reduce(
703
703
  (acc, [k, d]) => {
704
704
  const value = k in o ? o[k] : k in query
705
- ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k])
706
- : env['PG' + k.toUpperCase()] || d
705
+ ? off(query[k])
706
+ : env['PG' + k.toUpperCase()] ? off(env['PG' + k.toUpperCase()]) : d
707
707
  acc[k] = typeof value === 'string' && ints.includes(k)
708
708
  ? +value
709
709
  : value
@@ -736,6 +736,10 @@ function parseOptions(a, b) {
736
736
  return options
737
737
  }
738
738
 
739
+ function off(x) {
740
+ return x === 'disable' || x === 'false' ? false : x
741
+ }
742
+
739
743
  function tsa(o, url, env) {
740
744
  const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS
741
745
  if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x))
package/src/query.js CHANGED
@@ -55,6 +55,7 @@ export class Query extends Promise {
55
55
  if (!this.canceller || this.settled)
56
56
  return Promise.resolve()
57
57
 
58
+ Promise.prototype.then.call(this, undefined, () => undefined)
58
59
  const cancelled = this.canceller(this)
59
60
  this.canceller = null
60
61
  cancelled.catch(() => undefined)
package/src/types.js CHANGED
@@ -346,14 +346,14 @@ function arrayParserLoop(s, x, parser, typarray) {
346
346
  export const toCamel = x => {
347
347
  let str = x[0]
348
348
  for (let i = 1; i < x.length; i++)
349
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
349
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
350
350
  return str
351
351
  }
352
352
 
353
353
  export const toPascal = x => {
354
354
  let str = x[0].toUpperCase()
355
355
  for (let i = 1; i < x.length; i++)
356
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
356
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
357
357
  return str
358
358
  }
359
359
 
package/types/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Readable, Writable } from 'node:stream'
1
+ import { Duplex, Readable, Writable } from 'node:stream'
2
2
 
3
3
  /**
4
4
  * Establish a connection to a PostgreSQL server.
@@ -45,6 +45,8 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
45
45
  * @default false
46
46
  */
47
47
  ssl: 'require' | 'allow' | 'prefer' | 'verify-full' | boolean | object;
48
+ /** Set to 'direct' to start TLS immediately with ALPN instead of sending an SSLRequest first */
49
+ sslnegotiation: 'direct' | 'postgres' | null;
48
50
  /**
49
51
  * Max number of connections
50
52
  * @default 10
@@ -126,6 +128,8 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
126
128
  backoff: boolean | ((attemptNum: number) => number);
127
129
  max_lifetime: number | null;
128
130
  keep_alive: number | null;
131
+ /** Function, sync or async, returning the socket-like duplex stream to connect through; it receives the parsed options */
132
+ socket?: (options: postgres.ParsedOptions) => Duplex | Promise<Duplex>;
129
133
  }
130
134
 
131
135
 
@@ -203,6 +207,8 @@ type Return<T, K extends readonly any[]> =
203
207
 
204
208
  declare namespace postgres {
205
209
  class PostgresError extends Error {
210
+ constructor(message: string);
211
+ constructor(fields: Partial<Omit<PostgresError, 'name' | 'stack' | 'cause'>>);
206
212
  name: 'PostgresError';
207
213
  severity_local: string;
208
214
  severity: string;