postgrejs 3.5.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.
- package/CHANGELOG.md +26 -1
- package/README.md +17 -3
- package/connection/connection.js +13 -7
- package/connection/cursor.js +22 -1
- package/connection/intl-connection.d.ts +1 -1
- package/connection/intl-connection.js +106 -39
- package/connection/pool.js +28 -6
- package/connection/portal.d.ts +1 -1
- package/connection/portal.js +3 -2
- package/connection/prepared-statement.js +8 -4
- package/data-type-map.d.ts +11 -0
- package/data-type-map.js +20 -2
- package/data-types/char-type.js +15 -0
- package/data-types/date-type.js +4 -18
- package/data-types/float4-type.js +79 -1
- package/data-types/json-type.js +7 -12
- package/data-types/jsonb-type.js +7 -12
- package/data-types/time-type.js +2 -15
- package/data-types/timestamp-type.js +8 -29
- package/data-types/timestamptz-type.js +7 -31
- package/interfaces/batch-result.d.ts +1 -1
- package/interfaces/command-result.d.ts +21 -1
- package/interfaces/data-mapping-options.d.ts +16 -0
- package/interfaces/data-type.d.ts +11 -0
- package/interfaces/query-options.d.ts +25 -3
- package/package.json +1 -1
- package/protocol/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- package/util/channel-name.d.ts +24 -0
- package/util/channel-name.js +44 -0
- package/util/get-parsers.d.ts +2 -1
- package/util/get-parsers.js +16 -1
- package/util/resolve-column-formats.d.ts +33 -0
- package/util/resolve-column-formats.js +67 -0
- package/util/wrap-row-description.d.ts +2 -1
- package/util/wrap-row-description.js +7 -1
package/data-type-map.d.ts
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/data-types/char-type.js
CHANGED
|
@@ -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,
|
package/data-types/date-type.js
CHANGED
|
@@ -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
|
|
35
|
+
return Infinity;
|
|
37
36
|
if (t === -0x80000000)
|
|
38
|
-
return
|
|
37
|
+
return -Infinity;
|
|
39
38
|
// Shift from 2000 to 1970
|
|
40
39
|
let d = new Date(t * 1000 * 86400 + timeShift);
|
|
41
|
-
if (
|
|
40
|
+
if (!options.utcDates)
|
|
42
41
|
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
43
|
-
return
|
|
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
|
+
}
|
package/data-types/json-type.js
CHANGED
|
@@ -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
|
|
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
|
|
29
|
-
|
|
30
|
-
|
|
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',
|
package/data-types/jsonb-type.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
37
|
-
return
|
|
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',
|
package/data-types/time-type.js
CHANGED
|
@@ -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 (
|
|
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
|
|
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
|
|
43
|
+
return Infinity;
|
|
45
44
|
if (lo === 0x00000000 && hi === -0x80000000)
|
|
46
|
-
return
|
|
45
|
+
return -Infinity;
|
|
47
46
|
// Shift from 2000 to 1970
|
|
48
47
|
let d = new Date((lo + hi * timeMul) / 1000 + timeShift);
|
|
49
|
-
if (
|
|
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
|
|
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
|
|
61
|
-
//
|
|
62
|
-
//
|
|
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
|
|
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
|
|
41
|
+
return Infinity;
|
|
43
42
|
if (lo === 0x00000000 && hi === -0x80000000)
|
|
44
|
-
return
|
|
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
|
-
|
|
55
|
-
return fetchAsString ? dateToTimestamptzString(d) : d;
|
|
53
|
+
return new Date((lo + hi * timeMul) / 1000 + timeShift);
|
|
56
54
|
},
|
|
57
55
|
decodeText(v, options) {
|
|
58
|
-
|
|
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
|
|
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
|
|
@@ -24,7 +24,27 @@ export interface CommandResult {
|
|
|
24
24
|
*/
|
|
25
25
|
executeTime?: number;
|
|
26
26
|
/**
|
|
27
|
-
*
|
|
27
|
+
* True when the server stopped at the `fetchCount` limit instead of
|
|
28
|
+
* running the statement to completion: the portal suspended and the rows
|
|
29
|
+
* behind it were never fetched, so `rows` is a prefix rather than the
|
|
30
|
+
* whole result.
|
|
31
|
+
*
|
|
32
|
+
* Only ever set when an explicit `fetchCount` was given - the default
|
|
33
|
+
* asks for every row. Note the server does not look ahead: a statement
|
|
34
|
+
* whose result is exactly `fetchCount` rows long suspends too, so this
|
|
35
|
+
* means "the limit was reached", not "there are definitely more rows".
|
|
36
|
+
*
|
|
37
|
+
* Use a Cursor when the point is to read a large result in batches; a
|
|
38
|
+
* suspended portal on this path is simply discarded by the Sync that
|
|
39
|
+
* follows.
|
|
40
|
+
*/
|
|
41
|
+
suspended?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* How many rows the statement changed, for INSERT/UPDATE/DELETE/MERGE
|
|
44
|
+
* (MERGE reports its own total of inserted, updated and deleted rows,
|
|
45
|
+
* and needs PostgreSQL 15+). Left undefined for every other command,
|
|
46
|
+
* including SELECT - the count those report describes rows returned or
|
|
47
|
+
* moved, not rows affected.
|
|
28
48
|
*/
|
|
29
49
|
rowsAffected?: number;
|
|
30
50
|
}
|
|
@@ -5,5 +5,21 @@ export interface DataMappingOptions {
|
|
|
5
5
|
* @default false
|
|
6
6
|
*/
|
|
7
7
|
utcDates?: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* OIDs whose columns are to be handed back exactly as the server renders
|
|
10
|
+
* them, instead of being converted to a JavaScript value.
|
|
11
|
+
*
|
|
12
|
+
* This is a wire-level request, not a formatting choice made here: the
|
|
13
|
+
* listed columns are asked for in PostgreSQL's text format and returned
|
|
14
|
+
* unparsed, so the string is the server's own and nothing can drift
|
|
15
|
+
* between the two. Bind's result format codes are positional, though, so
|
|
16
|
+
* the OIDs can only be mapped onto columns once their types are known -
|
|
17
|
+
* a query whose statement is not prepared asks for the whole row as text
|
|
18
|
+
* instead (see IntlConnection.queryOnce()).
|
|
19
|
+
*
|
|
20
|
+
* An array column is selected by its own array OID (`_timestamptz`),
|
|
21
|
+
* never by its element's, and then the whole array literal is the string
|
|
22
|
+
* that comes back.
|
|
23
|
+
*/
|
|
8
24
|
fetchAsString?: OID[];
|
|
9
25
|
}
|
|
@@ -7,6 +7,17 @@ export interface DataType {
|
|
|
7
7
|
jsType: string;
|
|
8
8
|
arraySeparator?: string;
|
|
9
9
|
isType: (v: any) => boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Whether DataTypeMap.determine() may pick this type for a value, which
|
|
12
|
+
* it does by asking `isType`. Defaults to true.
|
|
13
|
+
*
|
|
14
|
+
* `false` is for a type that can genuinely hold the value - so `isType`
|
|
15
|
+
* answers truthfully for anyone who asks it directly - but that no
|
|
16
|
+
* caller means when they pass a plain JavaScript value. Inference then
|
|
17
|
+
* passes it by and the type is reached only by asking for it, with
|
|
18
|
+
* `new BindParam(oid, value)`.
|
|
19
|
+
*/
|
|
20
|
+
inferrable?: boolean;
|
|
10
21
|
decodeBinary: DecodeBinaryFunction;
|
|
11
22
|
decodeText: DecodeTextFunction;
|
|
12
23
|
decodeTextBuffer?: DecodeTextBufferFunction;
|
|
@@ -37,7 +37,15 @@ export interface QueryOptions extends DataMappingOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
typeMap?: DataTypeMap;
|
|
39
39
|
/**
|
|
40
|
-
* If true, returns Cursor instance instead of rows
|
|
40
|
+
* If true, returns Cursor instance instead of rows.
|
|
41
|
+
*
|
|
42
|
+
* The cursor reads through a portal, and a portal lives only as long as
|
|
43
|
+
* the transaction that created it. Outside an explicit transaction that
|
|
44
|
+
* is the implicit one, which any other statement on the same connection
|
|
45
|
+
* ends - so running another query on that connection while the cursor is
|
|
46
|
+
* open destroys it. Open the cursor inside a transaction, or give it a
|
|
47
|
+
* connection of its own; `Pool.query()` does the latter for you and
|
|
48
|
+
* keeps the connection out of the pool until the cursor closes.
|
|
41
49
|
*/
|
|
42
50
|
cursor?: boolean;
|
|
43
51
|
/**
|
|
@@ -50,8 +58,22 @@ export interface QueryOptions extends DataMappingOptions {
|
|
|
50
58
|
*/
|
|
51
59
|
columnFormat?: DataFormat | DataFormat[];
|
|
52
60
|
/**
|
|
53
|
-
*
|
|
54
|
-
*
|
|
61
|
+
* Maximum number of rows to fetch.
|
|
62
|
+
*
|
|
63
|
+
* `query()` fetches every row by default. Giving an explicit limit stops
|
|
64
|
+
* the server at that many rows and leaves the rest unfetched: `rows` is
|
|
65
|
+
* then a prefix and `suspended` is set on the result to say so. There is
|
|
66
|
+
* no way to ask for the remainder afterwards - the portal is discarded
|
|
67
|
+
* by the Sync that closes the call - so use a Cursor when the point is
|
|
68
|
+
* to read a large result a piece at a time.
|
|
69
|
+
*
|
|
70
|
+
* For a Cursor this is the batch size instead: how many rows each fetch
|
|
71
|
+
* asks for, defaulting to 100, with the cursor itself walking the whole
|
|
72
|
+
* result.
|
|
73
|
+
*
|
|
74
|
+
* 0 means unlimited, as it does in the protocol.
|
|
75
|
+
*
|
|
76
|
+
* @default 0 for query(), 100 for a Cursor's batches
|
|
55
77
|
*/
|
|
56
78
|
fetchCount?: number;
|
|
57
79
|
/**
|
package/package.json
CHANGED
package/protocol/frontend.d.ts
CHANGED
|
@@ -20,6 +20,13 @@ export declare namespace Frontend {
|
|
|
20
20
|
paramTypes?: Maybe<OID>[];
|
|
21
21
|
params?: any[];
|
|
22
22
|
queryOptions: QueryOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Result format codes to write, overriding queryOptions.columnFormat.
|
|
25
|
+
* Set by callers that know the statement's column types and have
|
|
26
|
+
* turned fetchAsString's OID list into positional codes - see
|
|
27
|
+
* resolveColumnFormats().
|
|
28
|
+
*/
|
|
29
|
+
columnFormat?: Protocol.DataFormat | Protocol.DataFormat[];
|
|
23
30
|
}
|
|
24
31
|
interface ParseMessageArgs {
|
|
25
32
|
statement?: string;
|
package/protocol/frontend.js
CHANGED
|
@@ -133,9 +133,11 @@ export class Frontend {
|
|
|
133
133
|
io.writeCString(args.portal || '', 'utf8');
|
|
134
134
|
io.writeCString(args.statement || '', 'utf8');
|
|
135
135
|
const { params, paramTypes, queryOptions } = args;
|
|
136
|
-
const columnFormat =
|
|
137
|
-
?
|
|
138
|
-
:
|
|
136
|
+
const columnFormat = args.columnFormat != null
|
|
137
|
+
? args.columnFormat
|
|
138
|
+
: queryOptions.columnFormat != null
|
|
139
|
+
? queryOptions.columnFormat
|
|
140
|
+
: DEFAULT_COLUMN_FORMAT;
|
|
139
141
|
if (params && params.length) {
|
|
140
142
|
io.writeInt16BE(params.length);
|
|
141
143
|
const formatOffset = io.position;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks a LISTEN/NOTIFY channel name and answers with the name the server
|
|
3
|
+
* will actually use for it.
|
|
4
|
+
*
|
|
5
|
+
* The name is written into LISTEN/UNLISTEN as an identifier, and an
|
|
6
|
+
* identifier can never be a parameter - so it goes through
|
|
7
|
+
* escapeIdentifier() at the call site rather than being restricted to a
|
|
8
|
+
* pattern. Any name PostgreSQL accepts then works, one-character and
|
|
9
|
+
* underscore-leading names included, which the old `/^[A-Z]\w+$/i` guard
|
|
10
|
+
* refused.
|
|
11
|
+
*
|
|
12
|
+
* Quoting alone would change what a name means, though, which is what this
|
|
13
|
+
* function is for: `LISTEN Foo` unquoted subscribes to `foo`, and every
|
|
14
|
+
* notification for it comes back as `foo`. Callers were registering their
|
|
15
|
+
* callback under the name they passed and never hearing from it - any
|
|
16
|
+
* channel name that was not already lower case was silently dead. Folding
|
|
17
|
+
* the same way the server would keeps the registration key and the name
|
|
18
|
+
* the server reports the same string, and keeps `NOTIFY Foo` written by
|
|
19
|
+
* hand on the other side reaching a `listen('Foo')` here.
|
|
20
|
+
*
|
|
21
|
+
* A name that could not have been written unquoted (a space, a dash) has
|
|
22
|
+
* no folding question to answer and is passed through as it is.
|
|
23
|
+
*/
|
|
24
|
+
export declare function normalizeChannelName(channel: string): string;
|