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

jrmccluskey pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git


The following commit(s) were added to refs/heads/master by this push:
     new e87f50e3a9a Fix Python VarIntCoder OverflowError on uint64 values 
(#39047)
e87f50e3a9a is described below

commit e87f50e3a9a0d5650cfb53af805fa8691981d277
Author: Avi Kondareddy <[email protected]>
AuthorDate: Wed Jul 1 07:24:09 2026 -0700

    Fix Python VarIntCoder OverflowError on uint64 values (#39047)
    
    * [BEAM] Fix Python VarIntCoder OverflowError on uint64 values
    
    The Cython write_var_int64/get_varint_size stream methods take a signed
    int64_t parameter. A Python int in the unsigned 64-bit range [2**63,
    2**64) -- a uint64 -- is converted to that signed parameter at the call
    boundary and rejected with an OverflowError before the method body runs,
    even though the body already operates on the unsigned bit pattern and the
    VarInt wire encoding is well-defined.
    
    Fold such values to the signed int64 with the identical bit pattern (and
    thus identical VarInt encoding) before handing them to the stream. This
    matches Java's signed VarIntCoder on the wire; decoding remains signed.
    Values past 64 bits are left unchanged and still overflow downstream,
    preserving the coder's documented 64-bit limit.
    
    Adds test_varint_coder_uint64 covering no-overflow encoding, wire
    equivalence to the signed twin, size estimation, signed decode, and the
    still-raising out-of-range case (guarded on the compiled implementation).
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * Enforce 64-bit range in _as_signed_int64 for path parity
    
    Address review feedback: raise OverflowError in _as_signed_int64 for
    values outside the 64-bit range (reusing fits_in_64_bits) so the
    pure-Python path matches the Cython int64_t overflow instead of silently
    encoding out-of-range values. Both encode_to_stream and estimate_size
    already wrap OverflowError, so the user-facing message is unchanged.
    
    Drop the is_compiled guard in test_varint_coder_uint64 so the
    out-of-range case runs on both paths, add the negative lower-bound case
    (-(2**63) - 1), and remove the now-unused coder_impl import.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    * Fix uint64 overflow in VarIntCoderImpl.encode fast path
    
    The compiled encode() cast value to a C int64_t (ivalue, typed via
    coder_impl.pxd) for the small-ints fast path, which overflowed on uint64
    values before reaching encode_to_stream's fold -- the cause of the
    test_varint_coder_uint64 CI failure on the Python PreCommit suites.
    
    Do the small-ints check with a plain Python-object comparison so it can't
    overflow; non-small values (including uint64 and out-of-range ints) fall
    through to StreamCoderImpl.encode -> encode_to_stream, where
    _as_signed_int64 folds them and raises the wrapped OverflowError for
    genuinely out-of-range values. Drop the now-unused ivalue int64_t local
    hint from coder_impl.pxd.
    
    Co-Authored-By: Claude Opus 4.8 <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 4.8 <[email protected]>
---
 sdks/python/apache_beam/coders/coder_impl.pxd      |  1 -
 sdks/python/apache_beam/coders/coder_impl.py       | 34 +++++++++++++++++-----
 .../apache_beam/coders/coders_test_common.py       | 18 ++++++++++++
 3 files changed, 44 insertions(+), 9 deletions(-)

diff --git a/sdks/python/apache_beam/coders/coder_impl.pxd 
b/sdks/python/apache_beam/coders/coder_impl.pxd
index e64177e6fd3..89730c17fe1 100644
--- a/sdks/python/apache_beam/coders/coder_impl.pxd
+++ b/sdks/python/apache_beam/coders/coder_impl.pxd
@@ -135,7 +135,6 @@ cdef class TimestampCoderImpl(StreamCoderImpl):
 
 cdef list small_ints
 cdef class VarIntCoderImpl(StreamCoderImpl):
-  @cython.locals(ivalue=libc.stdint.int64_t)
   cpdef bytes encode(self, value)
 
 
diff --git a/sdks/python/apache_beam/coders/coder_impl.py 
b/sdks/python/apache_beam/coders/coder_impl.py
index 0bded25e05d..e6b1fb6c37c 100644
--- a/sdks/python/apache_beam/coders/coder_impl.py
+++ b/sdks/python/apache_beam/coders/coder_impl.py
@@ -90,6 +90,20 @@ else:
 is_compiled = False
 fits_in_64_bits = lambda x: -(1 << 63) <= x <= (1 << 63) - 1
 
+
+def _as_signed_int64(value):
+  # type: (int) -> int
+
+  """Folds a uint64 to the signed int64 with the same bits (and VarInt
+  encoding), which the Cython int64_t stream params accept. Values outside the
+  64-bit range raise here so the pure-Python path matches Cython's overflow."""
+  if (1 << 63) <= value < (1 << 64):
+    return int(value) - (1 << 64)
+  if not fits_in_64_bits(value):
+    raise OverflowError("%d is out of range for a 64-bit integer." % value)
+  return value
+
+
 if TYPE_CHECKING or SLOW_STREAM:
   from .slow_stream import ByteCountingOutputStream
   from .slow_stream import InputStream as create_InputStream
@@ -1044,12 +1058,14 @@ class VarIntCoderImpl(StreamCoderImpl):
   def encode_to_stream(self, value, out, nested):
     # type: (int, create_OutputStream, bool) -> None
     try:
-      out.write_var_int64(value)
+      # Fold uint64 values into signed int64 so Cython doesn't overflow.
+      out.write_var_int64(_as_signed_int64(value))
     except OverflowError as e:
       raise OverflowError(
           f"Integer value '{value}' is out of the encodable range for "
-          f"VarIntCoder. This coder is limited to values that fit "
-          f"within a 64-bit signed integer (-(2**63) to 2**63 - 1). "
+          f"VarIntCoder. This coder is limited to 64-bit integers: the "
+          f"signed range -(2**63) to 2**63 - 1, plus unsigned values up to "
+          f"2**64 - 1 which share the same wire encoding. "
           f"Original error: {e}") from e
 
   def decode_from_stream(self, in_stream, nested):
@@ -1057,9 +1073,11 @@ class VarIntCoderImpl(StreamCoderImpl):
     return in_stream.read_var_int64()
 
   def encode(self, value):
-    ivalue = value  # type cast
-    if 0 <= ivalue < len(small_ints):
-      return small_ints[ivalue]
+    # Compare as a Python object: a uint64 value overflows the int64_t cast
+    # the compiled fast path used to do here. Non-small values (including
+    # uint64) fall through to encode_to_stream, which folds them.
+    if 0 <= value < len(small_ints):
+      return small_ints[value]
     return StreamCoderImpl.encode(self, value)
 
   def decode(self, encoded):
@@ -1073,11 +1091,11 @@ class VarIntCoderImpl(StreamCoderImpl):
     # type: (Any, bool) -> int
     # Note that VarInts are encoded the same way regardless of nesting.
     try:
-      return get_varint_size(value)
+      return get_varint_size(_as_signed_int64(value))
     except OverflowError as e:
       raise OverflowError(
           f"Cannot estimate size for integer value '{value}'. "
-          f"Value is out of the range for VarIntCoder (64-bit signed integer). 
"
+          f"Value is out of the range for VarIntCoder (64-bit integer). "
           f"Original error: {e}") from e
 
 
diff --git a/sdks/python/apache_beam/coders/coders_test_common.py 
b/sdks/python/apache_beam/coders/coders_test_common.py
index 422d494b61c..939e44aff46 100644
--- a/sdks/python/apache_beam/coders/coders_test_common.py
+++ b/sdks/python/apache_beam/coders/coders_test_common.py
@@ -437,6 +437,24 @@ class CodersTest(unittest.TestCase):
             for k in range(0, int(math.log(MAX_64_BIT_INT)))
         ])
 
+  def test_varint_coder_uint64(self):
+    # uint64 values [2**63, 2**64) must encode like the signed int64 with the
+    # same bits instead of overflowing Cython's int64_t. Decoding is signed,
+    # matching Java's VarIntCoder.
+    coder = coders.VarIntCoder()
+    impl = coder.get_impl()
+    for v in [1 << 63, (1 << 63) + 12345, (1 << 64) - 1]:
+      signed_twin = v - (1 << 64)
+      encoded = coder.encode(v)
+      self.assertEqual(encoded, coder.encode(signed_twin))
+      self.assertEqual(impl.estimate_size(v), len(encoded))
+      self.assertEqual(coder.decode(encoded), signed_twin)
+
+    # Values outside the 64-bit range stay out of range on both paths.
+    for v in [1 << 64, (1 << 70), -(1 << 63) - 1]:
+      with self.assertRaises(OverflowError):
+        coder.encode(v)
+
   def test_varint32_coder(self):
     # Small ints.
     self.check_coder(coders.VarInt32Coder(), *range(-10, 10))

Reply via email to