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

zeroshade pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-go.git


The following commit(s) were added to refs/heads/main by this push:
     new b7a385a0 fix(literals): decimal literal marshalbinary (#745)
b7a385a0 is described below

commit b7a385a06e18bb4792d0f3d7c2245d0b4ebe6bed
Author: Matt Topol <[email protected]>
AuthorDate: Thu Feb 19 21:11:42 2026 -0500

    fix(literals): decimal literal marshalbinary (#745)
    
    fixes #731
---
 literals.go      | 66 ++++++++++++++++++++++++++++++++++++++++++++++++--------
 literals_test.go | 36 +++++++++++++++++++++++++++++++
 2 files changed, 93 insertions(+), 9 deletions(-)

diff --git a/literals.go b/literals.go
index a50f155e..28bcacc5 100644
--- a/literals.go
+++ b/literals.go
@@ -1299,15 +1299,56 @@ func (d DecimalLiteral) MarshalBinary() (data []byte, 
err error) {
        // stored as unscaled value in two's compliment big-endian values
        // using the minimum number of bytes for the values
        n := decimal128.Num(d.Val).BigInt()
-       minBytes := (n.BitLen() + 8) / 8
-       // bytes gives absolute value as big-endian bytes
-       data = n.FillBytes(make([]byte, minBytes))
-       if n.Sign() < 0 {
-               // convert to 2's complement for negative value
-               for i, v := range data {
-                       data[i] = ^v
+
+       if n.Sign() >= 0 {
+               // For non-negative values, use standard encoding
+               minBytes := (n.BitLen() + 8) / 8
+               if minBytes == 0 {
+                       minBytes = 1
                }
-               data[len(data)-1] += 1
+               data = n.FillBytes(make([]byte, minBytes))
+
+               return data, err
+       }
+
+       // For negative values, we need to compute minBytes differently
+       // to handle byte boundaries correctly
+       bitLen := n.BitLen()
+       minBytes := (bitLen + 7) / 8
+
+       // When BitLen is exactly at a byte boundary (multiple of 8),
+       // we need to check if we need an extra byte for the sign bit
+       if bitLen%8 == 0 {
+               absVal := new(big.Int).Abs(n)
+               // If |n| is exactly 2^(bitLen-1), it fits perfectly in 
bitLen/8 bytes
+               // Examples: -128 (2^7), -32768 (2^15), -8388608 (2^23)
+               powerOf2 := new(big.Int).Lsh(big.NewInt(1), uint(bitLen-1))
+               if absVal.Cmp(powerOf2) == 0 {
+                       minBytes = bitLen / 8
+               } else {
+                       // BitLen is at boundary but value isn't exactly 
2^(bitLen-1)
+                       // Examples: -255, -32767
+                       // We need an extra byte to ensure the sign bit is set
+                       minBytes++
+               }
+       }
+
+       if minBytes == 0 {
+               minBytes = 1
+       }
+
+       // Convert to two's complement using proper carry propagation
+       data = n.FillBytes(make([]byte, minBytes))
+       // Invert all bits
+       for i := range data {
+               data[i] = ^data[i]
+       }
+       // Add 1 with proper carry propagation
+       carry := byte(1)
+       for i := len(data) - 1; i >= 0 && carry > 0; i-- {
+               sum := uint16(data[i]) + uint16(carry)
+               data[i] = byte(sum)
+               carry = byte(sum >> 8)
        }
 
        return data, err
@@ -1334,7 +1375,14 @@ func (d *DecimalLiteral) UnmarshalBinary(data []byte) 
error {
        for i, b := range data {
                out[i] = ^b
        }
-       out[len(out)-1] += 1
+
+       // Add 1 with proper carry propagation
+       carry := byte(1)
+       for i := len(out) - 1; i >= 0 && carry > 0; i-- {
+               sum := uint16(out[i]) + uint16(carry)
+               out[i] = byte(sum)
+               carry = byte(sum >> 8)
+       }
 
        value := (&big.Int{}).SetBytes(out)
        d.Val = decimal128.FromBigInt(value.Neg(value))
diff --git a/literals_test.go b/literals_test.go
index d90170cb..3ba9f396 100644
--- a/literals_test.go
+++ b/literals_test.go
@@ -1308,3 +1308,39 @@ func TestDecimalMaxMinRoundTrip(t *testing.T) {
                })
        }
 }
+
+func TestDecimalMarshalBinaryIssue731(t *testing.T) {
+       tests := []struct {
+               name     string
+               value    int64
+               expected []byte
+       }{
+               // Issue 1: Lost carry for -256
+               {"lost carry -256", -256, []byte{0xff, 0x00}},
+               // Issue 2: Wrong minBytes for powers of 2
+               {"-128 (-2^7)", -128, []byte{0x80}},
+               {"-32768 (-2^15)", -32768, []byte{0x80, 0x00}},
+               {"-8388608 (-2^23)", -8388608, []byte{0x80, 0x00, 0x00}},
+               // Additional edge cases
+               {"-127 (not power of 2)", -127, []byte{0x81}},
+               {"-255", -255, []byte{0xff, 0x01}},
+               {"-257", -257, []byte{0xfe, 0xff}},
+               {"-32767", -32767, []byte{0x80, 0x01}},
+               {"-1", -1, []byte{0xff}},
+       }
+
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       dec := iceberg.DecimalLiteral{Val: 
decimal128.FromI64(tt.value), Scale: 0}
+                       data, err := dec.MarshalBinary()
+                       require.NoError(t, err)
+                       assert.Equal(t, tt.expected, data, "value: %d", 
tt.value)
+
+                       // Round-trip test
+                       var decoded iceberg.DecimalLiteral
+                       err = decoded.UnmarshalBinary(data)
+                       require.NoError(t, err)
+                       assert.True(t, dec.Equals(decoded), "round-trip failed 
for %d: got %v", tt.value, decoded)
+               })
+       }
+}

Reply via email to