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/arrow-go.git


The following commit(s) were added to refs/heads/main by this push:
     new 8100cd75 fix(arrow/decimal): handle native word widths in big integer 
conversions (#1409)
8100cd75 is described below

commit 8100cd75076d8fe4ea5ae31190534d1212a7a5c5
Author: Matt Topol <[email protected]>
AuthorDate: Mon Oct 26 15:48:31 2026 +0000

    fix(arrow/decimal): handle native word widths in big integer conversions 
(#1409)
    
    ### Rationale for this change
    
    Found while fixing the 32-bit test failures in #1408.
    
    Decimal conversions are wrong on 32-bit platforms. `decimal128.Num` and
    `decimal256.Num` use 64-bit limbs, but their `big.Int` conversions
    assumed `big.Word` is also 64 bits. On 386 it is 32 bits, so:
    
    - `decimal128.FromBigInt` read `b[0]`/`b[1]` as the low and high limbs
    (`decimal128.go:95–97`), misplacing words and dropping the rest;
    - `decimal128`'s `toBigIntPositive` built `[]big.Word{big.Word(n.lo),
    big.Word(n.hi)}` (`decimal128.go:362`), truncating each limb;
    - `decimal256.FromBigInt` copied word *i* into limb *i*
    (`decimal256.go:370`), misplacing words and panicking when a value
    needed more than four native words;
    - `decimal256`'s `toBigIntPositive` truncated its limbs the same way
    (`decimal256.go:379`).
    
    Anything going through `big.Int` (string formatting and parsing,
    rescaling, `GetOneForMarshal`, CSV decimal parsing) produced wrong
    values or spurious precision errors on 386, e.g. `TestDecimal128Slice`
    printed `-1717986918` instead of `-7.378697629e+18`, and
    `TestDecimal128GetOneForMarshal` turned `1234567890.123456789` into
    `437278697.08398250845211745659132109676`.
    
    The run-end-encoded validation fix that was originally part of this PR
    has moved to #1421.
    
    ### What changes are included in this PR?
    
    - **`arrow/decimal128`, `arrow/decimal256`:** when `bits.UintSize ==
    32`, combine pairs of 32-bit `big.Word`s into each 64-bit limb in
    `FromBigInt`, and split each limb into two words in `toBigIntPositive`.
    `bits.UintSize` is a constant, so 64-bit builds compile the existing
    code unchanged. Also corrects the `decimal128.FromBigInt` doc (it panics
    above 127 bits, as the code already does).
    - **CI:** the 386 job also runs the decimal packages' tests.
    
    ### Are these changes tested?
    
    Yes.
    
    - **On `GOARCH=386`:** `TestDecimal128Slice`,
    `TestDecimal128StringRoundTrip`, `TestDecimal128GetOneForMarshal`,
    `TestDecimal256Slice`, `TestDecimal256GetOneForMarshal`, all nine
    `TestDecimal/TestWithNulls#01` precisions,
    `TestDecimalScalarStringCastUsesExactScaling` and CSV's
    `TestReadCSVDecimalCols` all failed before and pass now. The
    `TestDecimalToBigInt` tests in both packages gain word-boundary cases
    with independently computed expectations, which fail on 386 without the
    fix (decimal256 panics). `arrow/decimal128`, `arrow/decimal256` and
    `arrow/decimal/...` pass in full on 386.
    - **amd64:** `arrow/decimal128`, `arrow/decimal256`, `arrow/decimal/...`
    pass.
    - `go vet` and `gofmt -l` are clean on the changed packages.
    
    ### Are there any user-facing changes?
    
    Yes: decimal values convert correctly on 32-bit platforms. No API
    changes.
---
 .github/workflows/test.yml          |  2 +-
 arrow/decimal128/decimal128.go      | 28 +++++++++++++++++++++-------
 arrow/decimal128/decimal128_test.go | 11 +++++++++++
 arrow/decimal256/decimal256.go      | 20 ++++++++++++++++++--
 arrow/decimal256/decimal256_test.go | 16 ++++++++++++++++
 5 files changed, 67 insertions(+), 10 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1e8985d4..c8801ec9 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -317,7 +317,7 @@ jobs:
         run: GOARCH=386 go build ./...
       - name: Test
         # WIP refactor, only tests in the specified dirs have been fixed
-        run: GOARCH=386 go test ./parquet/file/...
+        run: GOARCH=386 go test ./parquet/file/... ./arrow/decimal/... 
./arrow/decimal128/... ./arrow/decimal256/...
   tinygo:
     name: TinyGo
     runs-on: ubuntu-latest
diff --git a/arrow/decimal128/decimal128.go b/arrow/decimal128/decimal128.go
index fca8c799..2febecea 100644
--- a/arrow/decimal128/decimal128.go
+++ b/arrow/decimal128/decimal128.go
@@ -75,7 +75,7 @@ func FromI64(v int64) Num {
 }
 
 // FromBigInt will convert a big.Int to a Num, if the value in v has a
-// BitLen > 128, this will panic.
+// BitLen > 127, this will panic.
 func FromBigInt(v *big.Int) (n Num) {
        bitlen := v.BitLen()
        if bitlen > 127 {
@@ -86,14 +86,22 @@ func FromBigInt(v *big.Int) (n Num) {
                return
        }
 
-       // if the value is negative, then get the high and low bytes from
-       // v, and then negate it. this is because Num uses a two's compliment
-       // representation of values and big.Int stores the value as a bool for
-       // the sign and the absolute value of the integer. This means that the
-       // raw bytes are *always* the absolute value.
+       // big.Int stores the magnitude in native-sized words with a separate 
sign,
+       // while Num uses two's complement with 64-bit limbs. On 32-bit 
platforms,
+       // combine pairs of words before applying the sign.
        b := v.Bits()
        n.lo = uint64(b[0])
-       if len(b) > 1 {
+       if bits.UintSize == 32 {
+               if len(b) > 1 {
+                       n.lo |= uint64(b[1]) << 32
+               }
+               if len(b) > 2 {
+                       n.hi = int64(b[2])
+               }
+               if len(b) > 3 {
+                       n.hi |= int64(b[3]) << 32
+               }
+       } else if len(b) > 1 {
                n.hi = int64(b[1])
        }
        if v.Sign() < 0 {
@@ -359,6 +367,12 @@ func (n Num) Sign() int {
 }
 
 func toBigIntPositive(n Num) *big.Int {
+       if bits.UintSize == 32 {
+               return (&big.Int{}).SetBits([]big.Word{
+                       big.Word(n.lo), big.Word(n.lo >> 32),
+                       big.Word(n.hi), big.Word(n.hi >> 32),
+               })
+       }
        return (&big.Int{}).SetBits([]big.Word{big.Word(n.lo), big.Word(n.hi)})
 }
 
diff --git a/arrow/decimal128/decimal128_test.go 
b/arrow/decimal128/decimal128_test.go
index ba5539f1..95da30ae 100644
--- a/arrow/decimal128/decimal128_test.go
+++ b/arrow/decimal128/decimal128_test.go
@@ -374,6 +374,13 @@ func TestDecimalToBigInt(t *testing.T) {
                {-18446744073709552, 7083549724304524577, 
"-340282366920938463463374607431711455"},
                {1, 4611686018427387904, "23058430092136939520"},
                {0, 0, "0"},
+               {0, 1, "1"},
+               {0, math.MaxUint32, "4294967295"},
+               {0, 1 << 32, "4294967296"},
+               {0, math.MaxUint64, "18446744073709551615"},
+               {1, 0, "18446744073709551616"},
+               {1 << 32, 0, "79228162514264337593543950336"},
+               {math.MaxInt64, math.MaxUint64, 
"170141183460469231731687303715884105727"},
        }
        for _, tc := range tests {
                t.Run("", func(t *testing.T) {
@@ -384,6 +391,10 @@ func TestDecimalToBigInt(t *testing.T) {
                        n2 := decimal128.FromBigInt(bi)
                        assert.Equal(t, n.LowBits(), n2.LowBits())
                        assert.Equal(t, n.HighBits(), n2.HighBits())
+
+                       bi, ok := new(big.Int).SetString(tc.exp, 10)
+                       require.True(t, ok)
+                       assert.Equal(t, n, decimal128.FromBigInt(bi))
                })
        }
 }
diff --git a/arrow/decimal256/decimal256.go b/arrow/decimal256/decimal256.go
index a473505f..0b45d2e7 100644
--- a/arrow/decimal256/decimal256.go
+++ b/arrow/decimal256/decimal256.go
@@ -357,6 +357,7 @@ func (n Num) Sign() int {
        return int(1 | (int64(n.arr[3]) >> 63))
 }
 
+// FromBigInt converts a big.Int to a Num. It panics if v.BitLen() > 255.
 func FromBigInt(v *big.Int) (n Num) {
        bitlen := v.BitLen()
        if bitlen > 255 {
@@ -366,8 +367,15 @@ func FromBigInt(v *big.Int) (n Num) {
        }
 
        b := v.Bits()
-       for i, bits := range b {
-               n.arr[i] = uint64(bits)
+       if bits.UintSize == 32 {
+               // Each 64-bit limb contains two native-sized big.Int words.
+               for i, word := range b {
+                       n.arr[i/2] |= uint64(word) << (32 * (i % 2))
+               }
+       } else {
+               for i, word := range b {
+                       n.arr[i] = uint64(word)
+               }
        }
        if v.Sign() < 0 {
                return n.Negate()
@@ -376,6 +384,14 @@ func FromBigInt(v *big.Int) (n Num) {
 }
 
 func toBigIntPositive(n Num) *big.Int {
+       if bits.UintSize == 32 {
+               return new(big.Int).SetBits([]big.Word{
+                       big.Word(n.arr[0]), big.Word(n.arr[0] >> 32),
+                       big.Word(n.arr[1]), big.Word(n.arr[1] >> 32),
+                       big.Word(n.arr[2]), big.Word(n.arr[2] >> 32),
+                       big.Word(n.arr[3]), big.Word(n.arr[3] >> 32),
+               })
+       }
        return new(big.Int).SetBits([]big.Word{big.Word(n.arr[0]), 
big.Word(n.arr[1]), big.Word(n.arr[2]), big.Word(n.arr[3])})
 }
 
diff --git a/arrow/decimal256/decimal256_test.go 
b/arrow/decimal256/decimal256_test.go
index 0d1f14f2..5112cb4a 100644
--- a/arrow/decimal256/decimal256_test.go
+++ b/arrow/decimal256/decimal256_test.go
@@ -25,6 +25,7 @@ import (
 
        "github.com/apache/arrow-go/v18/arrow/decimal256"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
 func TestFromU64(t *testing.T) {
@@ -412,6 +413,17 @@ func TestDecimalToBigInt(t *testing.T) {
                {[4]uint64{0, 0, 0, 0}, "0"},
                {[4]uint64{17877984925544397504, 5352188884907840935, 
234631617561833724, 196678011949953713}, 
"1234567890123456789012345678901234567890123456789012345678901234567890123456"},
                {[4]uint64{568759148165154112, 13094555188801710680, 
18212112456147717891, 18250066061759597902}, 
"-1234567890123456789012345678901234567890123456789012345678901234567890123456"},
+               {[4]uint64{1}, "1"},
+               {[4]uint64{math.MaxUint32}, "4294967295"},
+               {[4]uint64{1 << 32}, "4294967296"},
+               {[4]uint64{math.MaxUint64}, "18446744073709551615"},
+               {[4]uint64{0, 1}, "18446744073709551616"},
+               {[4]uint64{0, 1 << 32}, "79228162514264337593543950336"},
+               {[4]uint64{0, 0, 1}, "340282366920938463463374607431768211456"},
+               {[4]uint64{0, 0, 1 << 32}, 
"1461501637330902918203684832716283019655932542976"},
+               {[4]uint64{0, 0, 0, 1}, 
"6277101735386680763835789423207666416102355444464034512896"},
+               {[4]uint64{0, 0, 0, 1 << 32}, 
"26959946667150639794667015087019630673637144422540572481103610249216"},
+               {[4]uint64{math.MaxUint64, math.MaxUint64, math.MaxUint64, 
math.MaxInt64}, 
"57896044618658097711785492504343953926634992332820282019728792003956564819967"},
        }
        for _, tc := range tests {
                t.Run("", func(t *testing.T) {
@@ -421,6 +433,10 @@ func TestDecimalToBigInt(t *testing.T) {
                        assert.Equal(t, tc.exp, bi.String())
                        n2 := decimal256.FromBigInt(bi)
                        assert.Equal(t, n2.Array(), n.Array())
+
+                       bi, ok := new(big.Int).SetString(tc.exp, 10)
+                       require.True(t, ok)
+                       assert.Equal(t, n, decimal256.FromBigInt(bi))
                })
        }
 }

Reply via email to