postgrejs 3.12.1 → 3.13.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/README.md +1 -1
- package/connection/connection.js +7 -2
- package/connection/intl-connection.d.ts +1 -0
- package/connection/intl-connection.js +4 -3
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/data-types/date-type.js +8 -1
- package/package.json +2 -2
- package/protocol/frontend.d.ts +14 -1
- package/protocol/frontend.js +39 -5
- package/protocol/pg-socket.js +41 -22
- package/util/resolved-bind-type.d.ts +15 -0
- package/util/resolved-bind-type.js +85 -0
- package/util/stringify-arrayliteral.js +9 -2
- package/util/unspecified-param.d.ts +14 -0
- package/util/unspecified-param.js +40 -0
- package/util/unspecified-value.d.ts +26 -0
- package/util/unspecified-value.js +63 -0
package/README.md
CHANGED
|
@@ -62,7 +62,7 @@ PostgreJS is a driver, not a framework, so what you build on top of it stays you
|
|
|
62
62
|
<a href="https://www.sqbjs.org"><img src="https://www.postgrejs.com/img/sqb-logo.svg" alt="SQB" height="60"></a><br><br>
|
|
63
63
|
<b><a href="https://www.sqbjs.org">SQB</a></b><br>
|
|
64
64
|
<a href="https://www.npmjs.com/package/@sqb/postgres"><code>@sqb/postgres</code></a><br>
|
|
65
|
-
<sub>
|
|
65
|
+
<sub>first-party adapter</sub>
|
|
66
66
|
</td>
|
|
67
67
|
<td align="center" width="230" valign="top">
|
|
68
68
|
<a href="https://kysely.dev"><img src="https://www.postgrejs.com/img/kysely-logo.svg" alt="Kysely" height="60"></a><br><br>
|
package/connection/connection.js
CHANGED
|
@@ -5,7 +5,7 @@ import { withAbortSignal } from '../util/abort-signal.js';
|
|
|
5
5
|
import { normalizeChannelName } from '../util/channel-name.js';
|
|
6
6
|
import { escapeIdentifier } from '../util/escape-identifier.js';
|
|
7
7
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
8
|
-
import { isUnspecifiedParam } from '../util/unspecified-param.js';
|
|
8
|
+
import { isUnspecifiedParam, needsNumericParam, } from '../util/unspecified-param.js';
|
|
9
9
|
import { BindParam } from './bind-param.js';
|
|
10
10
|
import { IntlConnection } from './intl-connection.js';
|
|
11
11
|
import { LargeObject, LargeObjectMode } from './large-object.js';
|
|
@@ -807,7 +807,12 @@ export class Connection extends SafeEventEmitter {
|
|
|
807
807
|
// out as is written in getBindMessage().
|
|
808
808
|
isUnspecifiedParam(prm)
|
|
809
809
|
? 0
|
|
810
|
-
:
|
|
810
|
+
: // A non-integer is declared numeric rather than the float8
|
|
811
|
+
// its value suggests - see `needsNumericParam()` for the
|
|
812
|
+
// cast that makes the difference, and what it costs.
|
|
813
|
+
needsNumericParam(prm)
|
|
814
|
+
? DataTypeOIDs.numeric
|
|
815
|
+
: typeMap.determine(prm));
|
|
811
816
|
// A money parameter is written as an int64 of minor units, so the
|
|
812
817
|
// scale has to be known before the Bind goes out - the probe fired
|
|
813
818
|
// at connect answers in time for every result, but not necessarily
|
|
@@ -24,6 +24,7 @@ import { CopyFromStream, CopyToStream } from './copy-stream.js';
|
|
|
24
24
|
interface PreparedCacheEntry {
|
|
25
25
|
name: string;
|
|
26
26
|
fields?: Protocol.RowDescription[];
|
|
27
|
+
resolvedParamTypes?: OID[];
|
|
27
28
|
}
|
|
28
29
|
/** See IntlConnection._decodeDeferredRows(). */
|
|
29
30
|
interface DeferredRows {
|
|
@@ -15,6 +15,7 @@ import { escapeLiteral } from '../util/escape-literal.js';
|
|
|
15
15
|
import { fetchAsStringEqual, fetchAsStringNamesElement, fetchAsStringNamesOid, validateFetchAsString, } from '../util/fetch-as-string.js';
|
|
16
16
|
import { getParsers } from '../util/get-parsers.js';
|
|
17
17
|
import { resolveColumnFormats } from '../util/resolve-column-formats.js';
|
|
18
|
+
import { resolvedBindTypes } from '../util/resolved-bind-type.js';
|
|
18
19
|
import { dataRowBytes, resolveRowDecoder, resolveRowType, } from '../util/row-decoder.js';
|
|
19
20
|
import { requireTimeZone } from '../util/temporal.js';
|
|
20
21
|
import { isTransactionCommand, refusesSavepoint, } from '../util/transaction-command.js';
|
|
@@ -983,7 +984,7 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
983
984
|
this._preparedCache.delete(key);
|
|
984
985
|
this._preparedCache.set(key, cached);
|
|
985
986
|
try {
|
|
986
|
-
return await this.executeReused(cached.name, cached.fields, paramTypes, params, options, savepoint);
|
|
987
|
+
return await this.executeReused(cached.name, cached.fields, resolvedBindTypes(paramTypes, params, cached.resolvedParamTypes), params, options, savepoint);
|
|
987
988
|
}
|
|
988
989
|
catch (e) {
|
|
989
990
|
if (!isStaleCachedPlan(e))
|
|
@@ -1052,8 +1053,8 @@ export class IntlConnection extends SafeEventEmitter {
|
|
|
1052
1053
|
_prepareForCache(key, sql, paramTypes) {
|
|
1053
1054
|
const name = 'C_' + ++this._preparedCounter;
|
|
1054
1055
|
return (async () => {
|
|
1055
|
-
const { fields } = await this.prepareOnce(sql, paramTypes, name);
|
|
1056
|
-
const entry = { name, fields };
|
|
1056
|
+
const { fields, resolvedParamTypes } = await this.prepareOnce(sql, paramTypes, name);
|
|
1057
|
+
const entry = { name, fields, resolvedParamTypes };
|
|
1057
1058
|
await this._evictPreparedIfFull();
|
|
1058
1059
|
this._preparedCache.set(key, entry);
|
|
1059
1060
|
return entry;
|
package/constants.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Protocol } from './protocol/protocol.js';
|
|
2
2
|
import DataFormat = Protocol.DataFormat;
|
|
3
|
-
export declare const version = "3.
|
|
3
|
+
export declare const version = "3.13.0";
|
|
4
4
|
export { DataFormat };
|
|
5
5
|
/**
|
|
6
6
|
* What columns come back in unless something asks otherwise.
|
package/constants.js
CHANGED
|
@@ -2,7 +2,7 @@ import { Protocol } from './protocol/protocol.js';
|
|
|
2
2
|
var DataFormat = Protocol.DataFormat;
|
|
3
3
|
// @ts-ignore
|
|
4
4
|
Symbol.asyncDispose ??= Symbol('Symbol.asyncDispose');
|
|
5
|
-
export const version = '3.
|
|
5
|
+
export const version = '3.13.0';
|
|
6
6
|
export { DataFormat };
|
|
7
7
|
/**
|
|
8
8
|
* What columns come back in unless something asks otherwise.
|
package/data-types/date-type.js
CHANGED
|
@@ -28,7 +28,14 @@ export const DateType = {
|
|
|
28
28
|
? v.getTime()
|
|
29
29
|
: v.getTime() - v.getTimezoneOffset() * 60 * 1000;
|
|
30
30
|
n = (n - timeShift) / 1000 / 86400;
|
|
31
|
-
|
|
31
|
+
/* Floor, not trunc: a date before 2000-01-01 makes `n` negative, and
|
|
32
|
+
truncation there rounds *towards* the epoch - every pre-2000
|
|
33
|
+
instant with a time of day was stored one day late, silently.
|
|
34
|
+
`1999-12-31T22:00Z` under TZ=UTC went in as 2000-01-01 where the
|
|
35
|
+
text path, and `pg`, both say 1999-12-31. The epsilon stays: it is
|
|
36
|
+
what keeps an exact midnight from landing a hair under its own
|
|
37
|
+
day and flooring to the one before. */
|
|
38
|
+
const t = Math.floor(n + Number.EPSILON);
|
|
32
39
|
buf.writeInt32BE(t);
|
|
33
40
|
},
|
|
34
41
|
decodeBinary(v, offset = 0, _len, options) {
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgrejs",
|
|
3
3
|
"description": "Professional PostgreSQL client NodeJS",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.13.0",
|
|
5
5
|
"author": "Panates",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@jsopen/objects": "^2.3.4",
|
|
9
9
|
"doublylinked": "^2.5.6",
|
|
10
|
-
"flexy-buffer": "^1.1.
|
|
10
|
+
"flexy-buffer": "^1.1.2",
|
|
11
11
|
"lightning-pool": "^4.14.0",
|
|
12
12
|
"power-tasks": "^1.14.2",
|
|
13
13
|
"putil-promisify": "^1.10.1",
|
package/protocol/frontend.d.ts
CHANGED
|
@@ -101,7 +101,20 @@ export declare class Frontend {
|
|
|
101
101
|
getSASLMessage(sasl: SASL.Session): Buffer;
|
|
102
102
|
getSASLFinalMessage(session: SASL.Session): Buffer;
|
|
103
103
|
getParseMessage(args: Frontend.ParseMessageArgs): Buffer;
|
|
104
|
-
|
|
104
|
+
/**
|
|
105
|
+
* Bind, carrying the parameter values - the one message in a statement
|
|
106
|
+
* whose size is the caller's data rather than a few dozen bytes.
|
|
107
|
+
*
|
|
108
|
+
* @param args The portal, statement, parameters and result formats.
|
|
109
|
+
* @param copy When false, the returned buffer is a view into the
|
|
110
|
+
* shared send buffer rather than a copy of it. It stays valid only
|
|
111
|
+
* until the next message is written, so a caller passing false must
|
|
112
|
+
* consume it - `Buffer.concat()`, a write - before building anything
|
|
113
|
+
* else, and must never hand it to the socket, which does not copy
|
|
114
|
+
* synchronously.
|
|
115
|
+
* @returns The Bind message.
|
|
116
|
+
*/
|
|
117
|
+
getBindMessage(args: Frontend.BindMessageArgs, copy?: boolean): Buffer;
|
|
105
118
|
getDescribeMessage(args: Frontend.DescribeMessageArgs): Buffer;
|
|
106
119
|
getExecuteMessage(args: Frontend.ExecuteMessageArgs): Buffer;
|
|
107
120
|
getCloseMessage(args: Frontend.CloseMessageArgs): Buffer;
|
package/protocol/frontend.js
CHANGED
|
@@ -4,6 +4,7 @@ import { arrayLeaf } from '../util/array-leaf.js';
|
|
|
4
4
|
import { encodeBinaryArray } from '../util/encode-binaryarray.js';
|
|
5
5
|
import { formatDateParam } from '../util/format-datetime.js';
|
|
6
6
|
import { writeArrayLiteral } from '../util/stringify-arrayliteral.js';
|
|
7
|
+
import { resolveToPostgres, unspecifiedText, } from '../util/unspecified-value.js';
|
|
7
8
|
import { Protocol } from './protocol.js';
|
|
8
9
|
import { SmartBuffer } from './smart-buffer.js';
|
|
9
10
|
const DataFormat = Protocol.DataFormat;
|
|
@@ -131,7 +132,28 @@ export class Frontend {
|
|
|
131
132
|
}
|
|
132
133
|
return setLengthAndFlush(io, 1);
|
|
133
134
|
}
|
|
134
|
-
|
|
135
|
+
/**
|
|
136
|
+
* Bind, carrying the parameter values - the one message in a statement
|
|
137
|
+
* whose size is the caller's data rather than a few dozen bytes.
|
|
138
|
+
*
|
|
139
|
+
* @param args The portal, statement, parameters and result formats.
|
|
140
|
+
* @param copy When false, the returned buffer is a view into the
|
|
141
|
+
* shared send buffer rather than a copy of it. It stays valid only
|
|
142
|
+
* until the next message is written, so a caller passing false must
|
|
143
|
+
* consume it - `Buffer.concat()`, a write - before building anything
|
|
144
|
+
* else, and must never hand it to the socket, which does not copy
|
|
145
|
+
* synchronously.
|
|
146
|
+
* @returns The Bind message.
|
|
147
|
+
*/
|
|
148
|
+
/* The copy exists for buffer reuse, not for the socket: every message
|
|
149
|
+
is built in this one SmartBuffer, so message N+1's start() would
|
|
150
|
+
overwrite N's bytes before they were assembled. Nothing flushed here
|
|
151
|
+
ever reaches socket.write() - only the concat of them does, which is
|
|
152
|
+
checked in a test. So a caller that builds the small messages first
|
|
153
|
+
and this one last can take a view and let the concat read it, which
|
|
154
|
+
is one copy of the payload rather than two: measured on a 100 000
|
|
155
|
+
element int4[], 2148 KB of copying a call against 1074. */
|
|
156
|
+
getBindMessage(args, copy = true) {
|
|
135
157
|
if (args.portal && args.portal.length > 63)
|
|
136
158
|
throw new Error('Portal name length must be lower than 63');
|
|
137
159
|
if (args.statement && args.statement.length > 63)
|
|
@@ -166,6 +188,18 @@ export class Frontend {
|
|
|
166
188
|
}
|
|
167
189
|
const dataTypeOid = paramTypes ? paramTypes[i] : undefined;
|
|
168
190
|
const dt = dataTypeOid ? args.typeMap.get(dataTypeOid) : undefined;
|
|
191
|
+
// No declared type: the value writes itself if it knows how, and
|
|
192
|
+
// what it hands back decides which branch below takes it - a
|
|
193
|
+
// toPostgres() returning a Date belongs in the Date branch, not
|
|
194
|
+
// in `'' + v`. A declared type is left alone; it owns its own
|
|
195
|
+
// encoding.
|
|
196
|
+
if (!dt) {
|
|
197
|
+
v = resolveToPostgres(v);
|
|
198
|
+
if (v === null || v === undefined) {
|
|
199
|
+
io.writeInt32BE(-1);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
169
203
|
if (dt) {
|
|
170
204
|
if (typeof dt.encodeAsNull === 'function' &&
|
|
171
205
|
dt.encodeAsNull(v, queryOptions)) {
|
|
@@ -220,7 +254,7 @@ export class Frontend {
|
|
|
220
254
|
io.buffer.writeInt32BE(io.size - dataOffset, dataOffset - 4); // Update length
|
|
221
255
|
}
|
|
222
256
|
else {
|
|
223
|
-
io.writeLString(
|
|
257
|
+
io.writeLString(unspecifiedText(v), 'utf8');
|
|
224
258
|
}
|
|
225
259
|
}
|
|
226
260
|
}
|
|
@@ -242,7 +276,7 @@ export class Frontend {
|
|
|
242
276
|
}
|
|
243
277
|
else
|
|
244
278
|
io.writeUInt16BE(0);
|
|
245
|
-
return setLengthAndFlush(io, 1);
|
|
279
|
+
return setLengthAndFlush(io, 1, copy);
|
|
246
280
|
}
|
|
247
281
|
getDescribeMessage(args) {
|
|
248
282
|
if (args.name && args.name.length > 63) {
|
|
@@ -348,7 +382,7 @@ export class Frontend {
|
|
|
348
382
|
return StaticSyncBuffer;
|
|
349
383
|
}
|
|
350
384
|
}
|
|
351
|
-
function setLengthAndFlush(io, lengthOffset) {
|
|
385
|
+
function setLengthAndFlush(io, lengthOffset, copy = true) {
|
|
352
386
|
io.buffer.writeUInt32BE(io.size - lengthOffset, lengthOffset);
|
|
353
|
-
return io.flush();
|
|
387
|
+
return io.flush(copy);
|
|
354
388
|
}
|
package/protocol/pg-socket.js
CHANGED
|
@@ -250,23 +250,41 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
250
250
|
* statement/portal at the next Parse/Bind referencing the unnamed name.
|
|
251
251
|
*/
|
|
252
252
|
sendExtendedQueryMessages(args, cb) {
|
|
253
|
+
/* The Bind is built LAST and taken as a view, then concatenated here
|
|
254
|
+
rather than in _send(): it carries the caller's data, where the
|
|
255
|
+
other four messages are a few dozen bytes, and the copy flush()
|
|
256
|
+
makes exists only because this one SmartBuffer is reused for the
|
|
257
|
+
next message. Building the small ones first leaves nothing to
|
|
258
|
+
overwrite it before Buffer.concat() reads it, so the payload is
|
|
259
|
+
copied once instead of twice - measured on a 100 000-element
|
|
260
|
+
int4[], 2148 KB of copying a call against 1074, with the wire bytes
|
|
261
|
+
and the stored value unchanged. The view never reaches the socket;
|
|
262
|
+
only the concat does. */
|
|
263
|
+
const parse = this._frontend.getParseMessage(args.parse);
|
|
264
|
+
const describe = this._frontend.getDescribeMessage(args.describe);
|
|
265
|
+
const execute = this._frontend.getExecuteMessage(args.execute);
|
|
266
|
+
const sync = this._frontend.getSyncMessage();
|
|
253
267
|
if (!args.before && !args.after) {
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
this._frontend.getBindMessage(args.bind),
|
|
257
|
-
this._frontend.getDescribeMessage(args.describe),
|
|
258
|
-
this._frontend.getExecuteMessage(args.execute),
|
|
259
|
-
this._frontend.getSyncMessage(),
|
|
260
|
-
];
|
|
268
|
+
const bind = this._frontend.getBindMessage(args.bind, false);
|
|
269
|
+
const data = Buffer.concat([parse, bind, describe, execute, sync]);
|
|
261
270
|
return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
|
|
262
271
|
}
|
|
263
|
-
const
|
|
272
|
+
const before = [];
|
|
264
273
|
if (args.before)
|
|
265
|
-
this._pushUtilityStatement(
|
|
266
|
-
|
|
274
|
+
this._pushUtilityStatement(before, args.before, args.bind);
|
|
275
|
+
const after = [];
|
|
267
276
|
if (args.after)
|
|
268
|
-
this._pushUtilityStatement(
|
|
269
|
-
|
|
277
|
+
this._pushUtilityStatement(after, args.after, args.bind);
|
|
278
|
+
const bind = this._frontend.getBindMessage(args.bind, false);
|
|
279
|
+
const data = Buffer.concat([
|
|
280
|
+
...before,
|
|
281
|
+
parse,
|
|
282
|
+
bind,
|
|
283
|
+
describe,
|
|
284
|
+
execute,
|
|
285
|
+
...after,
|
|
286
|
+
sync,
|
|
287
|
+
]);
|
|
270
288
|
return this._sendAndCapture(data, cb, 'sendExtendedQueryMessages', args);
|
|
271
289
|
}
|
|
272
290
|
/**
|
|
@@ -336,21 +354,22 @@ export class PgSocket extends SafeEventEmitter {
|
|
|
336
354
|
* auto-clears an unnamed portal at the next Bind naming it.
|
|
337
355
|
*/
|
|
338
356
|
sendBindExecuteMessages(args, cb) {
|
|
357
|
+
// See sendExtendedQueryMessages() for why the Bind is built last.
|
|
358
|
+
const execute = this._frontend.getExecuteMessage(args.execute);
|
|
359
|
+
const sync = this._frontend.getSyncMessage();
|
|
339
360
|
if (!args.before && !args.after) {
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
this._frontend.getExecuteMessage(args.execute),
|
|
343
|
-
this._frontend.getSyncMessage(),
|
|
344
|
-
];
|
|
361
|
+
const bind = this._frontend.getBindMessage(args.bind, false);
|
|
362
|
+
const data = Buffer.concat([bind, execute, sync]);
|
|
345
363
|
return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
|
|
346
364
|
}
|
|
347
|
-
const
|
|
365
|
+
const before = [];
|
|
348
366
|
if (args.before)
|
|
349
|
-
this._pushUtilityStatement(
|
|
350
|
-
|
|
367
|
+
this._pushUtilityStatement(before, args.before, args.bind);
|
|
368
|
+
const after = [];
|
|
351
369
|
if (args.after)
|
|
352
|
-
this._pushUtilityStatement(
|
|
353
|
-
|
|
370
|
+
this._pushUtilityStatement(after, args.after, args.bind);
|
|
371
|
+
const bind = this._frontend.getBindMessage(args.bind, false);
|
|
372
|
+
const data = Buffer.concat([...before, bind, execute, ...after, sync]);
|
|
354
373
|
return this._sendAndCapture(data, cb, 'sendBindExecuteMessages', args);
|
|
355
374
|
}
|
|
356
375
|
/**
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Maybe, OID } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* The types to encode a Bind's parameters against, given what the caller
|
|
4
|
+
* declared and what the server resolved.
|
|
5
|
+
*
|
|
6
|
+
* Returns `declared` unchanged unless some parameter can be improved, so
|
|
7
|
+
* the common call allocates nothing.
|
|
8
|
+
*
|
|
9
|
+
* @param declared What went into Parse - `0`/undefined where this client
|
|
10
|
+
* declared nothing and left the type to the server.
|
|
11
|
+
* @param params The values about to be written.
|
|
12
|
+
* @param resolved What the server answered for this statement, in order.
|
|
13
|
+
* @returns The types to hand `getBindMessage`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolvedBindTypes(declared: Maybe<Maybe<OID>[]>, params: Maybe<Maybe<any>[]>, resolved: Maybe<OID[]>): Maybe<Maybe<OID>[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DataTypeOIDs } from '../constants.js';
|
|
2
|
+
import { arrayLeaf } from './array-leaf.js';
|
|
3
|
+
/**
|
|
4
|
+
* The types a `Date` is allowed to be encoded into binary against, by
|
|
5
|
+
* whether the value is an array.
|
|
6
|
+
*/
|
|
7
|
+
/* Deliberately short. `time` and `timetz` are left out because
|
|
8
|
+
TimeType.encodeBinary() multiplies the whole epoch by 1000 and
|
|
9
|
+
overflows for any ordinary Date - measured, every one of four test
|
|
10
|
+
instants answered 22008 "time out of range", where the text path
|
|
11
|
+
stores the wall clock correctly. Nothing else here may be reached
|
|
12
|
+
until it has been checked the same way. */
|
|
13
|
+
const DATE_SCALARS = new Set([
|
|
14
|
+
DataTypeOIDs.timestamptz,
|
|
15
|
+
DataTypeOIDs.timestamp,
|
|
16
|
+
DataTypeOIDs.date,
|
|
17
|
+
]);
|
|
18
|
+
const DATE_ARRAYS = new Set([
|
|
19
|
+
DataTypeOIDs._timestamptz,
|
|
20
|
+
DataTypeOIDs._timestamp,
|
|
21
|
+
DataTypeOIDs._date,
|
|
22
|
+
]);
|
|
23
|
+
/**
|
|
24
|
+
* The types to encode a Bind's parameters against, given what the caller
|
|
25
|
+
* declared and what the server resolved.
|
|
26
|
+
*
|
|
27
|
+
* Returns `declared` unchanged unless some parameter can be improved, so
|
|
28
|
+
* the common call allocates nothing.
|
|
29
|
+
*
|
|
30
|
+
* @param declared What went into Parse - `0`/undefined where this client
|
|
31
|
+
* declared nothing and left the type to the server.
|
|
32
|
+
* @param params The values about to be written.
|
|
33
|
+
* @param resolved What the server answered for this statement, in order.
|
|
34
|
+
* @returns The types to hand `getBindMessage`.
|
|
35
|
+
*/
|
|
36
|
+
/* A Date goes out as text because nothing but the server knows whether
|
|
37
|
+
the column is an instant or a wall clock - see `isUnspecifiedParam()`.
|
|
38
|
+
Once the statement has a name, the server has already said which, and
|
|
39
|
+
saying it does not have to be repeated: a prepared statement's
|
|
40
|
+
parameter types are fixed at Parse, so a later Bind may send binary
|
|
41
|
+
against them with no re-Parse. Checked on the wire: Parse with no
|
|
42
|
+
declared types resolves `$1` to 1184, and a Bind of eight binary bytes
|
|
43
|
+
against that statement reads back correctly.
|
|
44
|
+
|
|
45
|
+
Worth it only because the text is wide and the binary is not. Measured
|
|
46
|
+
over a loopback connection, 20 000 elements:
|
|
47
|
+
|
|
48
|
+
timestamptz[] text 625 KB / 11.61ms binary 234 KB / 3.79ms
|
|
49
|
+
|
|
50
|
+
The same measurement is why this is not done for numbers: an int4[] of
|
|
51
|
+
ordinary values is 380 KB of text against 781 KB of binary, because
|
|
52
|
+
the binary array format carries a 4-byte length per element whatever
|
|
53
|
+
its magnitude - there, text is both smaller and twice as fast.
|
|
54
|
+
|
|
55
|
+
Binary and text store the same value, which is the thing that had to
|
|
56
|
+
be true before any of this: checked for timestamptz, timestamp and
|
|
57
|
+
date, over four instants spanning a DST boundary and including
|
|
58
|
+
milliseconds, and for the array forms of two of them. */
|
|
59
|
+
export function resolvedBindTypes(declared, params, resolved) {
|
|
60
|
+
if (!resolved || !params)
|
|
61
|
+
return declared;
|
|
62
|
+
const l = params.length;
|
|
63
|
+
let out;
|
|
64
|
+
let i;
|
|
65
|
+
for (i = 0; i < l; i++) {
|
|
66
|
+
// A type the caller named is an answer, not a guess - left alone.
|
|
67
|
+
if (declared?.[i])
|
|
68
|
+
continue;
|
|
69
|
+
const oid = resolved[i];
|
|
70
|
+
if (!oid)
|
|
71
|
+
continue;
|
|
72
|
+
const v = params[i];
|
|
73
|
+
if (!(arrayLeaf(v) instanceof Date))
|
|
74
|
+
continue;
|
|
75
|
+
// An array value needs an array type and a scalar needs a scalar
|
|
76
|
+
// one: getBindMessage() picks its encoder from the type alone and
|
|
77
|
+
// would hand an array to a scalar encoder.
|
|
78
|
+
if (!(Array.isArray(v) ? DATE_ARRAYS : DATE_SCALARS).has(oid))
|
|
79
|
+
continue;
|
|
80
|
+
if (!out)
|
|
81
|
+
out = declared ? declared.slice() : new Array(l).fill(0);
|
|
82
|
+
out[i] = oid;
|
|
83
|
+
}
|
|
84
|
+
return out || declared;
|
|
85
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { arrayCalculateDim } from './array-calculatedim.js';
|
|
2
2
|
import { arrayLeaf } from './array-leaf.js';
|
|
3
|
+
import { unspecifiedText } from './unspecified-value.js';
|
|
3
4
|
/**
|
|
4
5
|
* What a number's own text looks like: digits, one optional dot, an
|
|
5
6
|
* optional exponent, an optional leading minus. An element that matches
|
|
@@ -66,7 +67,11 @@ export function stringifyArrayLiteral(value, options, encode) {
|
|
|
66
67
|
const isNumber = typeof x === 'number' || typeof x === 'bigint';
|
|
67
68
|
if (encode)
|
|
68
69
|
x = encode(x, options || {});
|
|
69
|
-
|
|
70
|
+
// With an encoder the element's text is the type's to produce;
|
|
71
|
+
// without one this is an undeclared array, where a value that
|
|
72
|
+
// knows how to write itself does, and a plain object goes out as
|
|
73
|
+
// JSON rather than as `[object Object]`.
|
|
74
|
+
const s = encode ? '' + x : unspecifiedText(x);
|
|
70
75
|
out += isNumber && BARE_NUMBER.test(s) ? s : escapeArrayItem(s);
|
|
71
76
|
}
|
|
72
77
|
return out + '}';
|
|
@@ -241,7 +246,9 @@ export function writeArrayLiteral(io, value, options, encode) {
|
|
|
241
246
|
// of its spelling - the same rule the string path states: a
|
|
242
247
|
// string's own text is quoted whatever it looks like.
|
|
243
248
|
const isNumber = typeof x === 'number' || typeof x === 'bigint';
|
|
244
|
-
|
|
249
|
+
// This lane is only entered with no element encoder, so the same
|
|
250
|
+
// rules the string path applies without one apply here.
|
|
251
|
+
s = unspecifiedText(x);
|
|
245
252
|
const bare = isNumber && BARE_NUMBER.test(s);
|
|
246
253
|
if (!bare)
|
|
247
254
|
s =
|
|
@@ -30,6 +30,9 @@
|
|
|
30
30
|
* bigint[] = integer[]`, and `numeric[] = double precision[]` the
|
|
31
31
|
* same. The scalars are left declared because they do have those
|
|
32
32
|
* operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
|
|
33
|
+
* That is true of the operators *inside* the numeric family and not
|
|
34
|
+
* of the casts out of it, which is what `needsNumericParam()` below
|
|
35
|
+
* exists for.
|
|
33
36
|
*
|
|
34
37
|
* A scalar number, a boolean, a Buffer, and an array of anything but
|
|
35
38
|
* numbers keep their declared types: an array of booleans, of Buffers
|
|
@@ -64,3 +67,14 @@
|
|
|
64
67
|
* answer rather than a guess.
|
|
65
68
|
*/
|
|
66
69
|
export declare function isUnspecifiedParam(v: any): boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether a number must be declared `numeric` rather than the `float8`
|
|
72
|
+
* its value suggests.
|
|
73
|
+
*
|
|
74
|
+
* True for a finite non-integer, false for everything else - an integer,
|
|
75
|
+
* a bigint, a non-finite number, and anything that is not a number.
|
|
76
|
+
*
|
|
77
|
+
* @param v The value a parameter is about to be declared for.
|
|
78
|
+
* @returns Whether to declare `numeric` instead of asking `determine()`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function needsNumericParam(v: any): boolean;
|
|
@@ -31,6 +31,9 @@ import { arrayLeaf } from './array-leaf.js';
|
|
|
31
31
|
* bigint[] = integer[]`, and `numeric[] = double precision[]` the
|
|
32
32
|
* same. The scalars are left declared because they do have those
|
|
33
33
|
* operators - `1::int8 = $1` and `1.5::numeric = $1` both resolve.
|
|
34
|
+
* That is true of the operators *inside* the numeric family and not
|
|
35
|
+
* of the casts out of it, which is what `needsNumericParam()` below
|
|
36
|
+
* exists for.
|
|
34
37
|
*
|
|
35
38
|
* A scalar number, a boolean, a Buffer, and an array of anything but
|
|
36
39
|
* numbers keep their declared types: an array of booleans, of Buffers
|
|
@@ -70,3 +73,40 @@ export function isUnspecifiedParam(v) {
|
|
|
70
73
|
return true;
|
|
71
74
|
return Array.isArray(v) && (typeof x === 'number' || typeof x === 'bigint');
|
|
72
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Whether a number must be declared `numeric` rather than the `float8`
|
|
78
|
+
* its value suggests.
|
|
79
|
+
*
|
|
80
|
+
* True for a finite non-integer, false for everything else - an integer,
|
|
81
|
+
* a bigint, a non-finite number, and anything that is not a number.
|
|
82
|
+
*
|
|
83
|
+
* @param v The value a parameter is about to be declared for.
|
|
84
|
+
* @returns Whether to declare `numeric` instead of asking `determine()`.
|
|
85
|
+
*/
|
|
86
|
+
/* `determine()` answers by value, so 12 is int4 and 12.34 is float8 -
|
|
87
|
+
and `int4 -> money` is an assignment cast while `float8 -> money` has
|
|
88
|
+
no pg_cast row at all. So `select ($1::money)::text` worked with 12
|
|
89
|
+
and failed 42846 with 12.34: the same SQL, the same column, decided by
|
|
90
|
+
whether the amount happened to have a fractional part, which for money
|
|
91
|
+
it usually does. A test written with 12 passed and production failed
|
|
92
|
+
on 12.34. `numeric -> money` is an assignment cast, which an explicit
|
|
93
|
+
`::money` accepts, and `numeric -> float8` is implicit, so a float8
|
|
94
|
+
column is unaffected.
|
|
95
|
+
|
|
96
|
+
Measured, inserting into a float8 column over a loopback connection,
|
|
97
|
+
three interleaved rounds of 3000: 0.197ms a call against float8's
|
|
98
|
+
0.198, and 51 bytes on the socket against 47. Round-tripping a double
|
|
99
|
+
through numeric is exact - checked against float8 for 0.1, 5e-324,
|
|
100
|
+
1.7976931348623157e308, 0.30000000000000004 and pi, among others.
|
|
101
|
+
|
|
102
|
+
Non-finite values keep float8 deliberately: `numeric` only grew
|
|
103
|
+
Infinity in PostgreSQL 14, and NaN would be the only one of the three
|
|
104
|
+
that worked on 12 and 13.
|
|
105
|
+
|
|
106
|
+
This does not close `$1::interval` from a number, which fails the same
|
|
107
|
+
way - there is no cast to interval from any numeric type, so only an
|
|
108
|
+
undeclared parameter reaches it. An integer scalar is left declared,
|
|
109
|
+
and `new BindParam(0, 5)` is the way to say it. */
|
|
110
|
+
export function needsNumericParam(v) {
|
|
111
|
+
return typeof v === 'number' && Number.isFinite(v) && !Number.isInteger(v);
|
|
112
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follows a value's `toPostgres()` until it reaches one that has none.
|
|
3
|
+
*
|
|
4
|
+
* `toPostgres()` is `pg`'s extension point for a type that knows how to
|
|
5
|
+
* write itself: a library with its own geometry, money or domain class
|
|
6
|
+
* implements it, and the value goes out as that class says rather than
|
|
7
|
+
* as whatever `String()` makes of it. The result is followed rather than
|
|
8
|
+
* returned, because what it hands back may itself be a `Date`, an array,
|
|
9
|
+
* a `Buffer` or another such value.
|
|
10
|
+
*
|
|
11
|
+
* @param v The value a parameter is about to be written from.
|
|
12
|
+
* @returns The value to write, which is `v` itself unless it had one.
|
|
13
|
+
* @throws TypeError if the values form a cycle.
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveToPostgres(v: any): any;
|
|
16
|
+
/**
|
|
17
|
+
* The text an undeclared parameter - or one element of an undeclared
|
|
18
|
+
* array - goes out as.
|
|
19
|
+
*
|
|
20
|
+
* Follows `toPostgres()` first, then writes a plain object as JSON and
|
|
21
|
+
* anything else as its own text.
|
|
22
|
+
*
|
|
23
|
+
* @param v The value to render.
|
|
24
|
+
* @returns What to put on the wire, before any array quoting.
|
|
25
|
+
*/
|
|
26
|
+
export declare function unspecifiedText(v: any): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follows a value's `toPostgres()` until it reaches one that has none.
|
|
3
|
+
*
|
|
4
|
+
* `toPostgres()` is `pg`'s extension point for a type that knows how to
|
|
5
|
+
* write itself: a library with its own geometry, money or domain class
|
|
6
|
+
* implements it, and the value goes out as that class says rather than
|
|
7
|
+
* as whatever `String()` makes of it. The result is followed rather than
|
|
8
|
+
* returned, because what it hands back may itself be a `Date`, an array,
|
|
9
|
+
* a `Buffer` or another such value.
|
|
10
|
+
*
|
|
11
|
+
* @param v The value a parameter is about to be written from.
|
|
12
|
+
* @returns The value to write, which is `v` itself unless it had one.
|
|
13
|
+
* @throws TypeError if the values form a cycle.
|
|
14
|
+
*/
|
|
15
|
+
/* Only for a parameter with no declared type. A declared type owns its
|
|
16
|
+
own encoding and is an answer the caller gave, not a guess.
|
|
17
|
+
|
|
18
|
+
This client's own classes are unaffected: every one of their
|
|
19
|
+
`toPostgres()` is `return this.toString()`, which is what the path
|
|
20
|
+
below used to reach anyway. What changes is a caller's class, which
|
|
21
|
+
used to be written `[object Object]` - silently, with a row written
|
|
22
|
+
and no error. */
|
|
23
|
+
export function resolveToPostgres(v) {
|
|
24
|
+
if (v === null || typeof v !== 'object' || typeof v.toPostgres !== 'function')
|
|
25
|
+
return v;
|
|
26
|
+
const seen = [];
|
|
27
|
+
while (v !== null &&
|
|
28
|
+
typeof v === 'object' &&
|
|
29
|
+
typeof v.toPostgres === 'function') {
|
|
30
|
+
if (seen.includes(v))
|
|
31
|
+
throw new TypeError('Circular reference while preparing a parameter: toPostgres() returned a value that leads back to itself');
|
|
32
|
+
seen.push(v);
|
|
33
|
+
v = v.toPostgres();
|
|
34
|
+
}
|
|
35
|
+
return v;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The text an undeclared parameter - or one element of an undeclared
|
|
39
|
+
* array - goes out as.
|
|
40
|
+
*
|
|
41
|
+
* Follows `toPostgres()` first, then writes a plain object as JSON and
|
|
42
|
+
* anything else as its own text.
|
|
43
|
+
*
|
|
44
|
+
* @param v The value to render.
|
|
45
|
+
* @returns What to put on the wire, before any array quoting.
|
|
46
|
+
*/
|
|
47
|
+
/* The two rules `pg`'s `prepareValue` has that this path did not. The
|
|
48
|
+
rest of its rules are either already here in their own branch - a
|
|
49
|
+
Date, an array, a Buffer - or deliberately not taken: it writes a Date
|
|
50
|
+
with a `T` where this client writes a space, and quotes the elements
|
|
51
|
+
of a number array where this client writes them bare, which is 10-26%
|
|
52
|
+
smaller and parses identically. Those are spellings the server reads
|
|
53
|
+
the same way; these two are a value that arrives as something else. */
|
|
54
|
+
export function unspecifiedText(v) {
|
|
55
|
+
v = resolveToPostgres(v);
|
|
56
|
+
if (v !== null &&
|
|
57
|
+
typeof v === 'object' &&
|
|
58
|
+
!(v instanceof Date) &&
|
|
59
|
+
!Buffer.isBuffer(v) &&
|
|
60
|
+
!Array.isArray(v))
|
|
61
|
+
return JSON.stringify(v);
|
|
62
|
+
return '' + v;
|
|
63
|
+
}
|