This is an automated email from the ASF dual-hosted git repository.

Cole-Greer pushed a commit to branch 3.7-dev
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git


The following commit(s) were added to refs/heads/3.7-dev by this push:
     new a7a6158612 TINKERPOP-3275 Fix gremlin-python GraphBinary 
BigInteger/BigDecimal serialization (#3539)
a7a6158612 is described below

commit a7a6158612bfcdb6082651c4e6b0073e7873fe8b
Author: Guian Gumpac <[email protected]>
AuthorDate: Thu Jul 23 12:50:17 2026 -0700

    TINKERPOP-3275 Fix gremlin-python GraphBinary BigInteger/BigDecimal 
serialization (#3539)
    
    gremlin-python's GraphBinary BigIntIO writer computed the byte length from 
the
    value's magnitude bit-length but encoded the bytes as signed 
two's-complement. Negative
    boundary values (e.g. -129, -255) raised OverflowError, and positive/zero 
values
    produced non-canonical bytes that diverge from the Java reference 
(BigInteger.toByteArray()).
    
    The fix computes the minimal signed two's-complement length, unifying the 
positive, negative,
    and zero paths. BigDecimal is fixed transitively (its unscaled value is 
serialized as a
    BigInteger).
    
    Assisted-by: Kiro:claude-opus-4.8
---
 CHANGELOG.asciidoc                                 |  1 +
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  1 +
 gremlin-go/driver/cucumber/gremlin.go              |  1 +
 .../gremlin-javascript/test/cucumber/gremlin.js    |  1 +
 .../gremlin_python/structure/io/graphbinaryV1.py   | 18 ++++-------
 .../src/main/python/tests/feature/gremlin.py       |  1 +
 .../python/tests/unit/io/test_graphbinaryV1.py     | 37 ++++++++++++++++++++++
 .../test/features/sideEffect/Inject.feature        | 19 ++++++++++-
 8 files changed, 67 insertions(+), 12 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 208cefd4a7..d6088e041e 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -38,6 +38,7 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Fixed a panic in `gremlin-go` `PartitionStrategy` when `ReadPartitions` was 
left unset.
 * Fixed `gremlin-python` `ProductiveByStrategy` to pass through the 
`productiveKeys` argument, which was previously accepted but never serialized 
to the server.
 * Deprecated `ProductiveByStrategy` which was introduced as a temporary way to 
mimic pre-3.5.0 null processing behavior.
+* Fixed `gremlin-python` GraphBinary serialization of 
`BigInteger`/`BigDecimal` negative boundary values (e.g. `-129`) that raised 
`OverflowError`.
 
 [[release-3-7-6]]
 === TinkerPop 3.7.6 (Release Date: April 1, 2026)
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index baf112b027..ba2f9ab357 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -1538,6 +1538,7 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_VX1X_valuesXageX_inject", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.V(p["xx1"]).Values<object>("age").Inject()}}, 
                {"g_injectXnull_1_3_nullX_asXaX_selectXaX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject<object>(null,1,3,null).As("a").Select<object>("a")}}, 
                {"g_injectX1_3lX_injectX100_300X", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject(1,3).Inject(100,300)}}, 
+               {"g_injectXbigintBoundaryValuesX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Inject(p["xx1"],p["xx2"],p["xx3"])}}, 
                {"g_io_readXkryoX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.Io<object>("data/tinkerpop-modern.kryo").Read(), (g,p) =>g.V(), (g,p) 
=>g.E()}}, 
                {"g_io_read_withXreader_gryoX", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) 
=>g.Io<object>("data/tinkerpop-modern.kryo").With("~tinkerpop.io.reader","gryo").Read(),
 (g,p) =>g.V(), (g,p) =>g.E()}}, 
                {"g_io_readXgraphsonX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.Io<object>("data/tinkerpop-modern.json").Read(), (g,p) =>g.V(), (g,p) 
=>g.E()}}, 
diff --git a/gremlin-go/driver/cucumber/gremlin.go 
b/gremlin-go/driver/cucumber/gremlin.go
index 4d284cb8e9..23cb7dfeba 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -1509,6 +1509,7 @@ var translationMap = map[string][]func(g 
*gremlingo.GraphTraversalSource, p map[
     "g_VX1X_valuesXageX_inject": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.V(p["xx1"]).Values("age").Inject()}}, 
     "g_injectXnull_1_3_nullX_asXaX_selectXaX": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.Inject(nil, 1, 3, 
nil).As("a").Select("a")}}, 
     "g_injectX1_3lX_injectX100_300X": {func(g *gremlingo.GraphTraversalSource, 
p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(1, 
3).Inject(100, 300)}}, 
+    "g_injectXbigintBoundaryValuesX": {func(g *gremlingo.GraphTraversalSource, 
p map[string]interface{}) *gremlingo.GraphTraversal {return g.Inject(p["xx1"], 
p["xx2"], p["xx3"])}}, 
     "g_io_readXkryoX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Io("data/tinkerpop-modern.kryo").Read()}, func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V()}, func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.E()}}, 
     "g_io_read_withXreader_gryoX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Io("data/tinkerpop-modern.kryo").With("~tinkerpop.io.reader", 
"gryo").Read()}, func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.V()}, func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.E()}}, 
     "g_io_readXgraphsonX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Io("data/tinkerpop-modern.json").Read()}, func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V()}, func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.E()}}, 
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index b49f992762..46f7b0d1a2 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -1529,6 +1529,7 @@ const gremlins = {
     g_VX1X_valuesXageX_inject: [function({g, xx1}) { return 
g.V(xx1).values("age").inject() }], 
     g_injectXnull_1_3_nullX_asXaX_selectXaX: [function({g}) { return 
g.inject(null,1,3,null).as("a").select("a") }], 
     g_injectX1_3lX_injectX100_300X: [function({g}) { return 
g.inject(1,3).inject(100,300) }], 
+    g_injectXbigintBoundaryValuesX: [function({g, xx1, xx3, xx2}) { return 
g.inject(xx1,xx2,xx3) }], 
     g_io_readXkryoX: [function({g}) { return 
g.io("data/tinkerpop-modern.kryo").read() }, function({g}) { return g.V() }, 
function({g}) { return g.E() }], 
     g_io_read_withXreader_gryoX: [function({g}) { return 
g.io("data/tinkerpop-modern.kryo").with_("~tinkerpop.io.reader","gryo").read() 
}, function({g}) { return g.V() }, function({g}) { return g.E() }], 
     g_io_readXgraphsonX: [function({g}) { return 
g.io("data/tinkerpop-modern.json").read() }, function({g}) { return g.V() }, 
function({g}) { return g.E() }], 
diff --git 
a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py 
b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
index 14c73e48a3..e82d0ed41c 100644
--- 
a/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
+++ 
b/gremlin-python/src/main/python/gremlin_python/structure/io/graphbinaryV1.py
@@ -300,17 +300,13 @@ class BigIntIO(_GraphBinaryTypeIO):
 
     @classmethod
     def write_bigint(cls, obj, to_extend):
-        length = (obj.bit_length() + 7) // 8
-        if obj > 0:
-            b = obj.to_bytes(length, byteorder='big')
-            to_extend.extend(int32_pack(length + 1))
-            to_extend.extend(int8_pack(0))
-            to_extend.extend(b)
-        else:
-            # handle negative
-            b = obj.to_bytes(length, byteorder='big', signed=True)
-            to_extend.extend(int32_pack(length))
-            to_extend.extend(b)
+        # Compute the minimal signed two's-complement byte length, matching the
+        # Java reference serializer (BigInteger.toByteArray()).
+        bit_length = obj.bit_length() if obj >= 0 else (obj + 1).bit_length()
+        length = bit_length // 8 + 1
+        b = obj.to_bytes(length, byteorder='big', signed=True)
+        to_extend.extend(int32_pack(length))
+        to_extend.extend(b)
         return to_extend
 
     @classmethod
diff --git a/gremlin-python/src/main/python/tests/feature/gremlin.py 
b/gremlin-python/src/main/python/tests/feature/gremlin.py
index 22e51e7b4b..93907ae78e 100644
--- a/gremlin-python/src/main/python/tests/feature/gremlin.py
+++ b/gremlin-python/src/main/python/tests/feature/gremlin.py
@@ -1511,6 +1511,7 @@ world.gremlins = {
     'g_VX1X_valuesXageX_inject': [(lambda g, xx1=None:g.V(xx1).age.inject())], 
     'g_injectXnull_1_3_nullX_asXaX_selectXaX': [(lambda 
g:g.inject(None,1,3,None).as_('a').select('a'))], 
     'g_injectX1_3lX_injectX100_300X': [(lambda 
g:g.inject(1,3).inject(100,300))], 
+    'g_injectXbigintBoundaryValuesX': [(lambda g, 
xx1=None,xx3=None,xx2=None:g.inject(xx1,xx2,xx3))], 
     'g_io_readXkryoX': [(lambda g:g.io('data/tinkerpop-modern.kryo').read()), 
(lambda g:g.V()), (lambda g:g.E())], 
     'g_io_read_withXreader_gryoX': [(lambda 
g:g.io('data/tinkerpop-modern.kryo').with_('~tinkerpop.io.reader','gryo').read()),
 (lambda g:g.V()), (lambda g:g.E())], 
     'g_io_readXgraphsonX': [(lambda 
g:g.io('data/tinkerpop-modern.json').read()), (lambda g:g.V()), (lambda 
g:g.E())], 
diff --git a/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py 
b/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
index b1234369c1..b4624584b4 100644
--- a/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
+++ b/gremlin-python/src/main/python/tests/unit/io/test_graphbinaryV1.py
@@ -54,6 +54,35 @@ class TestGraphSONWriter(object):
         output = 
self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
         assert x == output
 
+    def test_bigint_boundaries(self):
+        # Boundary values around signed byte-width transitions, including
+        # negative boundaries that previously raised OverflowError 
(TINKERPOP-3275).
+        # One representative per equivalence class of the length formula: zero,
+        # positive byte-width boundary (127/128), negative power-of-two (-128),
+        # negative just past a boundary (-129/-255, previously OverflowError), 
the
+        # obj+1==0 edge (-1), and arbitrary-precision values of both signs.
+        values = [0, 1, -1, 127, 128, -128, -129, -255,
+                  0x1000_0000_0000_0000_0000, -0x1000_0000_0000_0000_0000]
+        for v in values:
+            x = bigint(v)
+            output = 
self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
+            assert x == output
+
+    def test_bigint_wire_bytes(self):
+        # Lock in exact minimal signed two's-complement bytes, matching the 
Java
+        # reference BigInteger.toByteArray() (guards against non-minimal 
encoding).
+        from gremlin_python.structure.io.graphbinaryV1 import BigIntIO
+        cases = {
+            0: b'\x00\x00\x00\x01\x00',
+            127: b'\x00\x00\x00\x01\x7f',
+            128: b'\x00\x00\x00\x02\x00\x80',
+            -128: b'\x00\x00\x00\x01\x80',
+            -129: b'\x00\x00\x00\x02\xff\x7f',
+            -256: b'\x00\x00\x00\x02\xff\x00',
+        }
+        for value, expected in cases.items():
+            assert bytes(BigIntIO.write_bigint(value, bytearray())) == expected
+
     def test_float(self):
         x = float(100.001)
         output = 
self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
@@ -82,6 +111,14 @@ class TestGraphSONWriter(object):
         assert x.scale == output.scale
         assert x.unscaled_value == output.unscaled_value
 
+    def test_bigdecimal_negative_boundaries(self):
+        # Negative unscaled values at byte-width boundaries previously raised
+        # OverflowError during serialization (TINKERPOP-3275).
+        for x in [BigDecimal(0, -129), BigDecimal(2, -255)]:
+            output = 
self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
+            assert x.scale == output.scale
+            assert x.unscaled_value == output.unscaled_value
+
     def test_date(self):
         x = datetime.datetime(2016, 12, 14, 16, 14, 36, 295000)
         output = 
self.graphbinary_reader.read_object(self.graphbinary_writer.write_object(x))
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Inject.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Inject.feature
index 72504e173d..9acb57253a 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Inject.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/sideEffect/Inject.feature
@@ -217,4 +217,21 @@ Feature: Step - inject()
       | d[100].i |
       | d[300].i |
       | d[1].i |
-      | d[3].i |
\ No newline at end of file
+      | d[3].i |
+
+  @GraphComputerVerificationInjectionNotSupported
+  Scenario: g_injectXbigintBoundaryValuesX
+    Given the empty graph
+    And using the parameter xx1 defined as "d[0].n"
+    And using the parameter xx2 defined as "d[-129].n"
+    And using the parameter xx3 defined as "d[-32769].n"
+    And the traversal of
+      """
+      g.inject(xx1, xx2, xx3)
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | d[0].n |
+      | d[-129].n |
+      | d[-32769].n |

Reply via email to