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)');
     });

Reply via email to