Skip to content

Commit 020be65

Browse files
authored
Merge pull request #323 from lutovich/1.6-native-numbers
Introduce 'useNativeNumbers' config option
2 parents 167a139 + 7cc68d8 commit 020be65

13 files changed

+256
-98
lines changed

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/v1/index.js

+12
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,18 @@ 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 native JavaScript numbers 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>.
164+
* // <b>Warning:</b> It is not always safe to enable this setting when JavaScript applications are not the only ones
165+
* // interacting with the database. Stored numbers might in such case be not representable by native
166+
* // {@link Number} type and thus driver will return lossy values. This might also happen when data was
167+
* // initially imported using neo4j import tool and contained numbers larger than
168+
* // <code>Number.MAX_SAFE_INTEGER</code>. Driver will then return positive infinity, which is lossy.
169+
* // Default value for this option is <code>false</code> because native JavaScript numbers might result
170+
* // in loss of precision in the general case.
171+
* disableLosslessIntegers: false,
160172
* }
161173
*
162174
* @param {string} url The URL for the Neo4j database, for instance "bolt://localhost"

src/v1/integer.js

+15
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

+7-7
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,11 @@ 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} disableLosslessIntegers if this connection should convert all received integers to native JS numbers.
168168
*/
169-
constructor (channel, url) {
169+
constructor(channel, url, disableLosslessIntegers = false) {
170170
/**
171171
* An ordered queue of observers, each exchange response (zero or more
172172
* RECORD messages followed by a SUCCESS message) we recieve will be routed
@@ -180,8 +180,8 @@ class Connection {
180180
this._ch = channel;
181181
this._dechunker = new Dechunker();
182182
this._chunker = new Chunker( channel );
183-
this._packer = new Packer( this._chunker );
184-
this._unpacker = new Unpacker();
183+
this._packer = new Packer(this._chunker);
184+
this._unpacker = new Unpacker(disableLosslessIntegers);
185185

186186
this._isHandlingFailure = false;
187187
this._currentFailure = null;
@@ -588,7 +588,7 @@ function connect(url, config = {}, connectionErrorCode = null) {
588588
const Ch = config.channel || Channel;
589589
const parsedUrl = urlUtil.parseBoltUrl(url);
590590
const channelConfig = new ChannelConfig(parsedUrl, config, connectionErrorCode);
591-
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort);
591+
return new Connection(new Ch(channelConfig), parsedUrl.hostAndPort, config.disableLosslessIntegers);
592592
}
593593

594594
export {

src/v1/internal/packstream.js

+26-10
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,7 +76,12 @@ 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+
*/
84+
constructor(channel) {
7985
this._ch = channel;
8086
this._byteArraysSupported = true;
8187
}
@@ -98,7 +104,7 @@ class Packer {
98104
} else if (typeof(x) == "string") {
99105
return () => this.packString(x, onError);
100106
} else if (isInt(x)) {
101-
return () => this.packInteger( x );
107+
return () => this.packInteger(x);
102108
} else if (x instanceof Int8Array) {
103109
return () => this.packBytes(x, onError);
104110
} else if (x instanceof Array) {
@@ -178,6 +184,7 @@ class Packer {
178184
this._ch.writeInt32(low);
179185
}
180186
}
187+
181188
packFloat(x) {
182189
this._ch.writeUInt8(FLOAT_64);
183190
this._ch.writeFloat64(x);
@@ -309,11 +316,17 @@ class Packer {
309316
* @access private
310317
*/
311318
class Unpacker {
312-
constructor () {
319+
320+
/**
321+
* @constructor
322+
* @param {boolean} disableLosslessIntegers if this unpacker should convert all received integers to native JS numbers.
323+
*/
324+
constructor(disableLosslessIntegers = false) {
313325
// 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,
326+
// If we receive a struct that has a signature that does not have a mapper,
315327
// we simply return a Structure object.
316328
this.structMappers = {};
329+
this._disableLosslessIntegers = disableLosslessIntegers;
317330
}
318331

319332
unpack(buffer) {
@@ -330,9 +343,12 @@ class Unpacker {
330343
return boolean;
331344
}
332345

333-
const number = this._unpackNumber(marker, buffer);
334-
if (number !== null) {
335-
return number;
346+
const numberOrInteger = this._unpackNumberOrInteger(marker, buffer);
347+
if (numberOrInteger !== null) {
348+
if (this._disableLosslessIntegers && isInt(numberOrInteger)) {
349+
return numberOrInteger.toNumberOrInfinity();
350+
}
351+
return numberOrInteger;
336352
}
337353

338354
const string = this._unpackString(marker, markerHigh, markerLow, buffer);
@@ -373,7 +389,7 @@ class Unpacker {
373389
}
374390
}
375391

376-
_unpackNumber(marker, buffer) {
392+
_unpackNumberOrInteger(marker, buffer) {
377393
if (marker == FLOAT_64) {
378394
return buffer.readFloat64();
379395
} else if (marker >= 0 && marker < 128) {
@@ -388,8 +404,8 @@ class Unpacker {
388404
let b = buffer.readInt32();
389405
return int(b);
390406
} else if (marker == INT_64) {
391-
let high = buffer.readInt32();
392-
let low = buffer.readInt32();
407+
const high = buffer.readInt32();
408+
const low = buffer.readInt32();
393409
return new Integer(low, high);
394410
} else {
395411
return null;

test/types/v1/driver.test.ts

+4-10
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,7 @@
1717
* limitations under the License.
1818
*/
1919

20-
import Driver, {
21-
AuthToken,
22-
Config,
23-
EncryptionLevel,
24-
LoadBalancingStrategy,
25-
READ,
26-
SessionMode,
27-
TrustStrategy,
28-
WRITE
29-
} from "../../../types/v1/driver";
20+
import Driver, {AuthToken, Config, EncryptionLevel, LoadBalancingStrategy, READ, SessionMode, TrustStrategy, WRITE} from "../../../types/v1/driver";
3021
import {Parameters} from "../../../types/v1/statement-runner";
3122
import Session from "../../../types/v1/session";
3223
import {Neo4jError} from "../../../types/v1/error";
@@ -59,6 +50,9 @@ const connectionPoolSize: undefined | number = config.connectionPoolSize;
5950
const maxTransactionRetryTime: undefined | number = config.maxTransactionRetryTime;
6051
const loadBalancingStrategy1: undefined | LoadBalancingStrategy = config.loadBalancingStrategy;
6152
const loadBalancingStrategy2: undefined | string = config.loadBalancingStrategy;
53+
const maxConnectionLifetime: undefined | number = config.maxConnectionLifetime;
54+
const connectionTimeout: undefined | number = config.connectionTimeout;
55+
const disableLosslessIntegers: undefined | boolean = config.disableLosslessIntegers;
6256

6357
const sessionMode: SessionMode = dummy;
6458
const sessionModeStr: string = sessionMode;

test/types/v1/graph-types.test.ts

+25
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ const node1Id: Integer = node1.identity;
2626
const node1Labels: string[] = node1.labels;
2727
const node1Props: object = node1.properties;
2828

29+
const node2: Node<number> = new Node(2, ["Person", "Employee"], {name: "Alice"});
30+
const node2Id: number = node2.identity;
31+
2932
const rel1: Relationship = new Relationship(int(1), int(2), int(3), "KNOWS", {since: 12345});
3033
const rel1String: string = rel1.toString();
3134
const rel1Id: Integer = rel1.identity;
@@ -41,13 +44,35 @@ const rel2Id: Integer = rel2.identity;
4144
const rel2Type: string = rel2.type;
4245
const rel2Props: object = rel2.properties;
4346

47+
const rel4: Relationship<number> = new Relationship(2, 3, 4, "KNOWS", {since: 12345});
48+
const rel4Id: number = rel4.identity;
49+
const rel4Start: number = rel4.start;
50+
const rel4End: number = rel4.end;
51+
52+
const rel5: UnboundRelationship<number> = new UnboundRelationship(5, "KNOWS", {since: 12345});
53+
const rel5Id: number = rel5.identity;
54+
const rel6 = rel5.bind(24, 42);
55+
const rel6Id: number = rel6.identity;
56+
const rel6Start: number = rel6.start;
57+
const rel6End: number = rel6.end;
58+
4459
const pathSegment1: PathSegment = new PathSegment(node1, rel1, node1);
4560
const pathSegment1Start: Node = pathSegment1.start;
4661
const pathSegment1Rel: Relationship = pathSegment1.relationship;
4762
const pathSegment1End: Node = pathSegment1.end;
4863

64+
const pathSegment2: PathSegment<number> = new PathSegment(node2, rel4, node2);
65+
const pathSegment2Start: Node<number> = pathSegment2.start;
66+
const pathSegment2Rel: Relationship<number> = pathSegment2.relationship;
67+
const pathSegment2End: Node<number> = pathSegment2.end;
68+
4969
const path1: Path = new Path(node1, node1, [pathSegment1]);
5070
const path1Start: Node = path1.start;
5171
const path1End: Node = path1.end;
5272
const path1Segments: PathSegment[] = path1.segments;
5373
const path1Length: number = path1.length;
74+
75+
const path2: Path<number> = new Path(node2, node2, [pathSegment2]);
76+
const path2Start: Node<number> = path2.start;
77+
const path2End: Node<number> = path2.end;
78+
const path2Segments: PathSegment<number>[] = path2.segments;

test/types/v1/result-summary.test.ts

+21-20
Original file line numberDiff line numberDiff line change
@@ -17,27 +17,20 @@
1717
* limitations under the License.
1818
*/
1919

20-
import ResultSummary, {
21-
Notification,
22-
NotificationPosition,
23-
Plan,
24-
ProfiledPlan,
25-
ServerInfo,
26-
StatementStatistic
27-
} from "../../../types/v1/result-summary";
20+
import ResultSummary, {Notification, NotificationPosition, Plan, ProfiledPlan, ServerInfo, StatementStatistic} from "../../../types/v1/result-summary";
2821
import Integer from "../../../types/v1/integer";
2922

3023
const dummy: any = null;
3124

32-
const sum: ResultSummary = dummy;
25+
const sum1: ResultSummary = dummy;
3326

34-
const stmt = sum.statement;
27+
const stmt = sum1.statement;
3528
const stmtText: string = stmt.text;
3629
const stmtParams: object = stmt.parameters;
3730

38-
const str: string = sum.statementType;
31+
const str: string = sum1.statementType;
3932

40-
const counters: StatementStatistic = sum.counters;
33+
const counters: StatementStatistic = sum1.counters;
4134

4235
const containsUpdates: boolean = counters.containsUpdates();
4336
const nodesCreated: number = counters.nodesCreated();
@@ -52,21 +45,21 @@ const indexesRemoved: number = counters.indexesRemoved();
5245
const constraintsAdded: number = counters.constraintsAdded();
5346
const constraintsRemoved: number = counters.constraintsRemoved();
5447

55-
const plan: Plan = sum.plan;
48+
const plan: Plan = sum1.plan;
5649
const planOperatorType: string = plan.operatorType;
5750
const planIdentifiers: string[] = plan.identifiers;
5851
const planArguments: { [key: string]: string } = plan.arguments;
5952
const planChildren: Plan[] = plan.children;
6053

61-
const profile: ProfiledPlan = sum.profile;
54+
const profile: ProfiledPlan = sum1.profile;
6255
const profileOperatorType: string = profile.operatorType;
6356
const profileIdentifiers: string[] = profile.identifiers;
6457
const profileArguments: { [key: string]: string } = profile.arguments;
6558
const profileDbHits: number = profile.dbHits;
6659
const profileRows: number = profile.rows;
6760
const profileChildren: ProfiledPlan[] = profile.children;
6861

69-
const notifications: Notification[] = sum.notifications;
62+
const notifications: Notification[] = sum1.notifications;
7063
const notification: Notification = notifications[0];
7164
const code: string = notification.code;
7265
const title: string = notification.title;
@@ -78,12 +71,20 @@ const offset: number = position2.offset;
7871
const line: number = position2.line;
7972
const column: number = position2.column;
8073

81-
const server: ServerInfo = sum.server;
74+
const server: ServerInfo = sum1.server;
8275
const address: string = server.address;
8376
const version: string = server.version;
8477

85-
const resultConsumedAfter: Integer = sum.resultConsumedAfter;
86-
const resultAvailableAfter: Integer = sum.resultAvailableAfter;
78+
const resultConsumedAfter1: Integer = sum1.resultConsumedAfter;
79+
const resultAvailableAfter1: Integer = sum1.resultAvailableAfter;
8780

88-
const hasPlan: boolean = sum.hasPlan();
89-
const hasProfile: boolean = sum.hasProfile();
81+
const hasPlan: boolean = sum1.hasPlan();
82+
const hasProfile: boolean = sum1.hasProfile();
83+
84+
const sum2: ResultSummary<number> = dummy;
85+
const resultConsumedAfter2: number = sum2.resultConsumedAfter;
86+
const resultAvailableAfter2: number = sum2.resultAvailableAfter;
87+
88+
const sum3: ResultSummary<Integer> = dummy;
89+
const resultConsumedAfter3: Integer = sum3.resultConsumedAfter;
90+
const resultAvailableAfter3: Integer = sum3.resultAvailableAfter;

0 commit comments

Comments
 (0)