Skip to content

Commit 06e03ba

Browse files
Add GremlinLang number type suffixes and fix Long deserialization in JS GLV (#3340)
1 parent 6ddc578 commit 06e03ba

8 files changed

Lines changed: 65 additions & 20 deletions

File tree

CHANGELOG.asciidoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
2525
[[release-4-0-0]]
2626
=== TinkerPop 4.0.0 (NOT OFFICIALLY RELEASED YET)
2727
28+
* Improved number type handling in `gremlin-javascript`: smart GremlinLang type suffixes, `bigint` as BigInteger, and Long deserialization now returns `bigint` for values beyond safe integer range.
2829
* Added grammar-based `Translator` for `gremlin-javascript` supporting translation to JavaScript, Python, Go, .NET, Java, Groovy, canonical, and anonymized output.
2930
* Added `translate_gremlin_query` tool to `gremlin-mcp` that translates Gremlin queries to a target language variant, with optional LLM-assisted normalization via MCP sampling for non-canonical input.
3031
* Modified `gremlin-mcp` to support offline mode where utility tools (translate, format) remain available without a configured `GREMLIN_MCP_ENDPOINT`.

docs/src/reference/gremlin-variants.asciidoc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1928,6 +1928,16 @@ the possibility of `null`.
19281928
[[gremlin-javascript-limitations]]
19291929
=== Limitations
19301930
1931+
* JavaScript's `Number` type is an IEEE 754 double-precision float. `Float`, `Byte`, and `Short` values from the server
1932+
are deserialized as `Number` and lose their original type information.
1933+
* `Long` values outside the safe integer range (|n| > 2^53 - 1) are deserialized as `BigInt` to preserve precision.
1934+
Values within the safe range are deserialized as `Number`. The same server type may produce different JavaScript types.
1935+
* `Number.isInteger(1.0)` is `true` in JavaScript, so the driver cannot distinguish integer values from whole-number
1936+
doubles. `BigDecimal` is not implemented.
1937+
* The driver applies GremlinLang type suffixes automatically based on value characteristics: integers within the 32-bit
1938+
signed range are unsuffixed (Int), integers beyond that up to `Number.MAX_SAFE_INTEGER` use the `L` suffix (Long),
1939+
non-integer numbers and integers beyond the safe range use the `D` suffix (Double), and `BigInt` values use the `N`
1940+
suffix (BigInteger).
19311941
* The `subgraph()`-step is not supported by any variant that is not running on the Java Virtual Machine as there is
19321942
no `Graph` instance to deserialize a result into on the client-side. A workaround is to replace the step with
19331943
`aggregate(local)` and then convert those results to something the client can use locally.

gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,22 @@ export default class GremlinLang {
7171
if (arg instanceof Long) {
7272
return String(arg.value) + 'L';
7373
}
74-
if (arg instanceof Date) {
75-
const iso = arg.toISOString();
76-
return `datetime("${iso}")`;
74+
if (typeof arg === 'bigint') {
75+
return String(arg) + 'N';
7776
}
7877
if (typeof arg === 'number') {
7978
if (Number.isNaN(arg)) return 'NaN';
8079
if (arg === Infinity) return '+Infinity';
8180
if (arg === -Infinity) return '-Infinity';
82-
return String(arg);
81+
if (!Number.isInteger(arg)) return String(arg) + 'D';
82+
if (arg >= -2147483648 && arg <= 2147483647) return String(arg);
83+
// Outside safe integer range, values have lost precision and may exceed Java Long — emit as Double.
84+
if (arg > Number.MAX_SAFE_INTEGER || arg < -Number.MAX_SAFE_INTEGER) return String(arg) + 'D';
85+
return String(arg) + 'L';
86+
}
87+
if (arg instanceof Date) {
88+
const iso = arg.toISOString();
89+
return `datetime("${iso}")`;
8390
}
8491
if (typeof arg === 'string') {
8592
// JSON.stringify handles all special character escaping in one call.

gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,10 @@ export default class LongSerializer {
9292
len += 8;
9393

9494
let v = cursor.readBigInt64BE();
95-
if (v < Number.MIN_SAFE_INTEGER || v > Number.MAX_SAFE_INTEGER) {
96-
// Keeps the same contract as GraphSON LongSerializer — converts to Number (loses precision beyond 2^53).
97-
v = parseFloat(v.toString());
98-
} else {
95+
if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {
9996
v = Number(v);
10097
}
98+
// Values outside safe integer range stay as BigInt to preserve precision.
10199

102100
return { v, len };
103101
} catch (err) {

gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,10 @@ export default class NumberSerializationStrategy {
5858
// INT32_MIN/MAX
5959
return this.ioc.intSerializer.serialize(item, fullyQualifiedFormat);
6060
}
61-
// eslint-disable-next-line no-loss-of-precision
62-
if (item >= -9223372036854775808 && item < 9223372036854775807) {
63-
// INT64_MIN/MAX
61+
if (item >= Number.MIN_SAFE_INTEGER && item <= Number.MAX_SAFE_INTEGER) {
6462
return this.ioc.longSerializer.serialize(item, fullyQualifiedFormat);
6563
}
64+
// Integers outside safe range are huge doubles that only appear integral due to IEEE 754 precision loss
6665
return this.ioc.doubleSerializer.serialize(item, fullyQualifiedFormat);
6766
}
6867

gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ function invalidDateComparator(actual, expected) {
174174
}
175175

176176
describe('GraphBinary v4 Model Tests', () => {
177-
// run mode (32 entries)
177+
// run mode (31 entries)
178178
run('pos-biginteger');
179179
run('neg-biginteger');
180180
run('empty-binary');
@@ -196,7 +196,6 @@ describe('GraphBinary v4 Model Tests', () => {
196196
run('no-prop-edge');
197197
run('max-int');
198198
run('min-int');
199-
run('min-long');
200199
run('empty-map');
201200
run('traversal-path');
202201
run('empty-path');
@@ -208,7 +207,7 @@ describe('GraphBinary v4 Model Tests', () => {
208207
run('out-direction');
209208
run('neg-zero-double', negZeroComparator);
210209

211-
// runWriteRead mode (22 entries)
210+
// runWriteRead mode (23 entries)
212211
runWriteRead('min-byte');
213212
runWriteRead('max-byte');
214213
runWriteRead('max-float');
@@ -221,6 +220,7 @@ describe('GraphBinary v4 Model Tests', () => {
221220
runWriteRead('var-bulklist');
222221
runWriteRead('empty-bulklist');
223222
runWriteRead('traversal-edge');
223+
runWriteRead('min-long');
224224
runWriteRead('max-long');
225225
runWriteRead('var-type-set', setComparator);
226226
runWriteRead('max-short');

gremlin-js/gremlin-javascript/test/unit/graphbinary/model.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,9 @@ model['no-prop-edge'] = new Edge(
9999
model['max-int'] = 2147483647;
100100
model['min-int'] = -2147483648;
101101

102-
// Long values (lose precision beyond 2^53 in JS)
103-
model['max-long'] = 9223372036854776000; // Number, not BigInt
104-
model['min-long'] = -9223372036854776000;
102+
// Long values
103+
model['max-long'] = 9223372036854775807n;
104+
model['min-long'] = -9223372036854775808n;
105105

106106
// Map values
107107
const dateKey = new Date(Date.UTC(1970, 0, 1, 0, 24, 41, 295)); // 1481295 ms

gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe('GremlinLang', function () {
4343
// #3
4444
[g.V().constant(5), 'g.V().constant(5)'],
4545
// #4
46-
[g.V().constant(1.5), 'g.V().constant(1.5)'],
46+
[g.V().constant(1.5), 'g.V().constant(1.5D)'],
4747
// #5
4848
[g.V().constant('Hello'), "g.V().constant('Hello')"],
4949
// #6
@@ -117,7 +117,7 @@ describe('GremlinLang', function () {
117117
// #40
118118
[g.V().has('runways',P.gt(5)).count(), "g.V().has('runways',gt(5)).count()"],
119119
// #41
120-
[g.V().has('runways',P.lte(5.3)).count(), "g.V().has('runways',lte(5.3)).count()"],
120+
[g.V().has('runways',P.lte(5.3)).count(), "g.V().has('runways',lte(5.3D)).count()"],
121121
// #42
122122
[g.V().has('code',P.within([123,124])), "g.V().has('code',within([123,124]))"],
123123
// #43
@@ -137,7 +137,7 @@ describe('GremlinLang', function () {
137137
// #50
138138
[g.V('3').choose(__.out().count()).option(0,__.constant('none')).option(1,__.constant('one')).option(2,__.constant('two')), "g.V('3').choose(__.out().count()).option(0,__.constant('none')).option(1,__.constant('one')).option(2,__.constant('two'))"],
139139
// #51
140-
[g.V('3').choose(__.out().count()).option(1.5,__.constant('one and a half')), "g.V('3').choose(__.out().count()).option(1.5,__.constant('one and a half'))"],
140+
[g.V('3').choose(__.out().count()).option(1.5,__.constant('one and a half')), "g.V('3').choose(__.out().count()).option(1.5D,__.constant('one and a half'))"],
141141
// #52
142142
[g.V().repeat(__.out()).until(__.or(__.loops().is(3),__.has('code','AGR'))).count(), "g.V().repeat(__.out()).until(__.or(__.loops().is(3),__.has('code','AGR'))).count()"],
143143
// #53
@@ -309,10 +309,40 @@ describe('GremlinLang', function () {
309309
assert.strictEqual(g.V(new Long('9007199254740993')).getGremlinLang().getGremlin(), 'g.V(9007199254740993L)');
310310
});
311311

312+
it('should handle bigint as BigInteger (N suffix)', function () {
313+
assert.strictEqual(g.inject(BigInt(5)).getGremlinLang().getGremlin(), 'g.inject(5N)');
314+
assert.strictEqual(g.inject(BigInt('9223372036854775807')).getGremlinLang().getGremlin(), 'g.inject(9223372036854775807N)');
315+
assert.strictEqual(g.inject(BigInt(10)**BigInt(30)).getGremlinLang().getGremlin(), 'g.inject(1000000000000000000000000000000N)');
316+
});
317+
318+
it('should handle number integer in Int32 range', function () {
319+
assert.strictEqual(g.inject(42).getGremlinLang().getGremlin(), 'g.inject(42)');
320+
});
321+
322+
it('should handle number integer beyond Int32 range', function () {
323+
assert.strictEqual(g.inject(3000000000).getGremlinLang().getGremlin(), 'g.inject(3000000000L)');
324+
});
325+
326+
it('should handle number at Int32 boundaries', function () {
327+
assert.strictEqual(g.inject(2147483647).getGremlinLang().getGremlin(), 'g.inject(2147483647)');
328+
assert.strictEqual(g.inject(2147483648).getGremlinLang().getGremlin(), 'g.inject(2147483648L)');
329+
assert.strictEqual(g.inject(-2147483648).getGremlinLang().getGremlin(), 'g.inject(-2147483648)');
330+
assert.strictEqual(g.inject(-2147483649).getGremlinLang().getGremlin(), 'g.inject(-2147483649L)');
331+
});
332+
333+
it('should handle number float with D suffix', function () {
334+
assert.strictEqual(g.inject(3.14).getGremlinLang().getGremlin(), 'g.inject(3.14D)');
335+
});
336+
312337
it('should handle NaN', function () {
313338
assert.strictEqual(g.inject(NaN).getGremlinLang().getGremlin(), 'g.inject(NaN)');
314339
});
315340

341+
it('should handle number at safe integer boundaries', function () {
342+
assert.strictEqual(g.inject(9007199254740991).getGremlinLang().getGremlin(), 'g.inject(9007199254740991L)');
343+
assert.strictEqual(g.inject(-9007199254740991).getGremlinLang().getGremlin(), 'g.inject(-9007199254740991L)');
344+
});
345+
316346
it('should handle Infinity', function () {
317347
assert.strictEqual(g.inject(Infinity).getGremlinLang().getGremlin(), 'g.inject(+Infinity)');
318348
});

0 commit comments

Comments
 (0)