postgrejs 3.4.0 → 3.6.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +29 -4
  3. package/connection/connection.d.ts +37 -0
  4. package/connection/connection.js +85 -7
  5. package/connection/cursor.d.ts +8 -0
  6. package/connection/cursor.js +39 -1
  7. package/connection/intl-connection.d.ts +6 -1
  8. package/connection/intl-connection.js +284 -122
  9. package/connection/pool.d.ts +16 -0
  10. package/connection/pool.js +52 -6
  11. package/connection/portal.d.ts +1 -1
  12. package/connection/portal.js +3 -2
  13. package/connection/prepared-statement.js +8 -4
  14. package/data-type-map.d.ts +11 -0
  15. package/data-type-map.js +20 -2
  16. package/data-types/char-type.js +15 -0
  17. package/data-types/date-type.js +4 -18
  18. package/data-types/float4-type.js +79 -1
  19. package/data-types/json-type.js +7 -12
  20. package/data-types/jsonb-type.js +7 -12
  21. package/data-types/time-type.js +2 -15
  22. package/data-types/timestamp-type.js +8 -29
  23. package/data-types/timestamptz-type.js +7 -31
  24. package/interfaces/batch-result.d.ts +1 -1
  25. package/interfaces/command-result.d.ts +21 -1
  26. package/interfaces/data-mapping-options.d.ts +16 -0
  27. package/interfaces/data-type.d.ts +11 -0
  28. package/interfaces/query-options.d.ts +25 -3
  29. package/package.json +1 -1
  30. package/protocol/frontend.d.ts +7 -0
  31. package/protocol/frontend.js +5 -3
  32. package/protocol/pg-socket.d.ts +2 -2
  33. package/protocol/pg-socket.js +12 -6
  34. package/util/channel-name.d.ts +24 -0
  35. package/util/channel-name.js +44 -0
  36. package/util/get-parsers.d.ts +2 -1
  37. package/util/get-parsers.js +16 -1
  38. package/util/resolve-column-formats.d.ts +33 -0
  39. package/util/resolve-column-formats.js +67 -0
  40. package/util/wrap-row-description.d.ts +2 -1
  41. package/util/wrap-row-description.js +7 -1
@@ -2,6 +2,7 @@ import { Pool as LightningPool, } from 'lightning-pool';
2
2
  import { coerceToBoolean, coerceToInt } from 'putil-varhelpers';
3
3
  import { ConnectionState } from '../constants.js';
4
4
  import { SafeEventEmitter } from '../safe-event-emitter.js';
5
+ import { normalizeChannelName } from '../util/channel-name.js';
5
6
  import { getConnectionConfig } from '../util/connection-config.js';
6
7
  import { QueryRequest } from '../util/sql-tag.js';
7
8
  import { startsCopy, startsTransaction } from '../util/starts-transaction.js';
@@ -205,12 +206,32 @@ export class Pool extends SafeEventEmitter {
205
206
  : undefined;
206
207
  if (!slot) {
207
208
  const connection = await this.acquire();
209
+ let result;
208
210
  try {
209
- return await connection.query(sql, options);
211
+ result = await connection.query(sql, options);
210
212
  }
211
- finally {
213
+ catch (e) {
212
214
  await this.release(connection);
215
+ throw e;
216
+ }
217
+ // A portal lives only as long as the transaction it was created in,
218
+ // and outside an explicit one that is the implicit transaction any
219
+ // other statement's Sync ends. So a connection carrying an open
220
+ // cursor cannot go back in the pool when this call returns - handing
221
+ // it to the next caller would destroy the cursor's portal, and did
222
+ // ("portal ... does not exist", nondeterministically, depending on
223
+ // which connection that caller happened to get). It goes back when
224
+ // the cursor closes instead, which for-await and `await using` both
225
+ // do on their own; a cursor that is never closed holds its
226
+ // connection until the pool itself is closed.
227
+ if (result.cursor) {
228
+ result.cursor.once('close', () => {
229
+ this.release(connection).catch(e => this.emit('error', e));
230
+ });
231
+ return result;
213
232
  }
233
+ await this.release(connection);
234
+ return result;
214
235
  }
215
236
  try {
216
237
  const shared = slot.connection || (await slot.promise);
@@ -222,6 +243,30 @@ export class Pool extends SafeEventEmitter {
222
243
  slot.load--;
223
244
  }
224
245
  }
246
+ /**
247
+ * Takes a connection out of the pool, runs `fn` inside a transaction on
248
+ * it, and gives the connection back however that ends.
249
+ *
250
+ * ```ts
251
+ * await pool.transaction(async tx => {
252
+ * await tx.query('insert into orders (total) values ($1)', { params: [total] });
253
+ * await tx.query('update stock set n = n - 1 where sku = $1', { params: [sku] });
254
+ * });
255
+ * ```
256
+ *
257
+ * A transaction cannot be spread over pool.query() calls - each of those
258
+ * is free to pick a different connection, and a transaction lives on
259
+ * one. This is the way to get several statements onto the same one.
260
+ */
261
+ async transaction(fn) {
262
+ const connection = await this.acquire();
263
+ try {
264
+ return await connection.transaction(fn);
265
+ }
266
+ finally {
267
+ await this.release(connection);
268
+ }
269
+ }
225
270
  async prepare(sql, options) {
226
271
  const connection = await this.acquire();
227
272
  const statement = await connection.prepare(sql, options);
@@ -232,8 +277,10 @@ export class Pool extends SafeEventEmitter {
232
277
  return this._pool.releaseAsync(getIntlConnection(connection));
233
278
  }
234
279
  async listen(channel, callback) {
235
- if (!/^[A-Z]\w+$/i.test(channel))
236
- throw new TypeError(`Invalid channel name`);
280
+ // Folded here as well as in Connection.listen(): this emitter's keys
281
+ // have to be the same strings the inner connection registers, or
282
+ // unListen() would miss them.
283
+ channel = normalizeChannelName(channel);
237
284
  // Bug: _initNotificationConnection() only ever bootstraps the shared
238
285
  // connection and registers every channel known at that moment - it
239
286
  // returns immediately once that connection already exists, so a second,
@@ -250,8 +297,7 @@ export class Pool extends SafeEventEmitter {
250
297
  }
251
298
  }
252
299
  async unListen(channel) {
253
- if (!/^[A-Z]\w+$/i.test(channel))
254
- throw new TypeError(`Invalid channel name`);
300
+ channel = normalizeChannelName(channel);
255
301
  this._notificationListeners.removeAllListeners(channel);
256
302
  if (!this._notificationListeners.eventNames().length) {
257
303
  await this.unListenAll();
@@ -23,7 +23,7 @@ export declare class Portal {
23
23
  * no explicit BEGIN left transactionStatus 'I' and a later Execute on the
24
24
  * same named portal failed with "portal ... does not exist").
25
25
  */
26
- bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions): Promise<Protocol.RowDescription[]>;
26
+ bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions, columnFormat?: Protocol.DataFormat | Protocol.DataFormat[]): Promise<Protocol.RowDescription[]>;
27
27
  execute(fetchCount?: number): Promise<PortalExecuteResult>;
28
28
  /**
29
29
  * Close + Sync as one request (see PgSocket.sendCloseAndSyncMessages()
@@ -22,7 +22,7 @@ export class Portal {
22
22
  * no explicit BEGIN left transactionStatus 'I' and a later Execute on the
23
23
  * same named portal failed with "portal ... does not exist").
24
24
  */
25
- async bindAndRetrieveFields(params, queryOptions) {
25
+ async bindAndRetrieveFields(params, queryOptions, columnFormat) {
26
26
  const intoCon = getIntlConnection(this.connection);
27
27
  intoCon.ref();
28
28
  try {
@@ -35,6 +35,7 @@ export class Portal {
35
35
  paramTypes: this._statement.paramTypes,
36
36
  params,
37
37
  queryOptions,
38
+ columnFormat,
38
39
  },
39
40
  describe: { type: 'P', name: this.name },
40
41
  }, (code, msg, done) => {
@@ -68,7 +69,7 @@ export class Portal {
68
69
  const rows = [];
69
70
  const executePromise = socket.sendExecuteMessage({
70
71
  portal: this.name,
71
- fetchCount: fetchCount || 100,
72
+ fetchCount: fetchCount ?? 100,
72
73
  }, (code, msg, done) => {
73
74
  switch (code) {
74
75
  case Protocol.BackendMessageCode.NoticeResponse:
@@ -1,9 +1,9 @@
1
- import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
2
1
  import { GlobalTypeMap } from '../data-type-map.js';
3
2
  import { Protocol } from '../protocol/protocol.js';
4
3
  import { SafeEventEmitter } from '../safe-event-emitter.js';
5
4
  import { withAbortSignal } from '../util/abort-signal.js';
6
5
  import { getParsers } from '../util/get-parsers.js';
6
+ import { resolveColumnFormats } from '../util/resolve-column-formats.js';
7
7
  import { resolveRowType } from '../util/row-decoder.js';
8
8
  import { wrapRowDescription } from '../util/wrap-row-description.js';
9
9
  import { Cursor } from './cursor.js';
@@ -208,10 +208,14 @@ export class PreparedStatement extends SafeEventEmitter {
208
208
  intlCon.ref();
209
209
  let portal = new Portal(this, 'P_' + ++portalCounter);
210
210
  try {
211
- const fields = await portal.bindAndRetrieveFields(options.params, options);
211
+ // prepare() already fetched this statement's RowDescription, so
212
+ // fetchAsString's OID list can become per-column result format
213
+ // codes on the Bind below with no round trip of its own.
214
+ const columnFormat = resolveColumnFormats(this._fields, options);
215
+ const fields = await portal.bindAndRetrieveFields(options.params, options, columnFormat);
212
216
  const typeMap = options.typeMap || GlobalTypeMap;
213
- const parsers = getParsers(typeMap, fields);
214
- const resultFields = wrapRowDescription(typeMap, fields, options.columnFormat || DEFAULT_COLUMN_FORMAT);
217
+ const parsers = getParsers(typeMap, fields, options);
218
+ const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
215
219
  const result = {
216
220
  command: undefined,
217
221
  fields: resultFields,
@@ -3,6 +3,17 @@ import type { OID } from './types.js';
3
3
  export declare class DataTypeMap {
4
4
  private _itemsByOID;
5
5
  private _items;
6
+ /**
7
+ * Copies another map, so a connection can start from GlobalTypeMap and
8
+ * override a type or two without touching the global one.
9
+ *
10
+ * Both indexes have to be copied. `_items` alone is what `determine()`
11
+ * walks, but every decode goes through `get()`, which reads
12
+ * `_itemsByOID` - a copy carrying only the first one answers `undefined`
13
+ * for every OID, and get-parsers.ts then falls back to its default
14
+ * parser, handing the caller raw Buffers instead of decoded values with
15
+ * nothing reported.
16
+ */
6
17
  constructor(other?: DataTypeMap);
7
18
  get(oid: OID): DataType;
8
19
  register(dataTypes: DataType | DataType[]): void;
package/data-type-map.js CHANGED
@@ -26,9 +26,22 @@ import { ArrayVarcharType, VarcharType } from './data-types/varchar-type.js';
26
26
  export class DataTypeMap {
27
27
  _itemsByOID = {};
28
28
  _items = [];
29
+ /**
30
+ * Copies another map, so a connection can start from GlobalTypeMap and
31
+ * override a type or two without touching the global one.
32
+ *
33
+ * Both indexes have to be copied. `_items` alone is what `determine()`
34
+ * walks, but every decode goes through `get()`, which reads
35
+ * `_itemsByOID` - a copy carrying only the first one answers `undefined`
36
+ * for every OID, and get-parsers.ts then falls back to its default
37
+ * parser, handing the caller raw Buffers instead of decoded values with
38
+ * nothing reported.
39
+ */
29
40
  constructor(other) {
30
- if (other instanceof DataTypeMap)
31
- Object.assign(this._items, other._items);
41
+ if (other instanceof DataTypeMap) {
42
+ this._items = [...other._items];
43
+ this._itemsByOID = { ...other._itemsByOID };
44
+ }
32
45
  }
33
46
  get(oid) {
34
47
  return this._itemsByOID[oid];
@@ -52,6 +65,11 @@ export class DataTypeMap {
52
65
  let t;
53
66
  for (i = this._items.length - 1; i >= 0; i--) {
54
67
  t = this._items[i];
68
+ // Walked newest-registered first, so a later registration of the
69
+ // same OID wins - and a type marked `inferrable: false` is never
70
+ // picked here at all, however well `isType` matches.
71
+ if (t.inferrable === false)
72
+ continue;
55
73
  if (valueIsArray) {
56
74
  if (t.elementsOID && t.isType(value[0]))
57
75
  return t.oid;
@@ -21,6 +21,21 @@ export const CharType = {
21
21
  isType(v) {
22
22
  return typeof v === 'string' && Buffer.byteLength(v, 'utf8') === 1;
23
23
  },
24
+ /**
25
+ * 18 is PostgreSQL's internal single-byte `"char"`, not `char(n)` and
26
+ * not `varchar`. A one-character JavaScript string fits it, which is why
27
+ * `isType` says so, but nobody passing `'A'` to a query means that type:
28
+ * inference used to answer 18 for every such value and declare the
29
+ * parameter `"char"` to the server.
30
+ *
31
+ * Comparisons and inserts survived on an implicit cast, but concatenation
32
+ * had no single obvious one - `select code || $1` answered `operator is
33
+ * not unique: character varying || "char"` - and an array was worse
34
+ * still. determine() types an array from its first element alone, so
35
+ * `['A', 'BB', 'CCC']` became `_char` and came back `['A', 'B', 'C']`,
36
+ * every later element cut to one byte with nothing reported.
37
+ */
38
+ inferrable: false,
24
39
  };
25
40
  export const ArrayCharType = {
26
41
  ...CharType,
@@ -30,22 +30,18 @@ export const DateType = {
30
30
  buf.writeInt32BE(t);
31
31
  },
32
32
  decodeBinary(v, offset = 0, _len, options) {
33
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.date);
34
33
  const t = v.readInt32BE(offset);
35
34
  if (t === 0x7fffffff)
36
- return fetchAsString ? 'infinity' : Infinity;
35
+ return Infinity;
37
36
  if (t === -0x80000000)
38
- return fetchAsString ? '-infinity' : -Infinity;
37
+ return -Infinity;
39
38
  // Shift from 2000 to 1970
40
39
  let d = new Date(t * 1000 * 86400 + timeShift);
41
- if (fetchAsString || !options.utcDates)
40
+ if (!options.utcDates)
42
41
  d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
43
- return fetchAsString ? dateToDateString(d) : d;
42
+ return d;
44
43
  },
45
44
  decodeText(v, options) {
46
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.date);
47
- if (fetchAsString)
48
- return v;
49
45
  return parseDate(v, options.utcDates);
50
46
  },
51
47
  decodeTextBuffer(buf, offset, len, options) {
@@ -59,16 +55,6 @@ export const DateType = {
59
55
  v.getMilliseconds() === 0);
60
56
  },
61
57
  };
62
- function padZero(v) {
63
- return v < 9 ? '0' + v : '' + v;
64
- }
65
- function dateToDateString(d) {
66
- return (d.getFullYear() +
67
- '-' +
68
- padZero(d.getMonth() + 1) +
69
- '-' +
70
- padZero(d.getDate()));
71
- }
72
58
  export const ArrayDateType = {
73
59
  ...DateType,
74
60
  name: '_date',
@@ -12,7 +12,7 @@ export const Float4Type = {
12
12
  buf.writeFloatBE(typeof v === 'number' ? v : assertCoercedNumber(parseFloat(v), 'float4'));
13
13
  },
14
14
  decodeBinary(v, offset = 0) {
15
- return v.readFloatBE(offset);
15
+ return shortestFloat4(v.readFloatBE(offset));
16
16
  },
17
17
  decodeText: parseFloat,
18
18
  decodeTextBuffer: fastParseFloatBuffer,
@@ -26,3 +26,81 @@ export const ArrayFloat4Type = {
26
26
  oid: DataTypeOIDs._float4,
27
27
  elementsOID: DataTypeOIDs.float4,
28
28
  };
29
+ /**
30
+ * Powers of ten that a double holds exactly. Past 1e22 the literal is
31
+ * already rounded, and scaling by it stops being a faithful operation.
32
+ */
33
+ const POW10 = [
34
+ 1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14,
35
+ 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
36
+ ];
37
+ /**
38
+ * The shortest decimal that reads back as this same float4.
39
+ *
40
+ * A float4 arrives as four bytes and widens into a double that spells out
41
+ * the binary approximation in full: the column PostgreSQL prints as `1.1`
42
+ * read back as `1.100000023841858`. The text path never had the problem,
43
+ * since the server does this itself before sending - so the same column
44
+ * answered differently depending on a wire format the caller may not know
45
+ * is there, and the binary default was the wrong-looking one.
46
+ *
47
+ * Nine significant digits always round-trip a binary32 and most values
48
+ * need far fewer, so this tries successively longer forms and takes the
49
+ * first that reads back unchanged. Integers inside +-2^24 are already
50
+ * exact in float4 and skip the search; past that they are not (1e20
51
+ * widens to 100000002004087730000) so the bound matters. Infinities and
52
+ * NaN have no decimal to shorten.
53
+ *
54
+ * Rounding by hand, rather than through toPrecision(), is what makes it
55
+ * affordable - 24ns a value against 115ns, measured, because toPrecision()
56
+ * costs four times what the engine's own shortest-form toString does and
57
+ * there is no cheaper built-in. Scaling by a power of ten is only faithful
58
+ * while both the digits and the power are exact in a double: the digits
59
+ * always are at nine or fewer, and the magnitude check keeps the power
60
+ * inside its own exact range. A value sitting exactly halfway is left to
61
+ * the string path as well - Math.round() breaks those ties away from zero
62
+ * and dtoa breaks them to even, and the disagreement would show. Checked
63
+ * against the string path over 299,007 values, structured and random:
64
+ * same answer every time.
65
+ */
66
+ function shortestFloat4(n) {
67
+ if (!Number.isFinite(n))
68
+ return n;
69
+ if (Number.isInteger(n) && n >= -16777216 && n <= 16777216)
70
+ return n;
71
+ const abs = n < 0 ? -n : n;
72
+ // Two comparisons keep log10 and the loop off the path entirely for
73
+ // magnitudes whose power of ten could not be exact anyway.
74
+ if (abs >= 1e-14 && abs < 1e28) {
75
+ const e = Math.floor(Math.log10(abs));
76
+ let digits;
77
+ let k;
78
+ let pow;
79
+ let scaled;
80
+ let rounded;
81
+ let candidate;
82
+ for (digits = 7; digits <= 9; digits++) {
83
+ k = digits - 1 - e;
84
+ // Unreachable for the magnitudes let through above unless log10()
85
+ // lands a step out at a boundary, which it is entitled to do.
86
+ if (k < -22 || k > 22)
87
+ break;
88
+ pow = POW10[k < 0 ? -k : k];
89
+ scaled = k >= 0 ? n * pow : n / pow;
90
+ rounded = Math.round(scaled);
91
+ if (scaled - rounded === 0.5 || rounded - scaled === 0.5)
92
+ break;
93
+ candidate = k >= 0 ? rounded / pow : rounded * pow;
94
+ if (Math.fround(candidate) === n)
95
+ return candidate;
96
+ }
97
+ }
98
+ let p;
99
+ let s;
100
+ for (p = 7; p <= 9; p++) {
101
+ s = Number(n.toPrecision(p));
102
+ if (Math.fround(s) === n)
103
+ return s;
104
+ }
105
+ return n;
106
+ }
@@ -3,11 +3,8 @@ export const JsonType = {
3
3
  name: 'json',
4
4
  oid: DataTypeOIDs.json,
5
5
  jsType: 'string',
6
- decodeBinary(v, offset = 0, len, options) {
6
+ decodeBinary(v, offset = 0, len) {
7
7
  const content = v.toString('utf8', offset, offset + len);
8
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.json);
9
- if (fetchAsString)
10
- return content;
11
8
  return content ? JSON.parse(content) : undefined;
12
9
  },
13
10
  // json is stored as the text itself, so its binary form is those bytes.
@@ -25,19 +22,17 @@ export const JsonType = {
25
22
  return v ? 'true' : 'false';
26
23
  return '' + v;
27
24
  },
28
- decodeText(v, options) {
29
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.json);
30
- if (fetchAsString)
31
- return v;
32
- return v ? JSON.parse(v) : null;
33
- },
34
- decodeTextBuffer(buf, offset, len, options) {
35
- return JsonType.decodeText(buf.toString('utf8', offset, offset + len), options);
25
+ decodeText: decodeJsonText,
26
+ decodeTextBuffer(buf, offset, len) {
27
+ return decodeJsonText(buf.toString('utf8', offset, offset + len));
36
28
  },
37
29
  isType(v) {
38
30
  return v && typeof v === 'object';
39
31
  },
40
32
  };
33
+ function decodeJsonText(v) {
34
+ return v ? JSON.parse(v) : null;
35
+ }
41
36
  export const ArrayJsonType = {
42
37
  ...JsonType,
43
38
  name: '_json',
@@ -14,32 +14,27 @@ export const JsonbType = {
14
14
  buf.writeUInt8(1);
15
15
  buf.writeString(JsonbType.encodeText(v, {}), 'utf8');
16
16
  },
17
- decodeBinary(v, offset = 0, len, options) {
17
+ decodeBinary(v, offset = 0, len) {
18
18
  // jsonb's binary form is a one-byte version followed by the JSON text,
19
19
  // which carries no terminator of its own - so its end is whatever
20
20
  // `len` says, not the end of the buffer it happens to sit in.
21
21
  if (v[offset] !== 1)
22
22
  throw new Error('Unexpected Jsonb version value in header');
23
23
  const content = v.toString('utf8', offset + 1, offset + len);
24
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.jsonb);
25
- if (fetchAsString)
26
- return content;
27
24
  return content ? JSON.parse(content) : undefined;
28
25
  },
29
- decodeText(v, options) {
30
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.jsonb);
31
- if (fetchAsString)
32
- return v;
33
- return v ? JSON.parse(v) : null;
34
- },
26
+ decodeText: decodeJsonbText,
35
27
  // See json-type.ts's decodeTextBuffer comment - same rationale.
36
- decodeTextBuffer(buf, offset, len, options) {
37
- return JsonbType.decodeText(buf.toString('utf8', offset, offset + len), options);
28
+ decodeTextBuffer(buf, offset, len) {
29
+ return decodeJsonbText(buf.toString('utf8', offset, offset + len));
38
30
  },
39
31
  isType(v) {
40
32
  return v && typeof v === 'object';
41
33
  },
42
34
  };
35
+ function decodeJsonbText(v) {
36
+ return v ? JSON.parse(v) : null;
37
+ }
43
38
  export const ArrayJsonbType = {
44
39
  ...JsonbType,
45
40
  name: '_jsonb',
@@ -25,18 +25,15 @@ export const TimeType = {
25
25
  buf.writeUInt32BE(lo);
26
26
  },
27
27
  decodeBinary(v, offset = 0, _len, options) {
28
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.time);
29
28
  const hi = v.readInt32BE(offset);
30
29
  const lo = v.readUInt32BE(offset + 4);
31
30
  let d = new Date((lo + hi * timeMul) / 1000);
32
- if (fetchAsString || !options.utcDates) {
31
+ if (!options.utcDates) {
33
32
  d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
34
33
  }
35
- return fetchAsString ? dateToTimeString(d) : d;
34
+ return d;
36
35
  },
37
36
  decodeText(v, options) {
38
- if (options.fetchAsString?.includes(DataTypeOIDs.time))
39
- return v;
40
37
  return parseTime(v, false, options.utcDates);
41
38
  },
42
39
  // See date-type.ts's decodeTextBuffer comment - same rationale.
@@ -51,16 +48,6 @@ export const TimeType = {
51
48
  (typeof v === 'string' && STRICT_TIME_PATTERN.test(v)));
52
49
  },
53
50
  };
54
- function padZero(v) {
55
- return v < 9 ? '0' + v : '' + v;
56
- }
57
- function dateToTimeString(d) {
58
- return (padZero(d.getHours()) +
59
- ':' +
60
- padZero(d.getMinutes()) +
61
- ':' +
62
- padZero(d.getSeconds()));
63
- }
64
51
  export const ArrayTimeType = {
65
52
  ...TimeType,
66
53
  name: '_time',
@@ -37,33 +37,28 @@ export const TimestampType = {
37
37
  buf.writeUInt32BE(lo);
38
38
  },
39
39
  decodeBinary(v, offset = 0, _len, options) {
40
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.timestamp);
41
40
  const hi = v.readInt32BE(offset);
42
41
  const lo = v.readUInt32BE(offset + 4);
43
42
  if (lo === 0xffffffff && hi === 0x7fffffff)
44
- return fetchAsString ? 'infinity' : Infinity;
43
+ return Infinity;
45
44
  if (lo === 0x00000000 && hi === -0x80000000)
46
- return fetchAsString ? '-infinity' : -Infinity;
45
+ return -Infinity;
47
46
  // Shift from 2000 to 1970
48
47
  let d = new Date((lo + hi * timeMul) / 1000 + timeShift);
49
- if (fetchAsString || !options.utcDates) {
48
+ if (!options.utcDates) {
50
49
  d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
51
50
  }
52
- return fetchAsString ? dateToTimestampString(d) : d;
51
+ return d;
53
52
  },
54
53
  decodeText(v, options) {
55
- if (options.fetchAsString?.includes(DataTypeOIDs.timestamp))
56
- return v;
57
54
  return parseDateTime(v, options.utcDates);
58
55
  },
59
56
  // Reads PostgreSQL's own timestamp shape straight from the wire bytes -
60
- // see timestamptz-type.ts's decodeTextBuffer for why. The two guards are
61
- // what decodeText()/parseDateTime() would have applied anyway: with
62
- // fetchAsString the raw string is the answer, and with utcDates the
63
- // string has to go through the regex path that reads it as UTC.
57
+ // see timestamptz-type.ts's decodeTextBuffer for why. The guard is what
58
+ // decodeText()/parseDateTime() would have applied anyway: with utcDates
59
+ // the string has to go through the regex path that reads it as UTC.
64
60
  decodeTextBuffer(buf, offset, len, options) {
65
- if (!options.utcDates &&
66
- !options.fetchAsString?.includes(DataTypeOIDs.timestamp)) {
61
+ if (!options.utcDates) {
67
62
  const d = parsePgTimestampBuffer(buf, offset, offset + len);
68
63
  if (d !== undefined)
69
64
  return d;
@@ -79,22 +74,6 @@ export const TimestampType = {
79
74
  !(v.getFullYear() === 1970 && v.getMonth() === 0 && v.getDate() === 1));
80
75
  },
81
76
  };
82
- function padZero(v) {
83
- return v < 9 ? '0' + v : '' + v;
84
- }
85
- function dateToTimestampString(d) {
86
- return (d.getFullYear() +
87
- '-' +
88
- padZero(d.getMonth() + 1) +
89
- '-' +
90
- padZero(d.getDate()) +
91
- ' ' +
92
- padZero(d.getHours()) +
93
- ':' +
94
- padZero(d.getMinutes()) +
95
- ':' +
96
- padZero(d.getSeconds()));
97
- }
98
77
  export const ArrayTimestampType = {
99
78
  ...TimestampType,
100
79
  name: '_timestamp',
@@ -34,14 +34,13 @@ export const TimestamptzType = {
34
34
  buf.writeInt32BE(hi);
35
35
  buf.writeUInt32BE(lo);
36
36
  },
37
- decodeBinary(v, offset = 0, _len, options) {
38
- const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.timestamptz);
37
+ decodeBinary(v, offset = 0) {
39
38
  const hi = v.readInt32BE(offset);
40
39
  const lo = v.readUInt32BE(offset + 4);
41
40
  if (lo === 0xffffffff && hi === 0x7fffffff)
42
- return fetchAsString ? 'infinity' : Infinity;
41
+ return Infinity;
43
42
  if (lo === 0x00000000 && hi === -0x80000000)
44
- return fetchAsString ? '-infinity' : -Infinity;
43
+ return -Infinity;
45
44
  // Shift from 2000 to 1970. A timestamptz is an absolute instant, so
46
45
  // this is already the value - there is nothing to reinterpret against
47
46
  // the local zone the way `timestamp` (which carries no zone of its
@@ -51,23 +50,10 @@ export const TimestamptzType = {
51
50
  // ambiguous read as wall-clock, so the rebuild silently picked the
52
51
  // other one and moved the value an hour. The text path never did that,
53
52
  // so the same row decoded binary and text disagreed.
54
- const d = new Date((lo + hi * timeMul) / 1000 + timeShift);
55
- return fetchAsString ? dateToTimestamptzString(d) : d;
53
+ return new Date((lo + hi * timeMul) / 1000 + timeShift);
56
54
  },
57
55
  decodeText(v, options) {
58
- const d = parseDateTimeTz(v, options.utcDates);
59
- if (options.fetchAsString?.includes(DataTypeOIDs.timestamptz)) {
60
- if (d instanceof Date)
61
- return dateToTimestamptzString(d);
62
- if (d === Infinity)
63
- return 'infinity';
64
- if (d === -Infinity)
65
- return '-infinity';
66
- // parseDateTimeTz() only ever returns a Date, Infinity or -Infinity,
67
- // never anything else, so this is unreachable.
68
- return '';
69
- }
70
- return d;
56
+ return parseDateTimeTz(v, options.utcDates);
71
57
  },
72
58
  // Reads PostgreSQL's own timestamp shape straight from the wire bytes.
73
59
  // Building the string first and handing it to decodeText() costs about
@@ -75,26 +61,16 @@ export const TimestamptzType = {
75
61
  // scanned a character at a time. Anything not in that exact shape
76
62
  // (infinity, a BC suffix, an LMT-style offset) has no Date to give back
77
63
  // and takes the original path, which still needs the string.
78
- //
79
- // Note this type's decodeText always parses first and only branches on
80
- // fetchAsString afterward (unlike date/time/timestamp, which early-return
81
- // the raw string), so the same order is kept here.
82
64
  decodeTextBuffer(buf, offset, len, options) {
83
65
  const d = parsePgTimestampBuffer(buf, offset, offset + len);
84
- if (d !== undefined) {
85
- return options.fetchAsString?.includes(DataTypeOIDs.timestamptz)
86
- ? dateToTimestamptzString(d)
87
- : d;
88
- }
66
+ if (d !== undefined)
67
+ return d;
89
68
  return TimestamptzType.decodeText(buf.toString('latin1', offset, offset + len), options);
90
69
  },
91
70
  isType(v) {
92
71
  return v instanceof Date;
93
72
  },
94
73
  };
95
- function dateToTimestamptzString(d) {
96
- return d.toISOString().replace('T', ' ');
97
- }
98
74
  export const ArrayTimestamptzType = {
99
75
  ...TimestamptzType,
100
76
  name: '_timestamptz',
@@ -8,7 +8,7 @@ import type { FieldInfo } from './field-info.js';
8
8
  export interface BatchCommandResult {
9
9
  /** The command tag PostgreSQL reported for this set (INSERT/UPDATE/...). */
10
10
  command?: string;
11
- /** Rows affected, for INSERT/UPDATE/DELETE - as in QueryResult. */
11
+ /** Rows affected, for INSERT/UPDATE/DELETE/MERGE - as in QueryResult. */
12
12
  rowsAffected?: number;
13
13
  /**
14
14
  * Rows this set returned, decoded the same way `execute()` decodes them