postgrejs 3.11.0 → 3.12.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 +41 -1
- package/connection/intl-connection.d.ts +8 -0
- package/connection/intl-connection.js +12 -2
- package/connection/prepared-statement.d.ts +41 -0
- package/connection/prepared-statement.js +79 -5
- package/data-types/date-type.js +6 -5
- package/data-types/int2-type.js +1 -1
- package/data-types/int4-type.js +1 -1
- package/data-types/int8-type.js +2 -2
- package/data-types/macaddr-type.js +14 -5
- package/data-types/money-type.js +28 -7
- package/data-types/oid-type.js +1 -1
- 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/data-types/xid-type.js +2 -1
- package/package.json +1 -1
- package/util/assert-integer.d.ts +40 -25
- package/util/assert-integer.js +97 -32
- package/util/hex-text.d.ts +26 -0
- package/util/hex-text.js +38 -0
- package/util/local-date.d.ts +52 -0
- package/util/local-date.js +69 -0
- package/util/stringify-arrayliteral.js +42 -7
- package/util/unspecified-param.d.ts +26 -2
- package/util/unspecified-param.js +26 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
## Changelog
|
|
2
2
|
|
|
3
|
-
### [v3.
|
|
3
|
+
### [v3.12.0](https://github.com/panates/postgrejs/compare/v3.11.1...v3.12.0) -
|
|
4
|
+
|
|
5
|
+
#### Breaking Changes :warning:
|
|
6
|
+
|
|
7
|
+
- fix!: refuse what the server refuses instead of storing another number @Eray Hanoğlu
|
|
8
|
+
|
|
9
|
+
#### 🚀 New Features
|
|
10
|
+
|
|
11
|
+
- feat(benchmark): count the bytes sent, and what a run keeps @Eray Hanoğlu
|
|
12
|
+
- feat: report the parameter types the server resolved @Eray Hanoğlu
|
|
13
|
+
|
|
14
|
+
#### 🪲 Fixes
|
|
15
|
+
|
|
16
|
+
- fix(benchmark): a figure nobody could measure is not a bar of zero @Eray Hanoğlu
|
|
17
|
+
- fix: take the value out of a BindParam handed to a prepared statement @Eray Hanoğlu
|
|
18
|
+
|
|
19
|
+
#### 📖 Documentation Changes
|
|
20
|
+
|
|
21
|
+
- docs(benchmark): update benchmarks to include new memory and network metrics @Eray Hanoğlu
|
|
22
|
+
- docs: say what an untyped numeric array actually costs @Eray Hanoğlu
|
|
23
|
+
- docs(benchmark): say what Peak Heap is not, and what survives a re-run @Eray Hanoğlu
|
|
24
|
+
- docs(benchmark): Retained is a working set, not a permanent footprint @Eray Hanoğlu
|
|
25
|
+
|
|
26
|
+
#### ⚡️ Performance Improvments
|
|
27
|
+
|
|
28
|
+
- perf: stop quoting and re-escaping every element of an array literal @Eray Hanoğlu
|
|
29
|
+
|
|
30
|
+
### [v3.11.1](https://github.com/panates/postgrejs/compare/v3.11.0...v3.11.1) - 24 September 2026
|
|
31
|
+
|
|
32
|
+
#### 🪲 Fixes
|
|
33
|
+
|
|
34
|
+
- fix(bench): count off-heap memory in Peak Heap @Eray Hanoğlu
|
|
35
|
+
|
|
36
|
+
#### ⚡️ Performance Improvments
|
|
37
|
+
|
|
38
|
+
- perf: shift the instant instead of rebuilding a local Date from its parts @Eray Hanoğlu
|
|
39
|
+
- perf: build uuid and macaddr text in one pass instead of five @Eray Hanoğlu
|
|
40
|
+
- perf: stop asking whether a small money value round-trips @Eray Hanoğlu
|
|
41
|
+
- perf: write a pg_lsn without four strings a value @Eray Hanoğlu
|
|
42
|
+
|
|
43
|
+
### [v3.11.0](https://github.com/panates/postgrejs/compare/v3.10.1...v3.11.0) - 23 September 2026
|
|
4
44
|
|
|
5
45
|
#### 🚀 New Features
|
|
6
46
|
|
|
@@ -332,9 +332,17 @@ export declare class IntlConnection extends SafeEventEmitter {
|
|
|
332
332
|
* a separately-awaited Sync, and fetches the RowDescription/NoData in the
|
|
333
333
|
* same round trip so the caller can cache it (see executeReused()) rather
|
|
334
334
|
* than every later execute() re-Describing its own portal.
|
|
335
|
+
*
|
|
336
|
+
* The Describe answers with the parameters too, and that answer is worth
|
|
337
|
+
* keeping: it is the only place the server says what it made of a
|
|
338
|
+
* parameter the client declared nothing for. `insert into t(v)
|
|
339
|
+
* values($1)` resolves `$1` from the column, not from the value - which
|
|
340
|
+
* is why it is stable across executions and why a caller who wants the
|
|
341
|
+
* binary encoding back can read it and name that type with BindParam.
|
|
335
342
|
*/
|
|
336
343
|
prepareOnce(sql: string, paramTypes: Maybe<Maybe<OID>[]>, statementName: string): Promise<{
|
|
337
344
|
fields?: Protocol.RowDescription[];
|
|
345
|
+
resolvedParamTypes?: OID[];
|
|
338
346
|
}>;
|
|
339
347
|
/**
|
|
340
348
|
* Bind+Execute+Sync as a single round trip against an unnamed portal, for
|
|
@@ -1239,12 +1239,20 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1239
1239
|
* a separately-awaited Sync, and fetches the RowDescription/NoData in the
|
|
1240
1240
|
* same round trip so the caller can cache it (see executeReused()) rather
|
|
1241
1241
|
* than every later execute() re-Describing its own portal.
|
|
1242
|
+
*
|
|
1243
|
+
* The Describe answers with the parameters too, and that answer is worth
|
|
1244
|
+
* keeping: it is the only place the server says what it made of a
|
|
1245
|
+
* parameter the client declared nothing for. `insert into t(v)
|
|
1246
|
+
* values($1)` resolves `$1` from the column, not from the value - which
|
|
1247
|
+
* is why it is stable across executions and why a caller who wants the
|
|
1248
|
+
* binary encoding back can read it and name that type with BindParam.
|
|
1242
1249
|
*/
|
|
1243
1250
|
async prepareOnce(sql, paramTypes, statementName) {
|
|
1244
1251
|
this.assertConnected();
|
|
1245
1252
|
this.ref();
|
|
1246
1253
|
try {
|
|
1247
1254
|
let fields;
|
|
1255
|
+
let resolvedParamTypes;
|
|
1248
1256
|
let error;
|
|
1249
1257
|
this.runningQueryCount++;
|
|
1250
1258
|
await this.socket
|
|
@@ -1257,8 +1265,10 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1257
1265
|
// Not an error: handed over, and the loop carries on.
|
|
1258
1266
|
this.emit('notice', msg);
|
|
1259
1267
|
break;
|
|
1260
|
-
case Protocol.BackendMessageCode.ParseComplete:
|
|
1261
1268
|
case Protocol.BackendMessageCode.ParameterDescription:
|
|
1269
|
+
resolvedParamTypes = msg.parameterIds;
|
|
1270
|
+
break;
|
|
1271
|
+
case Protocol.BackendMessageCode.ParseComplete:
|
|
1262
1272
|
case Protocol.BackendMessageCode.NoData:
|
|
1263
1273
|
break;
|
|
1264
1274
|
case Protocol.BackendMessageCode.RowDescription:
|
|
@@ -1280,7 +1290,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1280
1290
|
.finally(() => {
|
|
1281
1291
|
this.runningQueryCount--;
|
|
1282
1292
|
});
|
|
1283
|
-
return { fields };
|
|
1293
|
+
return { fields, resolvedParamTypes };
|
|
1284
1294
|
}
|
|
1285
1295
|
finally {
|
|
1286
1296
|
this.unref();
|
|
@@ -10,6 +10,7 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
|
|
|
10
10
|
private readonly _sql;
|
|
11
11
|
private readonly _name;
|
|
12
12
|
private readonly _paramTypes?;
|
|
13
|
+
private _resolvedParamTypes?;
|
|
13
14
|
private _fields?;
|
|
14
15
|
protected _onErrorSavePoint: string;
|
|
15
16
|
private _refCount;
|
|
@@ -20,6 +21,24 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
|
|
|
20
21
|
get name(): Maybe<string>;
|
|
21
22
|
get sql(): string;
|
|
22
23
|
get paramTypes(): Maybe<Maybe<OID>[]>;
|
|
24
|
+
/**
|
|
25
|
+
* What the server made of each parameter, in order, as its Describe
|
|
26
|
+
* answered - not what was asked for in `paramTypes`.
|
|
27
|
+
*
|
|
28
|
+
* A parameter the caller declared nothing for is resolved by the
|
|
29
|
+
* server from where it appears: the column it is inserted into, the
|
|
30
|
+
* operator it sits beside, the function it is passed to. It never
|
|
31
|
+
* looks at the value, which is still three messages away, so this is
|
|
32
|
+
* a property of the statement and does not change between executions.
|
|
33
|
+
*
|
|
34
|
+
* Useful for two things. It says what a parameter with no context
|
|
35
|
+
* became - `select $1` resolves to `text`, which is why an array
|
|
36
|
+
* there comes back as an array literal rather than an array. And it
|
|
37
|
+
* is the type to name with `new BindParam(oid, v)` for a caller who
|
|
38
|
+
* wants the binary encoding a large numeric array gives up by going
|
|
39
|
+
* out untyped (see `isUnspecifiedParam`).
|
|
40
|
+
*/
|
|
41
|
+
get resolvedParamTypes(): Maybe<OID[]>;
|
|
23
42
|
execute(options?: QueryOptions): Promise<QueryResult>;
|
|
24
43
|
/**
|
|
25
44
|
* Runs this statement once per parameter set, sending every Bind/Execute
|
|
@@ -78,6 +97,28 @@ export declare class PreparedStatement extends SafeEventEmitter implements Async
|
|
|
78
97
|
* holding a `Connection` never saw. Hard to provoke - `DEALLOCATE`
|
|
79
98
|
* does not normally raise one - which is why there is no test for it.
|
|
80
99
|
*/
|
|
100
|
+
/**
|
|
101
|
+
* Takes the value out of a `BindParam`, and refuses one that names a
|
|
102
|
+
* type this statement is not.
|
|
103
|
+
*
|
|
104
|
+
* A prepared statement's parameter types are fixed by its Parse, so a
|
|
105
|
+
* `BindParam` here cannot change one: the Bind's format code says
|
|
106
|
+
* binary or text, and the server decodes with the type it already
|
|
107
|
+
* resolved. Naming a different one and being believed would read the
|
|
108
|
+
* bytes as something else - which is why the mismatch is an error
|
|
109
|
+
* rather than something to quietly honour.
|
|
110
|
+
*
|
|
111
|
+
* It used to be neither: nothing unwrapped the object, so it reached
|
|
112
|
+
* the encoder as a value with no type of its own and went out as
|
|
113
|
+
* `'' + v`. A text column took `"[object Object]"` and said nothing.
|
|
114
|
+
*
|
|
115
|
+
* What it is compared against is what the caller declared at
|
|
116
|
+
* `prepare()`, or failing that what the server resolved - which is the
|
|
117
|
+
* same type either way, and is why an agreeing `BindParam` is simply
|
|
118
|
+
* unwrapped. Where there is nothing to compare against, the value is
|
|
119
|
+
* taken as given.
|
|
120
|
+
*/
|
|
121
|
+
protected _unwrapBindParams(params: Maybe<any[]>): Maybe<any[]>;
|
|
81
122
|
protected _emitNotice(msg: any): void;
|
|
82
123
|
protected _executeBatchWithTransaction(paramSets: any[][], options: QueryOptions): Promise<BatchResult>;
|
|
83
124
|
protected _executeWithTransaction(options?: QueryOptions): Promise<QueryResult>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataTypeOIDs } from '../constants.js';
|
|
1
|
+
import { DataTypeNames, DataTypeOIDs } from '../constants.js';
|
|
2
2
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
3
3
|
import { Protocol } from '../protocol/protocol.js';
|
|
4
4
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
@@ -8,6 +8,7 @@ import { resolveColumnFormats } from '../util/resolve-column-formats.js';
|
|
|
8
8
|
import { resolveRowType } from '../util/row-decoder.js';
|
|
9
9
|
import { isTransactionCommand, refusesSavepoint, } from '../util/transaction-command.js';
|
|
10
10
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
11
|
+
import { BindParam } from './bind-param.js';
|
|
11
12
|
import { Cursor } from './cursor.js';
|
|
12
13
|
import { getIntlConnection } from './intl-connection.js';
|
|
13
14
|
import { Portal } from './portal.js';
|
|
@@ -18,6 +19,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
18
19
|
_sql = '';
|
|
19
20
|
_name = '';
|
|
20
21
|
_paramTypes;
|
|
22
|
+
_resolvedParamTypes;
|
|
21
23
|
_fields;
|
|
22
24
|
_onErrorSavePoint;
|
|
23
25
|
_refCount = 0;
|
|
@@ -34,8 +36,9 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
34
36
|
const intoCon = getIntlConnection(connection);
|
|
35
37
|
intoCon.assertConnected();
|
|
36
38
|
const statement = new PreparedStatement(connection, sql, options?.paramTypes);
|
|
37
|
-
const { fields } = await intoCon.prepareOnce(sql, statement.paramTypes, statement.name);
|
|
39
|
+
const { fields, resolvedParamTypes } = await intoCon.prepareOnce(sql, statement.paramTypes, statement.name);
|
|
38
40
|
statement._fields = fields;
|
|
41
|
+
statement._resolvedParamTypes = resolvedParamTypes;
|
|
39
42
|
statement._refCount = 1;
|
|
40
43
|
return statement;
|
|
41
44
|
}
|
|
@@ -51,6 +54,26 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
51
54
|
get paramTypes() {
|
|
52
55
|
return this._paramTypes;
|
|
53
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* What the server made of each parameter, in order, as its Describe
|
|
59
|
+
* answered - not what was asked for in `paramTypes`.
|
|
60
|
+
*
|
|
61
|
+
* A parameter the caller declared nothing for is resolved by the
|
|
62
|
+
* server from where it appears: the column it is inserted into, the
|
|
63
|
+
* operator it sits beside, the function it is passed to. It never
|
|
64
|
+
* looks at the value, which is still three messages away, so this is
|
|
65
|
+
* a property of the statement and does not change between executions.
|
|
66
|
+
*
|
|
67
|
+
* Useful for two things. It says what a parameter with no context
|
|
68
|
+
* became - `select $1` resolves to `text`, which is why an array
|
|
69
|
+
* there comes back as an array literal rather than an array. And it
|
|
70
|
+
* is the type to name with `new BindParam(oid, v)` for a caller who
|
|
71
|
+
* wants the binary encoding a large numeric array gives up by going
|
|
72
|
+
* out untyped (see `isUnspecifiedParam`).
|
|
73
|
+
*/
|
|
74
|
+
get resolvedParamTypes() {
|
|
75
|
+
return this._resolvedParamTypes;
|
|
76
|
+
}
|
|
54
77
|
async execute(options = {}) {
|
|
55
78
|
const intlCon = getIntlConnection(this.connection);
|
|
56
79
|
// Folded in here rather than deeper: a cursor keeps these options
|
|
@@ -130,13 +153,58 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
130
153
|
* holding a `Connection` never saw. Hard to provoke - `DEALLOCATE`
|
|
131
154
|
* does not normally raise one - which is why there is no test for it.
|
|
132
155
|
*/
|
|
156
|
+
/**
|
|
157
|
+
* Takes the value out of a `BindParam`, and refuses one that names a
|
|
158
|
+
* type this statement is not.
|
|
159
|
+
*
|
|
160
|
+
* A prepared statement's parameter types are fixed by its Parse, so a
|
|
161
|
+
* `BindParam` here cannot change one: the Bind's format code says
|
|
162
|
+
* binary or text, and the server decodes with the type it already
|
|
163
|
+
* resolved. Naming a different one and being believed would read the
|
|
164
|
+
* bytes as something else - which is why the mismatch is an error
|
|
165
|
+
* rather than something to quietly honour.
|
|
166
|
+
*
|
|
167
|
+
* It used to be neither: nothing unwrapped the object, so it reached
|
|
168
|
+
* the encoder as a value with no type of its own and went out as
|
|
169
|
+
* `'' + v`. A text column took `"[object Object]"` and said nothing.
|
|
170
|
+
*
|
|
171
|
+
* What it is compared against is what the caller declared at
|
|
172
|
+
* `prepare()`, or failing that what the server resolved - which is the
|
|
173
|
+
* same type either way, and is why an agreeing `BindParam` is simply
|
|
174
|
+
* unwrapped. Where there is nothing to compare against, the value is
|
|
175
|
+
* taken as given.
|
|
176
|
+
*/
|
|
177
|
+
_unwrapBindParams(params) {
|
|
178
|
+
if (!params)
|
|
179
|
+
return params;
|
|
180
|
+
let out;
|
|
181
|
+
const l = params.length;
|
|
182
|
+
let i;
|
|
183
|
+
let prm;
|
|
184
|
+
let expected;
|
|
185
|
+
for (i = 0; i < l; i++) {
|
|
186
|
+
prm = params[i];
|
|
187
|
+
if (!(prm instanceof BindParam))
|
|
188
|
+
continue;
|
|
189
|
+
expected = this._paramTypes?.[i] ?? this._resolvedParamTypes?.[i];
|
|
190
|
+
if (expected != null && prm.oid !== expected)
|
|
191
|
+
throw new TypeError(`Parameter $${i + 1} was given as ${typeLabel(prm.oid)}, but this ` +
|
|
192
|
+
`prepared statement's $${i + 1} is ${typeLabel(expected)}. A ` +
|
|
193
|
+
'prepared statement takes its parameter types at ' +
|
|
194
|
+
'prepare(sql, { paramTypes }), and they cannot be changed per ' +
|
|
195
|
+
'execution.');
|
|
196
|
+
out = out || params.slice();
|
|
197
|
+
out[i] = prm.value;
|
|
198
|
+
}
|
|
199
|
+
return out || params;
|
|
200
|
+
}
|
|
133
201
|
_emitNotice(msg) {
|
|
134
202
|
this.emit('notice', msg);
|
|
135
203
|
getIntlConnection(this.connection).emit('notice', msg);
|
|
136
204
|
}
|
|
137
205
|
_executeBatchWithTransaction(paramSets, options) {
|
|
138
206
|
const intlCon = getIntlConnection(this.connection);
|
|
139
|
-
return this._withTransaction(options, () => intlCon.executeBatchReused(this.name, this._fields, this.paramTypes, paramSets, options));
|
|
207
|
+
return this._withTransaction(options, () => intlCon.executeBatchReused(this.name, this._fields, this.paramTypes, paramSets.map(set => this._unwrapBindParams(set)), options));
|
|
140
208
|
}
|
|
141
209
|
async _executeWithTransaction(options = {}) {
|
|
142
210
|
// The Bind+Execute+Sync path can carry the rollbackOnError savepoint
|
|
@@ -238,6 +306,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
238
306
|
}
|
|
239
307
|
async _execute(options = {}, savepoint) {
|
|
240
308
|
const intlCon = getIntlConnection(this.connection);
|
|
309
|
+
const params = this._unwrapBindParams(options.params);
|
|
241
310
|
if (options.cursor && this._fields) {
|
|
242
311
|
intlCon.ref();
|
|
243
312
|
let portal = new Portal(this, 'P_' + ++portalCounter);
|
|
@@ -247,7 +316,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
247
316
|
// codes on the Bind below with no round trip of its own.
|
|
248
317
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
249
318
|
const columnFormat = resolveColumnFormats(this._fields, options, typeMap);
|
|
250
|
-
const fields = await portal.bindAndRetrieveFields(
|
|
319
|
+
const fields = await portal.bindAndRetrieveFields(params, options, columnFormat);
|
|
251
320
|
const parsers = getParsers(typeMap, fields, options);
|
|
252
321
|
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
253
322
|
const result = {
|
|
@@ -270,7 +339,7 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
270
339
|
// unnamed portal, reusing the RowDescription prepare() already fetched
|
|
271
340
|
// instead of re-Describing (and re-Close-ing) a fresh named portal on
|
|
272
341
|
// every call.
|
|
273
|
-
return intlCon.executeReused(this.name, this._fields, this.paramTypes,
|
|
342
|
+
return intlCon.executeReused(this.name, this._fields, this.paramTypes, params, options, savepoint);
|
|
274
343
|
}
|
|
275
344
|
/**
|
|
276
345
|
* Called by Cursor.close() instead of separately awaiting
|
|
@@ -372,3 +441,8 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
372
441
|
return this.close();
|
|
373
442
|
}
|
|
374
443
|
}
|
|
444
|
+
/** `int2 (21)` - the name when there is one, and always the oid. */
|
|
445
|
+
function typeLabel(oid) {
|
|
446
|
+
const name = DataTypeNames[oid];
|
|
447
|
+
return name ? `${name} (${oid})` : String(oid);
|
|
448
|
+
}
|
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);
|
package/data-types/int2-type.js
CHANGED
|
@@ -9,7 +9,7 @@ export const Int2Type = {
|
|
|
9
9
|
return '' + v;
|
|
10
10
|
},
|
|
11
11
|
encodeBinary(buf, v) {
|
|
12
|
-
buf.writeInt16BE(assertInteger(
|
|
12
|
+
buf.writeInt16BE(assertInteger(v, 'int2', -32768, 32767));
|
|
13
13
|
},
|
|
14
14
|
decodeBinary(v, offset = 0) {
|
|
15
15
|
return v.readInt16BE(offset);
|
package/data-types/int4-type.js
CHANGED
|
@@ -9,7 +9,7 @@ export const Int4Type = {
|
|
|
9
9
|
return '' + v;
|
|
10
10
|
},
|
|
11
11
|
encodeBinary(buf, v) {
|
|
12
|
-
buf.writeInt32BE(assertInteger(
|
|
12
|
+
buf.writeInt32BE(assertInteger(v, 'int4', -2147483648, 2147483647));
|
|
13
13
|
},
|
|
14
14
|
decodeBinary(v, offset = 0) {
|
|
15
15
|
return v.readInt32BE(offset);
|
package/data-types/int8-type.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
-
import {
|
|
2
|
+
import { assertBigInt } from '../util/assert-integer.js';
|
|
3
3
|
import { readBigInt64BE } from '../util/bigint-methods.js';
|
|
4
4
|
import { fastParseIntBuffer } from '../util/fast-parseint.js';
|
|
5
5
|
const maxSafeInteger = BigInt(Number.MAX_SAFE_INTEGER);
|
|
@@ -11,7 +11,7 @@ export const Int8Type = {
|
|
|
11
11
|
return '' + v;
|
|
12
12
|
},
|
|
13
13
|
encodeBinary(buf, v) {
|
|
14
|
-
buf.writeBigInt64BE(
|
|
14
|
+
buf.writeBigInt64BE(assertBigInt(v, 'int8'));
|
|
15
15
|
},
|
|
16
16
|
decodeBinary(buf, offset = 0) {
|
|
17
17
|
const v = typeof buf.readBigInt64BE === 'function'
|
|
@@ -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
|
/**
|
package/data-types/oid-type.js
CHANGED
|
@@ -9,7 +9,7 @@ export const OidType = {
|
|
|
9
9
|
return '' + v;
|
|
10
10
|
},
|
|
11
11
|
encodeBinary(buf, v) {
|
|
12
|
-
buf.writeUInt32BE(assertInteger(
|
|
12
|
+
buf.writeUInt32BE(assertInteger(v, 'oid', 0, 4294967295));
|
|
13
13
|
},
|
|
14
14
|
decodeBinary(v, offset = 0) {
|
|
15
15
|
return v.readUInt32BE(offset);
|
|
@@ -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/data-types/xid-type.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { assertInteger } from '../util/assert-integer.js';
|
|
2
3
|
import { fastParseInt, fastParseIntBuffer } from '../util/fast-parseint.js';
|
|
3
4
|
/**
|
|
4
5
|
* `xid`, `xid8` and `cid` - the transaction and command counters behind
|
|
@@ -41,7 +42,7 @@ function createUInt32Type(name, oid) {
|
|
|
41
42
|
return '' + v;
|
|
42
43
|
},
|
|
43
44
|
encodeBinary(buf, v) {
|
|
44
|
-
buf.writeUInt32BE(
|
|
45
|
+
buf.writeUInt32BE(assertInteger(v, name, 0, 4294967295));
|
|
45
46
|
},
|
|
46
47
|
decodeBinary(v, offset = 0) {
|
|
47
48
|
return v.readUInt32BE(offset);
|
package/package.json
CHANGED
package/util/assert-integer.d.ts
CHANGED
|
@@ -1,22 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Returns `v`
|
|
2
|
+
* Returns `v` as the integer an integer column can store, or throws.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* The rule is that the binary encoding must accept exactly what the text
|
|
5
|
+
* one does and refuse exactly what the server refuses, because which of
|
|
6
|
+
* the two a value goes out as is not the caller's choice: it depends on
|
|
7
|
+
* whether the parameter's type was declared, which statement cache the
|
|
8
|
+
* query hit, and what the server resolved. A value that means one thing
|
|
9
|
+
* through one path and another through the other is the worst outcome
|
|
10
|
+
* there is, and that is what this used to be:
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* ```
|
|
13
|
+
* text binary (before)
|
|
14
|
+
* [1.5] -> int4[] 22P02 error stored 1
|
|
15
|
+
* [-1.5] -> int4[] 22P02 error stored -2 (a floor, not a truncation)
|
|
16
|
+
* ['0x10'] -> int4[] 22P02 error stored 0
|
|
17
|
+
* [''] -> int8[] 22P02 error stored 0
|
|
18
|
+
* [40000] -> int2[] 22003 error RangeError, from Buffer
|
|
19
|
+
* ```
|
|
14
20
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
21
|
+
* Only the last of those was safe. The rest put a number in the table
|
|
22
|
+
* that the caller never wrote and the server would have refused, and
|
|
23
|
+
* silently - the row inserted, the copy succeeded.
|
|
24
|
+
*
|
|
25
|
+
* So a fractional value is refused rather than floored. Truncating 3.7
|
|
26
|
+
* to 3 was the long-standing behaviour here and the argument for it was
|
|
27
|
+
* that it is usually what a caller writing into an integer column means;
|
|
28
|
+
* what that argument misses is that the same caller writing the same
|
|
29
|
+
* value through the same client gets an error whenever the encoding
|
|
30
|
+
* happens to be text, so it cannot be what they meant. Round or truncate
|
|
31
|
+
* at the call site, or let the server do it with a cast.
|
|
32
|
+
*
|
|
33
|
+
* `min`/`max` are checked here as well, rather than left to Buffer's own
|
|
34
|
+
* bounds check, so that the message names the type and the value.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertInteger(v: any, typeName: string, min: number, max: number): number;
|
|
37
|
+
/**
|
|
38
|
+
* The same rule for int8, which counts past what a `number` holds.
|
|
39
|
+
*
|
|
40
|
+
* BigInt is happy to convert things that are not numbers at all -
|
|
41
|
+
* `BigInt('')` is 0n because an empty string reads as zero, `BigInt(true)`
|
|
42
|
+
* is 1n - so a bad value in a bigint column used to land as a plausible
|
|
43
|
+
* 0 or 1. Only what genuinely carries an exact integer is let through.
|
|
18
44
|
*/
|
|
19
|
-
export declare function
|
|
45
|
+
export declare function assertBigInt(v: any, typeName: string): bigint;
|
|
20
46
|
/**
|
|
21
47
|
* Returns `parsed` when a value that was NOT already a number coerced to
|
|
22
48
|
* one, and throws when it did not.
|
|
@@ -30,14 +56,3 @@ export declare function assertInteger(v: number, typeName: string): number;
|
|
|
30
56
|
* as NaN would be indistinguishable afterwards from a NaN that was meant.
|
|
31
57
|
*/
|
|
32
58
|
export declare function assertCoercedNumber(parsed: number, typeName: string): number;
|
|
33
|
-
/**
|
|
34
|
-
* Refuses a value that only has an integer reading through a coercion
|
|
35
|
-
* nobody asked for.
|
|
36
|
-
*
|
|
37
|
-
* int8 goes to `BigInt` rather than through `fastParseInt()`, and BigInt is
|
|
38
|
-
* happy to convert things that are not numbers at all: `BigInt([])` is 0n
|
|
39
|
-
* because an empty array stringifies to "", and `BigInt(true)` is 1n. A
|
|
40
|
-
* bad value in a bigint column would land as a plausible-looking 0 or 1,
|
|
41
|
-
* so only the types that genuinely carry an integer are let through.
|
|
42
|
-
*/
|
|
43
|
-
export declare function assertBigIntSource(v: any, typeName: string): bigint | number;
|
package/util/assert-integer.js
CHANGED
|
@@ -1,26 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* What the server reads as an integer from text: optional sign, digits,
|
|
3
|
+
* nothing else. `'1.5'`, `'1e3'` and `'0x10'` are all refused by
|
|
4
|
+
* `int4in()`, and surrounding whitespace is trimmed before it looks.
|
|
5
|
+
*/
|
|
6
|
+
const DECIMAL_INTEGER = /^[+-]?\d+$/;
|
|
7
|
+
/**
|
|
8
|
+
* Returns `v` as the integer an integer column can store, or throws.
|
|
9
|
+
*
|
|
10
|
+
* The rule is that the binary encoding must accept exactly what the text
|
|
11
|
+
* one does and refuse exactly what the server refuses, because which of
|
|
12
|
+
* the two a value goes out as is not the caller's choice: it depends on
|
|
13
|
+
* whether the parameter's type was declared, which statement cache the
|
|
14
|
+
* query hit, and what the server resolved. A value that means one thing
|
|
15
|
+
* through one path and another through the other is the worst outcome
|
|
16
|
+
* there is, and that is what this used to be:
|
|
3
17
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
18
|
+
* ```
|
|
19
|
+
* text binary (before)
|
|
20
|
+
* [1.5] -> int4[] 22P02 error stored 1
|
|
21
|
+
* [-1.5] -> int4[] 22P02 error stored -2 (a floor, not a truncation)
|
|
22
|
+
* ['0x10'] -> int4[] 22P02 error stored 0
|
|
23
|
+
* [''] -> int8[] 22P02 error stored 0
|
|
24
|
+
* [40000] -> int2[] 22003 error RangeError, from Buffer
|
|
25
|
+
* ```
|
|
9
26
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* is refused.
|
|
27
|
+
* Only the last of those was safe. The rest put a number in the table
|
|
28
|
+
* that the caller never wrote and the server would have refused, and
|
|
29
|
+
* silently - the row inserted, the copy succeeded.
|
|
14
30
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
31
|
+
* So a fractional value is refused rather than floored. Truncating 3.7
|
|
32
|
+
* to 3 was the long-standing behaviour here and the argument for it was
|
|
33
|
+
* that it is usually what a caller writing into an integer column means;
|
|
34
|
+
* what that argument misses is that the same caller writing the same
|
|
35
|
+
* value through the same client gets an error whenever the encoding
|
|
36
|
+
* happens to be text, so it cannot be what they meant. Round or truncate
|
|
37
|
+
* at the call site, or let the server do it with a cast.
|
|
38
|
+
*
|
|
39
|
+
* `min`/`max` are checked here as well, rather than left to Buffer's own
|
|
40
|
+
* bounds check, so that the message names the type and the value.
|
|
18
41
|
*/
|
|
19
|
-
export function assertInteger(v, typeName) {
|
|
20
|
-
|
|
42
|
+
export function assertInteger(v, typeName, min, max) {
|
|
43
|
+
let n;
|
|
44
|
+
const t = typeof v;
|
|
45
|
+
if (t === 'number') {
|
|
46
|
+
n = v;
|
|
47
|
+
if (Number.isNaN(n))
|
|
48
|
+
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
49
|
+
}
|
|
50
|
+
else if (t === 'bigint') {
|
|
51
|
+
// Exact up to 2^53; beyond that the range check below refuses it
|
|
52
|
+
// anyway, since nothing this function guards is that wide.
|
|
53
|
+
n = Number(v);
|
|
54
|
+
}
|
|
55
|
+
else if (t === 'string') {
|
|
56
|
+
const s = v.trim();
|
|
57
|
+
if (!DECIMAL_INTEGER.test(s))
|
|
58
|
+
throw new TypeError(`Cannot encode "${v}" as ${typeName}: it is not an integer`);
|
|
59
|
+
n = +s;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
21
62
|
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
22
63
|
}
|
|
23
|
-
|
|
64
|
+
if (!Number.isInteger(n))
|
|
65
|
+
throw new TypeError(`Cannot encode ${n} as ${typeName}: it is not an integer`);
|
|
66
|
+
if (n < min || n > max)
|
|
67
|
+
throw new RangeError(`Cannot encode ${n} as ${typeName}: it is out of range (${min} to ${max})`);
|
|
68
|
+
return n;
|
|
69
|
+
}
|
|
70
|
+
const INT8_MIN = BigInt('-9223372036854775808');
|
|
71
|
+
const INT8_MAX = BigInt('9223372036854775807');
|
|
72
|
+
/**
|
|
73
|
+
* The same rule for int8, which counts past what a `number` holds.
|
|
74
|
+
*
|
|
75
|
+
* BigInt is happy to convert things that are not numbers at all -
|
|
76
|
+
* `BigInt('')` is 0n because an empty string reads as zero, `BigInt(true)`
|
|
77
|
+
* is 1n - so a bad value in a bigint column used to land as a plausible
|
|
78
|
+
* 0 or 1. Only what genuinely carries an exact integer is let through.
|
|
79
|
+
*/
|
|
80
|
+
export function assertBigInt(v, typeName) {
|
|
81
|
+
let n;
|
|
82
|
+
const t = typeof v;
|
|
83
|
+
if (t === 'bigint') {
|
|
84
|
+
n = v;
|
|
85
|
+
}
|
|
86
|
+
else if (t === 'number') {
|
|
87
|
+
if (!Number.isInteger(v))
|
|
88
|
+
throw new TypeError(Number.isNaN(v)
|
|
89
|
+
? `Cannot encode value as ${typeName}: it has no integer value`
|
|
90
|
+
: `Cannot encode ${v} as ${typeName}: it is not an integer`);
|
|
91
|
+
n = BigInt(v);
|
|
92
|
+
}
|
|
93
|
+
else if (t === 'string') {
|
|
94
|
+
const s = v.trim();
|
|
95
|
+
if (!DECIMAL_INTEGER.test(s))
|
|
96
|
+
throw new TypeError(`Cannot encode "${v}" as ${typeName}: it is not an integer`);
|
|
97
|
+
n = BigInt(s);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
101
|
+
}
|
|
102
|
+
if (n < INT8_MIN || n > INT8_MAX)
|
|
103
|
+
throw new RangeError(`Cannot encode ${n} as ${typeName}: it is out of range (${INT8_MIN} to ${INT8_MAX})`);
|
|
104
|
+
return n;
|
|
24
105
|
}
|
|
25
106
|
/**
|
|
26
107
|
* Returns `parsed` when a value that was NOT already a number coerced to
|
|
@@ -40,19 +121,3 @@ export function assertCoercedNumber(parsed, typeName) {
|
|
|
40
121
|
}
|
|
41
122
|
return parsed;
|
|
42
123
|
}
|
|
43
|
-
/**
|
|
44
|
-
* Refuses a value that only has an integer reading through a coercion
|
|
45
|
-
* nobody asked for.
|
|
46
|
-
*
|
|
47
|
-
* int8 goes to `BigInt` rather than through `fastParseInt()`, and BigInt is
|
|
48
|
-
* happy to convert things that are not numbers at all: `BigInt([])` is 0n
|
|
49
|
-
* because an empty array stringifies to "", and `BigInt(true)` is 1n. A
|
|
50
|
-
* bad value in a bigint column would land as a plausible-looking 0 or 1,
|
|
51
|
-
* so only the types that genuinely carry an integer are let through.
|
|
52
|
-
*/
|
|
53
|
-
export function assertBigIntSource(v, typeName) {
|
|
54
|
-
const t = typeof v;
|
|
55
|
-
if (t === 'bigint' || t === 'number' || t === 'string')
|
|
56
|
-
return v;
|
|
57
|
-
throw new TypeError(`Cannot encode value as ${typeName}: it has no integer value`);
|
|
58
|
-
}
|
|
@@ -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,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
|
+
}
|
|
@@ -1,4 +1,20 @@
|
|
|
1
1
|
import { arrayCalculateDim } from './array-calculatedim.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a number's own text looks like: digits, one optional dot, an
|
|
4
|
+
* optional exponent, an optional leading minus. Nothing in it is special
|
|
5
|
+
* to the array literal grammar and it cannot be read as `NULL`, so an
|
|
6
|
+
* element that matches goes out bare - `{1,2}` rather than `{"1","2"}`,
|
|
7
|
+
* which is what the server itself prints and what every other client
|
|
8
|
+
* writes.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately narrower than "has no special characters": it is applied
|
|
11
|
+
* only to what a JS number or bigint encoded to, and matching it is the
|
|
12
|
+
* evidence that the encoder really did produce a number's text. `NaN`
|
|
13
|
+
* and `Infinity` fail it and stay quoted (both forms parse the same, so
|
|
14
|
+
* this costs nothing), and so does anything an unexpected encoder made
|
|
15
|
+
* of a number - a date type handed a timestamp, say.
|
|
16
|
+
*/
|
|
17
|
+
const BARE_NUMBER = /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/;
|
|
2
18
|
export function stringifyArrayLiteral(value, options, encode) {
|
|
3
19
|
const dim = arrayCalculateDim(value);
|
|
4
20
|
// `dim` is built once above and never mutated, so its last index is
|
|
@@ -9,19 +25,24 @@ export function stringifyArrayLiteral(value, options, encode) {
|
|
|
9
25
|
const lastLevel = dim.length - 1;
|
|
10
26
|
const writeDim = (arr, level) => {
|
|
11
27
|
const elemCount = dim[level];
|
|
12
|
-
const out = [];
|
|
13
28
|
const isLeafLevel = level >= lastLevel;
|
|
29
|
+
// Concatenated rather than collected and joined: V8 builds a rope
|
|
30
|
+
// and flattens it once, where an array of one string per element is
|
|
31
|
+
// a second allocation per element before the join sees any of them.
|
|
32
|
+
let out = '{';
|
|
14
33
|
for (let i = 0; i < elemCount; i++) {
|
|
34
|
+
if (i)
|
|
35
|
+
out += ',';
|
|
15
36
|
let x = arr && arr[i];
|
|
16
37
|
if (!isLeafLevel) {
|
|
17
38
|
if (x != null && !Array.isArray(x))
|
|
18
39
|
x = [x];
|
|
19
|
-
out
|
|
40
|
+
out += writeDim(x, level + 1);
|
|
20
41
|
continue;
|
|
21
42
|
}
|
|
22
43
|
// if value is null
|
|
23
44
|
if (x == null) {
|
|
24
|
-
out
|
|
45
|
+
out += 'NULL';
|
|
25
46
|
continue;
|
|
26
47
|
}
|
|
27
48
|
/* c8 ignore start - dim is (re)computed above from a DFS that visits
|
|
@@ -29,18 +50,32 @@ export function stringifyArrayLiteral(value, options, encode) {
|
|
|
29
50
|
can never actually be an array once `level` reaches the leaf -
|
|
30
51
|
if it were, dim would already have gone one level deeper there. */
|
|
31
52
|
if (Array.isArray(x)) {
|
|
32
|
-
out
|
|
53
|
+
out += stringifyArrayLiteral(x, options, encode);
|
|
33
54
|
continue;
|
|
34
55
|
}
|
|
35
56
|
/* c8 ignore stop */
|
|
57
|
+
// Asked before the encoder runs, so the test below is only ever
|
|
58
|
+
// applied to a value that really was a number.
|
|
59
|
+
const isNumber = typeof x === 'number' || typeof x === 'bigint';
|
|
36
60
|
if (encode)
|
|
37
61
|
x = encode(x, options || {});
|
|
38
|
-
|
|
62
|
+
const s = '' + x;
|
|
63
|
+
out += isNumber && BARE_NUMBER.test(s) ? s : escapeArrayItem(s);
|
|
39
64
|
}
|
|
40
|
-
return
|
|
65
|
+
return out + '}';
|
|
41
66
|
};
|
|
42
67
|
return writeDim(value, 0);
|
|
43
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Backslash and double quote are the only two characters the literal
|
|
71
|
+
* grammar gives meaning to inside a quoted element, and neither is in
|
|
72
|
+
* most values - so they are looked for before anything is rewritten. The
|
|
73
|
+
* two `replace()` calls each walk the string and build another one even
|
|
74
|
+
* when they change nothing, which for a column of ordinary text was
|
|
75
|
+
* half the cost of writing the literal.
|
|
76
|
+
*/
|
|
44
77
|
function escapeArrayItem(str) {
|
|
45
|
-
return
|
|
78
|
+
return str.indexOf('\\') < 0 && str.indexOf('"') < 0
|
|
79
|
+
? '"' + str + '"'
|
|
80
|
+
: '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
46
81
|
}
|
|
@@ -36,7 +36,31 @@
|
|
|
36
36
|
* or of one of this client's own classes has one type it can be, so
|
|
37
37
|
* naming it says nothing the server would have decided differently -
|
|
38
38
|
* and it keeps the binary encoding, which the text literal gives up.
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
*
|
|
40
|
+
* What that giving up costs, which the commit making this change put at
|
|
41
|
+
* nothing measurable and is not: inserting the same value over a
|
|
42
|
+
* loopback connection, alternating within each round, medians of nine -
|
|
43
|
+
*
|
|
44
|
+
* ```
|
|
45
|
+
* as text as BindParam
|
|
46
|
+
* float8[] 1 000 2.33ms 0.91ms 2.6x
|
|
47
|
+
* float8[] 10 000 17.34ms 3.75ms 4.6x
|
|
48
|
+
* float8[] 100 000 143.0ms 26.9ms 5.3x
|
|
49
|
+
* int4[] 100 000 24.5ms 15.5ms 1.6x
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* It grows with the array and with how wide an element's text is
|
|
53
|
+
* against its binary: a float8 is 8 bytes against about 19 characters,
|
|
54
|
+
* an int4 is 4 against up to 11 - and most of what a float8[] costs is
|
|
55
|
+
* the server's own parse of that text, not the writing of it. The
|
|
56
|
+
* original measurement was of the smallest of these shapes and read it
|
|
57
|
+
* as level; it is not, at any size.
|
|
58
|
+
*
|
|
59
|
+
* So this is a real price, paid for a correctness the client cannot buy
|
|
60
|
+
* any other way before the server has spoken. A caller who knows the
|
|
61
|
+
* column - and for a large numeric array that is worth knowing - names
|
|
62
|
+
* the type with `new BindParam(DataTypeOIDs._float8, v)` and gets the
|
|
63
|
+
* binary encoding back with nothing given up, since a named type is an
|
|
64
|
+
* answer rather than a guess.
|
|
41
65
|
*/
|
|
42
66
|
export declare function isUnspecifiedParam(v: any): boolean;
|
|
@@ -37,8 +37,32 @@ import { arrayLeaf } from './array-leaf.js';
|
|
|
37
37
|
* or of one of this client's own classes has one type it can be, so
|
|
38
38
|
* naming it says nothing the server would have decided differently -
|
|
39
39
|
* and it keeps the binary encoding, which the text literal gives up.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
*
|
|
41
|
+
* What that giving up costs, which the commit making this change put at
|
|
42
|
+
* nothing measurable and is not: inserting the same value over a
|
|
43
|
+
* loopback connection, alternating within each round, medians of nine -
|
|
44
|
+
*
|
|
45
|
+
* ```
|
|
46
|
+
* as text as BindParam
|
|
47
|
+
* float8[] 1 000 2.33ms 0.91ms 2.6x
|
|
48
|
+
* float8[] 10 000 17.34ms 3.75ms 4.6x
|
|
49
|
+
* float8[] 100 000 143.0ms 26.9ms 5.3x
|
|
50
|
+
* int4[] 100 000 24.5ms 15.5ms 1.6x
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* It grows with the array and with how wide an element's text is
|
|
54
|
+
* against its binary: a float8 is 8 bytes against about 19 characters,
|
|
55
|
+
* an int4 is 4 against up to 11 - and most of what a float8[] costs is
|
|
56
|
+
* the server's own parse of that text, not the writing of it. The
|
|
57
|
+
* original measurement was of the smallest of these shapes and read it
|
|
58
|
+
* as level; it is not, at any size.
|
|
59
|
+
*
|
|
60
|
+
* So this is a real price, paid for a correctness the client cannot buy
|
|
61
|
+
* any other way before the server has spoken. A caller who knows the
|
|
62
|
+
* column - and for a large numeric array that is worth knowing - names
|
|
63
|
+
* the type with `new BindParam(DataTypeOIDs._float8, v)` and gets the
|
|
64
|
+
* binary encoding back with nothing given up, since a named type is an
|
|
65
|
+
* answer rather than a guess.
|
|
42
66
|
*/
|
|
43
67
|
export function isUnspecifiedParam(v) {
|
|
44
68
|
const x = arrayLeaf(v);
|