postgrejs 3.10.1 → 3.11.1
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 +24 -1
- package/data-types/date-type.js +6 -5
- package/data-types/macaddr-type.js +14 -5
- package/data-types/money-type.js +28 -7
- package/data-types/pg-lsn-type.js +27 -6
- package/data-types/time-type.js +3 -5
- package/data-types/timestamp-type.js +3 -5
- package/data-types/uuid-type.js +13 -9
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/util/hex-text.d.ts +26 -0
- package/util/hex-text.js +38 -0
- package/util/is-multi-statement.d.ts +47 -0
- package/util/is-multi-statement.js +184 -0
- package/util/local-date.d.ts +52 -0
- package/util/local-date.js +69 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.
|
|
3
|
+
### [v3.11.1](https://github.com/panates/postgrejs/compare/v3.11.0...v3.11.1) -
|
|
4
|
+
|
|
5
|
+
#### 🪲 Fixes
|
|
6
|
+
|
|
7
|
+
- fix(bench): count off-heap memory in Peak Heap @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### ⚡️ Performance Improvments
|
|
10
|
+
|
|
11
|
+
- perf: shift the instant instead of rebuilding a local Date from its parts @Eray Hanoğlu
|
|
12
|
+
- perf: build uuid and macaddr text in one pass instead of five @Eray Hanoğlu
|
|
13
|
+
- perf: stop asking whether a small money value round-trips @Eray Hanoğlu
|
|
14
|
+
- perf: write a pg_lsn without four strings a value @Eray Hanoğlu
|
|
15
|
+
|
|
16
|
+
### [v3.11.0](https://github.com/panates/postgrejs/compare/v3.10.1...v3.11.0) - 23 September 2026
|
|
17
|
+
|
|
18
|
+
#### 🚀 New Features
|
|
19
|
+
|
|
20
|
+
- feat: add isMultiStatement(), so a caller can tell which of the two to use @Eray Hanoğlu
|
|
21
|
+
|
|
22
|
+
#### 🛠 Refactoring and Updates
|
|
23
|
+
|
|
24
|
+
- refactor: rename `multi-statement` to `is-multi-statement` for clarity @Eray Hanoğlu
|
|
25
|
+
|
|
26
|
+
### [v3.10.1](https://github.com/panates/postgrejs/compare/v3.10.0...v3.10.1) - 22 September 2026
|
|
4
27
|
|
|
5
28
|
#### 🚀 New Features
|
|
6
29
|
|
package/data-types/date-type.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { formatDate } from '../util/format-datetime.js';
|
|
3
|
+
import { utcPartsAsLocal } from '../util/local-date.js';
|
|
3
4
|
import { parseDate } from '../util/parse-datetime.js';
|
|
4
5
|
const timeShift = 946684800000;
|
|
5
6
|
export const DateType = {
|
|
@@ -35,11 +36,11 @@ export const DateType = {
|
|
|
35
36
|
return Infinity;
|
|
36
37
|
if (t === -0x80000000)
|
|
37
38
|
return -Infinity;
|
|
38
|
-
// Shift from 2000 to 1970
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return
|
|
39
|
+
// Shift from 2000 to 1970. A date's wire value is exactly midnight
|
|
40
|
+
// UTC, so the local wall clock it stands for is local midnight -
|
|
41
|
+
// which is what the shift lands on.
|
|
42
|
+
const ms = t * 1000 * 86400 + timeShift;
|
|
43
|
+
return options.utcDates ? new Date(ms) : utcPartsAsLocal(ms);
|
|
43
44
|
},
|
|
44
45
|
decodeText(v, options) {
|
|
45
46
|
return parseDate(v, options.utcDates, options.dateStyle);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { writeHexBytes } from '../util/hex-text.js';
|
|
2
3
|
/**
|
|
3
4
|
* `macaddr` and `macaddr8` share a file for the same reason `inet` and
|
|
4
5
|
* `cidr` do: one wire format - the address bytes and nothing else - and
|
|
@@ -33,6 +34,17 @@ function parseMacaddr(v, size) {
|
|
|
33
34
|
return Buffer.from(hex, 'hex');
|
|
34
35
|
}
|
|
35
36
|
function createType(name, oid, size) {
|
|
37
|
+
// The text this type writes, built once: `size` pairs of hex with a
|
|
38
|
+
// colon between them, and only the hex is rewritten per value. See
|
|
39
|
+
// util/hex-text.ts for why not `toString('hex')` per byte.
|
|
40
|
+
const text = Buffer.allocUnsafe(size * 3 - 1);
|
|
41
|
+
const positions = [];
|
|
42
|
+
let i;
|
|
43
|
+
for (i = 0; i < size; i++) {
|
|
44
|
+
positions.push(i * 3);
|
|
45
|
+
if (i)
|
|
46
|
+
text[i * 3 - 1] = 58; /* : */
|
|
47
|
+
}
|
|
36
48
|
return {
|
|
37
49
|
name,
|
|
38
50
|
oid,
|
|
@@ -52,11 +64,8 @@ function createType(name, oid, size) {
|
|
|
52
64
|
buf.writeBytes(b);
|
|
53
65
|
},
|
|
54
66
|
decodeBinary(v, offset = 0) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
for (i = 1; i < size; i++)
|
|
58
|
-
out += ':' + v.toString('hex', offset + i, offset + i + 1);
|
|
59
|
-
return out;
|
|
67
|
+
writeHexBytes(text, positions, v, offset, size);
|
|
68
|
+
return text.toString('latin1');
|
|
60
69
|
},
|
|
61
70
|
decodeText(v) {
|
|
62
71
|
return v;
|
package/data-types/money-type.js
CHANGED
|
@@ -59,6 +59,23 @@ export function moneyToString(minorUnits, scale) {
|
|
|
59
59
|
'.' +
|
|
60
60
|
digits.slice(digits.length - scale));
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Below this many minor units the decimal has at most fifteen
|
|
64
|
+
* significant digits, which a double both holds and prints back exactly
|
|
65
|
+
* - so the round trip below can only agree, and asking it is the most
|
|
66
|
+
* expensive thing in the decode: measured over values that vary,
|
|
67
|
+
* `parseFloat` plus the comparison is 241 ns against 113 ns without it,
|
|
68
|
+
* on a decode whose string-building is 109 ns of that.
|
|
69
|
+
*
|
|
70
|
+
* The scale bound is the other half: the test is about *printed* digits,
|
|
71
|
+
* and JavaScript switches to exponential notation below 1e-6, which
|
|
72
|
+
* PostgreSQL never writes. At scale 6 the smallest non-zero value is
|
|
73
|
+
* exactly 1e-6, and no currency has more.
|
|
74
|
+
*/
|
|
75
|
+
const FAITHFUL_LIMIT = 1000000000000000n; // 10^15
|
|
76
|
+
function isFaithful(minorUnits, scale) {
|
|
77
|
+
return (scale <= 6 && minorUnits < FAITHFUL_LIMIT && minorUnits > -FAITHFUL_LIMIT);
|
|
78
|
+
}
|
|
62
79
|
/**
|
|
63
80
|
* A number while a double carries the value exactly, a Numeric after
|
|
64
81
|
* that - the same rule `numeric` decodes by, and for the same reason.
|
|
@@ -143,18 +160,22 @@ export const MoneyType = {
|
|
|
143
160
|
const v = typeof buf.readBigInt64BE === 'function'
|
|
144
161
|
? buf.readBigInt64BE(offset)
|
|
145
162
|
: readBigInt64BE(buf, offset);
|
|
146
|
-
const
|
|
163
|
+
const { scale } = formatOf(options);
|
|
164
|
+
const s = moneyToString(v, scale);
|
|
147
165
|
// The exact decimal is what the decode already built; handing it
|
|
148
166
|
// back is the whole of `decimalAsString`.
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
167
|
+
if (wantsDecimalString(options, DataTypeOIDs.money))
|
|
168
|
+
return s;
|
|
169
|
+
return isFaithful(v, scale) ? parseFloat(s) : toNumberOrNumeric(s);
|
|
152
170
|
},
|
|
153
171
|
decodeText(s, options) {
|
|
154
172
|
const { scale } = formatOf(options);
|
|
155
|
-
const
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
const minorUnits = parseMoneyText(s);
|
|
174
|
+
const exact = moneyToString(minorUnits, scale);
|
|
175
|
+
if (wantsDecimalString(options, DataTypeOIDs.money))
|
|
176
|
+
return exact;
|
|
177
|
+
return isFaithful(minorUnits, scale)
|
|
178
|
+
? parseFloat(exact)
|
|
158
179
|
: toNumberOrNumeric(exact);
|
|
159
180
|
},
|
|
160
181
|
/**
|
|
@@ -14,6 +14,29 @@ import { DataTypeOIDs } from '../constants.js';
|
|
|
14
14
|
* compared rather than arithmetic.
|
|
15
15
|
*/
|
|
16
16
|
const LSN_PATTERN = /^([0-9a-fA-F]{1,8})\/([0-9a-fA-F]{1,8})$/;
|
|
17
|
+
/**
|
|
18
|
+
* `0`-`F` as bytes, and the text written into one reused scratch.
|
|
19
|
+
*
|
|
20
|
+
* `toString(16).toUpperCase()` per half is two strings each and four per
|
|
21
|
+
* value: 281 ns against 70 ns here, measured over values that vary. An
|
|
22
|
+
* LSN is not decoded in bulk - it comes back from monitoring queries a
|
|
23
|
+
* few rows at a time - so this is worth only the fifteen lines it takes
|
|
24
|
+
* and no machinery beyond them.
|
|
25
|
+
*/
|
|
26
|
+
const HEX_UPPER = Buffer.from('0123456789ABCDEF', 'latin1');
|
|
27
|
+
const TEXT = Buffer.allocUnsafe(17);
|
|
28
|
+
/** Writes one half at `p`, unpadded, and answers where it ended. */
|
|
29
|
+
function writeHalf(value, p) {
|
|
30
|
+
let shift = 28;
|
|
31
|
+
// Neither half is padded - the server writes `0/0` and `A/B` - so the
|
|
32
|
+
// leading zeros are skipped, and the last nibble is written whatever
|
|
33
|
+
// it is so that zero comes out as `0` rather than nothing.
|
|
34
|
+
while (shift && !((value >>> shift) & 15))
|
|
35
|
+
shift -= 4;
|
|
36
|
+
for (; shift >= 0; shift -= 4)
|
|
37
|
+
TEXT[p++] = HEX_UPPER[(value >>> shift) & 15];
|
|
38
|
+
return p;
|
|
39
|
+
}
|
|
17
40
|
export const PgLsnType = {
|
|
18
41
|
name: 'pg_lsn',
|
|
19
42
|
oid: DataTypeOIDs.pg_lsn,
|
|
@@ -32,12 +55,10 @@ export const PgLsnType = {
|
|
|
32
55
|
buf.writeUInt32BE(parseInt(m[2], 16));
|
|
33
56
|
},
|
|
34
57
|
decodeBinary(v, offset = 0) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
.toString(16)
|
|
40
|
-
.toUpperCase());
|
|
58
|
+
let p = writeHalf(v.readUInt32BE(offset), 0);
|
|
59
|
+
TEXT[p++] = 47; /* / */
|
|
60
|
+
p = writeHalf(v.readUInt32BE(offset + 4), p);
|
|
61
|
+
return TEXT.toString('latin1', 0, p);
|
|
41
62
|
},
|
|
42
63
|
decodeText(v) {
|
|
43
64
|
return v;
|
package/data-types/time-type.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { formatTime } from '../util/format-datetime.js';
|
|
3
|
+
import { utcPartsAsLocal } from '../util/local-date.js';
|
|
3
4
|
import { parseTime, STRICT_TIME_PATTERN } from '../util/parse-time.js';
|
|
4
5
|
const timeMul = 4294967296;
|
|
5
6
|
export const TimeType = {
|
|
@@ -27,11 +28,8 @@ export const TimeType = {
|
|
|
27
28
|
decodeBinary(v, offset = 0, _len, options) {
|
|
28
29
|
const hi = v.readInt32BE(offset);
|
|
29
30
|
const lo = v.readUInt32BE(offset + 4);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
33
|
-
}
|
|
34
|
-
return d;
|
|
31
|
+
const ms = (lo + hi * timeMul) / 1000;
|
|
32
|
+
return options.utcDates ? new Date(ms) : utcPartsAsLocal(ms);
|
|
35
33
|
},
|
|
36
34
|
decodeText(v, options) {
|
|
37
35
|
return parseTime(v, false, options.utcDates);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { formatTimestamp } from '../util/format-datetime.js';
|
|
3
|
+
import { utcPartsAsLocal } from '../util/local-date.js';
|
|
3
4
|
import { parseDateTime } from '../util/parse-datetime.js';
|
|
4
5
|
import { parsePgTimestampBuffer } from '../util/parse-pg-timestamp.js';
|
|
5
6
|
const timeShift = 946684800000;
|
|
@@ -44,11 +45,8 @@ export const TimestampType = {
|
|
|
44
45
|
if (lo === 0x00000000 && hi === -0x80000000)
|
|
45
46
|
return -Infinity;
|
|
46
47
|
// Shift from 2000 to 1970
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
50
|
-
}
|
|
51
|
-
return d;
|
|
48
|
+
const ms = (lo + hi * timeMul) / 1000 + timeShift;
|
|
49
|
+
return options.utcDates ? new Date(ms) : utcPartsAsLocal(ms);
|
|
52
50
|
},
|
|
53
51
|
decodeText(v, options) {
|
|
54
52
|
return parseDateTime(v, options.utcDates, options.dateStyle);
|
package/data-types/uuid-type.js
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { writeHexBytes } from '../util/hex-text.js';
|
|
3
|
+
/**
|
|
4
|
+
* The canonical text, written once and read back per value: 36 ASCII
|
|
5
|
+
* characters whose four dashes never move, so only the hex is rewritten.
|
|
6
|
+
* Reused rather than allocated because a decode fills it and converts it
|
|
7
|
+
* before anything else can run.
|
|
8
|
+
*/
|
|
9
|
+
const TEXT = Buffer.allocUnsafe(36);
|
|
10
|
+
TEXT[8] = TEXT[13] = TEXT[18] = TEXT[23] = 45; /* - */
|
|
11
|
+
/** Where each of the sixteen bytes writes its two characters. */
|
|
12
|
+
const POSITIONS = [0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34];
|
|
2
13
|
const GUID_PATTERN = /^[0-9a-fA-F]{8}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{4}-?[0-9a-fA-F]{12}$/;
|
|
3
14
|
export const UuidType = {
|
|
4
15
|
name: 'uuid',
|
|
@@ -14,15 +25,8 @@ export const UuidType = {
|
|
|
14
25
|
buf.writeBytes(b);
|
|
15
26
|
},
|
|
16
27
|
decodeBinary(v, offset = 0) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
v.toString('hex', offset + 4, offset + 6) +
|
|
20
|
-
'-' +
|
|
21
|
-
v.toString('hex', offset + 6, offset + 8) +
|
|
22
|
-
'-' +
|
|
23
|
-
v.toString('hex', offset + 8, offset + 10) +
|
|
24
|
-
'-' +
|
|
25
|
-
v.toString('hex', offset + 10, offset + 16));
|
|
28
|
+
writeHexBytes(TEXT, POSITIONS, v, offset, 16);
|
|
29
|
+
return TEXT.toString('latin1');
|
|
26
30
|
},
|
|
27
31
|
decodeText(v) {
|
|
28
32
|
return v;
|
package/index.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export * from './util/connection-config.js';
|
|
|
37
37
|
export * from './util/decimal-as-string.js';
|
|
38
38
|
export * from './util/escape-identifier.js';
|
|
39
39
|
export * from './util/escape-literal.js';
|
|
40
|
+
export * from './util/is-multi-statement.js';
|
|
40
41
|
export * from './util/parse-datetime.js';
|
|
41
42
|
export * from './util/row-decoder.js';
|
|
42
43
|
export * from './util/sql-tag.js';
|
package/index.js
CHANGED
|
@@ -37,6 +37,7 @@ export * from './util/connection-config.js';
|
|
|
37
37
|
export * from './util/decimal-as-string.js';
|
|
38
38
|
export * from './util/escape-identifier.js';
|
|
39
39
|
export * from './util/escape-literal.js';
|
|
40
|
+
export * from './util/is-multi-statement.js';
|
|
40
41
|
export * from './util/parse-datetime.js';
|
|
41
42
|
export * from './util/row-decoder.js';
|
|
42
43
|
export * from './util/sql-tag.js';
|
package/package.json
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes `count` bytes of `src` into `scratch` as lowercase hex, each
|
|
3
|
+
* byte's two characters starting at `positions[i]` - so the separators
|
|
4
|
+
* a type writes between its bytes are put in once, when the scratch is
|
|
5
|
+
* built, and never again.
|
|
6
|
+
*
|
|
7
|
+
* The scratch is then read back with `latin1`, which is what the text
|
|
8
|
+
* paths of these types already use: the output is fixed-format ASCII,
|
|
9
|
+
* so it decodes identically to `utf8` and skips V8's multi-byte-sequence
|
|
10
|
+
* detection.
|
|
11
|
+
*
|
|
12
|
+
* Why not `Buffer.toString('hex')`: a type whose text has separators in
|
|
13
|
+
* it has to call that once per group and join the pieces. For `uuid`
|
|
14
|
+
* that was five calls and four concatenations per value - 25 000 strings
|
|
15
|
+
* and 20 000 concatenations for a 5 000-row column, which is more work
|
|
16
|
+
* than `pg` does for the same column despite this client reading half
|
|
17
|
+
* the bytes. Measured over 5 000 values, medians of nine runs:
|
|
18
|
+
*
|
|
19
|
+
* ```
|
|
20
|
+
* five toString + concat 1.86 ms
|
|
21
|
+
* one toString, then slice 0.81 ms
|
|
22
|
+
* a byte -> two-char table 0.64 ms
|
|
23
|
+
* this 0.33 ms
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function writeHexBytes(scratch: Buffer, positions: number[], src: Buffer, offset: number, count: number): void;
|
package/util/hex-text.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** `0`-`f` as bytes, so a nibble indexes straight into it. */
|
|
2
|
+
const HEX = Buffer.from('0123456789abcdef', 'latin1');
|
|
3
|
+
/**
|
|
4
|
+
* Writes `count` bytes of `src` into `scratch` as lowercase hex, each
|
|
5
|
+
* byte's two characters starting at `positions[i]` - so the separators
|
|
6
|
+
* a type writes between its bytes are put in once, when the scratch is
|
|
7
|
+
* built, and never again.
|
|
8
|
+
*
|
|
9
|
+
* The scratch is then read back with `latin1`, which is what the text
|
|
10
|
+
* paths of these types already use: the output is fixed-format ASCII,
|
|
11
|
+
* so it decodes identically to `utf8` and skips V8's multi-byte-sequence
|
|
12
|
+
* detection.
|
|
13
|
+
*
|
|
14
|
+
* Why not `Buffer.toString('hex')`: a type whose text has separators in
|
|
15
|
+
* it has to call that once per group and join the pieces. For `uuid`
|
|
16
|
+
* that was five calls and four concatenations per value - 25 000 strings
|
|
17
|
+
* and 20 000 concatenations for a 5 000-row column, which is more work
|
|
18
|
+
* than `pg` does for the same column despite this client reading half
|
|
19
|
+
* the bytes. Measured over 5 000 values, medians of nine runs:
|
|
20
|
+
*
|
|
21
|
+
* ```
|
|
22
|
+
* five toString + concat 1.86 ms
|
|
23
|
+
* one toString, then slice 0.81 ms
|
|
24
|
+
* a byte -> two-char table 0.64 ms
|
|
25
|
+
* this 0.33 ms
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export function writeHexBytes(scratch, positions, src, offset, count) {
|
|
29
|
+
let i;
|
|
30
|
+
let p;
|
|
31
|
+
let b;
|
|
32
|
+
for (i = 0; i < count; i++) {
|
|
33
|
+
p = positions[i];
|
|
34
|
+
b = src[offset + i];
|
|
35
|
+
scratch[p] = HEX[b >> 4];
|
|
36
|
+
scratch[p + 1] = HEX[b & 15];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `sql` holds more than one statement.
|
|
3
|
+
*
|
|
4
|
+
* This client has two ways to send SQL and they are not interchangeable:
|
|
5
|
+
* `query()` speaks the extended protocol, which PostgreSQL will not let
|
|
6
|
+
* carry more than one command, and `execute()` sends a simple `Query`,
|
|
7
|
+
* which may hold a whole script but gives up the prepared plan. A caller
|
|
8
|
+
* that knows which it has picks the right one and needs nothing from
|
|
9
|
+
* here. A caller with a single SQL-taking API above it - an ORM adapter,
|
|
10
|
+
* a migration runner, a REPL, anything taking SQL from a user - has to
|
|
11
|
+
* choose before it can send, and both shapes arrive at the same method.
|
|
12
|
+
*
|
|
13
|
+
* Nobody can answer that from the wire: the server does the parsing, and
|
|
14
|
+
* its count arrives with the results, long after the choice was made.
|
|
15
|
+
* `pg` picks from whether the call happened to carry parameters, which
|
|
16
|
+
* is a guess about intent rather than an answer. So the question belongs
|
|
17
|
+
* here, beside the two functions whose difference raises it.
|
|
18
|
+
*
|
|
19
|
+
* It is a scanner and not a parser - one pass, one boolean - and what it
|
|
20
|
+
* skips is everything a `;` can hide inside:
|
|
21
|
+
*
|
|
22
|
+
* | construct | skipped to |
|
|
23
|
+
* | --- | --- |
|
|
24
|
+
* | `'...'` | the closing quote, `''` being an escaped one |
|
|
25
|
+
* | `E'...'` | the same, and `\'` too |
|
|
26
|
+
* | `"..."` | the closing quote of an identifier, `""` escaped |
|
|
27
|
+
* | `$tag$...$tag$` | the matching tag - a function body |
|
|
28
|
+
* | `-- ...` | end of line |
|
|
29
|
+
* | `/* ... *\/` | the matching close, which PostgreSQL nests |
|
|
30
|
+
*
|
|
31
|
+
* A trailing `;`, or a run of them with only whitespace and comments
|
|
32
|
+
* after, is not a second statement: the server takes `select 1;;` on the
|
|
33
|
+
* extended protocol without complaint.
|
|
34
|
+
*
|
|
35
|
+
* Which way a wrong answer goes is not symmetric, and the tests are built
|
|
36
|
+
* on that: saying *several* about one statement costs it its prepared
|
|
37
|
+
* plan and nothing else, while saying *one* about several reaches the
|
|
38
|
+
* caller as `42601 cannot insert multiple commands into a prepared
|
|
39
|
+
* statement`.
|
|
40
|
+
*
|
|
41
|
+
* One boundary, left as it is: under `standard_conforming_strings = off`
|
|
42
|
+
* a backslash escapes inside a plain `'...'` too, so `select 'a\'; select
|
|
43
|
+
* 2` is one malformed statement to the server and two to this. Both roads
|
|
44
|
+
* end in an error, nothing is decoded wrongly, and that setting has
|
|
45
|
+
* defaulted to `on` since PostgreSQL 9.1.
|
|
46
|
+
*/
|
|
47
|
+
export declare function isMultiStatement(sql: string): boolean;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `sql` holds more than one statement.
|
|
3
|
+
*
|
|
4
|
+
* This client has two ways to send SQL and they are not interchangeable:
|
|
5
|
+
* `query()` speaks the extended protocol, which PostgreSQL will not let
|
|
6
|
+
* carry more than one command, and `execute()` sends a simple `Query`,
|
|
7
|
+
* which may hold a whole script but gives up the prepared plan. A caller
|
|
8
|
+
* that knows which it has picks the right one and needs nothing from
|
|
9
|
+
* here. A caller with a single SQL-taking API above it - an ORM adapter,
|
|
10
|
+
* a migration runner, a REPL, anything taking SQL from a user - has to
|
|
11
|
+
* choose before it can send, and both shapes arrive at the same method.
|
|
12
|
+
*
|
|
13
|
+
* Nobody can answer that from the wire: the server does the parsing, and
|
|
14
|
+
* its count arrives with the results, long after the choice was made.
|
|
15
|
+
* `pg` picks from whether the call happened to carry parameters, which
|
|
16
|
+
* is a guess about intent rather than an answer. So the question belongs
|
|
17
|
+
* here, beside the two functions whose difference raises it.
|
|
18
|
+
*
|
|
19
|
+
* It is a scanner and not a parser - one pass, one boolean - and what it
|
|
20
|
+
* skips is everything a `;` can hide inside:
|
|
21
|
+
*
|
|
22
|
+
* | construct | skipped to |
|
|
23
|
+
* | --- | --- |
|
|
24
|
+
* | `'...'` | the closing quote, `''` being an escaped one |
|
|
25
|
+
* | `E'...'` | the same, and `\'` too |
|
|
26
|
+
* | `"..."` | the closing quote of an identifier, `""` escaped |
|
|
27
|
+
* | `$tag$...$tag$` | the matching tag - a function body |
|
|
28
|
+
* | `-- ...` | end of line |
|
|
29
|
+
* | `/* ... *\/` | the matching close, which PostgreSQL nests |
|
|
30
|
+
*
|
|
31
|
+
* A trailing `;`, or a run of them with only whitespace and comments
|
|
32
|
+
* after, is not a second statement: the server takes `select 1;;` on the
|
|
33
|
+
* extended protocol without complaint.
|
|
34
|
+
*
|
|
35
|
+
* Which way a wrong answer goes is not symmetric, and the tests are built
|
|
36
|
+
* on that: saying *several* about one statement costs it its prepared
|
|
37
|
+
* plan and nothing else, while saying *one* about several reaches the
|
|
38
|
+
* caller as `42601 cannot insert multiple commands into a prepared
|
|
39
|
+
* statement`.
|
|
40
|
+
*
|
|
41
|
+
* One boundary, left as it is: under `standard_conforming_strings = off`
|
|
42
|
+
* a backslash escapes inside a plain `'...'` too, so `select 'a\'; select
|
|
43
|
+
* 2` is one malformed statement to the server and two to this. Both roads
|
|
44
|
+
* end in an error, nothing is decoded wrongly, and that setting has
|
|
45
|
+
* defaulted to `on` since PostgreSQL 9.1.
|
|
46
|
+
*/
|
|
47
|
+
export function isMultiStatement(sql) {
|
|
48
|
+
const len = sql.length;
|
|
49
|
+
let i = 0;
|
|
50
|
+
let separator = false;
|
|
51
|
+
let c;
|
|
52
|
+
while (i < len) {
|
|
53
|
+
c = sql.charCodeAt(i);
|
|
54
|
+
// Whatever follows a `;` decides what that `;` was: more whitespace,
|
|
55
|
+
// comments or semicolons leave it a terminator, and anything else
|
|
56
|
+
// makes it the seam between two statements.
|
|
57
|
+
if (separator && !isSpace(c) && c !== SEMI && c !== DASH && c !== SLASH)
|
|
58
|
+
return true;
|
|
59
|
+
if (c === SEMI) {
|
|
60
|
+
separator = true;
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
else if (c === QUOTE) {
|
|
64
|
+
i = skipQuoted(sql, len, i, QUOTE, escapesBackslash(sql, i));
|
|
65
|
+
}
|
|
66
|
+
else if (c === DQUOTE) {
|
|
67
|
+
i = skipQuoted(sql, len, i, DQUOTE, false);
|
|
68
|
+
}
|
|
69
|
+
else if (c === DOLLAR) {
|
|
70
|
+
const end = skipDollarQuoted(sql, len, i);
|
|
71
|
+
// Not a dollar quote - a `$1` placeholder, or a bare `$`.
|
|
72
|
+
i = end < 0 ? i + 1 : end;
|
|
73
|
+
}
|
|
74
|
+
else if (c === DASH && sql.charCodeAt(i + 1) === DASH) {
|
|
75
|
+
const nl = sql.indexOf('\n', i + 2);
|
|
76
|
+
i = nl < 0 ? len : nl + 1;
|
|
77
|
+
}
|
|
78
|
+
else if (c === SLASH && sql.charCodeAt(i + 1) === STAR) {
|
|
79
|
+
i = skipBlockComment(sql, len, i);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// A `-` or `/` that began no comment is content like any other.
|
|
83
|
+
if (separator && !isSpace(c))
|
|
84
|
+
return true;
|
|
85
|
+
i++;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const SEMI = 59; // ;
|
|
91
|
+
const QUOTE = 39; // '
|
|
92
|
+
const DQUOTE = 34; // "
|
|
93
|
+
const DOLLAR = 36; // $
|
|
94
|
+
const DASH = 45; // -
|
|
95
|
+
const SLASH = 47; // /
|
|
96
|
+
const STAR = 42; // *
|
|
97
|
+
const BACKSLASH = 92; // \
|
|
98
|
+
const UNDERSCORE = 95; // _
|
|
99
|
+
function isSpace(c) {
|
|
100
|
+
return c === 32 || c === 10 || c === 9 || c === 13 || c === 12;
|
|
101
|
+
}
|
|
102
|
+
function isDigit(c) {
|
|
103
|
+
return c >= 48 && c <= 57;
|
|
104
|
+
}
|
|
105
|
+
/** A letter or `_`, which is what a dollar-quote tag may start with. */
|
|
106
|
+
function isIdentStart(c) {
|
|
107
|
+
return (c >= 65 && c <= 90) || (c >= 97 && c <= 122) || c === UNDERSCORE;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether the quote at `at` opens an `E'...'` literal, where a backslash
|
|
111
|
+
* escapes. The letter has to stand alone: the `e` of `value` is not a
|
|
112
|
+
* prefix.
|
|
113
|
+
*/
|
|
114
|
+
function escapesBackslash(sql, at) {
|
|
115
|
+
const prev = sql.charCodeAt(at - 1);
|
|
116
|
+
if (prev !== 69 /* E */ && prev !== 101 /* e */)
|
|
117
|
+
return false;
|
|
118
|
+
if (at < 2)
|
|
119
|
+
return true;
|
|
120
|
+
const before = sql.charCodeAt(at - 2);
|
|
121
|
+
return !(isIdentStart(before) ||
|
|
122
|
+
isDigit(before) ||
|
|
123
|
+
before === DOLLAR ||
|
|
124
|
+
before === UNDERSCORE);
|
|
125
|
+
}
|
|
126
|
+
/** Past the closing quote, or the end of the string if it never closes. */
|
|
127
|
+
function skipQuoted(sql, len, start, quote, backslash) {
|
|
128
|
+
let i = start + 1;
|
|
129
|
+
let c;
|
|
130
|
+
while (i < len) {
|
|
131
|
+
c = sql.charCodeAt(i);
|
|
132
|
+
if (backslash && c === BACKSLASH)
|
|
133
|
+
i += 2;
|
|
134
|
+
else if (c !== quote)
|
|
135
|
+
i++;
|
|
136
|
+
// A doubled quote is an escaped one and the literal carries on.
|
|
137
|
+
else if (sql.charCodeAt(i + 1) === quote)
|
|
138
|
+
i += 2;
|
|
139
|
+
else
|
|
140
|
+
return i + 1;
|
|
141
|
+
}
|
|
142
|
+
return len;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Past `$tag$ ... $tag$`, or -1 when the `$` opens none - which is the
|
|
146
|
+
* common case, since `$1` is a parameter placeholder. The tag is read
|
|
147
|
+
* character by character so that a placeholder costs no allocation.
|
|
148
|
+
*/
|
|
149
|
+
function skipDollarQuoted(sql, len, start) {
|
|
150
|
+
let i = start + 1;
|
|
151
|
+
let c;
|
|
152
|
+
while (i < len) {
|
|
153
|
+
c = sql.charCodeAt(i);
|
|
154
|
+
if (c === DOLLAR)
|
|
155
|
+
break;
|
|
156
|
+
if (!(isIdentStart(c) || (i > start + 1 && isDigit(c))))
|
|
157
|
+
return -1;
|
|
158
|
+
i++;
|
|
159
|
+
}
|
|
160
|
+
if (i >= len)
|
|
161
|
+
return -1;
|
|
162
|
+
const tag = sql.substring(start, i + 1);
|
|
163
|
+
const close = sql.indexOf(tag, i + 1);
|
|
164
|
+
return close < 0 ? len : close + tag.length;
|
|
165
|
+
}
|
|
166
|
+
/** Past the matching `*\/`. PostgreSQL nests block comments. */
|
|
167
|
+
function skipBlockComment(sql, len, start) {
|
|
168
|
+
let depth = 0;
|
|
169
|
+
let i = start;
|
|
170
|
+
while (i < len) {
|
|
171
|
+
if (sql.charCodeAt(i) === SLASH && sql.charCodeAt(i + 1) === STAR) {
|
|
172
|
+
depth++;
|
|
173
|
+
i += 2;
|
|
174
|
+
}
|
|
175
|
+
else if (sql.charCodeAt(i) === STAR && sql.charCodeAt(i + 1) === SLASH) {
|
|
176
|
+
i += 2;
|
|
177
|
+
if (--depth === 0)
|
|
178
|
+
return i;
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
i++;
|
|
182
|
+
}
|
|
183
|
+
return len;
|
|
184
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local `Date` whose wall clock reads what `ms` reads in UTC.
|
|
3
|
+
*
|
|
4
|
+
* `timestamp`, `date` and `time` carry no zone: the server sends a wall
|
|
5
|
+
* clock and the client decides which one it names. With `utcDates` off -
|
|
6
|
+
* the default - that is the local one, so a `2024-06-15 10:30` column
|
|
7
|
+
* comes back as a Date reading 10:30 locally rather than the instant
|
|
8
|
+
* 10:30Z.
|
|
9
|
+
*
|
|
10
|
+
* Doing that by reading the seven `getUTC*` components off one Date and
|
|
11
|
+
* handing them to another was the most expensive thing left in the
|
|
12
|
+
* common decode path - the getters alone cost more than either Date:
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* new Date(epochMs) 43.6 ns
|
|
16
|
+
* getUTC* x 7 97.4 ns
|
|
17
|
+
* new Date(y, m, d, h, mi, s, ms) 61.7 ns
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Shifting the instant by the zone's own offset lands on the same wall
|
|
21
|
+
* clock for a fraction of that, but only where the offset is a whole
|
|
22
|
+
* number of minutes and holds either side of where the shift lands.
|
|
23
|
+
* Two guards and a floor make that exact rather than usually-right:
|
|
24
|
+
*
|
|
25
|
+
* - **The offset must survive the shift.** Where it does not, the shift
|
|
26
|
+
* crossed a transition and the wall clock it landed on is not the one
|
|
27
|
+
* that was asked for - or does not exist at all.
|
|
28
|
+
* - **The offset must hold for the hour before the landing too.** That
|
|
29
|
+
* is the hour a backward transition repeats, where both instants read
|
|
30
|
+
* the same wall clock and the shift keeps the wrong one of the two.
|
|
31
|
+
* Pacific/Chatham found this: `2024-04-07T03:00Z` is `03:00` in both
|
|
32
|
+
* +12:45 and +13:45, an hour apart.
|
|
33
|
+
* - **Nothing before 1970.** A zone's pre-standard offset carries
|
|
34
|
+
* seconds - Europe/Istanbul was +01:55:52 - and `getTimezoneOffset()`
|
|
35
|
+
* answers in whole minutes, so the shift lands up to 59 seconds off.
|
|
36
|
+
* The tz database does not claim correctness before 1970 either.
|
|
37
|
+
*
|
|
38
|
+
* Whatever fails those goes back to the component route, which is what
|
|
39
|
+
* this has always answered. Checked against it over 1850-2040 at
|
|
40
|
+
* 90-minute steps - 1.1 million instants - in ten zones including the
|
|
41
|
+
* 45-minute and 30-minute DST ones: no disagreement, and
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* before after
|
|
45
|
+
* Europe/Istanbul 157.5 ns 123.0 ns
|
|
46
|
+
* America/New_York 245.6 ns 131.2 ns
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* The DST zone was dearer before because V8 pays for a transition lookup
|
|
50
|
+
* on every local construction; here it pays for one instant instead.
|
|
51
|
+
*/
|
|
52
|
+
export declare function utcPartsAsLocal(ms: number): Date;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The local `Date` whose wall clock reads what `ms` reads in UTC.
|
|
3
|
+
*
|
|
4
|
+
* `timestamp`, `date` and `time` carry no zone: the server sends a wall
|
|
5
|
+
* clock and the client decides which one it names. With `utcDates` off -
|
|
6
|
+
* the default - that is the local one, so a `2024-06-15 10:30` column
|
|
7
|
+
* comes back as a Date reading 10:30 locally rather than the instant
|
|
8
|
+
* 10:30Z.
|
|
9
|
+
*
|
|
10
|
+
* Doing that by reading the seven `getUTC*` components off one Date and
|
|
11
|
+
* handing them to another was the most expensive thing left in the
|
|
12
|
+
* common decode path - the getters alone cost more than either Date:
|
|
13
|
+
*
|
|
14
|
+
* ```
|
|
15
|
+
* new Date(epochMs) 43.6 ns
|
|
16
|
+
* getUTC* x 7 97.4 ns
|
|
17
|
+
* new Date(y, m, d, h, mi, s, ms) 61.7 ns
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Shifting the instant by the zone's own offset lands on the same wall
|
|
21
|
+
* clock for a fraction of that, but only where the offset is a whole
|
|
22
|
+
* number of minutes and holds either side of where the shift lands.
|
|
23
|
+
* Two guards and a floor make that exact rather than usually-right:
|
|
24
|
+
*
|
|
25
|
+
* - **The offset must survive the shift.** Where it does not, the shift
|
|
26
|
+
* crossed a transition and the wall clock it landed on is not the one
|
|
27
|
+
* that was asked for - or does not exist at all.
|
|
28
|
+
* - **The offset must hold for the hour before the landing too.** That
|
|
29
|
+
* is the hour a backward transition repeats, where both instants read
|
|
30
|
+
* the same wall clock and the shift keeps the wrong one of the two.
|
|
31
|
+
* Pacific/Chatham found this: `2024-04-07T03:00Z` is `03:00` in both
|
|
32
|
+
* +12:45 and +13:45, an hour apart.
|
|
33
|
+
* - **Nothing before 1970.** A zone's pre-standard offset carries
|
|
34
|
+
* seconds - Europe/Istanbul was +01:55:52 - and `getTimezoneOffset()`
|
|
35
|
+
* answers in whole minutes, so the shift lands up to 59 seconds off.
|
|
36
|
+
* The tz database does not claim correctness before 1970 either.
|
|
37
|
+
*
|
|
38
|
+
* Whatever fails those goes back to the component route, which is what
|
|
39
|
+
* this has always answered. Checked against it over 1850-2040 at
|
|
40
|
+
* 90-minute steps - 1.1 million instants - in ten zones including the
|
|
41
|
+
* 45-minute and 30-minute DST ones: no disagreement, and
|
|
42
|
+
*
|
|
43
|
+
* ```
|
|
44
|
+
* before after
|
|
45
|
+
* Europe/Istanbul 157.5 ns 123.0 ns
|
|
46
|
+
* America/New_York 245.6 ns 131.2 ns
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* The DST zone was dearer before because V8 pays for a transition lookup
|
|
50
|
+
* on every local construction; here it pays for one instant instead.
|
|
51
|
+
*/
|
|
52
|
+
export function utcPartsAsLocal(ms) {
|
|
53
|
+
if (ms >= 0) {
|
|
54
|
+
const offset = new Date(ms).getTimezoneOffset();
|
|
55
|
+
const local = new Date(ms + offset * 60000);
|
|
56
|
+
if (local.getTimezoneOffset() === offset &&
|
|
57
|
+
new Date(local.getTime() - 3600000).getTimezoneOffset() === offset)
|
|
58
|
+
return local;
|
|
59
|
+
}
|
|
60
|
+
const utc = new Date(ms);
|
|
61
|
+
const year = utc.getUTCFullYear();
|
|
62
|
+
const local = new Date(year, utc.getUTCMonth(), utc.getUTCDate(), utc.getUTCHours(), utc.getUTCMinutes(), utc.getUTCSeconds(), utc.getUTCMilliseconds());
|
|
63
|
+
// `new Date(44, 2, 15)` means 1944 - the constructor's own century
|
|
64
|
+
// rule, which is not what a server sending `0044-03-15` meant. It only
|
|
65
|
+
// reaches values this old, which take this route anyway.
|
|
66
|
+
if (year >= 0 && year <= 99)
|
|
67
|
+
local.setFullYear(year);
|
|
68
|
+
return local;
|
|
69
|
+
}
|