@steadybrew/postgresjs 4.3.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
@@ -1457,7 +1468,12 @@ If the only attempt times out, the error is `CONNECT_TIMEOUT`. Once earlier atte
1457
1468
  ##### COPY_IN_PROGRESS
1458
1469
  > You cannot execute queries during copy
1459
1470
 
1460
- 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`.
1461
1477
 
1462
1478
  ## TypeScript support
1463
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')]
@@ -226,6 +229,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
226
229
  owner: null,
227
230
  get active() { return query ? 1 : 0 },
228
231
  get pipelined() { return sent.length },
232
+ get accepting() { return accepting() },
233
+ get copying() { return stream !== null },
229
234
  get acquiring() { return acquisition && !acquisition.owner.cancelled ? 1 : 0 },
230
235
  count: 0,
231
236
  id
@@ -270,8 +275,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
270
275
  return !query && sent.length === 0
271
276
  }
272
277
 
278
+ function accepting() {
279
+ return !query || (!barrier && sent.length < max_pipeline)
280
+ }
281
+
273
282
  function backoffMs() {
274
- return (typeof backoff === 'function' ? backoff(options.shared.retries) : backoff) * 1000
283
+ return backoffSeconds(backoff, options.shared.retries) * 1000
275
284
  }
276
285
 
277
286
  function standbyPass() {
@@ -448,7 +457,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
448
457
  queryError(pending, err)
449
458
  pending.cancelled && pending.cancelled.resolve()
450
459
  }
451
- query = results = errorResponse = chunk = nextWriteTimer = null
460
+ query = results = errorResponse = chunk = nextWriteTimer = barrier = null
452
461
  result = new Result()
453
462
  rows = 0
454
463
  nonce = serverSignature = null
@@ -512,8 +521,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
512
521
  }
513
522
 
514
523
  function submit(q) {
515
- if (stream)
516
- 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
+ }
517
528
 
518
529
  if (q.cancelled)
519
530
  return true
@@ -527,17 +538,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
527
538
  : (query = q, query.active = true)
528
539
 
529
540
  build(q)
530
- return write(toBuffer(q))
531
- && !q.describeFirst
532
- && !q.cursorFn
533
- && 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()
534
545
  } catch (error) {
535
546
  sent.remove(q) || write(Sync)
536
547
  queryError(q, error)
537
- return sent.length < max_pipeline
548
+ return accepting()
538
549
  }
539
550
  }
540
551
 
552
+ function mayCopy(q) {
553
+ return q.options.simple && /\bcopy\b/i.test(q.string)
554
+ }
555
+
541
556
  function toBuffer(q) {
542
557
  if (q.parameters.length >= 65534)
543
558
  throw Errors.generic('MAX_PARAMETERS_EXCEEDED', 'Max number of parameters (65534) exceeded')
@@ -596,7 +611,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
596
611
  ? statements[q.signature]
597
612
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
598
613
 
599
- typeof options.debug === 'function' && options.debug(id, string, parameters, types)
614
+ typeof options.debug === 'function' && callback(options.debug, id, string, parameters, types)
600
615
  }
601
616
 
602
617
  function write(x, fn) {
@@ -767,18 +782,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
767
782
  backendParameters[k] = v
768
783
  if (options.parameters[k] !== v) {
769
784
  options.parameters[k] = v
770
- onparameter && onparameter(k, v)
785
+ callback(onparameter, k, v)
771
786
  }
772
787
  }
773
788
 
774
- function ReadyForQuery() {
789
+ function ReadyForQuery(x) {
790
+ query === barrier && (barrier = null)
775
791
  if (query) {
776
792
  if (errorResponse) {
777
793
  if (query.initialization)
778
794
  return dispatch('initFailed', errorResponse)
795
+ query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
779
796
  query.retried
780
797
  ? errored(query.retried)
781
- : 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))
782
800
  ? retry(query, errorResponse)
783
801
  : errored(errorResponse)
784
802
  } else {
@@ -1061,7 +1079,6 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1061
1079
  }
1062
1080
 
1063
1081
  function retry(q, error) {
1064
- delete statements[q.signature]
1065
1082
  q.retried = error
1066
1083
  execute(q)
1067
1084
  }
@@ -1072,7 +1089,8 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1072
1089
 
1073
1090
  let index = 9
1074
1091
  while (x[index++] !== 0);
1075
- onnotify(
1092
+ callback(
1093
+ onnotify,
1076
1094
  x.toString('utf8', 9, index - 1),
1077
1095
  x.toString('utf8', index, x.length - 1)
1078
1096
  )
@@ -1118,6 +1136,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1118
1136
  }
1119
1137
  })
1120
1138
  query.resolve(stream)
1139
+ connection.owner && connection.owner.next()
1121
1140
  }
1122
1141
 
1123
1142
  function CopyOutResponse() {
@@ -1125,6 +1144,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1125
1144
  read() { socket.resume() }
1126
1145
  })
1127
1146
  query.resolve(stream)
1147
+ connection.owner && connection.owner.next()
1128
1148
  }
1129
1149
 
1130
1150
  /* c8 ignore next 3 */
@@ -1147,6 +1167,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1147
1167
  }
1148
1168
  })
1149
1169
  query.resolve(stream)
1170
+ connection.owner && connection.owner.next()
1150
1171
  }
1151
1172
 
1152
1173
  function CopyData(x) {
@@ -1159,10 +1180,12 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1159
1180
  }
1160
1181
 
1161
1182
  function NoticeResponse(x) {
1183
+ if (onnotice === false)
1184
+ return
1185
+
1162
1186
  onnotice
1163
- ? onnotice(parseError(x))
1187
+ ? callback(onnotice, parseError(x))
1164
1188
  : console.log(parseError(x)) // eslint-disable-line
1165
-
1166
1189
  }
1167
1190
 
1168
1191
  /* c8 ignore next 3 */
@@ -1313,3 +1336,19 @@ function timer(fn, seconds) {
1313
1336
  fn.apply(null, args)
1314
1337
  }
1315
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
 
@@ -219,7 +220,7 @@ function Postgres(a, b) {
219
220
  })
220
221
  },
221
222
  onnotify(c, x) {
222
- 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))
223
224
  }
224
225
  }))
225
226
 
@@ -229,7 +230,7 @@ function Postgres(a, b) {
229
230
  if (exists) {
230
231
  channels[name].listeners.push(listener)
231
232
  const result = await channels[name].result
232
- listener.onlisten && listener.onlisten()
233
+ callback(listener.onlisten)
233
234
  return { state: result.state, unlisten }
234
235
  }
235
236
 
@@ -237,7 +238,7 @@ function Postgres(a, b) {
237
238
  sql.unsafe('"' + name.replace(/"/g, '""') + '"')
238
239
  }`, listeners: [listener] }
239
240
  const result = await channels[name].result
240
- listener.onlisten && listener.onlisten()
241
+ callback(listener.onlisten)
241
242
  return { state: result.state, unlisten }
242
243
 
243
244
  async function unlisten() {
@@ -295,9 +296,9 @@ function Postgres(a, b) {
295
296
  function send(l, q, expected = Lease.Active) {
296
297
  const c = l.connection
297
298
  l.state === expected
298
- ? c.queue === full
299
- ? (q.queue = l.queue, l.queue.push(q))
300
- : 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))
301
302
  : q.reject(leaseError(l.state))
302
303
  }
303
304
 
@@ -353,14 +354,16 @@ function Postgres(a, b) {
353
354
  }
354
355
 
355
356
  async function finish(statement) {
357
+ let result
356
358
  l.state = Lease.Settling
357
359
  try {
358
- await statement(Sql(q => send(l, q, Lease.Settling)))
360
+ result = await statement(Sql(q => send(l, q, Lease.Settling)))
359
361
  } catch (e) {
360
362
  settle(l, Lease.Discarded)
361
363
  throw e
362
364
  }
363
365
  settle(l, Lease.Released)
366
+ return result
364
367
  }
365
368
 
366
369
  async function scope(fn, name) {
@@ -375,7 +378,7 @@ function Postgres(a, b) {
375
378
  let uncaughtError
376
379
  , result
377
380
 
378
- name && await sql`savepoint ${ sql(name) }`
381
+ name && await sql.unsafe('savepoint ' + escapeIdentifier(name))
379
382
  try {
380
383
  result = await new Promise((resolve, reject) => {
381
384
  const x = fn(sql)
@@ -386,8 +389,8 @@ function Postgres(a, b) {
386
389
  throw uncaughtError
387
390
  } catch (e) {
388
391
  l.state === Lease.Active && await (name
389
- ? sql`rollback to ${ sql(name) }`
390
- : finish(final => final`rollback`)
392
+ ? sql.unsafe('rollback to ' + escapeIdentifier(name))
393
+ : finish(final => final.unsafe('rollback'))
391
394
  )
392
395
  throw e instanceof PostgresError && e.code === '25P02' && uncaughtError || e
393
396
  }
@@ -395,10 +398,18 @@ function Postgres(a, b) {
395
398
  if (l.state !== Lease.Active)
396
399
  throw l.error || leaseError(l.state)
397
400
 
398
- name || await finish(final => prepare
399
- ? final`prepare transaction ${ final.unsafe(prepare) }`
400
- : final`commit`
401
- )
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
+ }
402
413
 
403
414
  return result
404
415
 
@@ -427,14 +438,6 @@ function Postgres(a, b) {
427
438
  return c
428
439
  }
429
440
 
430
- function callback(fn, ...args) {
431
- try {
432
- fn && fn(...args)
433
- } catch (e) {
434
- queueMicrotask(() => { throw e })
435
- }
436
- }
437
-
438
441
  function json(x) {
439
442
  return new Parameter(x, 3802)
440
443
  }
@@ -457,11 +460,24 @@ function Postgres(a, b) {
457
460
  if (closed.length)
458
461
  return connect(closed.shift(), query)
459
462
 
460
- busy.length
461
- ? go(busy.shift(), query)
463
+ const c = takeBusy()
464
+ c
465
+ ? go(c, query)
462
466
  : queries.push(query)
463
467
  }
464
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
+
465
481
  function takeOpen() {
466
482
  while (open.length) {
467
483
  const c = open.shift()
@@ -546,6 +562,9 @@ function Postgres(a, b) {
546
562
  if (initial && initial.reserve)
547
563
  return initial.reserve(c)
548
564
 
565
+ if (!initial && !c.accepting)
566
+ return move(c, full)
567
+
549
568
  if (!initial && !queries.length)
550
569
  return move(c, open)
551
570
 
@@ -584,7 +603,7 @@ function Postgres(a, b) {
584
603
  function onclose(c, e) {
585
604
  move(c, closed)
586
605
  c.owner && settle(c.owner, Lease.Closed, e)
587
- options.onclose && options.onclose(c.id)
606
+ callback(options.onclose, c.id)
588
607
  if (ending)
589
608
  return closedWhileEnding(c, e)
590
609
 
@@ -639,7 +658,7 @@ function parseOptions(a, b) {
639
658
  connect_timeout : 30,
640
659
  max_lifetime : max_lifetime,
641
660
  max_pipeline : 100,
642
- backoff : backoff,
661
+ backoff : defaultBackoff,
643
662
  keep_alive : 60,
644
663
  prepare : true,
645
664
  debug : false,
@@ -698,10 +717,6 @@ function tsa(o, url, env) {
698
717
  throw new Error('target_session_attrs ' + x + ' is not supported')
699
718
  }
700
719
 
701
- function backoff(retries) {
702
- return (0.5 + Math.random() / 2) * Math.min(3 ** retries / 100, 20)
703
- }
704
-
705
720
  function max_lifetime() {
706
721
  return 60 * (30 + Math.random() * 30)
707
722
  }