Skip to content

Commit 4cf5e71

Browse files
committed
Introduce 'useNativeNumbers' config option
This boolean option allows users to make driver accept and represent integer values as native JavaScript `Number`s instead of driver's special `Integer`s. When this option is set to `true` driver will fail to encode `Integer` values passed as query parameters. All returned `Record`, `Node`, `Relationship`, etc. will have their integer fields as native `Number`s. Object returned by `Record#toObject()` will also contain native numbers. **Warning:** enabling this setting can potentially make driver return lossy integer values. This would be the case when database contain integer values which do not fit in `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]` range. Such integers will then be represented as `Number.NEGATIVE_INFINITY` or `Number.POSITIVE_INFINITY`, which is lossy and different from what is actually stored in the database. That is why this option is set to `false` by default. Driver's `Integer` class is able to represent integer values beyond `[Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER]` and thus is a much safer option in the general case.
1 parent 167a139 commit 4cf5e71

File tree

9 files changed

+188
-33
lines changed

9 files changed

+188
-33
lines changed

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/v1/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,17 @@ const USER_AGENT = "neo4j-javascript/" + VERSION;
157157
* // result in no timeout being applied. Connection establishment will be then bound by the timeout configured
158158
* // on the operating system level. Default value is 5000, which is 5 seconds.
159159
* connectionTimeout: 5000, // 5 seconds
160+
*
161+
* // Make this driver always return and accept native JavaScript number for integer values, instead of the
162+
* // dedicated {@link Integer} class. Values that do not fit in native number bit range will be represented as
163+
* // <code>Number.NEGATIVE_INFINITY</code> or <code>Number.POSITIVE_INFINITY</code>. Driver will fail to encode
164+
* // {@link Integer} values passed as query parameters when this setting is set to <code>true</code>.
165+
* // <b>Warning:</b> It is not safe to enable this setting when JavaScript applications are not the only ones
166+
* // interacting with the database. Stored numbers might in such case be not representable by native
167+
* // {@link Number} type and thus driver will return lossy values. For example, this might happen when data was
168+
* // initially imported using neo4j import tool and contained numbers larger than
169+
* // <code>Number.MAX_SAFE_INTEGER</code>. Driver will then return positive infinity, which is lossy.
170+
* useNativeNumbers: false,
160171
* }
161172
*
162173
* @param {string} url The URL for the Neo4j database, for instance "bolt://localhost"

src/v1/integer.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,21 @@ class Integer {
8888
*/
8989
toNumber(){ return this.high * TWO_PWR_32_DBL + (this.low >>> 0); }
9090

91+
/**
92+
* Converts the Integer to native number or -Infinity/+Infinity when it does not fit.
93+
* @return {number}
94+
* @package
95+
*/
96+
toNumberOrInfinity() {
97+
if (this.lessThan(Integer.MIN_SAFE_VALUE)) {
98+
return Number.NEGATIVE_INFINITY;
99+
} else if (this.greaterThan(Integer.MAX_SAFE_VALUE)) {
100+
return Number.POSITIVE_INFINITY;
101+
} else {
102+
return this.toNumber();
103+
}
104+
}
105+
91106
/**
92107
* Converts the Integer to a string written in the specified radix.
93108
* @param {number=} radix Radix (2-36), defaults to 10

src/v1/internal/connector.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,12 @@ class Connection {
162162

163163
/**
164164
* @constructor
165-
* @param channel - channel with a 'write' function and a 'onmessage'
166-
* callback property
167-
* @param url - url to connect to
165+
* @param {NodeChannel|WebSocketChannel} channel - channel with a 'write' function and a 'onmessage' callback property.
166+
* @param {string} url - the hostname and port to connect to.
167+
* @param {boolean} useNativeNumbers if this connection should treat/convert all received numbers
168+
* (including native {@link Number} type or our own {@link Integer}) as native {@link Number}.
168169
*/
169-
constructor (channel, url) {
170+
constructor(channel, url, useNativeNumbers = false) {
170171
/**
171172
* An ordered queue of observers, each exchange response (zero or more
172173
* RECORD messages followed by a SUCCESS message) we recieve will be routed
@@ -180,8 +181,8 @@ class Connection {
180181
this._ch = channel;
181182
this._dechunker = new Dechunker();
182183
this._chunker = new Chunker( channel );
183-
this._packer = new Packer( this._chunker );
184-
this._unpacker = new Unpacker();
184+
this._packer = new Packer(this._chunker, useNativeNumbers);
185+
this._unpacker = new Unpacker(useNativeNumbers);
185186

186187
this._isHandlingFailure = false;
187188
this._currentFailure = null;
@@ -588,7 +589,7 @@ function connect(url, config = {}, connectionErrorCode = null) {
588589
const Ch = config.channel || Channel;
589590
const parsedUrl = urlUtil.parseBoltUrl(url);
590591
const channelConfig = new ChannelConfig(parsedUrl, config, connectionErrorCode);
591-
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort);
592+
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort, config.useNativeNumbers);
592593
}
593594

594595
export {

src/v1/internal/packstream.js

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import utf8 from './utf8';
2020
import Integer, {int, isInt} from '../integer';
2121
import {newError} from './../error';
22+
import {Chunker} from './chunking';
2223

2324
const TINY_STRING = 0x80;
2425
const TINY_LIST = 0x90;
@@ -75,9 +76,17 @@ class Structure {
7576
* @access private
7677
*/
7778
class Packer {
78-
constructor (channel) {
79+
80+
/**
81+
* @constructor
82+
* @param {Chunker} channel the chunker backed by a network channel.
83+
* @param {boolean} useNativeNumbers if this packer should treat/convert all received numbers
84+
* (including native {@link Number} type or our own {@link Integer}) as native {@link Number}.
85+
*/
86+
constructor(channel, useNativeNumbers = false) {
7987
this._ch = channel;
8088
this._byteArraysSupported = true;
89+
this._useNativeNumbers = useNativeNumbers;
8190
}
8291

8392
/**
@@ -98,7 +107,7 @@ class Packer {
98107
} else if (typeof(x) == "string") {
99108
return () => this.packString(x, onError);
100109
} else if (isInt(x)) {
101-
return () => this.packInteger( x );
110+
return this._packableInteger(x, onError);
102111
} else if (x instanceof Int8Array) {
103112
return () => this.packBytes(x, onError);
104113
} else if (x instanceof Array) {
@@ -141,6 +150,28 @@ class Packer {
141150
}
142151
}
143152

153+
/**
154+
* Creates a packable function out of the provided {@link Integer} value.
155+
* @param {Integer} x the value to pack.
156+
* @param {function} onError the callback for the case when value cannot be packed.
157+
* @return {function}
158+
* @private
159+
*/
160+
_packableInteger(x, onError) {
161+
if (this._useNativeNumbers) {
162+
// pack Integer objects only when native numbers are not used, fail otherwise
163+
// Integer can't represent special values like Number.NEGATIVE_INFINITY
164+
// and should not be used at all when native numbers are enabled
165+
if (onError) {
166+
onError(newError(`Cannot pack Integer value ${x} (${JSON.stringify(x)}) when native numbers are enabled. ` +
167+
`Please use native Number instead or disable native number support on the driver level.`));
168+
}
169+
return () => undefined;
170+
} else {
171+
return () => this.packInteger(x);
172+
}
173+
}
174+
144175
/**
145176
* Packs a struct
146177
* @param signature the signature of the struct
@@ -309,11 +340,18 @@ class Packer {
309340
* @access private
310341
*/
311342
class Unpacker {
312-
constructor () {
343+
344+
/**
345+
* @constructor
346+
* @param {boolean} useNativeNumbers if this unpacker should treat/convert all received numbers
347+
* (including native {@link Number} type or our own {@link Integer}) as native {@link Number}.
348+
*/
349+
constructor(useNativeNumbers = false) {
313350
// Higher level layers can specify how to map structs to higher-level objects.
314-
// If we recieve a struct that has a signature that does not have a mapper,
351+
// If we receive a struct that has a signature that does not have a mapper,
315352
// we simply return a Structure object.
316353
this.structMappers = {};
354+
this._useNativeNumbers = useNativeNumbers;
317355
}
318356

319357
unpack(buffer) {
@@ -388,9 +426,10 @@ class Unpacker {
388426
let b = buffer.readInt32();
389427
return int(b);
390428
} else if (marker == INT_64) {
391-
let high = buffer.readInt32();
392-
let low = buffer.readInt32();
393-
return new Integer(low, high);
429+
const high = buffer.readInt32();
430+
const low = buffer.readInt32();
431+
const integer = new Integer(low, high);
432+
return this._useNativeNumbers ? integer.toNumberOrInfinity() : integer;
394433
} else {
395434
return null;
396435
}

test/types/v1/driver.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ const connectionPoolSize: undefined | number = config.connectionPoolSize;
5959
const maxTransactionRetryTime: undefined | number = config.maxTransactionRetryTime;
6060
const loadBalancingStrategy1: undefined | LoadBalancingStrategy = config.loadBalancingStrategy;
6161
const loadBalancingStrategy2: undefined | string = config.loadBalancingStrategy;
62+
const maxConnectionLifetime: undefined | number = config.maxConnectionLifetime;
63+
const connectionTimeout: undefined | number = config.connectionTimeout;
64+
const useNativeNumbers: undefined | boolean = config.useNativeNumbers;
6265

6366
const sessionMode: SessionMode = dummy;
6467
const sessionModeStr: string = sessionMode;

test/v1/driver.test.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,43 @@ describe('driver', () => {
325325
testIPv6Connection('bolt://[::1]:7687', done);
326326
});
327327

328+
const nativeNumbers = [
329+
Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY,
330+
Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER,
331+
-0, 0,
332+
-42, 42,
333+
-999, 999,
334+
-1000, 1000,
335+
-9000000, 9000000,
336+
Number.MIN_SAFE_INTEGER + 1, Number.MAX_SAFE_INTEGER - 1
337+
];
338+
339+
nativeNumbers.forEach(number => {
340+
341+
it(`should return native number ${number} when useNativeNumbers=true`, done => {
342+
testNativeNumberInReturnedRecord(number, done);
343+
});
344+
345+
});
346+
347+
it('should fail to pack Integer when useNativeNumbers=true', done => {
348+
driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, {useNativeNumbers: true});
349+
const session = driver.session();
350+
351+
session.run('RETURN $number', {number: neo4j.int(42)})
352+
.then(result => {
353+
done.fail(`Was somehow able to pack Integer and received result ${JSON.stringify(result)}`);
354+
})
355+
.catch(error => {
356+
const message = error.message;
357+
if (message.indexOf('Cannot pack Integer value') === -1) {
358+
done.fail(`Unexpected error message: ${message}`);
359+
} else {
360+
done();
361+
}
362+
});
363+
});
364+
328365
function testIPv6Connection(url, done) {
329366
if (serverVersion.compareTo(VERSION_3_1_0) < 0) {
330367
// IPv6 listen address only supported starting from neo4j 3.1, so let's ignore the rest
@@ -341,6 +378,29 @@ describe('driver', () => {
341378
});
342379
}
343380

381+
function testNativeNumberInReturnedRecord(number, done) {
382+
driver = neo4j.driver('bolt://localhost', sharedNeo4j.authToken, {useNativeNumbers: true});
383+
384+
const session = driver.session();
385+
session.run('RETURN $number AS n0, $number AS n1', {number: number}).then(result => {
386+
session.close();
387+
388+
const records = result.records;
389+
expect(records.length).toEqual(1);
390+
const record = records[0];
391+
392+
expect(record.get('n0')).toEqual(number);
393+
expect(record.get('n1')).toEqual(number);
394+
395+
expect(record.get(0)).toEqual(number);
396+
expect(record.get(1)).toEqual(number);
397+
398+
expect(record.toObject()).toEqual({n0: number, n1: number});
399+
400+
done();
401+
});
402+
}
403+
344404
/**
345405
* Starts new transaction to force new network connection.
346406
* @param {Driver} driver - the driver to use.

test/v1/integer.test.js

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,52 @@
1717
* limitations under the License.
1818
*/
1919

20-
var v1 = require('../../lib/v1');
21-
var int = v1.int;
22-
var integer = v1.integer;
23-
24-
describe('Pool', function() {
25-
it('exposes inSafeRange function', function () {
26-
expect(integer.inSafeRange(int("9007199254740991"))).toBeTruthy();
27-
expect(integer.inSafeRange(int("9007199254740992"))).toBeFalsy();
28-
expect(integer.inSafeRange(int("-9007199254740991"))).toBeTruthy();
29-
expect(integer.inSafeRange(int("-9007199254740992"))).toBeFalsy();
20+
import neo4j from '../../src/v1';
21+
import Integer from '../../src/v1/integer';
22+
23+
const int = neo4j.int;
24+
const integer = neo4j.integer;
25+
26+
describe('Integer', () => {
27+
28+
it('exposes inSafeRange function', () => {
29+
expect(integer.inSafeRange(int('9007199254740991'))).toBeTruthy();
30+
expect(integer.inSafeRange(int('9007199254740992'))).toBeFalsy();
31+
expect(integer.inSafeRange(int('-9007199254740991'))).toBeTruthy();
32+
expect(integer.inSafeRange(int('-9007199254740992'))).toBeFalsy();
33+
});
34+
35+
it('exposes toNumber function', () => {
36+
expect(integer.toNumber(int('9007199254740991'))).toEqual(9007199254740991);
37+
expect(integer.toNumber(int('-9007199254740991'))).toEqual(-9007199254740991);
3038
});
3139

32-
it('exposes toNumber function', function () {
33-
expect(integer.toNumber(int("9007199254740991"))).toEqual(9007199254740991);
34-
expect(integer.toNumber(int("-9007199254740991"))).toEqual(-9007199254740991);
40+
it('exposes toString function', () => {
41+
expect(integer.toString(int('9007199254740991'))).toEqual('9007199254740991');
42+
expect(integer.toString(int('9007199254740992'))).toEqual('9007199254740992');
43+
expect(integer.toString(int('-9007199254740991'))).toEqual('-9007199254740991');
44+
expect(integer.toString(int('-9007199254740992'))).toEqual('-9007199254740992');
3545
});
3646

37-
it('exposes toString function', function () {
38-
expect(integer.toString(int("9007199254740991"))).toEqual("9007199254740991");
39-
expect(integer.toString(int("9007199254740992"))).toEqual("9007199254740992");
40-
expect(integer.toString(int("-9007199254740991"))).toEqual("-9007199254740991");
41-
expect(integer.toString(int("-9007199254740992"))).toEqual("-9007199254740992");
47+
it('converts to number when safe', () => {
48+
expect(int('42').toNumberOrInfinity()).toEqual(42);
49+
expect(int('4242').toNumberOrInfinity()).toEqual(4242);
50+
expect(int('-999').toNumberOrInfinity()).toEqual(-999);
51+
expect(int('1000000000').toNumberOrInfinity()).toEqual(1000000000);
52+
expect(Integer.MIN_SAFE_VALUE.toNumberOrInfinity()).toEqual(Integer.MIN_SAFE_VALUE.toNumber());
53+
expect(Integer.MAX_SAFE_VALUE.toNumberOrInfinity()).toEqual(Integer.MAX_SAFE_VALUE.toNumber());
4254
});
55+
56+
it('converts to negative infinity when too small', () => {
57+
expect(Integer.MIN_SAFE_VALUE.subtract(1).toNumberOrInfinity()).toEqual(Number.NEGATIVE_INFINITY);
58+
expect(Integer.MIN_SAFE_VALUE.subtract(42).toNumberOrInfinity()).toEqual(Number.NEGATIVE_INFINITY);
59+
expect(Integer.MIN_SAFE_VALUE.subtract(100).toNumberOrInfinity()).toEqual(Number.NEGATIVE_INFINITY);
60+
});
61+
62+
it('converts to positive infinity when too large', () => {
63+
expect(Integer.MAX_SAFE_VALUE.add(1).toNumberOrInfinity()).toEqual(Number.POSITIVE_INFINITY);
64+
expect(Integer.MAX_SAFE_VALUE.add(24).toNumberOrInfinity()).toEqual(Number.POSITIVE_INFINITY);
65+
expect(Integer.MAX_SAFE_VALUE.add(999).toNumberOrInfinity()).toEqual(Number.POSITIVE_INFINITY);
66+
});
67+
4368
});

types/v1/driver.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ declare interface Config {
5454
loadBalancingStrategy?: LoadBalancingStrategy;
5555
maxConnectionLifetime?: number;
5656
connectionTimeout?: number;
57+
useNativeNumbers?: boolean;
5758
}
5859

5960
declare type SessionMode = "READ" | "WRITE";

0 commit comments

Comments
 (0)