This is an automated email from the ASF dual-hosted git repository.
colegreer pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
The following commit(s) were added to refs/heads/master by this push:
new 06e03bae51 Add GremlinLang number type suffixes and fix Long
deserialization in JS GLV (#3340)
06e03bae51 is described below
commit 06e03bae51a3f0ea4316fe423d1e7b9c76d67d8c
Author: kirill-stepanishin <[email protected]>
AuthorDate: Thu Mar 26 11:23:34 2026 -0700
Add GremlinLang number type suffixes and fix Long deserialization in JS GLV
(#3340)
---
CHANGELOG.asciidoc | 1 +
docs/src/reference/gremlin-variants.asciidoc | 10 ++++++
.../gremlin-javascript/lib/process/gremlin-lang.ts | 15 ++++++---
.../io/binary/internals/LongSerializer.js | 6 ++--
.../internals/NumberSerializationStrategy.js | 5 ++-
.../test/unit/graphbinary/model-test.js | 6 ++--
.../test/unit/graphbinary/model.js | 6 ++--
.../test/unit/gremlin-lang-test.js | 36 ++++++++++++++++++++--
8 files changed, 65 insertions(+), 20 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 31176d6d86..a3ea259df7 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -25,6 +25,7 @@
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
[[release-4-0-0]]
=== TinkerPop 4.0.0 (NOT OFFICIALLY RELEASED YET)
+* 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.
* Added grammar-based `Translator` for `gremlin-javascript` supporting
translation to JavaScript, Python, Go, .NET, Java, Groovy, canonical, and
anonymized output.
* 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.
* Modified `gremlin-mcp` to support offline mode where utility tools
(translate, format) remain available without a configured
`GREMLIN_MCP_ENDPOINT`.
diff --git a/docs/src/reference/gremlin-variants.asciidoc
b/docs/src/reference/gremlin-variants.asciidoc
index 02c0cc4218..25cfe72f54 100644
--- a/docs/src/reference/gremlin-variants.asciidoc
+++ b/docs/src/reference/gremlin-variants.asciidoc
@@ -1928,6 +1928,16 @@ the possibility of `null`.
[[gremlin-javascript-limitations]]
=== Limitations
+* JavaScript's `Number` type is an IEEE 754 double-precision float. `Float`,
`Byte`, and `Short` values from the server
+are deserialized as `Number` and lose their original type information.
+* `Long` values outside the safe integer range (|n| > 2^53 - 1) are
deserialized as `BigInt` to preserve precision.
+Values within the safe range are deserialized as `Number`. The same server
type may produce different JavaScript types.
+* `Number.isInteger(1.0)` is `true` in JavaScript, so the driver cannot
distinguish integer values from whole-number
+doubles. `BigDecimal` is not implemented.
+* The driver applies GremlinLang type suffixes automatically based on value
characteristics: integers within the 32-bit
+signed range are unsuffixed (Int), integers beyond that up to
`Number.MAX_SAFE_INTEGER` use the `L` suffix (Long),
+non-integer numbers and integers beyond the safe range use the `D` suffix
(Double), and `BigInt` values use the `N`
+suffix (BigInteger).
* The `subgraph()`-step is not supported by any variant that is not running on
the Java Virtual Machine as there is
no `Graph` instance to deserialize a result into on the client-side. A
workaround is to replace the step with
`aggregate(local)` and then convert those results to something the client can
use locally.
diff --git a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
index c75e742a2d..c25d8c595c 100644
--- a/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/gremlin-lang.ts
@@ -71,15 +71,22 @@ export default class GremlinLang {
if (arg instanceof Long) {
return String(arg.value) + 'L';
}
- if (arg instanceof Date) {
- const iso = arg.toISOString();
- return `datetime("${iso}")`;
+ if (typeof arg === 'bigint') {
+ return String(arg) + 'N';
}
if (typeof arg === 'number') {
if (Number.isNaN(arg)) return 'NaN';
if (arg === Infinity) return '+Infinity';
if (arg === -Infinity) return '-Infinity';
- return String(arg);
+ if (!Number.isInteger(arg)) return String(arg) + 'D';
+ if (arg >= -2147483648 && arg <= 2147483647) return String(arg);
+ // Outside safe integer range, values have lost precision and may exceed
Java Long — emit as Double.
+ if (arg > Number.MAX_SAFE_INTEGER || arg < -Number.MAX_SAFE_INTEGER)
return String(arg) + 'D';
+ return String(arg) + 'L';
+ }
+ if (arg instanceof Date) {
+ const iso = arg.toISOString();
+ return `datetime("${iso}")`;
}
if (typeof arg === 'string') {
// JSON.stringify handles all special character escaping in one call.
diff --git
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js
index 745ea58315..4476e48149 100644
---
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js
+++
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/LongSerializer.js
@@ -92,12 +92,10 @@ export default class LongSerializer {
len += 8;
let v = cursor.readBigInt64BE();
- if (v < Number.MIN_SAFE_INTEGER || v > Number.MAX_SAFE_INTEGER) {
- // Keeps the same contract as GraphSON LongSerializer — converts to
Number (loses precision beyond 2^53).
- v = parseFloat(v.toString());
- } else {
+ if (v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER) {
v = Number(v);
}
+ // Values outside safe integer range stay as BigInt to preserve
precision.
return { v, len };
} catch (err) {
diff --git
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
index 1950ac20f5..c457935e45 100644
---
a/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
+++
b/gremlin-js/gremlin-javascript/lib/structure/io/binary/internals/NumberSerializationStrategy.js
@@ -58,11 +58,10 @@ export default class NumberSerializationStrategy {
// INT32_MIN/MAX
return this.ioc.intSerializer.serialize(item, fullyQualifiedFormat);
}
- // eslint-disable-next-line no-loss-of-precision
- if (item >= -9223372036854775808 && item < 9223372036854775807) {
- // INT64_MIN/MAX
+ if (item >= Number.MIN_SAFE_INTEGER && item <= Number.MAX_SAFE_INTEGER) {
return this.ioc.longSerializer.serialize(item, fullyQualifiedFormat);
}
+ // Integers outside safe range are huge doubles that only appear
integral due to IEEE 754 precision loss
return this.ioc.doubleSerializer.serialize(item, fullyQualifiedFormat);
}
diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js
b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js
index d1b36cffe8..4f9beaeaa5 100644
--- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model-test.js
@@ -174,7 +174,7 @@ function invalidDateComparator(actual, expected) {
}
describe('GraphBinary v4 Model Tests', () => {
- // run mode (32 entries)
+ // run mode (31 entries)
run('pos-biginteger');
run('neg-biginteger');
run('empty-binary');
@@ -196,7 +196,6 @@ describe('GraphBinary v4 Model Tests', () => {
run('no-prop-edge');
run('max-int');
run('min-int');
- run('min-long');
run('empty-map');
run('traversal-path');
run('empty-path');
@@ -208,7 +207,7 @@ describe('GraphBinary v4 Model Tests', () => {
run('out-direction');
run('neg-zero-double', negZeroComparator);
- // runWriteRead mode (22 entries)
+ // runWriteRead mode (23 entries)
runWriteRead('min-byte');
runWriteRead('max-byte');
runWriteRead('max-float');
@@ -221,6 +220,7 @@ describe('GraphBinary v4 Model Tests', () => {
runWriteRead('var-bulklist');
runWriteRead('empty-bulklist');
runWriteRead('traversal-edge');
+ runWriteRead('min-long');
runWriteRead('max-long');
runWriteRead('var-type-set', setComparator);
runWriteRead('max-short');
diff --git a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model.js
b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model.js
index 3497c3361b..b064273aa8 100644
--- a/gremlin-js/gremlin-javascript/test/unit/graphbinary/model.js
+++ b/gremlin-js/gremlin-javascript/test/unit/graphbinary/model.js
@@ -99,9 +99,9 @@ model['no-prop-edge'] = new Edge(
model['max-int'] = 2147483647;
model['min-int'] = -2147483648;
-// Long values (lose precision beyond 2^53 in JS)
-model['max-long'] = 9223372036854776000; // Number, not BigInt
-model['min-long'] = -9223372036854776000;
+// Long values
+model['max-long'] = 9223372036854775807n;
+model['min-long'] = -9223372036854775808n;
// Map values
const dateKey = new Date(Date.UTC(1970, 0, 1, 0, 24, 41, 295)); // 1481295 ms
diff --git a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
index cf647cc954..9d359a163c 100644
--- a/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
+++ b/gremlin-js/gremlin-javascript/test/unit/gremlin-lang-test.js
@@ -43,7 +43,7 @@ describe('GremlinLang', function () {
// #3
[g.V().constant(5), 'g.V().constant(5)'],
// #4
- [g.V().constant(1.5), 'g.V().constant(1.5)'],
+ [g.V().constant(1.5), 'g.V().constant(1.5D)'],
// #5
[g.V().constant('Hello'), "g.V().constant('Hello')"],
// #6
@@ -117,7 +117,7 @@ describe('GremlinLang', function () {
// #40
[g.V().has('runways',P.gt(5)).count(),
"g.V().has('runways',gt(5)).count()"],
// #41
- [g.V().has('runways',P.lte(5.3)).count(),
"g.V().has('runways',lte(5.3)).count()"],
+ [g.V().has('runways',P.lte(5.3)).count(),
"g.V().has('runways',lte(5.3D)).count()"],
// #42
[g.V().has('code',P.within([123,124])),
"g.V().has('code',within([123,124]))"],
// #43
@@ -137,7 +137,7 @@ describe('GremlinLang', function () {
// #50
[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'))"],
// #51
- [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'))"],
+ [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'))"],
// #52
[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()"],
// #53
@@ -309,10 +309,40 @@ describe('GremlinLang', function () {
assert.strictEqual(g.V(new
Long('9007199254740993')).getGremlinLang().getGremlin(),
'g.V(9007199254740993L)');
});
+ it('should handle bigint as BigInteger (N suffix)', function () {
+ assert.strictEqual(g.inject(BigInt(5)).getGremlinLang().getGremlin(),
'g.inject(5N)');
+
assert.strictEqual(g.inject(BigInt('9223372036854775807')).getGremlinLang().getGremlin(),
'g.inject(9223372036854775807N)');
+
assert.strictEqual(g.inject(BigInt(10)**BigInt(30)).getGremlinLang().getGremlin(),
'g.inject(1000000000000000000000000000000N)');
+ });
+
+ it('should handle number integer in Int32 range', function () {
+ assert.strictEqual(g.inject(42).getGremlinLang().getGremlin(),
'g.inject(42)');
+ });
+
+ it('should handle number integer beyond Int32 range', function () {
+ assert.strictEqual(g.inject(3000000000).getGremlinLang().getGremlin(),
'g.inject(3000000000L)');
+ });
+
+ it('should handle number at Int32 boundaries', function () {
+ assert.strictEqual(g.inject(2147483647).getGremlinLang().getGremlin(),
'g.inject(2147483647)');
+ assert.strictEqual(g.inject(2147483648).getGremlinLang().getGremlin(),
'g.inject(2147483648L)');
+ assert.strictEqual(g.inject(-2147483648).getGremlinLang().getGremlin(),
'g.inject(-2147483648)');
+ assert.strictEqual(g.inject(-2147483649).getGremlinLang().getGremlin(),
'g.inject(-2147483649L)');
+ });
+
+ it('should handle number float with D suffix', function () {
+ assert.strictEqual(g.inject(3.14).getGremlinLang().getGremlin(),
'g.inject(3.14D)');
+ });
+
it('should handle NaN', function () {
assert.strictEqual(g.inject(NaN).getGremlinLang().getGremlin(),
'g.inject(NaN)');
});
+ it('should handle number at safe integer boundaries', function () {
+
assert.strictEqual(g.inject(9007199254740991).getGremlinLang().getGremlin(),
'g.inject(9007199254740991L)');
+
assert.strictEqual(g.inject(-9007199254740991).getGremlinLang().getGremlin(),
'g.inject(-9007199254740991L)');
+ });
+
it('should handle Infinity', function () {
assert.strictEqual(g.inject(Infinity).getGremlinLang().getGremlin(),
'g.inject(+Infinity)');
});