@steadybrew/postgresjs 4.4.1 → 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/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'
@@ -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
@@ -214,7 +215,10 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
214
215
  const connection = {
215
216
  queue: queues.closed,
216
217
  idleTimer,
217
- connect: request => dispatch('acquire', request),
218
+ // Sentry's hook wraps connect and reads the connection from this, so it must stay a method
219
+ connect(request) {
220
+ dispatch('acquire', request)
221
+ },
218
222
  terminate: () => dispatch('terminate'),
219
223
  execute,
220
224
  release: () => dispatch('release'),
@@ -459,6 +463,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
459
463
  query = results = errorResponse = chunk = nextWriteTimer = barrier = null
460
464
  result = new Result()
461
465
  rows = 0
466
+ copyPaused = false
462
467
  nonce = serverSignature = null
463
468
  }
464
469
 
@@ -538,7 +543,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
538
543
 
539
544
  build(q)
540
545
  const written = write(toBuffer(q))
541
- 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))
542
547
  barrier = q
543
548
  return written && accepting()
544
549
  } catch (error) {
@@ -576,7 +581,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
576
581
 
577
582
  function prepared(q) {
578
583
  return Buffer.concat([
579
- Bind(q.parameters, q.statement.types, q.statement.name, q.cursorName),
584
+ Bind(q),
580
585
  q.cursorFn
581
586
  ? Execute('', q.cursorRows)
582
587
  : ExecuteUnnamed
@@ -604,8 +609,11 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
604
609
  q.signature = q.prepare && types + string
605
610
  q.onlyDescribe && (delete statements[q.signature])
606
611
  q.parameters = q.parameters || parameters
612
+ q.inferred = options.parameter_types === 'server' ? null : types.slice()
607
613
  q.prepared = q.prepare && q.signature in statements
608
- q.describeFirst = q.onlyDescribe || (parameters.length && !q.prepared)
614
+ q.describeFirst = q.onlyDescribe || (!q.prepared && types.some((x, i) =>
615
+ !x && (options.parameter_types !== 'client' || Array.isArray(q.parameters[i]))
616
+ ))
609
617
  q.statement = q.prepared
610
618
  ? statements[q.signature]
611
619
  : { string, types, name: q.prepare ? statementId + statementCount++ : '' }
@@ -678,6 +686,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
678
686
 
679
687
  function errored(err) {
680
688
  stream && (stream.destroy(err), stream = null)
689
+ resumeCopy()
681
690
  query && queryError(query, err)
682
691
  }
683
692
 
@@ -689,7 +698,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
689
698
  err = new Error(err)
690
699
 
691
700
  'query' in err || 'parameters' in err || Object.defineProperties(err, {
692
- stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug },
701
+ stack: { value: err.stack + query.origin.replace(/.*\n/, '\n'), enumerable: options.debug, writable: true, configurable: true },
693
702
  query: { value: query.string, enumerable: options.debug },
694
703
  parameters: { value: query.parameters, enumerable: options.debug },
695
704
  args: { value: query.args, enumerable: options.debug },
@@ -794,7 +803,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
794
803
  query.prepared && retryRoutines.has(errorResponse.routine) && delete statements[query.signature]
795
804
  query.retried
796
805
  ? errored(query.retried)
797
- : 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
798
807
  && (x[5] !== 69 || endsAbortedState.test(query.string))
799
808
  ? retry(query, errorResponse)
800
809
  : errored(errorResponse)
@@ -808,6 +817,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
808
817
 
809
818
  query = results = errorResponse = null
810
819
  result = new Result()
820
+ rows = 0
811
821
 
812
822
  dispatch('ready')
813
823
  }
@@ -1040,8 +1050,9 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1040
1050
 
1041
1051
  function mismatchReason(x, xs) {
1042
1052
  return (
1053
+ (x === 'read-write' && xs.in_hot_standby === 'on' && 'is a standby') ||
1043
1054
  (x === 'read-write' && xs.default_transaction_read_only === 'on' && 'is read-only') ||
1044
- (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') ||
1045
1056
  (x === 'primary' && xs.in_hot_standby === 'on' && 'is a standby') ||
1046
1057
  (x === 'standby' && xs.in_hot_standby === 'off' && 'is a primary') ||
1047
1058
  (x === 'prefer-standby' && xs.in_hot_standby === 'off' && standbyPass() && 'is a primary') ||
@@ -1140,7 +1151,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1140
1151
 
1141
1152
  function CopyOutResponse() {
1142
1153
  stream = new Stream.Readable({
1143
- read() { socket.resume() }
1154
+ read() { copyPaused = false; socket.resume() }
1144
1155
  })
1145
1156
  query.resolve(stream)
1146
1157
  connection.owner && connection.owner.next()
@@ -1150,7 +1161,7 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1150
1161
  function CopyBothResponse() {
1151
1162
  stream = new Stream.Duplex({
1152
1163
  autoDestroy: true,
1153
- read() { socket.resume() },
1164
+ read() { copyPaused = false; socket.resume() },
1154
1165
  /* c8 ignore next 11 */
1155
1166
  write(chunk, encoding, callback) {
1156
1167
  socket.write(b().d().raw(chunk).end(), callback)
@@ -1170,12 +1181,23 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1170
1181
  }
1171
1182
 
1172
1183
  function CopyData(x) {
1173
- stream && (stream.push(x.subarray(5)) || socket.pause())
1184
+ if (stream && !stream.push(x.subarray(5))) {
1185
+ copyPaused = true
1186
+ socket.pause()
1187
+ }
1174
1188
  }
1175
1189
 
1176
1190
  function CopyDone() {
1177
1191
  stream && stream.push(null)
1178
1192
  stream = null
1193
+ resumeCopy()
1194
+ }
1195
+
1196
+ function resumeCopy() {
1197
+ if (copyPaused) {
1198
+ copyPaused = false
1199
+ socket.resume()
1200
+ }
1179
1201
  }
1180
1202
 
1181
1203
  function NoticeResponse(x) {
@@ -1213,20 +1235,21 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1213
1235
  }
1214
1236
 
1215
1237
  /* Messages */
1216
- function Bind(parameters, types, statement = '', portal = '') {
1238
+ function Bind(q) {
1239
+ const differences = []
1217
1240
  let prev
1218
1241
  , type
1219
1242
 
1220
- b().B().str(portal + b.N).str(statement + b.N).i16(0).i16(parameters.length)
1243
+ b().B().str((q.cursorName || '') + b.N).str(q.statement.name + b.N).i16(0).i16(q.parameters.length)
1221
1244
 
1222
- parameters.forEach((x, i) => {
1245
+ q.parameters.forEach((x, i) => {
1223
1246
  if (x === null)
1224
1247
  return b.i32(0xFFFFFFFF)
1225
1248
 
1226
- type = types[i]
1227
- x = type in options.serializers
1228
- ? options.serializers[type](x)
1229
- : '' + x
1249
+ type = q.statement.types[i]
1250
+ x = !q.inferred || q.inferred[i] || Array.isArray(x)
1251
+ ? serialize(x, type)
1252
+ : options.parameter_types === 'client' ? clientSerialize(x) : compare(x, type, i, differences)
1230
1253
 
1231
1254
  prev = b.i
1232
1255
  b.inc(4).str(x).i32(b.i - prev - 4, prev)
@@ -1234,7 +1257,27 @@ function Connection(options, queues = {}, { onopen = noop, onend = noop, onclose
1234
1257
 
1235
1258
  b.i16(0)
1236
1259
 
1237
- return b.end()
1260
+ const message = b.end()
1261
+ differences.length && callback(options.ontypemismatch || console.warn, { query: q.string, parameters: differences }) // eslint-disable-line
1262
+ return message
1263
+ }
1264
+
1265
+ function serialize(x, type) {
1266
+ return type in options.serializers
1267
+ ? options.serializers[type](x)
1268
+ : '' + x
1269
+ }
1270
+
1271
+ function compare(x, type, i, differences) {
1272
+ const serverText = serialize(x, type)
1273
+ let clientText
1274
+ try {
1275
+ clientText = clientSerialize(x)
1276
+ } catch (error) {
1277
+ clientText = error
1278
+ }
1279
+ serverText === clientText || differences.push({ position: i + 1, oid: type, typeof: typeof x, ...options.debug && { serverText, clientText } })
1280
+ return serverText
1238
1281
  }
1239
1282
 
1240
1283
  function Parse(str, parameters, types, name = '') {
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
@@ -86,7 +86,8 @@ function Postgres(a, b) {
86
86
  listen,
87
87
  begin,
88
88
  close,
89
- end
89
+ end,
90
+ ...Symbol.asyncDispose && { [Symbol.asyncDispose]: () => end() }
90
91
  })
91
92
 
92
93
  return sql
@@ -327,10 +328,17 @@ function Postgres(a, b) {
327
328
 
328
329
  async function reserve() {
329
330
  const l = await lease(false)
330
- const sql = Sql(q => send(l, q))
331
+ const sql = Sql(handler)
331
332
  sql.release = () => settle(l, Lease.Released)
333
+ Symbol.dispose && (sql[Symbol.dispose] = () => sql.release())
334
+ Symbol.asyncDispose && (sql[Symbol.asyncDispose] = async() => sql.release())
332
335
  sql.stats = stats
333
336
  return sql
337
+
338
+ // dd-trace starts query spans only inside function declarations named handler
339
+ function handler(q) {
340
+ send(l, q)
341
+ }
334
342
  }
335
343
 
336
344
  async function begin(options, fn) {
@@ -347,7 +355,7 @@ function Postgres(a, b) {
347
355
  }
348
356
 
349
357
  async function transaction() {
350
- await Sql(q => send(l, q)).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
358
+ await Sql(handler).unsafe('begin ' + options.replace(/[^a-z ]/ig, ''), []).execute().catch(e => {
351
359
  e instanceof PostgresError && settle(l, Lease.Released)
352
360
  throw e
353
361
  })
@@ -358,7 +366,7 @@ function Postgres(a, b) {
358
366
  let result
359
367
  l.state = Lease.Settling
360
368
  try {
361
- result = await statement(Sql(q => send(l, q, Lease.Settling)))
369
+ result = await statement(Sql(q => handler(q, Lease.Settling)))
362
370
  } catch (e) {
363
371
  settle(l, Lease.Discarded)
364
372
  throw e
@@ -368,7 +376,7 @@ function Postgres(a, b) {
368
376
  }
369
377
 
370
378
  async function scope(fn, name) {
371
- const sql = Sql(handler)
379
+ const sql = Sql(track)
372
380
  sql.savepoint = savepoint
373
381
  sql.prepare = x => {
374
382
  if (typeof x !== 'string' || !x || x.includes('\0'))
@@ -422,11 +430,16 @@ function Postgres(a, b) {
422
430
  return scope(fn, 's' + savepoints++ + (name ? '_' + name : ''))
423
431
  }
424
432
 
425
- function handler(q) {
433
+ function track(q) {
426
434
  q.catch(e => uncaughtError || (uncaughtError = e))
427
- send(l, q)
435
+ handler(q)
428
436
  }
429
437
  }
438
+
439
+ // dd-trace starts query spans only inside function declarations named handler
440
+ function handler(q, expected) {
441
+ send(l, q, expected)
442
+ }
430
443
  }
431
444
 
432
445
  function move(c, queue) {
@@ -674,6 +687,7 @@ function parseOptions(a, b) {
674
687
  prepare : true,
675
688
  debug : false,
676
689
  fetch_types : true,
690
+ parameter_types : 'server',
677
691
  publications : 'alltables',
678
692
  target_session_attrs: null
679
693
  }
@@ -688,8 +702,8 @@ function parseOptions(a, b) {
688
702
  ...Object.entries(defaults).reduce(
689
703
  (acc, [k, d]) => {
690
704
  const value = k in o ? o[k] : k in query
691
- ? (query[k] === 'disable' || query[k] === 'false' ? false : query[k])
692
- : env['PG' + k.toUpperCase()] || d
705
+ ? off(query[k])
706
+ : env['PG' + k.toUpperCase()] ? off(env['PG' + k.toUpperCase()]) : d
693
707
  acc[k] = typeof value === 'string' && ints.includes(k)
694
708
  ? +value
695
709
  : value
@@ -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 : {},
@@ -720,6 +736,10 @@ function parseOptions(a, b) {
720
736
  return options
721
737
  }
722
738
 
739
+ function off(x) {
740
+ return x === 'disable' || x === 'false' ? false : x
741
+ }
742
+
723
743
  function tsa(o, url, env) {
724
744
  const x = o.target_session_attrs || url.searchParams.get('target_session_attrs') || env.PGTARGETSESSIONATTRS
725
745
  if (!x || ['read-write', 'read-only', 'primary', 'standby', 'prefer-standby'].includes(x))
@@ -728,6 +748,14 @@ function tsa(o, url, env) {
728
748
  throw new Error('target_session_attrs ' + x + ' is not supported')
729
749
  }
730
750
 
751
+ function parameterTypes(o, url, env) {
752
+ const x = o.parameter_types || url.searchParams.get('parameter_types') || env.PGPARAMETER_TYPES || 'server'
753
+ if (['server', 'client', 'warn'].includes(x))
754
+ return x
755
+
756
+ throw new Error('parameter_types ' + x + ' is not supported')
757
+ }
758
+
731
759
  function max_lifetime() {
732
760
  return 60 * (30 + Math.random() * 30)
733
761
  }
package/src/query.js CHANGED
@@ -23,16 +23,9 @@ export class Query extends Promise {
23
23
  this.state = null
24
24
  this.statement = null
25
25
 
26
- this.resolve = x => {
27
- this.active = false
28
- this.settled = true
29
- resolve(x)
30
- }
31
- this.reject = x => {
32
- this.active = false
33
- this.settled = true
34
- reject(x)
35
- }
26
+ // dd-trace's load-time rewrite only accepts single-expression arrows here
27
+ this.resolve = x => (this.active = false, this.settled = true, resolve(x))
28
+ this.reject = x => (this.active = false, this.settled = true, reject(x))
36
29
 
37
30
  this.active = false
38
31
  this.settled = false
@@ -62,6 +55,7 @@ export class Query extends Promise {
62
55
  if (!this.canceller || this.settled)
63
56
  return Promise.resolve()
64
57
 
58
+ Promise.prototype.then.call(this, undefined, () => undefined)
65
59
  const cancelled = this.canceller(this)
66
60
  this.canceller = null
67
61
  cancelled.catch(() => undefined)
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
 
@@ -341,14 +346,14 @@ function arrayParserLoop(s, x, parser, typarray) {
341
346
  export const toCamel = x => {
342
347
  let str = x[0]
343
348
  for (let i = 1; i < x.length; i++)
344
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
349
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
345
350
  return str
346
351
  }
347
352
 
348
353
  export const toPascal = x => {
349
354
  let str = x[0].toUpperCase()
350
355
  for (let i = 1; i < x.length; i++)
351
- str += x[i] === '_' ? x[++i].toUpperCase() : x[i]
356
+ str += x[i] === '_' && i + 1 < x.length ? x[++i].toUpperCase() : x[i]
352
357
  return str
353
358
  }
354
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
@@ -110,6 +112,10 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
110
112
  * @default true
111
113
  */
112
114
  fetch_types: boolean;
115
+ /** 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 */
116
+ parameter_types: 'server' | 'client' | 'warn';
117
+ /** 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 */
118
+ ontypemismatch: (report: postgres.TypeMismatchReport) => void;
113
119
  /**
114
120
  * Publications to subscribe to (only relevant when calling `sql.subscribe()`)
115
121
  * @default 'alltables'
@@ -122,6 +128,8 @@ interface BaseOptions<T extends Record<string, postgres.PostgresType>> {
122
128
  backoff: boolean | ((attemptNum: number) => number);
123
129
  max_lifetime: number | null;
124
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>;
125
133
  }
126
134
 
127
135
 
@@ -199,6 +207,8 @@ type Return<T, K extends readonly any[]> =
199
207
 
200
208
  declare namespace postgres {
201
209
  class PostgresError extends Error {
210
+ constructor(message: string);
211
+ constructor(fields: Partial<Omit<PostgresError, 'name' | 'stack' | 'cause'>>);
202
212
  name: 'PostgresError';
203
213
  severity_local: string;
204
214
  severity: string;
@@ -411,6 +421,23 @@ declare namespace postgres {
411
421
  };
412
422
  }
413
423
 
424
+ interface TypeMismatchReport {
425
+ query: string;
426
+ parameters: TypeMismatch[];
427
+ }
428
+
429
+ interface TypeMismatch {
430
+ /** The `$n` number of the placeholder */
431
+ position: number;
432
+ /** The type OID PostgreSQL inferred for the placeholder */
433
+ oid: number;
434
+ typeof: string;
435
+ /** Only set when debug is enabled */
436
+ serverText?: string;
437
+ /** Only set when debug is enabled; the error is what the 'client' mode would throw */
438
+ clientText?: string | globalThis.Error;
439
+ }
440
+
414
441
  interface Notice {
415
442
  [field: string]: string;
416
443
  }
@@ -742,6 +769,9 @@ declare namespace postgres {
742
769
 
743
770
  end(options?: { timeout?: number | undefined } | undefined): Promise<void>;
744
771
 
772
+ /** Calls `end()` with no timeout when an `await using` scope exits */
773
+ [Symbol.asyncDispose](): Promise<void>;
774
+
745
775
  listen(channel: string, onnotify: (value: string) => void, onlisten?: (() => void) | undefined): ListenRequest;
746
776
 
747
777
  /** `onerror` receives replication connection errors and errors thrown while a change is decoded; such a change is skipped. */
@@ -767,6 +797,12 @@ declare namespace postgres {
767
797
 
768
798
  interface ReservedSql<TTypes extends Record<string, unknown> = {}> extends Sql<TTypes> {
769
799
  release(): void;
800
+
801
+ /** Calls `release()` when a `using` scope exits */
802
+ [Symbol.dispose](): void;
803
+
804
+ /** Calls `release()` when an `await using` scope exits */
805
+ [Symbol.asyncDispose](): Promise<void>;
770
806
  }
771
807
  }
772
808
 
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "devDependencies": {
3
- "@types/node": "^16"
3
+ "@types/node": "^22"
4
4
  }
5
5
  }