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 |