@steadybrew/postgresjs 4.2.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -592,6 +592,8 @@ for await (const chunk of readableStream) {
592
592
 
593
593
  > **NOTE** This is a low-level API which does not provide any type safety. To make this work, you must match your [`copy query` parameters](https://www.postgresql.org/docs/14/sql-copy.html) correctly to your [Node.js stream read or write](https://nodejs.org/api/stream.html) code. Ensure [Node.js stream backpressure](https://nodejs.org/en/learn/modules/backpressuring-in-streams) is handled correctly to avoid memory exhaustion.
594
594
 
595
+ A `COPY` holds its connection until it finishes. Postgres.js cannot tell that a simple-protocol query is a `COPY` until the server answers, so it sends nothing behind a simple query whose text contains the word `copy` (`sql.unsafe()` or `sql.file()` without parameters, `.simple()`, `.readable()`, `.writable()`). Queries sent through `sql` meanwhile run on another connection or wait for one. With `max: 1`, or when every connection is running a `COPY`, a query awaited before the `COPY` is ended therefore waits forever. Statements on a `sql.reserve()` or `sql.begin()` handle reject with `COPY_IN_PROGRESS` while its `COPY` runs.
596
+
595
597
  ### Canceling Queries in Progress
596
598
 
597
599
  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.
@@ -869,7 +871,7 @@ const sql = postgres({
869
871
  When you call `.listen`, a dedicated connection will be created to ensure that you receive notifications instantly. This connection will be used for any further calls to `.listen`. The connection will automatically reconnect according to a backoff reconnection pattern to not overload the database server.
870
872
 
871
873
  ### Listen `await sql.listen(channel, onnotify, [onlisten]) -> { state }`
872
- `.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active.
874
+ `.listen` takes the channel name, a function to handle each notify, and an optional function to run every time listen is registered and ready (happens on initial connect and reconnects). It returns a promise which resolves once the `LISTEN` query to Postgres completes, or if there is already a listener active. If either function throws, the error is rethrown as an uncaught exception, and the other listeners and the returned promise are not affected.
873
875
 
874
876
  ```js
875
877
  await sql.listen('news', payload => {
@@ -922,10 +924,17 @@ const { unsubscribe } = await sql.subscribe(
922
924
  },
923
925
  () => {
924
926
  // Callback on initial connect and potential reconnects
927
+ },
928
+ (error) => {
929
+ // Callback on connection errors and changes that could not be decoded
925
930
  }
926
931
  )
927
932
  ```
928
933
 
934
+ If any of these callbacks throws, the error is rethrown as an uncaught exception, and other subscribers and the replication stream are not affected.
935
+
936
+ `onerror` is called with errors of the replication connection, after which the subscription reconnects on a new replication slot and calls `onsubscribe` again (these are also logged with `console.error`, and changes committed while it was disconnected are not delivered; if the reconnect itself fails, it is retried with backoff, its error is logged with `console.error` and passed to `onerror`, and it never ends the process), and with errors thrown while a change is decoded by a [transform](#data-transformation) or a custom type parser. Such a change is skipped, and the replication stream and later changes carry on. A decode error goes only to the subscriptions that would have received the change, matched by operation and table; a subscription filtered by key gets every skipped change of its table, as the key is read from the decoded row, even if the table has no primary key. If a column name transform throws, each change of that table is skipped and reported until the server describes the table again, for example after `ALTER TABLE`. If one of the matching subscriptions has no `onerror`, the error is rethrown once as an uncaught exception, which ends the process under Node's default handling. `onerror` stays registered across reconnects until `unsubscribe()`.
937
+
929
938
  ### Subscribe pattern
930
939
 
931
940
  You can subscribe to specific operations, tables, or even rows with primary keys.
@@ -1051,6 +1060,8 @@ const sql = postgres('postgres://username:password@host:port/database', {
1051
1060
  })
1052
1061
  ```
1053
1062
 
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.
1064
+
1054
1065
  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.
1055
1066
 
1056
1067
  ### Dynamic passwords
@@ -1121,6 +1132,58 @@ There are no guarantees about queries executing in order unless using a transact
1121
1132
 
1122
1133
  Since this library automatically creates prepared statements, it also has a default max lifetime for connections to prevent memory bloat on the database itself. This is a random interval for each connection between 45 and 90 minutes. This allows multiple connections to independently come up and down without affecting the service.
1123
1134
 
1135
+ ### Pool stats
1136
+
1137
+ `sql.stats()` returns a snapshot of the pool as a plain object, ready to log, serialise from a health endpoint or map to metrics gauges:
1138
+
1139
+ ```js
1140
+ sql.stats()
1141
+ // { max: 10, totalCount: 7, idleCount: 2, waitingCount: 3,
1142
+ // activeCount: 4, availableCount: 5,
1143
+ // busyCount: 3, reservedCount: 1, connectingCount: 1, closingCount: 0,
1144
+ // executingCount: 4, listenCount: 1, subscribeCount: 0 }
1145
+ ```
1146
+
1147
+ | Field | Meaning |
1148
+ |---|---|
1149
+ | `max` | The `max` option |
1150
+ | `totalCount` | Connections that exist: connecting, idle or in use |
1151
+ | `idleCount` | Connections with a live session that are not in use. A connection released while its reserved handle still has queries in flight is idle with `executingCount` above 0 |
1152
+ | `waitingCount` | Issued queries and `reserve()` or `begin()` calls that are not running yet: those in the pool queue, those held by a connection that is still opening, those pipelined behind a connection's running query, and those a reserved handle or transaction has queued behind its own connection |
1153
+ | `activeCount` | `busyCount + reservedCount`, connections in use |
1154
+ | `availableCount` | `idleCount` plus the slots the pool can still open (`max - totalCount - closingCount`): connections a query can get without waiting behind other work |
1155
+ | `busyCount` | Pool connections running queries, including connections at the pipeline limit |
1156
+ | `reservedCount` | Connections held by `reserve()` or `begin()` |
1157
+ | `connectingCount` | Connections opening a session |
1158
+ | `closingCount` | Connections draining or closing. They are not in `totalCount`, but still hold a pool slot until closed |
1159
+ | `executingCount` | Queries the server is running right now, at most one per connection, on pool and reserved connections alike. It includes the queries the driver runs while a connection starts, and a `COPY` for its whole duration |
1160
+ | `listenCount` | Connections held by the internal `sql.listen()` pool (0 or 1) |
1161
+ | `subscribeCount` | Connections held by the internal `sql.subscribe()` pool (0 or 1) |
1162
+
1163
+ `totalCount` and `idleCount` have the same meaning as in node-postgres. `waitingCount` is node-postgres's `waitingCount` extended to queries pipelined on connections and queued in leases, and it includes queries sent with `sql` directly, as `pool.query()` does there. Once the pool is at `max`, Postgres.js pipelines new queries onto busy connections, so waiting work can sit on a connection rather than in the pool queue; `waitingCount` counts it too, so `executingCount + waitingCount` covers all issued work that has not finished. Every snapshot satisfies `totalCount === idleCount + busyCount + reservedCount + connectingCount` and `totalCount + closingCount <= max`, the remainder being slots the pool has not opened.
1164
+
1165
+ `listenCount` and `subscribeCount` are counted outside `max`, and the other fields only describe the `max` pool connections. A reserved connection's `stats()` returns the same snapshot, but `sql` inside `sql.begin()` and savepoints has no `stats()`, so call it on the main `sql`.
1166
+
1167
+ The pool is saturated when `waitingCount > 0 && availableCount === 0`: every connection is in use and more work is waiting. Saturation alone is not a reason to fail a health check, because a single pipelined query on a fully used pool, including a transaction pipelining its own statements, is enough to meet it. A health endpoint should compare `waitingCount` against a limit the application chooses:
1168
+
1169
+ ```js
1170
+ import http from 'node:http'
1171
+
1172
+ const MAX_WAITING = 100
1173
+
1174
+ http.createServer((req, res) => {
1175
+ const stats = sql.stats()
1176
+ const overloaded = stats.availableCount === 0 && stats.waitingCount > MAX_WAITING
1177
+
1178
+ res.writeHead(overloaded ? 503 : 200, { 'content-type': 'application/json' })
1179
+ res.end(JSON.stringify(stats))
1180
+ }).listen(8080)
1181
+ ```
1182
+
1183
+ How much waiting work is acceptable depends on the query durations and the latency the application can tolerate, so the limit has no universal value.
1184
+
1185
+ Change events and a running total of finished queries are planned together with the diagnostics events, so for now take snapshots on a timer or on request.
1186
+
1124
1187
  ### Connection timeout
1125
1188
 
1126
1189
  By default, connections will not close until `.end()` is called. However, it may be useful to have them close automatically when:
@@ -1405,7 +1468,12 @@ If the only attempt times out, the error is `CONNECT_TIMEOUT`. Once earlier atte
1405
1468
  ##### COPY_IN_PROGRESS
1406
1469
  > You cannot execute queries during copy
1407
1470
 
1408
- This error is thrown if trying to run a query during a copy operation (writable / readable).
1471
+ This error is thrown for a statement on a `sql.reserve()` or `sql.begin()` handle while its copy operation (writable / readable) runs, including statements sent before the server started it. Queries through `sql` wait instead.
1472
+
1473
+ ##### TRANSACTION_ROLLED_BACK
1474
+ > The server rolled the transaction back instead of committing it
1475
+
1476
+ This error is thrown by `sql.begin()` when the server answers the final `COMMIT` (or `PREPARE TRANSACTION`) with `ROLLBACK`, because the transaction was aborted by a statement whose error the callback did not surface, such as a failing query that was not awaited. Nothing was committed. When such a query's error was already recorded, it is the error's `cause`.
1409
1477
 
1410
1478
  ## TypeScript support
1411
1479
 
@@ -7,7 +7,7 @@ import Stream from 'node:stream'
7
7
  import { performance } from '../polyfills.js'
8
8
 
9
9
  import { stringify, handleValue, addArrayType } from './types.js'
10
- import { Errors } from './errors.js'
10
+ import { Errors, callback } from './errors.js'
11
11
  import Result from './result.js'
12
12
  import Queue from './queue.js'
13
13
  import clampedTimeout from './timeout.js'
@@ -15,7 +15,7 @@ import { SSLRequest, tlsConfig, cancelRequest } from './transport.js'
15
15
  import { Query, CLOSE } from './query.js'
16
16
  import b from './bytes.js'
17
17
 
18
- export default Connection
18
+ export { Connection }
19
19
 
20
20
  let uid = 1
21
21
 
@@ -28,6 +28,8 @@ const Sync = b().S().end()
28
28
  const Phase = { Closed: 0, Backoff: 1, Connecting: 2, Initializing: 3, Ready: 4, Draining: 5, Closing: 6 }
29
29
  const phaseNames = Object.keys(Phase)
30
30
 
31
+ const endsAbortedState = /^\s*(rollback|abort)\b/i
32
+
31
33
  const retryRoutines = new Set([
32
34
  'FetchPreparedStatement',
33
35
  'RevalidateCachedQuery',
@@ -112,6 +114,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
112
114
  , chunk = null
113
115
  , nonce = null
114
116
  , query = null
117
+ , barrier = null
115
118
  , final = null
116
119
 
117
120
  const check = globalThis[Symbol.for('postgres.js:check')]
@@ -224,6 +227,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
224
227
  },
225
228
  expired: () => idleTimer.expired() || lifeTimer.expired(),
226
229
  owner: null,
230
+ get active() { return query ? 1 : 0 },
231
+ get pipelined() { return sent.length },
232
+ get accepting() { return accepting() },
233
+ get copying() { return stream !== null },
234
+ get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
227
235
  count: 0,
228
236
  id
229
237
  }
@@ -267,8 +275,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
267
275
  return !query && sent.length === 0
268
276
  }
269
277
 
278
+ function accepting() {
279
+ return !query || (!barrier && sent.length < max_pipeline)
280
+ }
281
+
270
282
  function backoffMs() {
271
- return (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000
283
+ return backoffSeconds(backoff, options.shared.retries) * 1000
272
284
  }
273
285
 
274
286
  function standbyPass() {
@@ -445,7 +457,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
445
457
  queryError(pending, err)
446
458
  pending.cancelled && pending.cancelled.resolve()
447
459
  }
448
- query = results = errorResponse = chunk = nextWriteTimer = null
460
+ query = results = errorResponse = chunk = nextWriteTimer = barrier = null
449
461
  result = new Result()
450
462
  rows = 0
451
463
  nonce = serverSignature = null
@@ -509,8 +521,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
509
521
  }
510
522
 
511
523
  function submit(q) {
512
- if (stream)
513
- return queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
524
+ if (stream) {
525
+ queryError(q, Errors.generic('COPY_IN_PROGRESS', 'You cannot execute queries during copy'))
526
+ return true
527
+ }
514
528
 
515
529
  if (q.cancelled)
516
530
  return true
@@ -524,17 +538,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
524
538
  : (query = q, query.active = true)
525
539
 
526
540
  build(q)
527
- return write(toBuffer(q))
528
- && !q.describeFirst
529
- && !q.cursorFn
530
- && sent.length < max_pipeline
541
+ const written = write(toBuffer(q))
542
+ if (q.describeFirst || q.cursorFn || mayCopy(q))
543
+ barrier = q
544
+ return written && accepting()
531
545
  } catch (error) {
532
- sent.length === 0 && write(Sync)
533
- errored(error)
534
- return true
546
+ sent.remove(q) || write(Sync)
547
+ queryError(q, error)
548
+ return accepting()
535
549
  }
536
550
  }
537
551
 
552
+ function mayCopy(q) {
553
+ return q.options.simple && /\bcopy\b/i.test(q.string)
554
+ }
555
+
538
556
  function toBuffer(q) {
539
557
  if (q.parameters.length >= 65534)
540
558
  throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
@@ -593,7 +611,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
593
611
  ? statements[q.signature]
594
612
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
595
613
 
596
- typeof options.debug === 'function' && options.debug(id, string, parameters, types)
614
+ typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
597
615
  }
598
616
 
599
617
  function write(x, fn) {
@@ -764,18 +782,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
764
782
  backendParameters[k] = v
765
783
  if (options.parameters[k] !== v) {
766
784
  options.parameters[k] = v
767
- onparameter && onparameter(k, v)
785
+ callback(onparameter, k, v)
768
786
  }
769
787
  }
770
788
 
771
- function ReadyForQuery() {
789
+ function ReadyForQuery(x) {
790
+ query === barrier && (barrier = null)
772
791
  if (query) {
773
792
  if (errorResponse) {
774
793
  if (query.initialization)
775
794
  return dispatch('initFailed', errorResponse)
795
+ query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
776
796
  query.retried
777
797
  ? errored(query.retried)
778
- : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner
798
+ : query.prepared && retryRoutines.has(errorResponse.routine) && query.owner === connection.owner && !barrier
799
+ && (x[5] !== 69 || endsAbortedState.test(query.string))
779
800
  ? retry(query, errorResponse)
780
801
  : errored(errorResponse)
781
802
  } else {
@@ -1058,7 +1079,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1058
1079
  }
1059
1080
 
1060
1081
  function retry(q, error) {
1061
- delete statements[q.signature]
1062
1082
  q.retried = error
1063
1083
  execute(q)
1064
1084
  }
@@ -1069,7 +1089,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1069
1089
 
1070
1090
  let index = 9
1071
1091
  while (x[index++] !== 0);
1072
- onnotify(
1092
+ callback(
1093
+ onnotify,
1073
1094
  x.toString('utf8', 9, index - 1),
1074
1095
  x.toString('utf8', index, x.length - 1)
1075
1096
  )
@@ -1115,6 +1136,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1115
1136
  }
1116
1137
  })
1117
1138
  query.resolve(stream)
1139
+ connection.owner && connection.owner.next()
1118
1140
  }
1119
1141
 
1120
1142
  function CopyOutResponse() {
@@ -1122,6 +1144,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1122
1144
  read() { socket.resume() }
1123
1145
  })
1124
1146
  query.resolve(stream)
1147
+ connection.owner && connection.owner.next()
1125
1148
  }
1126
1149
 
1127
1150
  /* c8 ignore next 3 */
@@ -1144,6 +1167,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1144
1167
  }
1145
1168
  })
1146
1169
  query.resolve(stream)
1170
+ connection.owner && connection.owner.next()
1147
1171
  }
1148
1172
 
1149
1173
  function CopyData(x) {
@@ -1156,10 +1180,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1156
1180
  }
1157
1181
 
1158
1182
  function NoticeResponse(x) {
1183
+ if (onnotice === false)
1184
+ return
1185
+
1159
1186
  onnotice
1160
- ? onnotice(parseError(x))
1187
+ ? callback(onnotice, parseError(x))
1161
1188
  : console.log(parseError(x)) // eslint-disable-line
1162
-
1163
1189
  }
1164
1190
 
1165
1191
  /* c8 ignore next 3 */
@@ -1310,3 +1336,19 @@ function timer(fn, seconds) {
1310
1336
  fn.apply(null, args)
1311
1337
  }
1312
1338
  }
1339
+
1340
+ export function defaultBackoff(retries) {
1341
+ return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
1342
+ }
1343
+
1344
+ export function backoffSeconds(backoff, retries) {
1345
+ if (typeof backoff !== 'function')
1346
+ return backoff
1347
+
1348
+ try {
1349
+ return backoff(retries)
1350
+ } catch (e) {
1351
+ callback(() => { throw e })
1352
+ return defaultBackoff(retries)
1353
+ }
1354
+ }
package/cf/src/errors.js CHANGED
@@ -6,6 +6,14 @@ export class PostgresError extends Error {
6
6
  }
7
7
  }
8
8
 
9
+ export function callback(fn, ...args) {
10
+ try {
11
+ fn && fn(...args)
12
+ } catch (e) {
13
+ queueMicrotask(() => { throw e })
14
+ }
15
+ }
16
+
9
17
  export const Errors = {
10
18
  connection,
11
19
  postgres,
package/cf/src/index.js CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  Parameter,
10
10
  Identifier,
11
11
  Builder,
12
+ escapeIdentifier,
12
13
  toPascal,
13
14
  pascal,
14
15
  toCamel,
@@ -20,12 +21,12 @@ import {
20
21
  fromKebab
21
22
  } from './types.js'
22
23
 
23
- import Connection from './connection.js'
24
+ import { Connection, defaultBackoff } from './connection.js'
24
25
  import { cancelRequest } from './transport.js'
25
26
  import clampedTimeout from './timeout.js'
26
27
  import { Query, CLOSE } from './query.js'
27
28
  import Queue from './queue.js'
28
- import { Errors, PostgresError } from './errors.js'
29
+ import { Errors, PostgresError, callback } from './errors.js'
29
30
  import Subscribe from './subscribe.js'
30
31
  import largeObject from './large.js'
31
32
 
@@ -82,6 +83,7 @@ function Postgres(a, b) {
82
83
  PostgresError,
83
84
  options,
84
85
  reserve,
86
+ stats,
85
87
  listen,
86
88
  begin,
87
89
  close,
@@ -151,6 +153,56 @@ function Postgres(a, b) {
151
153
  }
152
154
  }
153
155
 
156
+ function stats() {
157
+ let idleCount = 0
158
+ , busyCount = 0
159
+ , reservedCount = 0
160
+ , connectingCount = 0
161
+ , closingCount = 0
162
+ , executingCount = 0
163
+ , waitingCount = queries.length
164
+
165
+ for (const c of connections) {
166
+ executingCount += c.active
167
+ waitingCount += c.pipelined + c.acquiring + (c.owner ? c.owner.queue.length : 0)
168
+ c.queue === open
169
+ ? idleCount++
170
+ : c.queue === connecting
171
+ ? connectingCount++
172
+ : c.queue === ended
173
+ ? closingCount++
174
+ : c.queue === reserved
175
+ ? reservedCount++
176
+ : (c.queue === busy || c.queue === full) && (c.owner ? reservedCount++ : busyCount++)
177
+ }
178
+
179
+ const totalCount = idleCount + busyCount + reservedCount + connectingCount
180
+
181
+ return {
182
+ max: options.max,
183
+ totalCount,
184
+ idleCount,
185
+ waitingCount,
186
+ activeCount: busyCount + reservedCount,
187
+ availableCount: idleCount + options.max - totalCount - closingCount,
188
+ busyCount,
189
+ reservedCount,
190
+ connectingCount,
191
+ closingCount,
192
+ executingCount,
193
+ listenCount: pooled(listen.sql),
194
+ subscribeCount: pooled(subscribe.sql)
195
+ }
196
+ }
197
+
198
+ function pooled(sql) {
199
+ if (!sql)
200
+ return 0
201
+
202
+ const s = sql.stats()
203
+ return s.totalCount + s.closingCount
204
+ }
205
+
154
206
  async function listen(name, fn, onlisten) {
155
207
  const listener = { fn, onlisten }
156
208
 
@@ -168,7 +220,7 @@ function Postgres(a, b) {
168
220
  })
169
221
  },
170
222
  onnotify(c, x) {
171
- c in listen.channels && listen.channels[c].listeners.forEach(l => l.fn(x))
223
+ c in listen.channels && listen.channels[c].listeners.forEach(l => callback(l.fn, x))
172
224
  }
173
225
  }))
174
226
 
@@ -178,7 +230,7 @@ function Postgres(a, b) {
178
230
  if (exists) {
179
231
  channels[name].listeners.push(listener)
180
232
  const result = await channels[name].result
181
- listener.onlisten && listener.onlisten()
233
+ callback(listener.onlisten)
182
234
  return { state: result.state, unlisten }
183
235
  }
184
236
 
@@ -186,7 +238,7 @@ function Postgres(a, b) {
186
238
  sql.unsafe('"' + name.replace(/"/g, '""') + '"')
187
239
  }`, listeners: [listener] }
188
240
  const result = await channels[name].result
189
- listener.onlisten && listener.onlisten()
241
+ callback(listener.onlisten)
190
242
  return { state: result.state, unlisten }
191
243
 
192
244
  async function unlisten() {
@@ -244,9 +296,9 @@ function Postgres(a, b) {
244
296
  function send(l, q, expected = Lease.Active) {
245
297
  const c = l.connection
246
298
  l.state === expected
247
- ? c.queue === full
248
- ? l.queue.push(q)
249
- : c.execute(q) || move(c, full)
299
+ ? c.copying || (c.queue !== full && c.accepting)
300
+ ? c.execute(q) || move(c, full)
301
+ : (move(c, full), q.queue = l.queue, l.queue.push(q))
250
302
  : q.reject(leaseError(l.state))
251
303
  }
252
304
 
@@ -276,6 +328,7 @@ function Postgres(a, b) {
276
328
  const l = await lease(false)
277
329
  const sql = Sql(q => send(l, q))
278
330
  sql.release = () => settle(l, Lease.Released)
331
+ sql.stats = stats
279
332
  return sql
280
333
  }
281
334
 
@@ -301,14 +354,16 @@ function Postgres(a, b) {
301
354
  }
302
355
 
303
356
  async function finish(statement) {
357
+ let result
304
358
  l.state = Lease.Settling
305
359
  try {
306
- await statement(Sql(q => send(l, q, Lease.Settling)))
360
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
307
361
  } catch (e) {
308
362
  settle(l, Lease.Discarded)
309
363
  throw e
310
364
  }
311
365
  settle(l, Lease.Released)
366
+ return result
312
367
  }
313
368
 
314
369
  async function scope(fn, name) {
@@ -323,7 +378,7 @@ function Postgres(a, b) {
323
378
  let uncaughtError
324
379
  , result
325
380
 
326
- name && await sql`savepoint ${ sql(name) }`
381
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
327
382
  try {
328
383
  result = await new Promise((resolve, reject) => {
329
384
  const x = fn(sql)
@@ -334,8 +389,8 @@ function Postgres(a, b) {
334
389
  throw uncaughtError
335
390
  } catch (e) {
336
391
  l.state === Lease.Active && await (name
337
- ? sql`rollback to ${ sql(name) }`
338
- : finish(final => final`rollback`)
392
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
393
+ : finish(final => final.unsafe('rollback'))
339
394
  )
340
395
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
341
396
  }
@@ -343,10 +398,18 @@ function Postgres(a, b) {
343
398
  if (l.state !== Lease.Active)
344
399
  throw l.error || leaseError(l.state)
345
400
 
346
- name || await finish(final => prepare
347
- ? final`prepare transaction ${ final.unsafe(prepare) }`
348
- : final`commit`
349
- )
401
+ if (!name) {
402
+ const done = await finish(final => prepare
403
+ ? final.unsafe('prepare transaction ' + prepare)
404
+ : final.unsafe('commit')
405
+ )
406
+ if (done.command === 'ROLLBACK') {
407
+ throw Object.assign(
408
+ Errors.generic('TRANSACTION_ROLLED_BACK', 'The server rolled the transaction back instead of committing it'),
409
+ uncaughtError && { cause: uncaughtError }
410
+ )
411
+ }
412
+ }
350
413
 
351
414
  return result
352
415
 
@@ -375,14 +438,6 @@ function Postgres(a, b) {
375
438
  return c
376
439
  }
377
440
 
378
- function callback(fn, ...args) {
379
- try {
380
- fn && fn(...args)
381
- } catch (e) {
382
- queueMicrotask(() => { throw e })
383
- }
384
- }
385
-
386
441
  function json(x) {
387
442
  return new Parameter(x, 3802)
388
443
  }
@@ -405,11 +460,24 @@ function Postgres(a, b) {
405
460
  if (closed.length)
406
461
  return connect(closed.shift(), query)
407
462
 
408
- busy.length
409
- ? go(busy.shift(), query)
463
+ const c = takeBusy()
464
+ c
465
+ ? go(c, query)
410
466
  : queries.push(query)
411
467
  }
412
468
 
469
+ function takeBusy() {
470
+ while (busy.length) {
471
+ const c = busy.shift()
472
+ if (c.accepting)
473
+ return c
474
+
475
+ move(c, full)
476
+ }
477
+
478
+ return null
479
+ }
480
+
413
481
  function takeOpen() {
414
482
  while (open.length) {
415
483
  const c = open.shift()
@@ -436,7 +504,7 @@ function Postgres(a, b) {
436
504
  if (query.state) {
437
505
  query.cancelled = { resolve, reject }
438
506
  } else {
439
- queries.remove(query)
507
+ (query.queue || queries).remove(query)
440
508
  query.cancelled = true
441
509
  query.reject(Errors.generic('57014', 'canceling statement due to user request'))
442
510
  resolve()
@@ -494,6 +562,9 @@ function Postgres(a, b) {
494
562
  if (initial && initial.reserve)
495
563
  return initial.reserve(c)
496
564
 
565
+ if (!initial && !c.accepting)
566
+ return move(c, full)
567
+
497
568
  if (!initial && !queries.length)
498
569
  return move(c, open)
499
570
 
@@ -532,7 +603,7 @@ function Postgres(a, b) {
532
603
  function onclose(c, e) {
533
604
  move(c, closed)
534
605
  c.owner && settle(c.owner, Lease.Closed, e)
535
- options.onclose && options.onclose(c.id)
606
+ callback(options.onclose, c.id)
536
607
  if (ending)
537
608
  return closedWhileEnding(c, e)
538
609
 
@@ -587,7 +658,7 @@ function parseOptions(a, b) {
587
658
  connect_timeout : 30,
588
659
  max_lifetime : max_lifetime,
589
660
  max_pipeline : 100,
590
- backoff : backoff,
661
+ backoff : defaultBackoff,
591
662
  keep_alive : 60,
592
663
  prepare : true,
593
664
  debug : false,
@@ -646,10 +717,6 @@ function tsa(o, url, env) {
646
717
  throw new Error('target_session_attrs ' + x + ' is not supported')
647
718
  }
648
719
 
649
- function backoff(retries) {
650
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
651
- }
652
-
653
720
  function max_lifetime() {
654
721
  return 60 * (30 + Math.random() * 30)
655
722
  }