postgrejs 3.1.3 → 3.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.
Files changed (80) hide show
  1. package/CHANGELOG.md +79 -1
  2. package/README.md +79 -13
  3. package/connection/connection.d.ts +97 -0
  4. package/connection/connection.js +141 -2
  5. package/connection/cursor.d.ts +2 -1
  6. package/connection/cursor.js +6 -6
  7. package/connection/intl-connection.d.ts +113 -3
  8. package/connection/intl-connection.js +530 -45
  9. package/connection/prepared-statement.d.ts +64 -1
  10. package/connection/prepared-statement.js +97 -6
  11. package/data-types/bool-type.js +0 -1
  12. package/data-types/box-type.js +0 -1
  13. package/data-types/bytea-type.js +5 -3
  14. package/data-types/char-type.js +2 -2
  15. package/data-types/circle-type.js +0 -1
  16. package/data-types/date-type.js +1 -2
  17. package/data-types/float4-type.js +4 -5
  18. package/data-types/float8-type.js +4 -6
  19. package/data-types/int2-type.js +2 -2
  20. package/data-types/int2-vector-type.js +3 -2
  21. package/data-types/int4-type.js +2 -2
  22. package/data-types/int8-type.js +2 -2
  23. package/data-types/json-type.js +2 -2
  24. package/data-types/jsonb-type.js +6 -5
  25. package/data-types/lseg-type.js +0 -1
  26. package/data-types/numeric-type.js +9 -4
  27. package/data-types/oid-type.js +2 -2
  28. package/data-types/oid-vector-type.js +3 -2
  29. package/data-types/point-type.js +0 -1
  30. package/data-types/time-type.js +1 -2
  31. package/data-types/timestamp-type.js +13 -3
  32. package/data-types/timestamptz-type.js +27 -10
  33. package/data-types/uuid-type.js +1 -2
  34. package/data-types/varchar-type.js +2 -2
  35. package/index.d.ts +4 -0
  36. package/index.js +4 -0
  37. package/interfaces/batch-result.d.ts +42 -0
  38. package/interfaces/batch-result.js +1 -0
  39. package/interfaces/command-result.d.ts +3 -2
  40. package/interfaces/copy-from-rows-options.d.ts +58 -0
  41. package/interfaces/copy-from-rows-options.js +1 -0
  42. package/interfaces/data-type.d.ts +0 -1
  43. package/interfaces/database-connection-params.d.ts +25 -0
  44. package/interfaces/pipeline-request.d.ts +16 -0
  45. package/interfaces/pipeline-request.js +1 -0
  46. package/interfaces/query-options.d.ts +18 -0
  47. package/interfaces/script-execute-options.d.ts +12 -0
  48. package/package.json +18 -3
  49. package/protocol/backend.js +6 -6
  50. package/protocol/buffer-reader.d.ts +7 -12
  51. package/protocol/buffer-reader.js +11 -58
  52. package/protocol/database-error.d.ts +16 -0
  53. package/protocol/database-error.js +15 -0
  54. package/protocol/frontend.js +86 -96
  55. package/protocol/pg-socket.d.ts +82 -33
  56. package/protocol/pg-socket.js +143 -86
  57. package/protocol/pgoutput.js +2 -2
  58. package/protocol/smart-buffer.d.ts +11 -29
  59. package/protocol/smart-buffer.js +19 -162
  60. package/types.d.ts +13 -1
  61. package/util/assert-integer.d.ts +43 -0
  62. package/util/assert-integer.js +58 -0
  63. package/util/bigint-methods.d.ts +0 -1
  64. package/util/bigint-methods.js +0 -22
  65. package/util/copy-from-rows.d.ts +56 -0
  66. package/util/copy-from-rows.js +95 -0
  67. package/util/decode-binaryarray.d.ts +1 -1
  68. package/util/decode-binaryarray.js +7 -7
  69. package/util/encode-binaryarray.js +6 -6
  70. package/util/encode-copy-binary.d.ts +48 -0
  71. package/util/encode-copy-binary.js +112 -0
  72. package/util/fast-parsefloat.d.ts +15 -0
  73. package/util/fast-parsefloat.js +141 -0
  74. package/util/get-parsers.js +4 -8
  75. package/util/parse-datetime.js +7 -0
  76. package/util/parse-pg-timestamp.d.ts +31 -0
  77. package/util/parse-pg-timestamp.js +199 -0
  78. package/util/parse-row.js +120 -0
  79. package/util/row-decoder.d.ts +49 -0
  80. package/util/row-decoder.js +63 -0
@@ -1,3 +1,4 @@
1
+ import type { BatchResult } from '../interfaces/batch-result.js';
1
2
  import type { QueryOptions } from '../interfaces/query-options.js';
2
3
  import type { QueryResult } from '../interfaces/query-result.js';
3
4
  import type { StatementPrepareOptions } from '../interfaces/statement-prepare-options.js';
@@ -20,7 +21,69 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
20
21
  get sql(): string;
21
22
  get paramTypes(): Maybe<Maybe<OID>[]>;
22
23
  execute(options?: QueryOptions): Promise<QueryResult>;
24
+ /**
25
+ * Runs this statement once per parameter set, sending every Bind/Execute
26
+ * before waiting for any of them and closing the lot with a single Sync.
27
+ *
28
+ * ```ts
29
+ * const st = await connection.prepare('update users set name = $1 where id = $2');
30
+ * const batch = await st.executeBatch([
31
+ * ['John', 1],
32
+ * ['Jane', 2],
33
+ * ['Bob', 3],
34
+ * ]);
35
+ * batch.results.map(r => r.rowsAffected); // [1, 1, 0]
36
+ * batch.totalRowsAffected; // 2
37
+ * ```
38
+ *
39
+ * Three things follow from the single Sync, and they are the reasons to
40
+ * reach for this over a loop or a Promise.all() of execute() calls:
41
+ *
42
+ * - **Speed.** Per-set Sync makes the server close an implicit
43
+ * transaction and answer ReadyForQuery every time. Collapsing that to
44
+ * one turned 1000 updates from 194ms (pipelined execute() calls) into
45
+ * 21ms locally, with socket reads dropping from 972 to 3.
46
+ * - **One transaction.** Unless an explicit transaction is already open,
47
+ * the whole batch commits or rolls back together. This differs from N
48
+ * separate execute() calls, where each commits on its own.
49
+ * - **A failing set stops the rest.** PostgreSQL discards everything
50
+ * between an error and the Sync, so sets after a rejected one never
51
+ * run. The error carries `failedIndex` (which set was rejected) and
52
+ * `batchResults` (the ones that had completed) so the caller can see
53
+ * how far it got.
54
+ *
55
+ * Every set runs the same statement, so results map to sets by position.
56
+ * Sets that return rows get them decoded into `rows` exactly as
57
+ * `execute()` would, `rowDecoder` included; `fetchCount` does not apply,
58
+ * since a portal suspended mid-batch would break that mapping - each set
59
+ * runs to completion and buffers its rows.
60
+ *
61
+ * For a bulk INSERT specifically, `connection.copyFrom()` is faster
62
+ * still, and a single `UPDATE ... FROM (VALUES ...)` beats this for bulk
63
+ * updates of one shape - at the cost of PostgreSQL's 65535-parameter
64
+ * ceiling. This is the general answer when neither fits.
65
+ *
66
+ * @param paramSets One array of bind parameters per execution.
67
+ * @param options Applied to every set - the same options `execute()`
68
+ * takes, minus `fetchCount` and `cursor`, which a batch cannot honour.
69
+ */
70
+ executeBatch(paramSets: any[][], options?: QueryOptions): Promise<BatchResult>;
71
+ protected _executeBatchWithTransaction(paramSets: any[][], options: QueryOptions): Promise<BatchResult>;
23
72
  protected _executeWithTransaction(options?: QueryOptions): Promise<QueryResult>;
73
+ /**
74
+ * Runs `fn` inside whatever transaction handling the connection's config
75
+ * and `options` call for: an implicit BEGIN when autoCommit is off, a
76
+ * SAVEPOINT to roll back to when one is already open, a COMMIT when the
77
+ * call is the one turning autoCommit back on.
78
+ *
79
+ * Generic over `fn` rather than calling _execute() directly so that
80
+ * executeBatch() gets the same handling for the batch as a whole. Per
81
+ * set would be wrong twice over: the savepoints alone would add two
82
+ * statements per set, and the batch already shares one transaction by
83
+ * virtue of its single Sync, so a per-set savepoint could not roll one
84
+ * set back independently anyway.
85
+ */
86
+ protected _withTransaction<T>(options: QueryOptions | undefined, fn: (inlineSavepoint?: string) => Promise<T>, canInlineSavepoint?: boolean): Promise<T>;
24
87
  close(): Promise<void>;
25
88
  /**
26
89
  * Asks the server to cancel whatever this statement's connection is
@@ -28,7 +91,7 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
28
91
  * option, which reports the abort to the caller that asked for it.
29
92
  */
30
93
  cancel(): Promise<void>;
31
- protected _execute(options?: QueryOptions): Promise<QueryResult>;
94
+ protected _execute(options?: QueryOptions, savepoint?: string): Promise<QueryResult>;
32
95
  /**
33
96
  * Called by Cursor.close() instead of separately awaiting
34
97
  * Portal.close() then this.close(). Returns false (no wire action taken,
@@ -4,6 +4,7 @@ import { Protocol } from '../protocol/protocol.js';
4
4
  import { SafeEventEmitter } from '../safe-event-emitter.js';
5
5
  import { withAbortSignal } from '../util/abort-signal.js';
6
6
  import { getParsers } from '../util/get-parsers.js';
7
+ import { resolveRowType } from '../util/row-decoder.js';
7
8
  import { wrapRowDescription } from '../util/wrap-row-description.js';
8
9
  import { Cursor } from './cursor.js';
9
10
  import { getIntlConnection } from './intl-connection.js';
@@ -54,7 +55,89 @@ export class PreparedStatement extends SafeEventEmitter {
54
55
  return withAbortSignal(options.signal, () => intlCon.cancel(), () => this._executeWithTransaction(options));
55
56
  return this._executeWithTransaction(options);
56
57
  }
58
+ /**
59
+ * Runs this statement once per parameter set, sending every Bind/Execute
60
+ * before waiting for any of them and closing the lot with a single Sync.
61
+ *
62
+ * ```ts
63
+ * const st = await connection.prepare('update users set name = $1 where id = $2');
64
+ * const batch = await st.executeBatch([
65
+ * ['John', 1],
66
+ * ['Jane', 2],
67
+ * ['Bob', 3],
68
+ * ]);
69
+ * batch.results.map(r => r.rowsAffected); // [1, 1, 0]
70
+ * batch.totalRowsAffected; // 2
71
+ * ```
72
+ *
73
+ * Three things follow from the single Sync, and they are the reasons to
74
+ * reach for this over a loop or a Promise.all() of execute() calls:
75
+ *
76
+ * - **Speed.** Per-set Sync makes the server close an implicit
77
+ * transaction and answer ReadyForQuery every time. Collapsing that to
78
+ * one turned 1000 updates from 194ms (pipelined execute() calls) into
79
+ * 21ms locally, with socket reads dropping from 972 to 3.
80
+ * - **One transaction.** Unless an explicit transaction is already open,
81
+ * the whole batch commits or rolls back together. This differs from N
82
+ * separate execute() calls, where each commits on its own.
83
+ * - **A failing set stops the rest.** PostgreSQL discards everything
84
+ * between an error and the Sync, so sets after a rejected one never
85
+ * run. The error carries `failedIndex` (which set was rejected) and
86
+ * `batchResults` (the ones that had completed) so the caller can see
87
+ * how far it got.
88
+ *
89
+ * Every set runs the same statement, so results map to sets by position.
90
+ * Sets that return rows get them decoded into `rows` exactly as
91
+ * `execute()` would, `rowDecoder` included; `fetchCount` does not apply,
92
+ * since a portal suspended mid-batch would break that mapping - each set
93
+ * runs to completion and buffers its rows.
94
+ *
95
+ * For a bulk INSERT specifically, `connection.copyFrom()` is faster
96
+ * still, and a single `UPDATE ... FROM (VALUES ...)` beats this for bulk
97
+ * updates of one shape - at the cost of PostgreSQL's 65535-parameter
98
+ * ceiling. This is the general answer when neither fits.
99
+ *
100
+ * @param paramSets One array of bind parameters per execution.
101
+ * @param options Applied to every set - the same options `execute()`
102
+ * takes, minus `fetchCount` and `cursor`, which a batch cannot honour.
103
+ */
104
+ async executeBatch(paramSets, options = {}) {
105
+ if (!Array.isArray(paramSets))
106
+ throw new TypeError('executeBatch() requires an array of parameter sets');
107
+ if (options.cursor)
108
+ throw new Error('executeBatch() cannot return a cursor - a batch runs every set to ' +
109
+ 'completion under one Sync, so there is no portal left to fetch from');
110
+ const intlCon = getIntlConnection(this.connection);
111
+ if (!paramSets.length)
112
+ return { results: [], totalRowsAffected: 0 };
113
+ if (options.signal)
114
+ return withAbortSignal(options.signal, () => intlCon.cancel(), () => this._executeBatchWithTransaction(paramSets, options));
115
+ return this._executeBatchWithTransaction(paramSets, options);
116
+ }
117
+ _executeBatchWithTransaction(paramSets, options) {
118
+ const intlCon = getIntlConnection(this.connection);
119
+ return this._withTransaction(options, () => intlCon.executeBatchReused(this.name, this._fields, this.paramTypes, paramSets, options));
120
+ }
57
121
  async _executeWithTransaction(options = {}) {
122
+ // The Bind+Execute+Sync path can carry the rollbackOnError savepoint
123
+ // in its own round trip; the cursor path cannot, because the portal it
124
+ // opens outlives this call and the savepoint has to outlive it too.
125
+ return this._withTransaction(options, savepoint => this._execute(options, savepoint), !(options.cursor && this._fields));
126
+ }
127
+ /**
128
+ * Runs `fn` inside whatever transaction handling the connection's config
129
+ * and `options` call for: an implicit BEGIN when autoCommit is off, a
130
+ * SAVEPOINT to roll back to when one is already open, a COMMIT when the
131
+ * call is the one turning autoCommit back on.
132
+ *
133
+ * Generic over `fn` rather than calling _execute() directly so that
134
+ * executeBatch() gets the same handling for the batch as a whole. Per
135
+ * set would be wrong twice over: the savepoints alone would add two
136
+ * statements per set, and the batch already shares one transaction by
137
+ * virtue of its single Sync, so a per-set savepoint could not roll one
138
+ * set back independently anyway.
139
+ */
140
+ async _withTransaction(options = {}, fn, canInlineSavepoint = false) {
58
141
  const intlCon = getIntlConnection(this.connection);
59
142
  const transactionCommand = this.sql.match(/^(\bBEGIN\b|\bCOMMIT\b|\bSTART\b|\bROLLBACK|SAVEPOINT|RELEASE\b)/i);
60
143
  let beginFirst = false;
@@ -76,13 +159,21 @@ export class PreparedStatement extends SafeEventEmitter {
76
159
  const rollbackOnError = !transactionCommand &&
77
160
  intlCon.inTransaction &&
78
161
  (options?.rollbackOnError ?? intlCon.config.rollbackOnError ?? true);
79
- if (rollbackOnError && intlCon.inTransaction)
162
+ // When `fn` can put the savepoint on the wire itself, SAVEPOINT and
163
+ // RELEASE ride along inside its single Sync instead of costing a round
164
+ // trip each - measured at 51us for the pair that way against 511us as
165
+ // two separate round trips. The error path is unchanged either way:
166
+ // PostgreSQL discards everything between a failed statement and the
167
+ // Sync, so an inlined RELEASE never runs, and ROLLBACK TO goes out on
168
+ // its own below exactly as before.
169
+ const inlineSavepoint = canInlineSavepoint && rollbackOnError && intlCon.inTransaction;
170
+ if (rollbackOnError && intlCon.inTransaction && !inlineSavepoint)
80
171
  await intlCon.execute('SAVEPOINT ' + this._onErrorSavePoint);
81
172
  try {
82
- const result = await this._execute(options);
173
+ const result = await fn(inlineSavepoint ? this._onErrorSavePoint : undefined);
83
174
  if (commitLast)
84
175
  await intlCon.execute('COMMIT');
85
- else if (rollbackOnError && intlCon.inTransaction) {
176
+ else if (rollbackOnError && intlCon.inTransaction && !inlineSavepoint) {
86
177
  await intlCon.execute('RELEASE ' + this._onErrorSavePoint + ';');
87
178
  }
88
179
  return result;
@@ -111,7 +202,7 @@ export class PreparedStatement extends SafeEventEmitter {
111
202
  async cancel() {
112
203
  return getIntlConnection(this.connection).cancel();
113
204
  }
114
- async _execute(options = {}) {
205
+ async _execute(options = {}, savepoint) {
115
206
  const intlCon = getIntlConnection(this.connection);
116
207
  if (options.cursor && this._fields) {
117
208
  intlCon.ref();
@@ -124,7 +215,7 @@ export class PreparedStatement extends SafeEventEmitter {
124
215
  const result = {
125
216
  command: undefined,
126
217
  fields: resultFields,
127
- rowType: options.objectRows ? 'object' : 'array',
218
+ rowType: resolveRowType(options),
128
219
  cursor: new Cursor(this, portal, resultFields, parsers, options),
129
220
  };
130
221
  this._refCount++;
@@ -141,7 +232,7 @@ export class PreparedStatement extends SafeEventEmitter {
141
232
  // unnamed portal, reusing the RowDescription prepare() already fetched
142
233
  // instead of re-Describing (and re-Close-ing) a fresh named portal on
143
234
  // every call.
144
- return intlCon.executeReused(this.name, this._fields, this.paramTypes, options.params, options);
235
+ return intlCon.executeReused(this.name, this._fields, this.paramTypes, options.params, options, savepoint);
145
236
  }
146
237
  /**
147
238
  * Called by Cursor.close() instead of separately awaiting
@@ -3,7 +3,6 @@ export const BoolType = {
3
3
  name: 'bool',
4
4
  oid: DataTypeOIDs.bool,
5
5
  jsType: 'boolean',
6
- fixedBinarySize: 1,
7
6
  // 't'/'f', the form decodeText above reads and the server itself
8
7
  // emits - encodeText and decodeText are a public pair and have to stay
9
8
  // each other's inverse.
@@ -7,7 +7,6 @@ export const BoxType = {
7
7
  oid: DataTypeOIDs.box,
8
8
  jsType: 'object',
9
9
  arraySeparator: ';',
10
- fixedBinarySize: 32,
11
10
  encodeText(v) {
12
11
  return `(${v.x1},${v.y1}),(${v.x2},${v.y2})`;
13
12
  },
@@ -9,10 +9,12 @@ export const ByteaType = {
9
9
  return '\\x' + v.toString('hex');
10
10
  },
11
11
  encodeBinary(buf, v) {
12
- buf.writeBuffer(v);
12
+ buf.writeBytes(v);
13
13
  },
14
- decodeBinary(v, offset = 0) {
15
- return offset ? v.subarray(offset) : v;
14
+ decodeBinary(v, offset = 0, len) {
15
+ // A view, not a copy - the value handed back is the caller's own
16
+ // window onto the wire bytes, which is what bytea has always returned.
17
+ return v.subarray(offset, offset + len);
16
18
  },
17
19
  decodeText: parseBytea,
18
20
  decodeTextBuffer: parseByteaBuffer,
@@ -9,8 +9,8 @@ export const CharType = {
9
9
  encodeBinary(buf, v) {
10
10
  buf.writeString((v ? '' + v : ' ')[0], 'utf8');
11
11
  },
12
- decodeBinary(v, offset = 0) {
13
- return v.toString('utf8', offset);
12
+ decodeBinary(v, offset = 0, len) {
13
+ return v.toString('utf8', offset, offset + len);
14
14
  },
15
15
  decodeText(v) {
16
16
  return v;
@@ -7,7 +7,6 @@ export const CircleType = {
7
7
  name: 'circle',
8
8
  oid: DataTypeOIDs.circle,
9
9
  jsType: 'object',
10
- fixedBinarySize: 24,
11
10
  encodeText(v) {
12
11
  return `<(${v.x},${v.y}),${v.r}>`;
13
12
  },
@@ -6,7 +6,6 @@ export const DateType = {
6
6
  name: 'date',
7
7
  oid: DataTypeOIDs.date,
8
8
  jsType: 'Date',
9
- fixedBinarySize: 4,
10
9
  encodeText(v, options) {
11
10
  return formatDate(v, options);
12
11
  },
@@ -30,7 +29,7 @@ export const DateType = {
30
29
  const t = Math.trunc(n + Number.EPSILON);
31
30
  buf.writeInt32BE(t);
32
31
  },
33
- decodeBinary(v, offset = 0, options) {
32
+ decodeBinary(v, offset = 0, _len, options) {
34
33
  const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.date);
35
34
  const t = v.readInt32BE(offset);
36
35
  if (t === 0x7fffffff)
@@ -1,22 +1,21 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertCoercedNumber } from '../util/assert-integer.js';
3
+ import { fastParseFloatBuffer } from '../util/fast-parsefloat.js';
2
4
  export const Float4Type = {
3
5
  name: 'float4',
4
6
  oid: DataTypeOIDs.float4,
5
7
  jsType: 'number',
6
- fixedBinarySize: 4,
7
8
  encodeText(v) {
8
9
  return '' + v;
9
10
  },
10
11
  encodeBinary(buf, v) {
11
- buf.writeFloatBE(typeof v === 'number' ? v : parseFloat(v));
12
+ buf.writeFloatBE(typeof v === 'number' ? v : assertCoercedNumber(parseFloat(v), 'float4'));
12
13
  },
13
14
  decodeBinary(v, offset = 0) {
14
15
  return v.readFloatBE(offset);
15
16
  },
16
17
  decodeText: parseFloat,
17
- decodeTextBuffer(buf, offset, len) {
18
- return parseFloat(buf.toString('latin1', offset, offset + len));
19
- },
18
+ decodeTextBuffer: fastParseFloatBuffer,
20
19
  isType(v) {
21
20
  return typeof v === 'number';
22
21
  },
@@ -1,24 +1,22 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertCoercedNumber } from '../util/assert-integer.js';
3
+ import { fastParseFloatBuffer } from '../util/fast-parsefloat.js';
2
4
  export const Float8Type = {
3
5
  name: 'float8',
4
6
  oid: DataTypeOIDs.float8,
5
7
  jsType: 'number',
6
- fixedBinarySize: 8,
7
8
  // Infinity/NaN stringify to the words PostgreSQL itself uses.
8
9
  encodeText(v) {
9
10
  return '' + v;
10
11
  },
11
12
  encodeBinary(buf, v) {
12
- buf.writeDoubleBE(typeof v === 'number' ? v : parseFloat(v));
13
+ buf.writeDoubleBE(typeof v === 'number' ? v : assertCoercedNumber(parseFloat(v), 'float8'));
13
14
  },
14
15
  decodeBinary(v, offset = 0) {
15
16
  return v.readDoubleBE(offset);
16
17
  },
17
18
  decodeText: parseFloat,
18
- // See float4-type.ts's decodeTextBuffer comment - same rationale.
19
- decodeTextBuffer(buf, offset, len) {
20
- return parseFloat(buf.toString('latin1', offset, offset + len));
21
- },
19
+ decodeTextBuffer: fastParseFloatBuffer,
22
20
  isType(v) {
23
21
  return typeof v === 'number';
24
22
  },
@@ -1,15 +1,15 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertInteger } from '../util/assert-integer.js';
2
3
  import { fastParseInt, fastParseIntBuffer } from '../util/fast-parseint.js';
3
4
  export const Int2Type = {
4
5
  name: 'int2',
5
6
  oid: DataTypeOIDs.int2,
6
7
  jsType: 'number',
7
- fixedBinarySize: 2,
8
8
  encodeText(v) {
9
9
  return '' + v;
10
10
  },
11
11
  encodeBinary(buf, v) {
12
- buf.writeInt16BE(fastParseInt(v));
12
+ buf.writeInt16BE(assertInteger(fastParseInt(v), 'int2'));
13
13
  },
14
14
  decodeBinary(v, offset = 0) {
15
15
  return v.readInt16BE(offset);
@@ -15,8 +15,9 @@ export const Int2VectorType = {
15
15
  encodeCalculateDim(v) {
16
16
  return [v.length];
17
17
  },
18
- decodeBinary(v) {
19
- return decodeBinaryArray(v, 0, b => b.readInt16BE()) || undefined;
18
+ decodeBinary(v, offset = 0) {
19
+ return (decodeBinaryArray(v, offset, (b, off) => b.readInt16BE(off)) ||
20
+ undefined);
20
21
  },
21
22
  decodeText(str) {
22
23
  return str.split(' ').map(fastParseInt);
@@ -1,15 +1,15 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertInteger } from '../util/assert-integer.js';
2
3
  import { fastParseInt, fastParseIntBuffer } from '../util/fast-parseint.js';
3
4
  export const Int4Type = {
4
5
  name: 'int4',
5
6
  oid: DataTypeOIDs.int4,
6
7
  jsType: 'number',
7
- fixedBinarySize: 4,
8
8
  encodeText(v) {
9
9
  return '' + v;
10
10
  },
11
11
  encodeBinary(buf, v) {
12
- buf.writeInt32BE(fastParseInt(v));
12
+ buf.writeInt32BE(assertInteger(fastParseInt(v), 'int4'));
13
13
  },
14
14
  decodeBinary(v, offset = 0) {
15
15
  return v.readInt32BE(offset);
@@ -1,4 +1,5 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertBigIntSource } from '../util/assert-integer.js';
2
3
  import { readBigInt64BE } from '../util/bigint-methods.js';
3
4
  import { fastParseIntBuffer } from '../util/fast-parseint.js';
4
5
  const maxSafeInteger = BigInt(Number.MAX_SAFE_INTEGER);
@@ -6,12 +7,11 @@ export const Int8Type = {
6
7
  name: 'int8',
7
8
  oid: DataTypeOIDs.int8,
8
9
  jsType: 'BigInt',
9
- fixedBinarySize: 8,
10
10
  encodeText(v) {
11
11
  return '' + v;
12
12
  },
13
13
  encodeBinary(buf, v) {
14
- buf.writeBigInt64BE(v);
14
+ buf.writeBigInt64BE(assertBigIntSource(v, 'int8'));
15
15
  },
16
16
  decodeBinary(buf, offset = 0) {
17
17
  const v = typeof buf.readBigInt64BE === 'function'
@@ -3,8 +3,8 @@ export const JsonType = {
3
3
  name: 'json',
4
4
  oid: DataTypeOIDs.json,
5
5
  jsType: 'string',
6
- decodeBinary(v, offset = 0, options) {
7
- const content = v.toString('utf8', offset);
6
+ decodeBinary(v, offset = 0, len, options) {
7
+ const content = v.toString('utf8', offset, offset + len);
8
8
  const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.json);
9
9
  if (fetchAsString)
10
10
  return content;
@@ -1,5 +1,4 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
- import { BufferReader } from '../protocol/buffer-reader.js';
3
2
  export const JsonbType = {
4
3
  name: 'jsonb',
5
4
  oid: DataTypeOIDs.jsonb,
@@ -15,12 +14,14 @@ export const JsonbType = {
15
14
  buf.writeUInt8(1);
16
15
  buf.writeString(JsonbType.encodeText(v, {}), 'utf8');
17
16
  },
18
- decodeBinary(v, offset = 0, options) {
19
- const buf = new BufferReader(offset ? v.subarray(offset) : v);
20
- if (buf.readUInt8() !== 1)
17
+ decodeBinary(v, offset = 0, len, options) {
18
+ // jsonb's binary form is a one-byte version followed by the JSON text,
19
+ // which carries no terminator of its own - so its end is whatever
20
+ // `len` says, not the end of the buffer it happens to sit in.
21
+ if (v[offset] !== 1)
21
22
  throw new Error('Unexpected Jsonb version value in header');
23
+ const content = v.toString('utf8', offset + 1, offset + len);
22
24
  const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.jsonb);
23
- const content = buf.readLString(buf.length - buf.offset);
24
25
  if (fetchAsString)
25
26
  return content;
26
27
  return content ? JSON.parse(content) : undefined;
@@ -7,7 +7,6 @@ export const LsegType = {
7
7
  name: 'lseg',
8
8
  oid: DataTypeOIDs.lseg,
9
9
  jsType: 'object',
10
- fixedBinarySize: 32,
11
10
  encodeText(v) {
12
11
  return `[(${v.x1},${v.y1}),(${v.x2},${v.y2})]`;
13
12
  },
@@ -1,4 +1,6 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
+ import { assertCoercedNumber } from '../util/assert-integer.js';
3
+ import { fastParseFloatBuffer } from '../util/fast-parsefloat.js';
2
4
  const NUMERIC_NEG = 0x4000;
3
5
  const NUMERIC_NAN = 0xc000;
4
6
  const NUMERIC_PINF = 0xd000;
@@ -36,6 +38,12 @@ export const NumericType = {
36
38
  return writeHeader(0, 0, NUMERIC_PINF, 0);
37
39
  if (str === '-Infinity')
38
40
  return writeHeader(0, 0, NUMERIC_NINF, 0);
41
+ // Anything with no numeric reading at all - `{}` stringifies to
42
+ // "[object Object]", `true` to "true" - would otherwise be walked
43
+ // digit by digit below and stored as 0.
44
+ // Number('') is 0, not NaN, so an empty string - what `[]` and `null`
45
+ // stringify to - would slip past the NaN check below.
46
+ assertCoercedNumber(str.length ? Number(str) : NaN, 'numeric');
39
47
  // Exponent form has no place in the wire format; go through Number to
40
48
  // get it back into plain notation. toFixed() cannot do this itself -
41
49
  // per spec it falls back to exponential notation once the magnitude
@@ -109,10 +117,7 @@ export const NumericType = {
109
117
  return '' + n;
110
118
  },
111
119
  decodeText: parseFloat,
112
- // See float4-type.ts's decodeTextBuffer comment - same rationale.
113
- decodeTextBuffer(buf, offset, len) {
114
- return parseFloat(buf.toString('latin1', offset, offset + len));
115
- },
120
+ decodeTextBuffer: fastParseFloatBuffer,
116
121
  isType(v) {
117
122
  return typeof v === 'number';
118
123
  },
@@ -1,15 +1,15 @@
1
1
  import { DataTypeNames, DataTypeOIDs } from '../constants.js';
2
+ import { assertInteger } from '../util/assert-integer.js';
2
3
  import { fastParseInt, fastParseIntBuffer } from '../util/fast-parseint.js';
3
4
  export const OidType = {
4
5
  name: 'oid',
5
6
  oid: DataTypeOIDs.oid,
6
7
  jsType: 'number',
7
- fixedBinarySize: 4,
8
8
  encodeText(v) {
9
9
  return '' + v;
10
10
  },
11
11
  encodeBinary(buf, v) {
12
- buf.writeUInt32BE(fastParseInt(v));
12
+ buf.writeUInt32BE(assertInteger(fastParseInt(v), 'oid'));
13
13
  },
14
14
  decodeBinary(v, offset = 0) {
15
15
  return v.readUInt32BE(offset);
@@ -21,8 +21,9 @@ export const OidVectorType = {
21
21
  encodeCalculateDim(v) {
22
22
  return [v.length];
23
23
  },
24
- decodeBinary(v) {
25
- return decodeBinaryArray(v, 0, b => b.readUInt32BE()) || undefined;
24
+ decodeBinary(v, offset = 0) {
25
+ return (decodeBinaryArray(v, offset, (b, off) => b.readUInt32BE(off)) ||
26
+ undefined);
26
27
  },
27
28
  decodeText(str) {
28
29
  return str.split(' ').map(fastParseInt);
@@ -5,7 +5,6 @@ export const PointType = {
5
5
  name: 'point',
6
6
  oid: DataTypeOIDs.point,
7
7
  jsType: 'object',
8
- fixedBinarySize: 16,
9
8
  encodeText(v) {
10
9
  return `(${v.x},${v.y})`;
11
10
  },
@@ -6,7 +6,6 @@ export const TimeType = {
6
6
  name: 'time',
7
7
  oid: DataTypeOIDs.time,
8
8
  jsType: 'string',
9
- fixedBinarySize: 8,
10
9
  encodeText(v, options) {
11
10
  return formatTime(v, options);
12
11
  },
@@ -25,7 +24,7 @@ export const TimeType = {
25
24
  buf.writeInt32BE(hi);
26
25
  buf.writeUInt32BE(lo);
27
26
  },
28
- decodeBinary(v, offset = 0, options) {
27
+ decodeBinary(v, offset = 0, _len, options) {
29
28
  const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.time);
30
29
  const hi = v.readInt32BE(offset);
31
30
  const lo = v.readUInt32BE(offset + 4);
@@ -1,13 +1,13 @@
1
1
  import { DataTypeOIDs } from '../constants.js';
2
2
  import { formatTimestamp } from '../util/format-datetime.js';
3
3
  import { parseDateTime } from '../util/parse-datetime.js';
4
+ import { parsePgTimestampBuffer } from '../util/parse-pg-timestamp.js';
4
5
  const timeShift = 946684800000;
5
6
  const timeMul = 4294967296;
6
7
  export const TimestampType = {
7
8
  name: 'timestamp',
8
9
  oid: DataTypeOIDs.timestamp,
9
10
  jsType: 'Date',
10
- fixedBinarySize: 8,
11
11
  encodeText(v, options) {
12
12
  return formatTimestamp(v, options);
13
13
  },
@@ -36,7 +36,7 @@ export const TimestampType = {
36
36
  buf.writeInt32BE(hi);
37
37
  buf.writeUInt32BE(lo);
38
38
  },
39
- decodeBinary(v, offset = 0, options) {
39
+ decodeBinary(v, offset = 0, _len, options) {
40
40
  const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.timestamp);
41
41
  const hi = v.readInt32BE(offset);
42
42
  const lo = v.readUInt32BE(offset + 4);
@@ -56,8 +56,18 @@ export const TimestampType = {
56
56
  return v;
57
57
  return parseDateTime(v, options.utcDates);
58
58
  },
59
- // See date-type.ts's decodeTextBuffer comment - same rationale.
59
+ // 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.
60
64
  decodeTextBuffer(buf, offset, len, options) {
65
+ if (!options.utcDates &&
66
+ !options.fetchAsString?.includes(DataTypeOIDs.timestamp)) {
67
+ const d = parsePgTimestampBuffer(buf, offset, offset + len);
68
+ if (d !== undefined)
69
+ return d;
70
+ }
61
71
  return TimestampType.decodeText(buf.toString('latin1', offset, offset + len), options);
62
72
  },
63
73
  isType(v) {