postgrejs 3.5.0 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -1
- package/README.md +22 -4
- package/connection/connection.js +13 -7
- package/connection/cursor.js +22 -1
- package/connection/intl-connection.d.ts +1 -1
- package/connection/intl-connection.js +110 -41
- package/connection/pool.d.ts +22 -0
- package/connection/pool.js +73 -9
- package/connection/portal.d.ts +1 -1
- package/connection/portal.js +3 -2
- package/connection/prepared-statement.js +8 -4
- package/data-type-map.d.ts +11 -0
- package/data-type-map.js +20 -2
- package/data-types/char-type.js +15 -0
- package/data-types/date-type.js +4 -18
- package/data-types/float4-type.js +79 -1
- package/data-types/json-type.js +7 -12
- package/data-types/jsonb-type.js +7 -12
- package/data-types/time-type.js +2 -15
- package/data-types/timestamp-type.js +8 -29
- package/data-types/timestamptz-type.js +7 -31
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/interfaces/batch-result.d.ts +1 -1
- package/interfaces/command-result.d.ts +21 -1
- package/interfaces/data-mapping-options.d.ts +16 -0
- package/interfaces/data-type.d.ts +11 -0
- package/interfaces/query-options.d.ts +25 -3
- package/package.json +1 -1
- package/protocol/connection-lost-error.d.ts +40 -0
- package/protocol/connection-lost-error.js +54 -0
- package/protocol/frontend.d.ts +7 -0
- package/protocol/frontend.js +5 -3
- package/protocol/pg-socket.d.ts +7 -0
- package/protocol/pg-socket.js +26 -2
- package/util/channel-name.d.ts +24 -0
- package/util/channel-name.js +44 -0
- package/util/get-parsers.d.ts +2 -1
- package/util/get-parsers.js +16 -1
- package/util/resolve-column-formats.d.ts +33 -0
- package/util/resolve-column-formats.js +67 -0
- package/util/wrap-row-description.d.ts +2 -1
- package/util/wrap-row-description.js +7 -1
package/connection/pool.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Pool as LightningPool, } from 'lightning-pool';
|
|
|
2
2
|
import { coerceToBoolean, coerceToInt } from 'putil-varhelpers';
|
|
3
3
|
import { ConnectionState } from '../constants.js';
|
|
4
4
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
5
|
+
import { normalizeChannelName } from '../util/channel-name.js';
|
|
5
6
|
import { getConnectionConfig } from '../util/connection-config.js';
|
|
6
7
|
import { QueryRequest } from '../util/sql-tag.js';
|
|
7
8
|
import { startsCopy, startsTransaction } from '../util/starts-transaction.js';
|
|
@@ -12,6 +13,14 @@ export class Pool extends SafeEventEmitter {
|
|
|
12
13
|
_notificationListeners = new SafeEventEmitter();
|
|
13
14
|
_notificationConnection;
|
|
14
15
|
_pipelineSlots = [];
|
|
16
|
+
/**
|
|
17
|
+
* Why a connection is about to be destroyed, for the ones that were not
|
|
18
|
+
* destroyed on purpose. lightning-pool's own 'destroy' carries the
|
|
19
|
+
* resource and nothing else, so the reason is recorded on the way in -
|
|
20
|
+
* when the connection reports its close - and read back out when the
|
|
21
|
+
* pool reports the removal.
|
|
22
|
+
*/
|
|
23
|
+
_destroyReasons = new WeakMap();
|
|
15
24
|
_pipelineMaxQueries;
|
|
16
25
|
_pipelineMaxConnections;
|
|
17
26
|
config;
|
|
@@ -45,7 +54,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
45
54
|
/* c8 ignore stop */
|
|
46
55
|
const intlCon = new IntlConnection(cfg);
|
|
47
56
|
await intlCon.connect();
|
|
48
|
-
intlCon.on('close', () => this.
|
|
57
|
+
intlCon.on('close', (reason) => this._onConnectionClosed(intlCon, reason));
|
|
49
58
|
/* c8 ignore start */
|
|
50
59
|
if (this.listenerCount('debug')) {
|
|
51
60
|
this.emit('debug', {
|
|
@@ -81,7 +90,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
81
90
|
/* c8 ignore stop */
|
|
82
91
|
intlCon.owner = undefined;
|
|
83
92
|
intlCon.removeAllListeners();
|
|
84
|
-
intlCon.once('close', () => this.
|
|
93
|
+
intlCon.once('close', (reason) => this._onConnectionClosed(intlCon, reason));
|
|
85
94
|
intlCon._refCount = 0;
|
|
86
95
|
},
|
|
87
96
|
validate: async (intlCon) => {
|
|
@@ -103,7 +112,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
103
112
|
this._pool.on('return', (...args) => this.emit('release', ...args));
|
|
104
113
|
this._pool.on('error', (...args) => this.emit('error', ...args));
|
|
105
114
|
this._pool.on('acquire', (...args) => this.emit('acquire', ...args));
|
|
106
|
-
this._pool.on('destroy', (
|
|
115
|
+
this._pool.on('destroy', (intlCon) => this._onConnectionDestroyed(intlCon));
|
|
107
116
|
}
|
|
108
117
|
/**
|
|
109
118
|
* Returns the number of connections that are currently acquired
|
|
@@ -205,12 +214,32 @@ export class Pool extends SafeEventEmitter {
|
|
|
205
214
|
: undefined;
|
|
206
215
|
if (!slot) {
|
|
207
216
|
const connection = await this.acquire();
|
|
217
|
+
let result;
|
|
208
218
|
try {
|
|
209
|
-
|
|
219
|
+
result = await connection.query(sql, options);
|
|
210
220
|
}
|
|
211
|
-
|
|
221
|
+
catch (e) {
|
|
212
222
|
await this.release(connection);
|
|
223
|
+
throw e;
|
|
224
|
+
}
|
|
225
|
+
// A portal lives only as long as the transaction it was created in,
|
|
226
|
+
// and outside an explicit one that is the implicit transaction any
|
|
227
|
+
// other statement's Sync ends. So a connection carrying an open
|
|
228
|
+
// cursor cannot go back in the pool when this call returns - handing
|
|
229
|
+
// it to the next caller would destroy the cursor's portal, and did
|
|
230
|
+
// ("portal ... does not exist", nondeterministically, depending on
|
|
231
|
+
// which connection that caller happened to get). It goes back when
|
|
232
|
+
// the cursor closes instead, which for-await and `await using` both
|
|
233
|
+
// do on their own; a cursor that is never closed holds its
|
|
234
|
+
// connection until the pool itself is closed.
|
|
235
|
+
if (result.cursor) {
|
|
236
|
+
result.cursor.once('close', () => {
|
|
237
|
+
this.release(connection).catch(e => this.emit('error', e));
|
|
238
|
+
});
|
|
239
|
+
return result;
|
|
213
240
|
}
|
|
241
|
+
await this.release(connection);
|
|
242
|
+
return result;
|
|
214
243
|
}
|
|
215
244
|
try {
|
|
216
245
|
const shared = slot.connection || (await slot.promise);
|
|
@@ -256,8 +285,10 @@ export class Pool extends SafeEventEmitter {
|
|
|
256
285
|
return this._pool.releaseAsync(getIntlConnection(connection));
|
|
257
286
|
}
|
|
258
287
|
async listen(channel, callback) {
|
|
259
|
-
|
|
260
|
-
|
|
288
|
+
// Folded here as well as in Connection.listen(): this emitter's keys
|
|
289
|
+
// have to be the same strings the inner connection registers, or
|
|
290
|
+
// unListen() would miss them.
|
|
291
|
+
channel = normalizeChannelName(channel);
|
|
261
292
|
// Bug: _initNotificationConnection() only ever bootstraps the shared
|
|
262
293
|
// connection and registers every channel known at that moment - it
|
|
263
294
|
// returns immediately once that connection already exists, so a second,
|
|
@@ -274,8 +305,7 @@ export class Pool extends SafeEventEmitter {
|
|
|
274
305
|
}
|
|
275
306
|
}
|
|
276
307
|
async unListen(channel) {
|
|
277
|
-
|
|
278
|
-
throw new TypeError(`Invalid channel name`);
|
|
308
|
+
channel = normalizeChannelName(channel);
|
|
279
309
|
this._notificationListeners.removeAllListeners(channel);
|
|
280
310
|
if (!this._notificationListeners.eventNames().length) {
|
|
281
311
|
await this.unListenAll();
|
|
@@ -291,6 +321,40 @@ export class Pool extends SafeEventEmitter {
|
|
|
291
321
|
await conn.close();
|
|
292
322
|
}
|
|
293
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* A pooled connection reported that its socket closed. `reason` is set
|
|
326
|
+
* only when that was not asked for - see PgSocket._handleClose().
|
|
327
|
+
*/
|
|
328
|
+
_onConnectionClosed(intlCon, reason) {
|
|
329
|
+
if (reason)
|
|
330
|
+
this._destroyReasons.set(intlCon, reason);
|
|
331
|
+
this._pool.destroy(intlCon);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* The pool has removed a connection. `destroy` fires the same way for an
|
|
335
|
+
* ordinary eviction (idle timeout, close(), a failed validate) as for a
|
|
336
|
+
* connection that died, so the reason is what tells them apart - an
|
|
337
|
+
* admin kill, a failover and a network fault were otherwise
|
|
338
|
+
* indistinguishable from a timeout, with nothing at all reported when
|
|
339
|
+
* the connection had no query in flight to reject.
|
|
340
|
+
*/
|
|
341
|
+
_onConnectionDestroyed(intlCon) {
|
|
342
|
+
const reason = this._destroyReasons.get(intlCon);
|
|
343
|
+
if (reason)
|
|
344
|
+
this._destroyReasons.delete(intlCon);
|
|
345
|
+
this.emit('destroy', intlCon, reason);
|
|
346
|
+
if (!reason)
|
|
347
|
+
return;
|
|
348
|
+
// Reported on 'error' as well, which is where pg puts a pooled
|
|
349
|
+
// connection dying and where code ported from it listens. Safe to do
|
|
350
|
+
// unconditionally here: SafeEventEmitter drops an 'error' that has no
|
|
351
|
+
// listener instead of throwing, so this cannot turn a recovery the
|
|
352
|
+
// pool already made into a crashed process. The error names the pid
|
|
353
|
+
// and carries the socket error as its cause; no second argument, so
|
|
354
|
+
// the existing `(err, meta)` shape lightning-pool emits for a
|
|
355
|
+
// connection that could not be created stays unambiguous.
|
|
356
|
+
this.emit('error', reason);
|
|
357
|
+
}
|
|
294
358
|
_canPipeline(pipeline, sql, autoCommit, signal) {
|
|
295
359
|
return (pipeline === true &&
|
|
296
360
|
!signal &&
|
package/connection/portal.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare class Portal {
|
|
|
23
23
|
* no explicit BEGIN left transactionStatus 'I' and a later Execute on the
|
|
24
24
|
* same named portal failed with "portal ... does not exist").
|
|
25
25
|
*/
|
|
26
|
-
bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions): Promise<Protocol.RowDescription[]>;
|
|
26
|
+
bindAndRetrieveFields(params: Maybe<any[]>, queryOptions: QueryOptions, columnFormat?: Protocol.DataFormat | Protocol.DataFormat[]): Promise<Protocol.RowDescription[]>;
|
|
27
27
|
execute(fetchCount?: number): Promise<PortalExecuteResult>;
|
|
28
28
|
/**
|
|
29
29
|
* Close + Sync as one request (see PgSocket.sendCloseAndSyncMessages()
|
package/connection/portal.js
CHANGED
|
@@ -22,7 +22,7 @@ export class Portal {
|
|
|
22
22
|
* no explicit BEGIN left transactionStatus 'I' and a later Execute on the
|
|
23
23
|
* same named portal failed with "portal ... does not exist").
|
|
24
24
|
*/
|
|
25
|
-
async bindAndRetrieveFields(params, queryOptions) {
|
|
25
|
+
async bindAndRetrieveFields(params, queryOptions, columnFormat) {
|
|
26
26
|
const intoCon = getIntlConnection(this.connection);
|
|
27
27
|
intoCon.ref();
|
|
28
28
|
try {
|
|
@@ -35,6 +35,7 @@ export class Portal {
|
|
|
35
35
|
paramTypes: this._statement.paramTypes,
|
|
36
36
|
params,
|
|
37
37
|
queryOptions,
|
|
38
|
+
columnFormat,
|
|
38
39
|
},
|
|
39
40
|
describe: { type: 'P', name: this.name },
|
|
40
41
|
}, (code, msg, done) => {
|
|
@@ -68,7 +69,7 @@ export class Portal {
|
|
|
68
69
|
const rows = [];
|
|
69
70
|
const executePromise = socket.sendExecuteMessage({
|
|
70
71
|
portal: this.name,
|
|
71
|
-
fetchCount: fetchCount
|
|
72
|
+
fetchCount: fetchCount ?? 100,
|
|
72
73
|
}, (code, msg, done) => {
|
|
73
74
|
switch (code) {
|
|
74
75
|
case Protocol.BackendMessageCode.NoticeResponse:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DEFAULT_COLUMN_FORMAT } from '../constants.js';
|
|
2
1
|
import { GlobalTypeMap } from '../data-type-map.js';
|
|
3
2
|
import { Protocol } from '../protocol/protocol.js';
|
|
4
3
|
import { SafeEventEmitter } from '../safe-event-emitter.js';
|
|
5
4
|
import { withAbortSignal } from '../util/abort-signal.js';
|
|
6
5
|
import { getParsers } from '../util/get-parsers.js';
|
|
6
|
+
import { resolveColumnFormats } from '../util/resolve-column-formats.js';
|
|
7
7
|
import { resolveRowType } from '../util/row-decoder.js';
|
|
8
8
|
import { wrapRowDescription } from '../util/wrap-row-description.js';
|
|
9
9
|
import { Cursor } from './cursor.js';
|
|
@@ -208,10 +208,14 @@ export class PreparedStatement extends SafeEventEmitter {
|
|
|
208
208
|
intlCon.ref();
|
|
209
209
|
let portal = new Portal(this, 'P_' + ++portalCounter);
|
|
210
210
|
try {
|
|
211
|
-
|
|
211
|
+
// prepare() already fetched this statement's RowDescription, so
|
|
212
|
+
// fetchAsString's OID list can become per-column result format
|
|
213
|
+
// codes on the Bind below with no round trip of its own.
|
|
214
|
+
const columnFormat = resolveColumnFormats(this._fields, options);
|
|
215
|
+
const fields = await portal.bindAndRetrieveFields(options.params, options, columnFormat);
|
|
212
216
|
const typeMap = options.typeMap || GlobalTypeMap;
|
|
213
|
-
const parsers = getParsers(typeMap, fields);
|
|
214
|
-
const resultFields = wrapRowDescription(typeMap, fields,
|
|
217
|
+
const parsers = getParsers(typeMap, fields, options);
|
|
218
|
+
const resultFields = wrapRowDescription(typeMap, fields, columnFormat, options);
|
|
215
219
|
const result = {
|
|
216
220
|
command: undefined,
|
|
217
221
|
fields: resultFields,
|
package/data-type-map.d.ts
CHANGED
|
@@ -3,6 +3,17 @@ import type { OID } from './types.js';
|
|
|
3
3
|
export declare class DataTypeMap {
|
|
4
4
|
private _itemsByOID;
|
|
5
5
|
private _items;
|
|
6
|
+
/**
|
|
7
|
+
* Copies another map, so a connection can start from GlobalTypeMap and
|
|
8
|
+
* override a type or two without touching the global one.
|
|
9
|
+
*
|
|
10
|
+
* Both indexes have to be copied. `_items` alone is what `determine()`
|
|
11
|
+
* walks, but every decode goes through `get()`, which reads
|
|
12
|
+
* `_itemsByOID` - a copy carrying only the first one answers `undefined`
|
|
13
|
+
* for every OID, and get-parsers.ts then falls back to its default
|
|
14
|
+
* parser, handing the caller raw Buffers instead of decoded values with
|
|
15
|
+
* nothing reported.
|
|
16
|
+
*/
|
|
6
17
|
constructor(other?: DataTypeMap);
|
|
7
18
|
get(oid: OID): DataType;
|
|
8
19
|
register(dataTypes: DataType | DataType[]): void;
|
package/data-type-map.js
CHANGED
|
@@ -26,9 +26,22 @@ import { ArrayVarcharType, VarcharType } from './data-types/varchar-type.js';
|
|
|
26
26
|
export class DataTypeMap {
|
|
27
27
|
_itemsByOID = {};
|
|
28
28
|
_items = [];
|
|
29
|
+
/**
|
|
30
|
+
* Copies another map, so a connection can start from GlobalTypeMap and
|
|
31
|
+
* override a type or two without touching the global one.
|
|
32
|
+
*
|
|
33
|
+
* Both indexes have to be copied. `_items` alone is what `determine()`
|
|
34
|
+
* walks, but every decode goes through `get()`, which reads
|
|
35
|
+
* `_itemsByOID` - a copy carrying only the first one answers `undefined`
|
|
36
|
+
* for every OID, and get-parsers.ts then falls back to its default
|
|
37
|
+
* parser, handing the caller raw Buffers instead of decoded values with
|
|
38
|
+
* nothing reported.
|
|
39
|
+
*/
|
|
29
40
|
constructor(other) {
|
|
30
|
-
if (other instanceof DataTypeMap)
|
|
31
|
-
|
|
41
|
+
if (other instanceof DataTypeMap) {
|
|
42
|
+
this._items = [...other._items];
|
|
43
|
+
this._itemsByOID = { ...other._itemsByOID };
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
get(oid) {
|
|
34
47
|
return this._itemsByOID[oid];
|
|
@@ -52,6 +65,11 @@ export class DataTypeMap {
|
|
|
52
65
|
let t;
|
|
53
66
|
for (i = this._items.length - 1; i >= 0; i--) {
|
|
54
67
|
t = this._items[i];
|
|
68
|
+
// Walked newest-registered first, so a later registration of the
|
|
69
|
+
// same OID wins - and a type marked `inferrable: false` is never
|
|
70
|
+
// picked here at all, however well `isType` matches.
|
|
71
|
+
if (t.inferrable === false)
|
|
72
|
+
continue;
|
|
55
73
|
if (valueIsArray) {
|
|
56
74
|
if (t.elementsOID && t.isType(value[0]))
|
|
57
75
|
return t.oid;
|
package/data-types/char-type.js
CHANGED
|
@@ -21,6 +21,21 @@ export const CharType = {
|
|
|
21
21
|
isType(v) {
|
|
22
22
|
return typeof v === 'string' && Buffer.byteLength(v, 'utf8') === 1;
|
|
23
23
|
},
|
|
24
|
+
/**
|
|
25
|
+
* 18 is PostgreSQL's internal single-byte `"char"`, not `char(n)` and
|
|
26
|
+
* not `varchar`. A one-character JavaScript string fits it, which is why
|
|
27
|
+
* `isType` says so, but nobody passing `'A'` to a query means that type:
|
|
28
|
+
* inference used to answer 18 for every such value and declare the
|
|
29
|
+
* parameter `"char"` to the server.
|
|
30
|
+
*
|
|
31
|
+
* Comparisons and inserts survived on an implicit cast, but concatenation
|
|
32
|
+
* had no single obvious one - `select code || $1` answered `operator is
|
|
33
|
+
* not unique: character varying || "char"` - and an array was worse
|
|
34
|
+
* still. determine() types an array from its first element alone, so
|
|
35
|
+
* `['A', 'BB', 'CCC']` became `_char` and came back `['A', 'B', 'C']`,
|
|
36
|
+
* every later element cut to one byte with nothing reported.
|
|
37
|
+
*/
|
|
38
|
+
inferrable: false,
|
|
24
39
|
};
|
|
25
40
|
export const ArrayCharType = {
|
|
26
41
|
...CharType,
|
package/data-types/date-type.js
CHANGED
|
@@ -30,22 +30,18 @@ export const DateType = {
|
|
|
30
30
|
buf.writeInt32BE(t);
|
|
31
31
|
},
|
|
32
32
|
decodeBinary(v, offset = 0, _len, options) {
|
|
33
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.date);
|
|
34
33
|
const t = v.readInt32BE(offset);
|
|
35
34
|
if (t === 0x7fffffff)
|
|
36
|
-
return
|
|
35
|
+
return Infinity;
|
|
37
36
|
if (t === -0x80000000)
|
|
38
|
-
return
|
|
37
|
+
return -Infinity;
|
|
39
38
|
// Shift from 2000 to 1970
|
|
40
39
|
let d = new Date(t * 1000 * 86400 + timeShift);
|
|
41
|
-
if (
|
|
40
|
+
if (!options.utcDates)
|
|
42
41
|
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
43
|
-
return
|
|
42
|
+
return d;
|
|
44
43
|
},
|
|
45
44
|
decodeText(v, options) {
|
|
46
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.date);
|
|
47
|
-
if (fetchAsString)
|
|
48
|
-
return v;
|
|
49
45
|
return parseDate(v, options.utcDates);
|
|
50
46
|
},
|
|
51
47
|
decodeTextBuffer(buf, offset, len, options) {
|
|
@@ -59,16 +55,6 @@ export const DateType = {
|
|
|
59
55
|
v.getMilliseconds() === 0);
|
|
60
56
|
},
|
|
61
57
|
};
|
|
62
|
-
function padZero(v) {
|
|
63
|
-
return v < 9 ? '0' + v : '' + v;
|
|
64
|
-
}
|
|
65
|
-
function dateToDateString(d) {
|
|
66
|
-
return (d.getFullYear() +
|
|
67
|
-
'-' +
|
|
68
|
-
padZero(d.getMonth() + 1) +
|
|
69
|
-
'-' +
|
|
70
|
-
padZero(d.getDate()));
|
|
71
|
-
}
|
|
72
58
|
export const ArrayDateType = {
|
|
73
59
|
...DateType,
|
|
74
60
|
name: '_date',
|
|
@@ -12,7 +12,7 @@ export const Float4Type = {
|
|
|
12
12
|
buf.writeFloatBE(typeof v === 'number' ? v : assertCoercedNumber(parseFloat(v), 'float4'));
|
|
13
13
|
},
|
|
14
14
|
decodeBinary(v, offset = 0) {
|
|
15
|
-
return v.readFloatBE(offset);
|
|
15
|
+
return shortestFloat4(v.readFloatBE(offset));
|
|
16
16
|
},
|
|
17
17
|
decodeText: parseFloat,
|
|
18
18
|
decodeTextBuffer: fastParseFloatBuffer,
|
|
@@ -26,3 +26,81 @@ export const ArrayFloat4Type = {
|
|
|
26
26
|
oid: DataTypeOIDs._float4,
|
|
27
27
|
elementsOID: DataTypeOIDs.float4,
|
|
28
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* Powers of ten that a double holds exactly. Past 1e22 the literal is
|
|
31
|
+
* already rounded, and scaling by it stops being a faithful operation.
|
|
32
|
+
*/
|
|
33
|
+
const POW10 = [
|
|
34
|
+
1, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14,
|
|
35
|
+
1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22,
|
|
36
|
+
];
|
|
37
|
+
/**
|
|
38
|
+
* The shortest decimal that reads back as this same float4.
|
|
39
|
+
*
|
|
40
|
+
* A float4 arrives as four bytes and widens into a double that spells out
|
|
41
|
+
* the binary approximation in full: the column PostgreSQL prints as `1.1`
|
|
42
|
+
* read back as `1.100000023841858`. The text path never had the problem,
|
|
43
|
+
* since the server does this itself before sending - so the same column
|
|
44
|
+
* answered differently depending on a wire format the caller may not know
|
|
45
|
+
* is there, and the binary default was the wrong-looking one.
|
|
46
|
+
*
|
|
47
|
+
* Nine significant digits always round-trip a binary32 and most values
|
|
48
|
+
* need far fewer, so this tries successively longer forms and takes the
|
|
49
|
+
* first that reads back unchanged. Integers inside +-2^24 are already
|
|
50
|
+
* exact in float4 and skip the search; past that they are not (1e20
|
|
51
|
+
* widens to 100000002004087730000) so the bound matters. Infinities and
|
|
52
|
+
* NaN have no decimal to shorten.
|
|
53
|
+
*
|
|
54
|
+
* Rounding by hand, rather than through toPrecision(), is what makes it
|
|
55
|
+
* affordable - 24ns a value against 115ns, measured, because toPrecision()
|
|
56
|
+
* costs four times what the engine's own shortest-form toString does and
|
|
57
|
+
* there is no cheaper built-in. Scaling by a power of ten is only faithful
|
|
58
|
+
* while both the digits and the power are exact in a double: the digits
|
|
59
|
+
* always are at nine or fewer, and the magnitude check keeps the power
|
|
60
|
+
* inside its own exact range. A value sitting exactly halfway is left to
|
|
61
|
+
* the string path as well - Math.round() breaks those ties away from zero
|
|
62
|
+
* and dtoa breaks them to even, and the disagreement would show. Checked
|
|
63
|
+
* against the string path over 299,007 values, structured and random:
|
|
64
|
+
* same answer every time.
|
|
65
|
+
*/
|
|
66
|
+
function shortestFloat4(n) {
|
|
67
|
+
if (!Number.isFinite(n))
|
|
68
|
+
return n;
|
|
69
|
+
if (Number.isInteger(n) && n >= -16777216 && n <= 16777216)
|
|
70
|
+
return n;
|
|
71
|
+
const abs = n < 0 ? -n : n;
|
|
72
|
+
// Two comparisons keep log10 and the loop off the path entirely for
|
|
73
|
+
// magnitudes whose power of ten could not be exact anyway.
|
|
74
|
+
if (abs >= 1e-14 && abs < 1e28) {
|
|
75
|
+
const e = Math.floor(Math.log10(abs));
|
|
76
|
+
let digits;
|
|
77
|
+
let k;
|
|
78
|
+
let pow;
|
|
79
|
+
let scaled;
|
|
80
|
+
let rounded;
|
|
81
|
+
let candidate;
|
|
82
|
+
for (digits = 7; digits <= 9; digits++) {
|
|
83
|
+
k = digits - 1 - e;
|
|
84
|
+
// Unreachable for the magnitudes let through above unless log10()
|
|
85
|
+
// lands a step out at a boundary, which it is entitled to do.
|
|
86
|
+
if (k < -22 || k > 22)
|
|
87
|
+
break;
|
|
88
|
+
pow = POW10[k < 0 ? -k : k];
|
|
89
|
+
scaled = k >= 0 ? n * pow : n / pow;
|
|
90
|
+
rounded = Math.round(scaled);
|
|
91
|
+
if (scaled - rounded === 0.5 || rounded - scaled === 0.5)
|
|
92
|
+
break;
|
|
93
|
+
candidate = k >= 0 ? rounded / pow : rounded * pow;
|
|
94
|
+
if (Math.fround(candidate) === n)
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
let p;
|
|
99
|
+
let s;
|
|
100
|
+
for (p = 7; p <= 9; p++) {
|
|
101
|
+
s = Number(n.toPrecision(p));
|
|
102
|
+
if (Math.fround(s) === n)
|
|
103
|
+
return s;
|
|
104
|
+
}
|
|
105
|
+
return n;
|
|
106
|
+
}
|
package/data-types/json-type.js
CHANGED
|
@@ -3,11 +3,8 @@ export const JsonType = {
|
|
|
3
3
|
name: 'json',
|
|
4
4
|
oid: DataTypeOIDs.json,
|
|
5
5
|
jsType: 'string',
|
|
6
|
-
decodeBinary(v, offset = 0, len
|
|
6
|
+
decodeBinary(v, offset = 0, len) {
|
|
7
7
|
const content = v.toString('utf8', offset, offset + len);
|
|
8
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.json);
|
|
9
|
-
if (fetchAsString)
|
|
10
|
-
return content;
|
|
11
8
|
return content ? JSON.parse(content) : undefined;
|
|
12
9
|
},
|
|
13
10
|
// json is stored as the text itself, so its binary form is those bytes.
|
|
@@ -25,19 +22,17 @@ export const JsonType = {
|
|
|
25
22
|
return v ? 'true' : 'false';
|
|
26
23
|
return '' + v;
|
|
27
24
|
},
|
|
28
|
-
decodeText
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return v;
|
|
32
|
-
return v ? JSON.parse(v) : null;
|
|
33
|
-
},
|
|
34
|
-
decodeTextBuffer(buf, offset, len, options) {
|
|
35
|
-
return JsonType.decodeText(buf.toString('utf8', offset, offset + len), options);
|
|
25
|
+
decodeText: decodeJsonText,
|
|
26
|
+
decodeTextBuffer(buf, offset, len) {
|
|
27
|
+
return decodeJsonText(buf.toString('utf8', offset, offset + len));
|
|
36
28
|
},
|
|
37
29
|
isType(v) {
|
|
38
30
|
return v && typeof v === 'object';
|
|
39
31
|
},
|
|
40
32
|
};
|
|
33
|
+
function decodeJsonText(v) {
|
|
34
|
+
return v ? JSON.parse(v) : null;
|
|
35
|
+
}
|
|
41
36
|
export const ArrayJsonType = {
|
|
42
37
|
...JsonType,
|
|
43
38
|
name: '_json',
|
package/data-types/jsonb-type.js
CHANGED
|
@@ -14,32 +14,27 @@ export const JsonbType = {
|
|
|
14
14
|
buf.writeUInt8(1);
|
|
15
15
|
buf.writeString(JsonbType.encodeText(v, {}), 'utf8');
|
|
16
16
|
},
|
|
17
|
-
decodeBinary(v, offset = 0, len
|
|
17
|
+
decodeBinary(v, offset = 0, len) {
|
|
18
18
|
// jsonb's binary form is a one-byte version followed by the JSON text,
|
|
19
19
|
// which carries no terminator of its own - so its end is whatever
|
|
20
20
|
// `len` says, not the end of the buffer it happens to sit in.
|
|
21
21
|
if (v[offset] !== 1)
|
|
22
22
|
throw new Error('Unexpected Jsonb version value in header');
|
|
23
23
|
const content = v.toString('utf8', offset + 1, offset + len);
|
|
24
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.jsonb);
|
|
25
|
-
if (fetchAsString)
|
|
26
|
-
return content;
|
|
27
24
|
return content ? JSON.parse(content) : undefined;
|
|
28
25
|
},
|
|
29
|
-
decodeText
|
|
30
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.jsonb);
|
|
31
|
-
if (fetchAsString)
|
|
32
|
-
return v;
|
|
33
|
-
return v ? JSON.parse(v) : null;
|
|
34
|
-
},
|
|
26
|
+
decodeText: decodeJsonbText,
|
|
35
27
|
// See json-type.ts's decodeTextBuffer comment - same rationale.
|
|
36
|
-
decodeTextBuffer(buf, offset, len
|
|
37
|
-
return
|
|
28
|
+
decodeTextBuffer(buf, offset, len) {
|
|
29
|
+
return decodeJsonbText(buf.toString('utf8', offset, offset + len));
|
|
38
30
|
},
|
|
39
31
|
isType(v) {
|
|
40
32
|
return v && typeof v === 'object';
|
|
41
33
|
},
|
|
42
34
|
};
|
|
35
|
+
function decodeJsonbText(v) {
|
|
36
|
+
return v ? JSON.parse(v) : null;
|
|
37
|
+
}
|
|
43
38
|
export const ArrayJsonbType = {
|
|
44
39
|
...JsonbType,
|
|
45
40
|
name: '_jsonb',
|
package/data-types/time-type.js
CHANGED
|
@@ -25,18 +25,15 @@ export const TimeType = {
|
|
|
25
25
|
buf.writeUInt32BE(lo);
|
|
26
26
|
},
|
|
27
27
|
decodeBinary(v, offset = 0, _len, options) {
|
|
28
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.time);
|
|
29
28
|
const hi = v.readInt32BE(offset);
|
|
30
29
|
const lo = v.readUInt32BE(offset + 4);
|
|
31
30
|
let d = new Date((lo + hi * timeMul) / 1000);
|
|
32
|
-
if (
|
|
31
|
+
if (!options.utcDates) {
|
|
33
32
|
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
34
33
|
}
|
|
35
|
-
return
|
|
34
|
+
return d;
|
|
36
35
|
},
|
|
37
36
|
decodeText(v, options) {
|
|
38
|
-
if (options.fetchAsString?.includes(DataTypeOIDs.time))
|
|
39
|
-
return v;
|
|
40
37
|
return parseTime(v, false, options.utcDates);
|
|
41
38
|
},
|
|
42
39
|
// See date-type.ts's decodeTextBuffer comment - same rationale.
|
|
@@ -51,16 +48,6 @@ export const TimeType = {
|
|
|
51
48
|
(typeof v === 'string' && STRICT_TIME_PATTERN.test(v)));
|
|
52
49
|
},
|
|
53
50
|
};
|
|
54
|
-
function padZero(v) {
|
|
55
|
-
return v < 9 ? '0' + v : '' + v;
|
|
56
|
-
}
|
|
57
|
-
function dateToTimeString(d) {
|
|
58
|
-
return (padZero(d.getHours()) +
|
|
59
|
-
':' +
|
|
60
|
-
padZero(d.getMinutes()) +
|
|
61
|
-
':' +
|
|
62
|
-
padZero(d.getSeconds()));
|
|
63
|
-
}
|
|
64
51
|
export const ArrayTimeType = {
|
|
65
52
|
...TimeType,
|
|
66
53
|
name: '_time',
|
|
@@ -37,33 +37,28 @@ export const TimestampType = {
|
|
|
37
37
|
buf.writeUInt32BE(lo);
|
|
38
38
|
},
|
|
39
39
|
decodeBinary(v, offset = 0, _len, options) {
|
|
40
|
-
const fetchAsString = options.fetchAsString?.includes(DataTypeOIDs.timestamp);
|
|
41
40
|
const hi = v.readInt32BE(offset);
|
|
42
41
|
const lo = v.readUInt32BE(offset + 4);
|
|
43
42
|
if (lo === 0xffffffff && hi === 0x7fffffff)
|
|
44
|
-
return
|
|
43
|
+
return Infinity;
|
|
45
44
|
if (lo === 0x00000000 && hi === -0x80000000)
|
|
46
|
-
return
|
|
45
|
+
return -Infinity;
|
|
47
46
|
// Shift from 2000 to 1970
|
|
48
47
|
let d = new Date((lo + hi * timeMul) / 1000 + timeShift);
|
|
49
|
-
if (
|
|
48
|
+
if (!options.utcDates) {
|
|
50
49
|
d = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds());
|
|
51
50
|
}
|
|
52
|
-
return
|
|
51
|
+
return d;
|
|
53
52
|
},
|
|
54
53
|
decodeText(v, options) {
|
|
55
|
-
if (options.fetchAsString?.includes(DataTypeOIDs.timestamp))
|
|
56
|
-
return v;
|
|
57
54
|
return parseDateTime(v, options.utcDates);
|
|
58
55
|
},
|
|
59
56
|
// Reads PostgreSQL's own timestamp shape straight from the wire bytes -
|
|
60
|
-
// see timestamptz-type.ts's decodeTextBuffer for why. The
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// string has to go through the regex path that reads it as UTC.
|
|
57
|
+
// see timestamptz-type.ts's decodeTextBuffer for why. The guard is what
|
|
58
|
+
// decodeText()/parseDateTime() would have applied anyway: with utcDates
|
|
59
|
+
// the string has to go through the regex path that reads it as UTC.
|
|
64
60
|
decodeTextBuffer(buf, offset, len, options) {
|
|
65
|
-
if (!options.utcDates
|
|
66
|
-
!options.fetchAsString?.includes(DataTypeOIDs.timestamp)) {
|
|
61
|
+
if (!options.utcDates) {
|
|
67
62
|
const d = parsePgTimestampBuffer(buf, offset, offset + len);
|
|
68
63
|
if (d !== undefined)
|
|
69
64
|
return d;
|
|
@@ -79,22 +74,6 @@ export const TimestampType = {
|
|
|
79
74
|
!(v.getFullYear() === 1970 && v.getMonth() === 0 && v.getDate() === 1));
|
|
80
75
|
},
|
|
81
76
|
};
|
|
82
|
-
function padZero(v) {
|
|
83
|
-
return v < 9 ? '0' + v : '' + v;
|
|
84
|
-
}
|
|
85
|
-
function dateToTimestampString(d) {
|
|
86
|
-
return (d.getFullYear() +
|
|
87
|
-
'-' +
|
|
88
|
-
padZero(d.getMonth() + 1) +
|
|
89
|
-
'-' +
|
|
90
|
-
padZero(d.getDate()) +
|
|
91
|
-
' ' +
|
|
92
|
-
padZero(d.getHours()) +
|
|
93
|
-
':' +
|
|
94
|
-
padZero(d.getMinutes()) +
|
|
95
|
-
':' +
|
|
96
|
-
padZero(d.getSeconds()));
|
|
97
|
-
}
|
|
98
77
|
export const ArrayTimestampType = {
|
|
99
78
|
...TimestampType,
|
|
100
79
|
name: '_timestamp',
|