postgrejs 3.12.1 → 3.13.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
@@ -62,7 +62,7 @@ PostgreJS is a driver, not a framework, so what you build on top of it stays you
62
62
  <a href="https://www.sqbjs.org"><img src="https://www.postgrejs.com/img/sqb-logo.svg" alt="SQB" height="60"></a><br><br>
63
63
  <b><a href="https://www.sqbjs.org">SQB</a></b><br>
64
64
  <a href="https://www.npmjs.com/package/@sqb/postgres"><code>@sqb/postgres</code></a><br>
65
- <sub>native driver</sub>
65
+ <sub>first-party adapter</sub>
66
66
  </td>
67
67
  <td align="center" width="230" valign="top">
68
68
  <a href="https://kysely.dev"><img src="https://www.postgrejs.com/img/kysely-logo.svg" alt="Kysely" height="60"></a><br><br>
@@ -5,7 +5,7 @@ import { withAbortSignal } from '../util/abort-signal.js';
5
5
  import { normalizeChannelName } from '../util/channel-name.js';
6
6
  import { escapeIdentifier } from '../util/escape-identifier.js';
7
7
  import { QueryRequest } from '../util/sql-tag.js';
8
- import { isUnspecifiedParam } from '../util/unspecified-param.js';
8
+ import { isUnspecifiedParam, needsNumericParam, } from '../util/unspecified-param.js';
9
9
  import { BindParam } from './bind-param.js';
10
10
  import { IntlConnection } from './intl-connection.js';
11
11
  import { LargeObject, LargeObjectMode } from './large-object.js';
@@ -807,7 +807,12 @@ export class Connection extends SafeEventEmitter {
807
807
  // out as is written in getBindMessage().
808
808
  isUnspecifiedParam(prm)
809
809
  ? 0
810
- : typeMap.determine(prm));
810
+ : // A non-integer is declared numeric rather than the float8
811
+ // its value suggests - see `needsNumericParam()` for the
812
+ // cast that makes the difference, and what it costs.
813
+ needsNumericParam(prm)
814
+ ? DataTypeOIDs.numeric
815
+ : typeMap.determine(prm));
811
816
  // A money parameter is written as an int64 of minor units, so the
812
817
  // scale has to be known before the Bind goes out - the probe fired
813
818
  // at connect answers in time for every result, but not necessarily
@@ -24,6 +24,7 @@ import { CopyFromStream, CopyToStream } from './copy-stream.js';
24
24
  interface PreparedCacheEntry {
25
25
  name: string;
26
26
  fields?: Protocol.RowDescription[];
27
+ resolvedParamTypes?: OID[];
27
28
  }
28
29
  /** See IntlConnection._decodeDeferredRows(). */
29
30
  interface DeferredRows {
@@ -15,6 +15,7 @@ import { escapeLiteral } from '../util/escape-literal.js';
15
15
  import { fetchAsStringEqual, fetchAsStringNamesElement, fetchAsStringNamesOid, validateFetchAsString, } from '../util/fetch-as-string.js';
16
16
  import { getParsers } from '../util/get-parsers.js';
17
17
  import { resolveColumnFormats } from '../util/resolve-column-formats.js';
18
+ import { resolvedBindTypes } from '../util/resolved-bind-type.js';
18
19
  import { dataRowBytes, resolveRowDecoder, resolveRowType, } from '../util/row-decoder.js';
19
20
  import { requireTimeZone } from '../util/temporal.js';
20
21
  import { isTransactionCommand, refusesSavepoint, } from '../util/transaction-command.js';
@@ -983,7 +984,7 @@ export class IntlConnection extends SafeEventEmitter {
983
984
  this._preparedCache.delete(key);
984
985
  this._preparedCache.set(key, cached);
985
986
  try {
986
- return await this.executeReused(cached.name, cached.fields, paramTypes, params, options, savepoint);
987
+ return await this.executeReused(cached.name, cached.fields, resolvedBindTypes(paramTypes, params, cached.resolvedParamTypes), params, options, savepoint);
987
988
  }
988
989
  catch (e) {
989
990
  if (!isStaleCachedPlan(e))
@@ -1052,8 +1053,8 @@ export class IntlConnection extends SafeEventEmitter {
1052
1053
  _prepareForCache(key, sql, paramTypes) {
1053
1054
  const name = 'C_' + ++this._preparedCounter;
1054
1055
  return (async () => {
1055
- const { fields } = await this.prepareOnce(sql, paramTypes, name);
1056
- const entry = { name, fields };
1056
+ const { fields, resolvedParamTypes } = await this.prepareOnce(sql, paramTypes, name);
1057
+ const entry = { name, fields, resolvedParamTypes };
1057
1058
  await this._evictPreparedIfFull();
1058
1059
  this._preparedCache.set(key, entry);
1059
1060
  return entry;
package/constants.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Protocol } from './protocol/protocol.js';
2
2
  import DataFormat = Protocol.DataFormat;
3
- export declare const version = "3.12.1";
3
+ export declare const version = "3.13.0";
4
4
  export { DataFormat };
5
5
  /**
6
6
  * What columns come back in unless something asks otherwise.
package/constants.js CHANGED
@@ -2,7 +2,7 @@ import { Protocol } from './protocol/protocol.js';
2
2
  var DataFormat = Protocol.DataFormat;
3
3
  // @ts-ignore
4
4
  Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
5
- export const version = '3.12.1';
5
+ export const version = '3.13.0';
6
6
  export { DataFormat };
7
7
  /**
8
8
  * What columns come back in unless something asks otherwise.
@@ -28,7 +28,14 @@ export const DateType = {
28
28
  ? v.getTime()
29
29
  : v.getTime() - v.getTimezoneOffset() * 60 * 1000;
30
30
  n = (n - timeShift) / 1000 / 86400;
31
- const t = Math.trunc(n + Number.EPSILON);
31
+ /* Floor, not trunc: a date before 2000-01-01 makes `n` negative, and
32
+ truncation there rounds *towards* the epoch - every pre-2000
33
+ instant with a time of day was stored one day late, silently.
34
+ `1999-12-31T22:00Z` under TZ=UTC went in as 2000-01-01 where the
35
+ text path, and `pg`, both say 1999-12-31. The epsilon stays: it is
36
+ what keeps an exact midnight from landing a hair under its own
37
+ day and flooring to the one before. */
38
+ const t = Math.floor(n + Number.EPSILON);
32
39
  buf.writeInt32BE(t);
33
40
  },
34
41
  decodeBinary(v, offset = 0, _len, options) {
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.12.1",
4
+ "version": "3.13.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
8
8
  "@jsopen/objects": "^2.3.4",
9
9
  "doublylinked": "^2.5.6",
10
- "flexy-buffer": "^1.1.1",
10
+ "flexy-buffer": "^1.1.2",
11
11
  "lightning-pool": "^4.14.0",
12
12
  "power-tasks": "^1.14.2",
13
13
  "putil-promisify": "^1.10.1",
@@ -101,7 +101,20 @@ export declare class Frontend {
101
101
  getSASLMessage(sasl: SASL.Session): Buffer;
102
102
  getSASLFinalMessage(session: SASL.Session): Buffer;
103
103
  getParseMessage(args: Frontend.ParseMessageArgs): Buffer;
104
- getBindMessage(args: Frontend.BindMessageArgs): Buffer;
104
+ /**
105
+ * Bind, carrying the parameter values - the one message in a statement
106
+ * whose size is the caller's data rather than a few dozen bytes.
107
+ *
108
+ * @param args The portal, statement, parameters and result formats.
109
+ * @param copy When false, the returned buffer is a view into the
110
+ * shared send buffer rather than a copy of it. It stays valid only
111
+ * until the next message is written, so a caller passing false must
112
+ * consume it - `Buffer.concat()`, a write - before building anything
113
+ * else, and must never hand it to the socket, which does not copy
114
+ * synchronously.
115
+ * @returns The Bind message.
116
+ */
117
+ getBindMessage(args: Frontend.BindMessageArgs, copy?: boolean): Buffer;
105
118
  getDescribeMessage(args: Frontend.DescribeMessageArgs): Buffer;
106
119
  getExecuteMessage(args: Frontend.ExecuteMessageArgs): Buffer;
107
120
  getCloseMessage(args: Frontend.CloseMessageArgs): Buffer;
@@ -4,6 +4,7 @@ import { arrayLeaf } from '../util/array-leaf.js';
4
4
  import { encodeBinaryArray } from '../util/encode-binaryarray.js';
5
5
  import { formatDateParam } from '../util/format-datetime.js';
6
6
  import { writeArrayLiteral } from '../util/stringify-arrayliteral.js';
7
+ import { resolveToPostgres, unspecifiedText, } from '../util/unspecified-value.js';
7
8
  import { Protocol } from './protocol.js';
8
9
  import { SmartBuffer } from './smart-buffer.js';
9
10
  const DataFormat = Protocol.DataFormat;
@@ -131,7 +132,28 @@ export class Frontend {
131
132
  }
132
133
  return setLengthAndFlush(io, 1);
133
134
  }
134
- getBindMessage(args) {
135
+ /**
136
+ * Bind, carrying the parameter values - the one message in a statement
137
+ * whose size is the caller's data rather than a few dozen bytes.
138
+ *
139
+ * @param args The portal, statement, parameters and result formats.
140
+ * @param copy When false, the returned buffer is a view into the
141
+ * shared send buffer rather than a copy of it. It stays valid only
142
+ * until the next message is written, so a caller passing false must
143
+ * consume it - `Buffer.concat()`, a write - before building anything
144
+ * else, and must never hand it to the socket, which does not copy
145
+ * synchronously.
146
+ * @returns The Bind message.
147
+ */
148
+ /* The copy exists for buffer reuse, not for the socket: every message
149
+ is built in this one SmartBuffer, so message N+1's start() would
150
+ overwrite N's bytes before they were assembled. Nothing flushed here
151
+ ever reaches socket.write() - only the concat of them does, which is
152
+ checked in a test. So a caller that builds the small messages first
153
+ and this one last can take a view and let the concat read it, which
154
+ is one copy of the payload rather than two: measured on a 100 000
155
+ element int4[], 2148 KB of copying a call against 1074. */
156
+ getBindMessage(args, copy = true) {
135
157
  if (args.portal && args.portal.length > 63)
136
158
  throw new Error('Portal name length must be lower than 63');
137
159
  if (args.statement && args.statement.length > 63)
@@ -166,6 +188,18 @@ export class Frontend {
166
188
  }
167
189
  const dataTypeOid = paramTypes ? paramTypes[i] : undefined;
168
190
  const dt = dataTypeOid ? args.typeMap.get(dataTypeOid) : undefined;
191
+ // No declared type: the value writes itself if it knows how, and
192
+ // what it hands back decides which branch below takes it - a
193
+ // toPostgres() returning a Date belongs in the Date branch, not
194
+ // in `'' + v`. A declared type is left alone; it owns its own
195
+ // encoding.
196
+ if (!dt) {
197
+ v = resolveToPostgres(v);
198
+ if (v === null || v === undefined) {
199
+ io.writeInt32BE(-1);
200
+ continue;
201
+ }
202
+ }
169
203
  if (dt) {
170
204
  if (typeof dt.encodeAsNull === 'function' &&
171
205
  dt.encodeAsNull(v, queryOptions)) {
@@ -220,7 +254,7 @@ export class Frontend {
220
254
  io.buffer.writeInt32BE(io.size - dataOffset, dataOffset - 4); // Update length
221
255
  }
222
256
  else {
223
- io.writeLString('' + v, 'utf8');
257
+ io.writeLString(unspecifiedText(v), 'utf8');
224
258
  }
225
259
  }
226
260
  }
@@ -242,7 +276,7 @@ export class Frontend {
242
276
  }
243
277
  else
244
278
  io.writeUInt16BE(0);
245
- return setLengthAndFlush(io, 1);
279
+ return setLengthAndFlush(io, 1, copy);
246
280
  }
247
281
  getDescribeMessage(args) {
248
282
  if (args.name && args.name.length > 63) {
@@ -348,7 +382,7 @@ export class Frontend {
348
382
  return StaticSyncBuffer;
349
383
  }
350
384
  }
351
- function setLengthAndFlush(io, lengthOffset) {
385
+ function setLengthAndFlush(io, lengthOffset, copy = true) {
352
386
  io.buffer.writeUInt32BE(io.size - lengthOffset, lengthOffset);
353
- return io.flush();
387
+ return io.flush(copy);
354
388
  }
@@ -250,23 +250,41 @@ export class PgSocket extends SafeEventEmitter {
250
250
  * statement/portal at the next Parse/Bind referencing the unnamed name.
251
251
  */
252
252
  sendExtendedQueryMessages(args, cb) {
253
+ /* The Bind is built LAST and taken as a view, then concatenated here
254
+ rather than in _send(): it carries the caller's data, where the
255
+ other four messages are a few dozen bytes, and the copy flush()
256
+ makes exists only because this one SmartBuffer is reused for the
257
+ next message. Building the small ones first leaves nothing to
258
+ overwrite it before Buffer.concat() reads it, so the payload is
259
+ copied once instead of twice - measured on a 100 000-element
260
+ int4[], 2148 KB of copying a call against 1074, with the wire bytes
261
+ and the stored value unchanged. The view never reaches the socket;
262
+ only the concat does. */
263
+ const parse = this._frontend.getParseMessage(args.parse);
264
+ const describe = this._frontend.getDescribeMessage(args.describe);
265
+ const execute = this._frontend.getExecuteMessage(args.execute);
266
+ const sync = this._frontend.getSyncMessage();
253
267
  if (!args.before && !args.after) {
254
- const data = [
255
- this._frontend.getParseMessage(args.parse),
256
- this._frontend.getBindMessage(args.bind),
257
- this._frontend.getDescribeMessage(args.describe),
258
- this._frontend.getExecuteMessage(args.execute),
259
- this._frontend.getSyncMessage(),
260
- ];
268
+ const bind = this._frontend.getBindMessage(args.bind, false);
269
+ const data = Buffer.concat([parse, bind, describe, execute, sync]);
261
270
  return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
262
271
  }
263
- const data = [];
272
+ const before = [];
264
273
  if (args.before)
265
- this._pushUtilityStatement(data, args.before, args.bind);
266
- data.push(this._frontend.getParseMessage(args.parse), this._frontend.getBindMessage(args.bind), this._frontend.getDescribeMessage(args.describe), this._frontend.getExecuteMessage(args.execute));
274
+ this._pushUtilityStatement(before, args.before, args.bind);
275
+ const after = [];
267
276
  if (args.after)
268
- this._pushUtilityStatement(data, args.after, args.bind);
269
- data.push(this._frontend.getSyncMessage());
277
+ this._pushUtilityStatement(after, args.after, args.bind);
278
+ const bind = this._frontend.getBindMessage(args.bind, false);
279
+ const data = Buffer.concat([
280
+ ...before,
281
+ parse,
282
+ bind,
283
+ describe,
284
+ execute,
285
+ ...after,
286
+ sync,
287
+ ]);
270
288
  return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
271
289
  }
272
290
  /**
@@ -336,21 +354,22 @@ export class PgSocket extends SafeEventEmitter {
336
354
  * auto-clears an unnamed portal at the next Bind naming it.
337
355
  */
338
356
  sendBindExecuteMessages(args, cb) {
357
+ // See sendExtendedQueryMessages() for why the Bind is built last.
358
+ const execute = this._frontend.getExecuteMessage(args.execute);
359
+ const sync = this._frontend.getSyncMessage();
339
360
  if (!args.before && !args.after) {
340
- const data = [
341
- this._frontend.getBindMessage(args.bind),
342
- this._frontend.getExecuteMessage(args.execute),
343
- this._frontend.getSyncMessage(),
344
- ];
361
+ const bind = this._frontend.getBindMessage(args.bind, false);
362
+ const data = Buffer.concat([bind, execute, sync]);
345
363
  return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
346
364
  }
347
- const data = [];
365
+ const before = [];
348
366
  if (args.before)
349
- this._pushUtilityStatement(data, args.before, args.bind);
350
- data.push(this._frontend.getBindMessage(args.bind), this._frontend.getExecuteMessage(args.execute));
367
+ this._pushUtilityStatement(before, args.before, args.bind);
368
+ const after = [];
351
369
  if (args.after)
352
- this._pushUtilityStatement(data, args.after, args.bind);
353
- data.push(this._frontend.getSyncMessage());
370
+ this._pushUtilityStatement(after, args.after, args.bind);
371
+ const bind = this._frontend.getBindMessage(args.bind, false);
372
+ const data = Buffer.concat([...before, bind, execute, ...after, sync]);
354
373
  return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
355
374
  }
356
375
  /**
@@ -0,0 +1,15 @@
1
+ import type { Maybe, OID } from '../types.js';
2
+ /**
3
+ * The types to encode a Bind's parameters against, given what the caller
4
+ * declared and what the server resolved.
5
+ *
6
+ * Returns `declared` unchanged unless some parameter can be improved, so
7
+ * the common call allocates nothing.
8
+ *
9
+ * @param declared What went into Parse - `0`/undefined where this client
10
+ * declared nothing and left the type to the server.
11
+ * @param params The values about to be written.
12
+ * @param resolved What the server answered for this statement, in order.
13
+ * @returns The types to hand `getBindMessage`.
14
+ */
15
+ export declare function resolvedBindTypes(declared: Maybe<Maybe<OID>[]>, params: Maybe<Maybe<any>[]>, resolved: Maybe<OID[]>): Maybe<Maybe<OID>[]>;
@@ -0,0 +1,85 @@
1
+ import { DataTypeOIDs } from '../constants.js';
2
+ import { arrayLeaf } from './array-leaf.js';
3
+ /**
4
+ * The types a `Date` is allowed to be encoded into binary against, by
5
+ * whether the value is an array.
6
+ */
7
+ /* Deliberately short. `time` and `timetz` are left out because
8
+ TimeType.encodeBinary() multiplies the whole epoch by 1000 and
9
+ overflows for any ordinary Date - measured, every one of four test
10
+ instants answered 22008 "time out of range", where the text path
11
+ stores the wall clock correctly. Nothing else here may be reached
12
+ until it has been checked the same way. */
13
+ const DATE_SCALARS = new Set([
14
+ DataTypeOIDs.timestamptz,
15
+ DataTypeOIDs.timestamp,
16
+ DataTypeOIDs.date,
17
+ ]);
18
+ const DATE_ARRAYS = new Set([
19
+ DataTypeOIDs._timestamptz,
20
+ DataTypeOIDs._timestamp,
21
+ DataTypeOIDs._date,
22
+ ]);
23
+ /**
24
+ * The types to encode a Bind's parameters against, given what the caller
25
+ * declared and what the server resolved.
26
+ *
27
+ * Returns `declared` unchanged unless some parameter can be improved, so
28
+ * the common call allocates nothing.
29
+ *
30
+ * @param declared What went into Parse - `0`/undefined where this client
31
+ * declared nothing and left the type to the server.
32
+ * @param params The values about to be written.
33
+ * @param resolved What the server answered for this statement, in order.
34
+ * @returns The types to hand `getBindMessage`.
35
+ */
36
+ /* A Date goes out as text because nothing but the server knows whether
37
+ the column is an instant or a wall clock - see `isUnspecifiedParam()`.
38
+ Once the statement has a name, the server has already said which, and
39
+ saying it does not have to be repeated: a prepared statement's
40
+ parameter types are fixed at Parse, so a later Bind may send binary
41
+ against them with no re-Parse. Checked on the wire: Parse with no
42
+ declared types resolves `$1` to 1184, and a Bind of eight binary bytes
43
+ against that statement reads back correctly.
44
+
45
+ Worth it only because the text is wide and the binary is not. Measured
46
+ over a loopback connection, 20 000 elements:
47
+
48
+ timestamptz[] text 625 KB / 11.61ms binary 234 KB / 3.79ms
49
+
50
+ The same measurement is why this is not done for numbers: an int4[] of
51
+ ordinary values is 380 KB of text against 781 KB of binary, because
52
+ the binary array format carries a 4-byte length per element whatever
53
+ its magnitude - there, text is both smaller and twice as fast.
54
+
55
+ Binary and text store the same value, which is the thing that had to
56
+ be true before any of this: checked for timestamptz, timestamp and
57
+ date, over four instants spanning a DST boundary and including
58
+ milliseconds, and for the array forms of two of them. */
59
+ export function resolvedBindTypes(declared, params, resolved) {
60
+ if (!resolved || !params)
61
+ return declared;
62
+ const l = params.length;
63
+ let out;
64
+ let i;
65
+ for (i = 0; i < l; i++) {
66
+ // A type the caller named is an answer, not a guess - left alone.
67
+ if (declared?.[i])
68
+ continue;
69
+ const oid = resolved[i];
70
+ if (!oid)
71
+ continue;
72
+ const v = params[i];
73
+ if (!(arrayLeaf(v) instanceof Date))
74
+ continue;
75
+ // An array value needs an array type and a scalar needs a scalar
76
+ // one: getBindMessage() picks its encoder from the type alone and
77
+ // would hand an array to a scalar encoder.
78
+ if (!(Array.isArray(v) ? DATE_ARRAYS : DATE_SCALARS).has(oid))
79
+ continue;
80
+ if (!out)
81
+ out = declared ? declared.slice() : new Array(l).fill(0);
82
+ out[i] = oid;
83
+ }
84
+ return out || declared;
85
+ }
@@ -1,5 +1,6 @@
1
1
  import { arrayCalculateDim } from './array-calculatedim.js';
2
2
  import { arrayLeaf } from './array-leaf.js';
3
+ import { unspecifiedText } from './unspecified-value.js';
3
4
  /**
4
5
  * What a number's own text looks like: digits, one optional dot, an
5
6
  * optional exponent, an optional leading minus. An element that matches
@@ -66,7 +67,11 @@ export function stringifyArrayLiteral(value, options, encode) {
66
67
  const isNumber = typeof x === 'number' || typeof x === 'bigint';
67
68
  if (encode)
68
69
  x = encode(x, options || {});
69
- const s = '' + x;
70
+ // With an encoder the element's text is the type's to produce;
71
+ // without one this is an undeclared array, where a value that
72
+ // knows how to write itself does, and a plain object goes out as
73
+ // JSON rather than as `[object Object]`.
74
+ const s = encode ? '' + x : unspecifiedText(x);
70
75
  out += isNumber && BARE_NUMBER.test(s) ? s : escapeArrayItem(s);
71
76
  }
72
77
  return out + '}';
@@ -241,7 +246,9 @@ export function writeArrayLiteral(io, value, options, encode) {
241
246
  // of its spelling - the same rule the string path states: a
242
247
  // string's own text is quoted whatever it looks like.
243
248
  const isNumber = typeof x === 'number' || typeof x === 'bigint';
244
- s = '' + x;
249
+ // This lane is only entered with no element encoder, so the same
250
+ // rules the string path applies without one apply here.
251
+ s = unspecifiedText(x);
245
252
  const bare = isNumber && BARE_NUMBER.test(s);
246
253
  if (!bare)
247
254
  s =
@@ -30,6 +30,9 @@
30
30
  * bigint[] = integer[]`, and `numeric[] = double precision[]` the
31
31
  * same. The scalars are left declared because they do have those
32
32
  * operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
33
+ * That is true of the operators *inside* the numeric family and not
34
+ * of the casts out of it, which is what `needsNumericParam()` below
35
+ * exists for.
33
36
  *
34
37
  * A scalar number, a boolean, a Buffer, and an array of anything but
35
38
  * numbers keep their declared types: an array of booleans, of Buffers
@@ -64,3 +67,14 @@
64
67
  * answer rather than a guess.
65
68
  */
66
69
  export declare function isUnspecifiedParam(v: any): boolean;
70
+ /**
71
+ * Whether a number must be declared `numeric` rather than the `float8`
72
+ * its value suggests.
73
+ *
74
+ * True for a finite non-integer, false for everything else - an integer,
75
+ * a bigint, a non-finite number, and anything that is not a number.
76
+ *
77
+ * @param v The value a parameter is about to be declared for.
78
+ * @returns Whether to declare `numeric` instead of asking `determine()`.
79
+ */
80
+ export declare function needsNumericParam(v: any): boolean;
@@ -31,6 +31,9 @@ import { arrayLeaf } from './array-leaf.js';
31
31
  * bigint[] = integer[]`, and `numeric[] = double precision[]` the
32
32
  * same. The scalars are left declared because they do have those
33
33
  * operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
34
+ * That is true of the operators *inside* the numeric family and not
35
+ * of the casts out of it, which is what `needsNumericParam()` below
36
+ * exists for.
34
37
  *
35
38
  * A scalar number, a boolean, a Buffer, and an array of anything but
36
39
  * numbers keep their declared types: an array of booleans, of Buffers
@@ -70,3 +73,40 @@ export function isUnspecifiedParam(v) {
70
73
  return true;
71
74
  return Array.isArray(v) && (typeof x === 'number' || typeof x === 'bigint');
72
75
  }
76
+ /**
77
+ * Whether a number must be declared `numeric` rather than the `float8`
78
+ * its value suggests.
79
+ *
80
+ * True for a finite non-integer, false for everything else - an integer,
81
+ * a bigint, a non-finite number, and anything that is not a number.
82
+ *
83
+ * @param v The value a parameter is about to be declared for.
84
+ * @returns Whether to declare `numeric` instead of asking `determine()`.
85
+ */
86
+ /* `determine()` answers by value, so 12 is int4 and 12.34 is float8 -
87
+ and `int4 -> money` is an assignment cast while `float8 -> money` has
88
+ no pg_cast row at all. So `select ($1::money)::text` worked with 12
89
+ and failed 42846 with 12.34: the same SQL, the same column, decided by
90
+ whether the amount happened to have a fractional part, which for money
91
+ it usually does. A test written with 12 passed and production failed
92
+ on 12.34. `numeric -> money` is an assignment cast, which an explicit
93
+ `::money` accepts, and `numeric -> float8` is implicit, so a float8
94
+ column is unaffected.
95
+
96
+ Measured, inserting into a float8 column over a loopback connection,
97
+ three interleaved rounds of 3000: 0.197ms a call against float8's
98
+ 0.198, and 51 bytes on the socket against 47. Round-tripping a double
99
+ through numeric is exact - checked against float8 for 0.1, 5e-324,
100
+ 1.7976931348623157e308, 0.30000000000000004 and pi, among others.
101
+
102
+ Non-finite values keep float8 deliberately: `numeric` only grew
103
+ Infinity in PostgreSQL 14, and NaN would be the only one of the three
104
+ that worked on 12 and 13.
105
+
106
+ This does not close `$1::interval` from a number, which fails the same
107
+ way - there is no cast to interval from any numeric type, so only an
108
+ undeclared parameter reaches it. An integer scalar is left declared,
109
+ and `new BindParam(0, 5)` is the way to say it. */
110
+ export function needsNumericParam(v) {
111
+ return typeof v === 'number' && Number.isFinite(v) && !Number.isInteger(v);
112
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Follows a value's `toPostgres()` until it reaches one that has none.
3
+ *
4
+ * `toPostgres()` is `pg`'s extension point for a type that knows how to
5
+ * write itself: a library with its own geometry, money or domain class
6
+ * implements it, and the value goes out as that class says rather than
7
+ * as whatever `String()` makes of it. The result is followed rather than
8
+ * returned, because what it hands back may itself be a `Date`, an array,
9
+ * a `Buffer` or another such value.
10
+ *
11
+ * @param v The value a parameter is about to be written from.
12
+ * @returns The value to write, which is `v` itself unless it had one.
13
+ * @throws TypeError if the values form a cycle.
14
+ */
15
+ export declare function resolveToPostgres(v: any): any;
16
+ /**
17
+ * The text an undeclared parameter - or one element of an undeclared
18
+ * array - goes out as.
19
+ *
20
+ * Follows `toPostgres()` first, then writes a plain object as JSON and
21
+ * anything else as its own text.
22
+ *
23
+ * @param v The value to render.
24
+ * @returns What to put on the wire, before any array quoting.
25
+ */
26
+ export declare function unspecifiedText(v: any): string;
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Follows a value's `toPostgres()` until it reaches one that has none.
3
+ *
4
+ * `toPostgres()` is `pg`'s extension point for a type that knows how to
5
+ * write itself: a library with its own geometry, money or domain class
6
+ * implements it, and the value goes out as that class says rather than
7
+ * as whatever `String()` makes of it. The result is followed rather than
8
+ * returned, because what it hands back may itself be a `Date`, an array,
9
+ * a `Buffer` or another such value.
10
+ *
11
+ * @param v The value a parameter is about to be written from.
12
+ * @returns The value to write, which is `v` itself unless it had one.
13
+ * @throws TypeError if the values form a cycle.
14
+ */
15
+ /* Only for a parameter with no declared type. A declared type owns its
16
+ own encoding and is an answer the caller gave, not a guess.
17
+
18
+ This client's own classes are unaffected: every one of their
19
+ `toPostgres()` is `return this.toString()`, which is what the path
20
+ below used to reach anyway. What changes is a caller's class, which
21
+ used to be written `[object Object]` - silently, with a row written
22
+ and no error. */
23
+ export function resolveToPostgres(v) {
24
+ if (v === null || typeof v !== 'object' || typeof v.toPostgres !== 'function')
25
+ return v;
26
+ const seen = [];
27
+ while (v !== null &&
28
+ typeof v === 'object' &&
29
+ typeof v.toPostgres === 'function') {
30
+ if (seen.includes(v))
31
+ throw new TypeError('Circular reference while preparing a parameter: toPostgres() returned a value that leads back to itself');
32
+ seen.push(v);
33
+ v = v.toPostgres();
34
+ }
35
+ return v;
36
+ }
37
+ /**
38
+ * The text an undeclared parameter - or one element of an undeclared
39
+ * array - goes out as.
40
+ *
41
+ * Follows `toPostgres()` first, then writes a plain object as JSON and
42
+ * anything else as its own text.
43
+ *
44
+ * @param v The value to render.
45
+ * @returns What to put on the wire, before any array quoting.
46
+ */
47
+ /* The two rules `pg`'s `prepareValue` has that this path did not. The
48
+ rest of its rules are either already here in their own branch - a
49
+ Date, an array, a Buffer - or deliberately not taken: it writes a Date
50
+ with a `T` where this client writes a space, and quotes the elements
51
+ of a number array where this client writes them bare, which is 10-26%
52
+ smaller and parses identically. Those are spellings the server reads
53
+ the same way; these two are a value that arrives as something else. */
54
+ export function unspecifiedText(v) {
55
+ v = resolveToPostgres(v);
56
+ if (v !== null &&
57
+ typeof v === 'object' &&
58
+ !(v instanceof Date) &&
59
+ !Buffer.isBuffer(v) &&
60
+ !Array.isArray(v))
61
+ return JSON.stringify(v);
62
+ return '' + v;
63
+ }