postgrejs 3.11.1 → 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 +28 -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/int2-type.js +1 -1
- package/data-types/int4-type.js +1 -1
- package/data-types/int8-type.js +2 -2
- package/data-types/oid-type.js +1 -1
- 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/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,33 @@
|
|
|
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
|
|
4
31
|
|
|
5
32
|
#### 🪲 Fixes
|
|
6
33
|
|
|
@@ -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/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'
|
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);
|
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
|
-
}
|
|
@@ -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);
|