postgrejs 3.6.1 → 3.7.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 (63) hide show
  1. package/CHANGELOG.md +50 -6
  2. package/README.md +9 -83
  3. package/connection/intl-connection.d.ts +5 -3
  4. package/connection/intl-connection.js +43 -15
  5. package/connection/prepared-statement.js +2 -2
  6. package/constants.d.ts +60 -0
  7. package/constants.js +119 -1
  8. package/data-type-map.js +82 -0
  9. package/data-types/bit-type.d.ts +5 -0
  10. package/data-types/bit-type.js +91 -0
  11. package/data-types/box-type.js +9 -19
  12. package/data-types/circle-type.js +6 -16
  13. package/data-types/classes/geometric.d.ts +105 -0
  14. package/data-types/classes/geometric.js +197 -0
  15. package/data-types/classes/interval.d.ts +73 -0
  16. package/data-types/classes/interval.js +181 -0
  17. package/data-types/classes/numeric.d.ts +37 -0
  18. package/data-types/classes/numeric.js +47 -0
  19. package/data-types/classes/range.d.ts +74 -0
  20. package/data-types/classes/range.js +111 -0
  21. package/data-types/classes/type-oid.d.ts +34 -0
  22. package/data-types/classes/type-oid.js +45 -0
  23. package/data-types/inet-type.d.ts +5 -0
  24. package/data-types/inet-type.js +286 -0
  25. package/data-types/interval-type.d.ts +5 -0
  26. package/data-types/interval-type.js +79 -0
  27. package/data-types/jsonpath-type.d.ts +3 -0
  28. package/data-types/jsonpath-type.js +53 -0
  29. package/data-types/line-type.d.ts +3 -0
  30. package/data-types/line-type.js +41 -0
  31. package/data-types/lseg-type.js +9 -19
  32. package/data-types/macaddr-type.d.ts +5 -0
  33. package/data-types/macaddr-type.js +86 -0
  34. package/data-types/numeric-type.js +98 -69
  35. package/data-types/path-type.d.ts +5 -0
  36. package/data-types/path-type.js +139 -0
  37. package/data-types/pg-lsn-type.d.ts +3 -0
  38. package/data-types/pg-lsn-type.js +58 -0
  39. package/data-types/point-type.js +8 -13
  40. package/data-types/range-type.d.ts +23 -0
  41. package/data-types/range-type.js +342 -0
  42. package/data-types/snapshot-type.d.ts +5 -0
  43. package/data-types/snapshot-type.js +96 -0
  44. package/data-types/tid-type.d.ts +3 -0
  45. package/data-types/tid-type.js +50 -0
  46. package/data-types/timetz-type.d.ts +3 -0
  47. package/data-types/timetz-type.js +184 -0
  48. package/data-types/tsvector-type.d.ts +5 -0
  49. package/data-types/tsvector-type.js +216 -0
  50. package/data-types/xid-type.d.ts +7 -0
  51. package/data-types/xid-type.js +110 -0
  52. package/index.d.ts +4 -0
  53. package/index.js +4 -0
  54. package/interfaces/data-mapping-options.d.ts +26 -0
  55. package/interfaces/data-type.d.ts +8 -9
  56. package/package.json +1 -1
  57. package/protocol/connection-lost-error.d.ts +5 -3
  58. package/protocol/connection-lost-error.js +5 -3
  59. package/protocol/pg-socket.js +16 -2
  60. package/util/parse-array.js +17 -2
  61. package/util/resolve-column-formats.d.ts +2 -1
  62. package/util/resolve-column-formats.js +35 -8
  63. package/util/stringify-for-sql.js +37 -1
@@ -0,0 +1,5 @@
1
+ import type { DataType } from '../interfaces/data-type.js';
2
+ export declare const TsVectorType: DataType;
3
+ export declare const ArrayTsVectorType: DataType;
4
+ export declare const TsQueryType: DataType;
5
+ export declare const ArrayTsQueryType: DataType;
@@ -0,0 +1,216 @@
1
+ import { DataTypeOIDs } from '../constants.js';
2
+ /**
3
+ * `tsvector` and `tsquery` decode to the strings PostgreSQL prints -
4
+ * `'cat':1,3 'dog':2` and `'a' & !'b'` - and encode by handing a string
5
+ * straight back to the server. They share this file because they share
6
+ * that decision and half their quoting.
7
+ *
8
+ * **Why the text form.** It is the canonical spelling of both types: what
9
+ * the server accepts back, what the documentation is written in, and what
10
+ * someone inspecting an indexed document or a stored search wants to
11
+ * read. A structure of lexemes and positions, or a query tree, would be a
12
+ * second representation to convert to and from for values that are
13
+ * produced by `to_tsvector`/`to_tsquery` and consumed by a match
14
+ * operator, rather than walked. `pg` leaves both as strings.
15
+ *
16
+ * **Why no encodeBinary.** The server's own input parser is what defines
17
+ * these values. A `tsvector` literal is sorted and deduplicated on the
18
+ * way in, by a byte ordering and a position-merging rule that belong to
19
+ * the server; a `tsquery` literal is a small expression language, with
20
+ * operators that have been added across releases. Re-implementing either
21
+ * here would mean `'b a'::tsvector` or `'a<->b'::tsquery` quietly meaning
22
+ * one thing as a literal and another as a parameter, depending only on
23
+ * which column format the value travelled in. Handing the string over
24
+ * instead makes them the same by construction - the cost is that these
25
+ * two types cannot appear in a binary `COPY`, which `copyFrom` reports
26
+ * by name before it writes a row.
27
+ *
28
+ * Decoding is a different matter and is done here: the tree has already
29
+ * been built by the server, so reading it back is not parsing a grammar.
30
+ */
31
+ /** The four weights, in the order the top two bits of a position hold. */
32
+ const VECTOR_WEIGHTS = ['', 'C', 'B', 'A'];
33
+ /** The weights a query value carries, as a bit each. */
34
+ const QUERY_WEIGHTS = [
35
+ [8, 'A'],
36
+ [4, 'B'],
37
+ [2, 'C'],
38
+ [1, 'D'],
39
+ ];
40
+ const POSITION_MASK = 0x3fff;
41
+ const QI_VAL = 1;
42
+ const OP_NOT = 1;
43
+ const OP_AND = 2;
44
+ const OP_OR = 3;
45
+ const OP_PHRASE = 4;
46
+ /**
47
+ * How tightly each operator binds, which is what decides where the
48
+ * server puts parentheses - and so where this has to put them too.
49
+ */
50
+ const PRIORITY = {
51
+ [OP_NOT]: 4,
52
+ [OP_PHRASE]: 3,
53
+ [OP_AND]: 2,
54
+ [OP_OR]: 1,
55
+ };
56
+ /** A lexeme is printed quoted, with `'` doubled and `\` escaped. */
57
+ function quote(lexeme) {
58
+ let out = "'";
59
+ const l = lexeme.length;
60
+ let i;
61
+ let c;
62
+ for (i = 0; i < l; i++) {
63
+ c = lexeme[i];
64
+ if (c === "'" || c === '\\')
65
+ out += c;
66
+ out += c;
67
+ }
68
+ return out + "'";
69
+ }
70
+ /**
71
+ * An int32 lexeme count, then for each: the lexeme as a NUL-terminated
72
+ * string, an int16 count of positions, and that many int16s holding the
73
+ * weight in the top two bits and the position in the low fourteen. A
74
+ * weight of D is the default and is not printed.
75
+ */
76
+ function decodeVector(v, offset) {
77
+ const count = v.readInt32BE(offset);
78
+ let p = offset + 4;
79
+ let out = '';
80
+ let i;
81
+ let j;
82
+ let end;
83
+ let positions;
84
+ let entry;
85
+ for (i = 0; i < count; i++) {
86
+ end = v.indexOf(0, p);
87
+ if (i)
88
+ out += ' ';
89
+ out += quote(v.toString('utf8', p, end));
90
+ p = end + 1;
91
+ positions = v.readInt16BE(p);
92
+ p += 2;
93
+ for (j = 0; j < positions; j++) {
94
+ entry = v.readUInt16BE(p);
95
+ p += 2;
96
+ out +=
97
+ (j ? ',' : ':') + (entry & POSITION_MASK) + VECTOR_WEIGHTS[entry >> 14];
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+ /**
103
+ * An int32 node count, then the nodes themselves: an operator before its
104
+ * operands, and its right operand before its left. A value node carries
105
+ * a weight bitmask, a prefix flag and a NUL-terminated lexeme; a phrase
106
+ * operator carries an int16 distance.
107
+ */
108
+ function readQueryNode(v, pos) {
109
+ const type = v[pos.p++];
110
+ if (type === QI_VAL) {
111
+ const weights = v[pos.p++];
112
+ const prefix = !!v[pos.p++];
113
+ const end = v.indexOf(0, pos.p);
114
+ const lexeme = v.toString('utf8', pos.p, end);
115
+ pos.p = end + 1;
116
+ return { lexeme, weights, prefix };
117
+ }
118
+ const oper = v[pos.p++];
119
+ if (oper === OP_NOT)
120
+ return { oper, right: readQueryNode(v, pos) };
121
+ let distance = 0;
122
+ if (oper === OP_PHRASE) {
123
+ distance = v.readInt16BE(pos.p);
124
+ pos.p += 2;
125
+ }
126
+ // Right before left, which is the order they were written in.
127
+ const right = readQueryNode(v, pos);
128
+ const left = readQueryNode(v, pos);
129
+ return { oper, distance, left, right };
130
+ }
131
+ /**
132
+ * Prints the tree the way the server prints it, parentheses included.
133
+ * An operand is wrapped when it binds more loosely than what it sits
134
+ * under; the extra `rightPhraseOp` case is what keeps
135
+ * `'a' <-> ( 'b' <-> 'c' )` distinct from `'a' <-> 'b' <-> 'c'`, which
136
+ * are different queries.
137
+ */
138
+ function printQuery(node, parentPriority, rightPhraseOp) {
139
+ if (node.oper === undefined) {
140
+ let out = quote(node.lexeme);
141
+ if (node.weights || node.prefix) {
142
+ // The star comes before the letters - `'d':*AB`, not `'d':AB*`.
143
+ out += ':';
144
+ if (node.prefix)
145
+ out += '*';
146
+ let w;
147
+ for (w of QUERY_WEIGHTS)
148
+ if (node.weights & w[0])
149
+ out += w[1];
150
+ }
151
+ return out;
152
+ }
153
+ const priority = PRIORITY[node.oper];
154
+ if (node.oper === OP_NOT)
155
+ return '!' + printQuery(node.right, priority, false);
156
+ const op = node.oper === OP_AND
157
+ ? '&'
158
+ : node.oper === OP_OR
159
+ ? '|'
160
+ : node.distance === 1
161
+ ? '<->'
162
+ : '<' + node.distance + '>';
163
+ const out = printQuery(node.left, priority, false) +
164
+ ' ' +
165
+ op +
166
+ ' ' +
167
+ printQuery(node.right, priority, node.oper === OP_PHRASE);
168
+ return priority < parentPriority ||
169
+ (rightPhraseOp && priority === parentPriority)
170
+ ? '( ' + out + ' )'
171
+ : out;
172
+ }
173
+ export const TsVectorType = {
174
+ name: 'tsvector',
175
+ oid: DataTypeOIDs.tsvector,
176
+ jsType: 'string',
177
+ // The text form is ordinary-looking text - see inet-type.ts for why
178
+ // none of these string-shaped types join inference.
179
+ inferrable: false,
180
+ encodeText(v) {
181
+ return '' + v;
182
+ },
183
+ decodeBinary(v, offset = 0) {
184
+ return decodeVector(v, offset);
185
+ },
186
+ decodeText(v) {
187
+ return v;
188
+ },
189
+ isType(v) {
190
+ return typeof v === 'string';
191
+ },
192
+ };
193
+ export const ArrayTsVectorType = {
194
+ ...TsVectorType,
195
+ name: '_tsvector',
196
+ oid: DataTypeOIDs._tsvector,
197
+ elementsOID: DataTypeOIDs.tsvector,
198
+ };
199
+ export const TsQueryType = {
200
+ ...TsVectorType,
201
+ name: 'tsquery',
202
+ oid: DataTypeOIDs.tsquery,
203
+ decodeBinary(v, offset = 0) {
204
+ const count = v.readInt32BE(offset);
205
+ // An empty query prints as nothing, and has no root to read.
206
+ if (!count)
207
+ return '';
208
+ return printQuery(readQueryNode(v, { p: offset + 4 }), 0, false);
209
+ },
210
+ };
211
+ export const ArrayTsQueryType = {
212
+ ...TsQueryType,
213
+ name: '_tsquery',
214
+ oid: DataTypeOIDs._tsquery,
215
+ elementsOID: DataTypeOIDs.tsquery,
216
+ };
@@ -0,0 +1,7 @@
1
+ import type { DataType } from '../interfaces/data-type.js';
2
+ export declare const XidType: DataType;
3
+ export declare const ArrayXidType: DataType;
4
+ export declare const CidType: DataType;
5
+ export declare const ArrayCidType: DataType;
6
+ export declare const Xid8Type: DataType;
7
+ export declare const ArrayXid8Type: DataType;
@@ -0,0 +1,110 @@
1
+ import { DataTypeOIDs } from '../constants.js';
2
+ import { fastParseInt, fastParseIntBuffer } from '../util/fast-parseint.js';
3
+ /**
4
+ * `xid`, `xid8` and `cid` - the transaction and command counters behind
5
+ * the `xmin`, `xmax`, `cmin` and `cmax` system columns, which an
6
+ * application selects when it wants a row's identity for optimistic
7
+ * locking. They are counters, so they decode to numbers.
8
+ *
9
+ * None of the three joins inference. A JavaScript number says nothing
10
+ * about which of these it is, and claiming integers here would take them
11
+ * from `int4` and `int8`, where they almost always belong - name the
12
+ * type to use one.
13
+ */
14
+ const BIG32 = BigInt(32);
15
+ /**
16
+ * A 64-bit value is only exactly a JavaScript number while its high half
17
+ * is at most this: 0x1fffff * 2^32 + 0xffffffff is exactly
18
+ * Number.MAX_SAFE_INTEGER.
19
+ */
20
+ const MAX_SAFE_HIGH = 0x1fffff;
21
+ /** Both 64-bit types here are unsigned, so the halves are read apart. */
22
+ function readUInt64(v, offset) {
23
+ const hi = v.readUInt32BE(offset);
24
+ const lo = v.readUInt32BE(offset + 4);
25
+ if (hi <= MAX_SAFE_HIGH)
26
+ return hi * 4294967296 + lo;
27
+ return (BigInt(hi) << BIG32) | BigInt(lo);
28
+ }
29
+ function writeUInt64(buf, v) {
30
+ const n = typeof v === 'bigint' ? v : BigInt(fastParseInt(v));
31
+ buf.writeUInt32BE(Number(n >> BIG32));
32
+ buf.writeUInt32BE(Number(n & BigInt(0xffffffff)));
33
+ }
34
+ function createUInt32Type(name, oid) {
35
+ return {
36
+ name,
37
+ oid,
38
+ jsType: 'number',
39
+ inferrable: false,
40
+ encodeText(v) {
41
+ return '' + v;
42
+ },
43
+ encodeBinary(buf, v) {
44
+ buf.writeUInt32BE(fastParseInt(v));
45
+ },
46
+ decodeBinary(v, offset = 0) {
47
+ return v.readUInt32BE(offset);
48
+ },
49
+ decodeText: fastParseInt,
50
+ decodeTextBuffer: fastParseIntBuffer,
51
+ isType(v) {
52
+ return (typeof v === 'number' &&
53
+ Number.isInteger(v) &&
54
+ v >= 0 &&
55
+ v <= 4294967295);
56
+ },
57
+ };
58
+ }
59
+ export const XidType = createUInt32Type('xid', DataTypeOIDs.xid);
60
+ export const ArrayXidType = {
61
+ ...XidType,
62
+ name: '_xid',
63
+ oid: DataTypeOIDs._xid,
64
+ elementsOID: DataTypeOIDs.xid,
65
+ };
66
+ export const CidType = createUInt32Type('cid', DataTypeOIDs.cid);
67
+ export const ArrayCidType = {
68
+ ...CidType,
69
+ name: '_cid',
70
+ oid: DataTypeOIDs._cid,
71
+ elementsOID: DataTypeOIDs.cid,
72
+ };
73
+ export const Xid8Type = {
74
+ name: 'xid8',
75
+ oid: DataTypeOIDs.xid8,
76
+ // A number while the value fits one exactly, a BigInt after that -
77
+ // the same contract int8 has.
78
+ jsType: 'BigInt',
79
+ inferrable: false,
80
+ encodeText(v) {
81
+ return '' + v;
82
+ },
83
+ encodeBinary: writeUInt64,
84
+ decodeBinary(v, offset = 0) {
85
+ return readUInt64(v, offset);
86
+ },
87
+ decodeText(s) {
88
+ // A full transaction id never has a sign, so the digit count alone
89
+ // decides whether it can be a number.
90
+ if (s.length <= 15)
91
+ return Number(s);
92
+ const v = BigInt(s);
93
+ return v <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(v) : v;
94
+ },
95
+ decodeTextBuffer(buf, offset, len) {
96
+ if (len <= 15)
97
+ return fastParseIntBuffer(buf, offset, len);
98
+ return Xid8Type.decodeText(buf.toString('latin1', offset, offset + len), {});
99
+ },
100
+ isType(v) {
101
+ return (typeof v === 'bigint' ||
102
+ (typeof v === 'number' && Number.isInteger(v) && v >= 0));
103
+ },
104
+ };
105
+ export const ArrayXid8Type = {
106
+ ...Xid8Type,
107
+ name: '_xid8',
108
+ oid: DataTypeOIDs._xid8,
109
+ elementsOID: DataTypeOIDs.xid8,
110
+ };
package/index.d.ts CHANGED
@@ -8,6 +8,10 @@ export * from './connection/pool.js';
8
8
  export * from './connection/prepared-statement.js';
9
9
  export * from './constants.js';
10
10
  export * from './data-type-map.js';
11
+ export * from './data-types/classes/geometric.js';
12
+ export * from './data-types/classes/interval.js';
13
+ export * from './data-types/classes/numeric.js';
14
+ export * from './data-types/classes/range.js';
11
15
  export * from './interfaces/batch-result.js';
12
16
  export * from './interfaces/command-result.js';
13
17
  export * from './interfaces/copy-from-rows-options.js';
package/index.js CHANGED
@@ -8,6 +8,10 @@ export * from './connection/pool.js';
8
8
  export * from './connection/prepared-statement.js';
9
9
  export * from './constants.js';
10
10
  export * from './data-type-map.js';
11
+ export * from './data-types/classes/geometric.js';
12
+ export * from './data-types/classes/interval.js';
13
+ export * from './data-types/classes/numeric.js';
14
+ export * from './data-types/classes/range.js';
11
15
  export * from './interfaces/batch-result.js';
12
16
  export * from './interfaces/command-result.js';
13
17
  export * from './interfaces/copy-from-rows-options.js';
@@ -22,4 +22,30 @@ export interface DataMappingOptions {
22
22
  * that comes back.
23
23
  */
24
24
  fetchAsString?: OID[];
25
+ /**
26
+ * Ask the server for text on any column this client has no way to
27
+ * decode, instead of taking bytes it cannot read.
28
+ *
29
+ * Result columns are requested in binary, and there is a decoder only
30
+ * for the types registered in the type map - so an enum, a composite,
31
+ * an extension type, and 54 of PostgreSQL's own built-ins (`interval`,
32
+ * `inet`, the range family, `tsvector`, `money`, ...) arrive as a raw
33
+ * `Buffer` that nothing can interpret. With this on they arrive as the
34
+ * string PostgreSQL would have printed, which is what `pg` gives for
35
+ * the same column.
36
+ *
37
+ * Off by default, and it is not free: the column types have to be known
38
+ * before the Bind that asks for them, so a query that has not been
39
+ * prepared yet is prepared on first sight (one extra round trip per
40
+ * distinct statement per connection, measured at 233µs -> 447µs on
41
+ * loopback), and one run with `prepare: false` asks for the whole row
42
+ * as text.
43
+ *
44
+ * A registered type is untouched, so nothing that decodes today starts
45
+ * arriving as a string. An array whose own OID is unregistered comes
46
+ * back as the array literal rather than a JS array - there is no way to
47
+ * know it is an array without reading the catalog, and `pg` answers the
48
+ * same way. `fields[i].dataTypeName` stays empty for the same reason.
49
+ */
50
+ unknownTypesAsString?: boolean;
25
51
  }
@@ -26,15 +26,14 @@ export interface DataType {
26
26
  encodeText?: EncodeTextFunction;
27
27
  encodeCalculateDim?: EncodeCalculateDimFunction;
28
28
  }
29
- export interface Point {
30
- x: number;
31
- y: number;
32
- }
33
- export interface Circle {
34
- x: number;
35
- y: number;
36
- r: number;
37
- }
29
+ /**
30
+ * The shape `box` and `lseg` share.
31
+ *
32
+ * @deprecated Both types now decode to a class of their own - `Box` and
33
+ * `LineSegment` - which is what tells them apart; they were
34
+ * indistinguishable while both were only this. Kept because a plain
35
+ * object of these four numbers is still accepted as a parameter.
36
+ */
38
37
  export interface Rectangle {
39
38
  x1: number;
40
39
  y1: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "postgrejs",
3
3
  "description": "Professional PostgreSQL client NodeJS",
4
- "version": "3.6.1",
4
+ "version": "3.7.0",
5
5
  "author": "Panates",
6
6
  "license": "BSD-3-Clause",
7
7
  "dependencies": {
@@ -2,9 +2,11 @@
2
2
  * A connection whose socket closed without anyone asking it to - the
3
3
  * backend terminated by an administrator, a failover, a network fault.
4
4
  *
5
- * Reported on `Connection`'s `'close'` event as its reason, and for a
6
- * pooled connection on `Pool`'s `'destroy'` (as the second argument) and
7
- * `'error'`. `Pool` emits `'error'` for this and for a connection that
5
+ * Reported on `Connection`'s `'close'` event as its reason, for a pooled
6
+ * connection on `Pool`'s `'destroy'` (as the second argument) and
7
+ * `'error'`, and as the rejection of whatever query was in flight when it
8
+ * happened - one object for all of them, so a caller that needs both can
9
+ * pair them by identity. `Pool` emits `'error'` for this and for a connection that
8
10
  * could not be created in the first place, and those want different
9
11
  * handling - this class is how a listener tells them apart:
10
12
  *
@@ -2,9 +2,11 @@
2
2
  * A connection whose socket closed without anyone asking it to - the
3
3
  * backend terminated by an administrator, a failover, a network fault.
4
4
  *
5
- * Reported on `Connection`'s `'close'` event as its reason, and for a
6
- * pooled connection on `Pool`'s `'destroy'` (as the second argument) and
7
- * `'error'`. `Pool` emits `'error'` for this and for a connection that
5
+ * Reported on `Connection`'s `'close'` event as its reason, for a pooled
6
+ * connection on `Pool`'s `'destroy'` (as the second argument) and
7
+ * `'error'`, and as the rejection of whatever query was in flight when it
8
+ * happened - one object for all of them, so a caller that needs both can
9
+ * pair them by identity. `Pool` emits `'error'` for this and for a connection that
8
10
  * could not be created in the first place, and those want different
9
11
  * handling - this class is how a listener tells them apart:
10
12
  *
@@ -644,7 +644,13 @@ export class PgSocket extends SafeEventEmitter {
644
644
  const reason = this._state === ConnectionState.CLOSING
645
645
  ? undefined
646
646
  : this._buildCloseReason();
647
- this._failPendingCaptures(new Error('Connection closed'));
647
+ // Whatever was in flight is rejected with the same object the 'close'
648
+ // event carries, so `err.code === '08006'` works in a caller's catch
649
+ // exactly as it does in a Pool listener - the generic message it used
650
+ // to get named no cause and carried no code at all. A close that was
651
+ // asked for keeps that message: nothing went wrong, the caller simply
652
+ // raced the shutdown.
653
+ this._failPendingCaptures(reason ?? new Error('Connection closed'));
648
654
  this._reset();
649
655
  this._socket = undefined;
650
656
  this._state = ConnectionState.CLOSED;
@@ -658,7 +664,15 @@ export class PgSocket extends SafeEventEmitter {
658
664
  // here and the fact of closing separately, and only the pair together
659
665
  // says what actually happened.
660
666
  this._closeReason = err instanceof Error ? err : new Error(String(err));
661
- this._failPendingCaptures(this._closeReason);
667
+ // An error on a live connection is that connection going away - Node
668
+ // always follows 'error' with 'close' - so the caller gets the same
669
+ // ConnectionLostError the close will carry, with this as its cause,
670
+ // rather than a raw ECONNRESET whose `code` is a Node errno where a
671
+ // SQLSTATE is expected. Before READY there is no connection to have
672
+ // lost, and the error itself is the useful thing.
673
+ this._failPendingCaptures(this._state === ConnectionState.READY
674
+ ? this._buildCloseReason()
675
+ : this._closeReason);
662
676
  if (this._state !== ConnectionState.READY) {
663
677
  this._socket?.end();
664
678
  }
@@ -21,13 +21,28 @@ export function parsePostgresArray(s, opts) {
21
21
  continue;
22
22
  }
23
23
  if (c === '}' || c === sep) {
24
- if (token) {
24
+ // An element is present when it had content OR when it was
25
+ // quoted: `""` is a real empty string, and testing the token
26
+ // alone dropped it - `{"",b}` came back as one element, with
27
+ // every later index shifted.
28
+ //
29
+ // An unquoted empty token is still skipped. PostgreSQL never
30
+ // emits one (a null prints as the bare word NULL, an empty
31
+ // string always as `""`), so `{a,,b}` is not output this has to
32
+ // read - and skipping it is what keeps the separator that
33
+ // follows a nested array from pushing an element of its own.
34
+ if (token || exactlyValue) {
25
35
  if (token === 'NULL' && !exactlyValue)
26
36
  arr.push(null);
27
37
  else
28
38
  arr.push(transform ? transform(token) : token);
29
- exactlyValue = false;
30
39
  }
40
+ // Reset per element rather than only when one was pushed.
41
+ // Leaving it set carried "this one was quoted" into the next
42
+ // element - and that flag is what tells a real NULL apart from
43
+ // the string "NULL", so a null following an empty string
44
+ // decoded as four characters of text.
45
+ exactlyValue = false;
31
46
  token = '';
32
47
  if (c === '}')
33
48
  return;
@@ -1,3 +1,4 @@
1
+ import type { DataTypeMap } from '../data-type-map.js';
1
2
  import type { DataMappingOptions } from '../interfaces/data-mapping-options.js';
2
3
  import { Protocol } from '../protocol/protocol.js';
3
4
  import type { Maybe, OID } from '../types.js';
@@ -23,7 +24,7 @@ export interface ColumnFormatOptions extends DataMappingOptions {
23
24
  * Returns `columnFormat` untouched when there is nothing to do, so the
24
25
  * common case allocates nothing and keeps sending a single format code.
25
26
  */
26
- export declare function resolveColumnFormats(fields: Maybe<Protocol.RowDescription[]>, options: ColumnFormatOptions): Protocol.DataFormat | Protocol.DataFormat[];
27
+ export declare function resolveColumnFormats(fields: Maybe<Protocol.RowDescription[]>, options: ColumnFormatOptions, typeMap?: DataTypeMap): Protocol.DataFormat | Protocol.DataFormat[];
27
28
  /**
28
29
  * Whether two `fetchAsString` lists select the same columns. Used to tell
29
30
  * a cached set of parsers apart from one built for a different list, which
@@ -1,6 +1,18 @@
1
1
  import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
2
2
  import { Protocol } from '../protocol/protocol.js';
3
3
  const DataFormat = Protocol.DataFormat;
4
+ /**
5
+ * Whether this client could read the column if it arrived in `format`.
6
+ * Only ever answers no for binary: a column with no registered type at
7
+ * all still decodes as text, into the string the server printed, which is
8
+ * the whole point of asking for it that way.
9
+ */
10
+ function canDecode(typeMap, oid, format) {
11
+ if (format !== DataFormat.binary)
12
+ return true;
13
+ const reg = typeMap.get(oid);
14
+ return !!reg && typeof reg.decodeBinary === 'function';
15
+ }
4
16
  /**
5
17
  * Turns `fetchAsString`'s OID list into the positional result format codes
6
18
  * a Bind message actually carries.
@@ -20,28 +32,43 @@ const DataFormat = Protocol.DataFormat;
20
32
  * Returns `columnFormat` untouched when there is nothing to do, so the
21
33
  * common case allocates nothing and keeps sending a single format code.
22
34
  */
23
- export function resolveColumnFormats(fields, options) {
35
+ export function resolveColumnFormats(fields, options, typeMap) {
24
36
  const base = options.columnFormat != null ? options.columnFormat : DEFAULT_COLUMN_FORMAT;
25
37
  const asString = options.fetchAsString;
26
- if (!fields || !asString || !asString.length)
38
+ // unknownTypesAsString needs the type map to answer "could this be
39
+ // decoded", so it is inert without one - the call sites that have no
40
+ // map in hand are the ones that have no fields either.
41
+ const unknownAsString = !!options.unknownTypesAsString && !!typeMap;
42
+ const named = !!asString && asString.length > 0;
43
+ if (!fields || (!named && !unknownAsString))
27
44
  return base;
28
45
  const l = fields.length;
29
46
  const baseIsArray = Array.isArray(base);
30
47
  const out = new Array(l);
31
48
  let hit = false;
32
49
  let i;
50
+ let oid;
51
+ let format;
33
52
  for (i = 0; i < l; i++) {
34
- if (asString.includes(fields[i].dataTypeId)) {
53
+ oid = fields[i].dataTypeId;
54
+ format = baseIsArray
55
+ ? (base[i] ?? DEFAULT_COLUMN_FORMAT)
56
+ : base;
57
+ if (named && asString.includes(oid)) {
35
58
  // fetchAsString names the value the caller wants verbatim, so it
36
59
  // overrides an explicit columnFormat for its own columns - the two
37
60
  // cannot both be honored and the OID list is the more specific ask.
38
- out[i] = DataFormat.text;
61
+ format = DataFormat.text;
62
+ hit = true;
63
+ }
64
+ else if (unknownAsString && !canDecode(typeMap, oid, format)) {
65
+ // Nothing here could read the binary, so the bytes would reach the
66
+ // caller as a Buffer. Text at least arrives as what the server
67
+ // printed.
68
+ format = DataFormat.text;
39
69
  hit = true;
40
70
  }
41
- else
42
- out[i] = baseIsArray
43
- ? (base[i] ?? DEFAULT_COLUMN_FORMAT)
44
- : base;
71
+ out[i] = format;
45
72
  }
46
73
  return hit ? out : base;
47
74
  }
@@ -1,9 +1,41 @@
1
+ import { DataTypeOIDs } from '../constants.js';
2
+ import { GlobalTypeMap } from '../data-type-map.js';
1
3
  import { UuidType } from '../data-types/uuid-type.js';
2
4
  import { escapeLiteral } from './escape-literal.js';
3
5
  export function stringifyArrayForSQL(v, options, encode) {
4
6
  const arr = v.map(x => stringifyValueForSQL(x, options, encode));
5
7
  return 'ARRAY[' + arr.join(',') + ']';
6
8
  }
9
+ /**
10
+ * Which type an object is is asked rather than assumed.
11
+ *
12
+ * Every geometric class, Interval, Range, Date and Buffer has a
13
+ * registered `encodeText()` that writes exactly the literal PostgreSQL
14
+ * reads back, and `determine()` already knows which type each one is -
15
+ * it is what the same value is sent as when it is bound as a parameter.
16
+ * Without asking, all of them became `::json`, and since these classes
17
+ * have a `toJSON()` returning their own literal the result was not even
18
+ * a JSON object but a JSON string holding one, which casts to nothing:
19
+ * `'"(1,2)"'::json` where `'(1,2)'::point` was meant. A Date became its
20
+ * ISO string and a Buffer leaked its bytes as `{"type":"Buffer",...}`.
21
+ */
22
+ function stringifyObjectForSQL(v, options) {
23
+ // determine() throws for a class that must carry a type OID and does
24
+ // not - a Range built without one - and that throw is deliberate: the
25
+ // alternative is writing a literal for a type nobody chose.
26
+ const oid = GlobalTypeMap.determine(v);
27
+ const dataType = GlobalTypeMap.get(oid);
28
+ // json and jsonb are the fallback below; reaching them through their
29
+ // own encodeText would only add a cast the literal does not need.
30
+ // Anything with no registered text encoder lands there too.
31
+ if (dataType?.encodeText &&
32
+ oid !== DataTypeOIDs.json &&
33
+ oid !== DataTypeOIDs.jsonb)
34
+ return (escapeLiteral(dataType.encodeText(v, options || {})) +
35
+ '::' +
36
+ dataType.name);
37
+ return escapeLiteral(JSON.stringify(v)) + '::json';
38
+ }
7
39
  export function stringifyValueForSQL(v, options, encode) {
8
40
  if (v == null)
9
41
  return 'null';
@@ -20,6 +52,10 @@ export function stringifyValueForSQL(v, options, encode) {
20
52
  if (typeof v === 'string' && UuidType.isType(v))
21
53
  return escapeLiteral('' + v) + '::uuid';
22
54
  if (typeof v === 'object')
23
- return escapeLiteral(JSON.stringify(v)) + '::json';
55
+ return stringifyObjectForSQL(v, options);
56
+ // A string is deliberately written bare. determine() would answer
57
+ // varchar, but an unadorned literal is `unknown` and takes the type of
58
+ // wherever it lands - `insert into t(n) values('5')` works and
59
+ // `'5'::varchar` would not.
24
60
  return escapeLiteral('' + v);
25
61
  }