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 d17f6870 fix(arrow/compute): accept type max value in safe 
decimal-to-int cast (#862)
d17f6870 is described below

commit d17f6870a0569b670d985e6eac60741af621bc88
Author: Matt Topol <[email protected]>
AuthorDate: Tue Jun 23 18:49:36 2026 -0400

    fix(arrow/compute): accept type max value in safe decimal-to-int cast (#862)
    
    ### Rationale for this change
    
    The safe `decimal128`/`decimal256` → integer cast kernel range-checks
    each value against the output type's bounds before truncating to the low
    64 bits. The upper-bound check used `value >= max`, where `max` is the
    *inclusive* maximum of the output integer type (e.g. `math.MaxInt64` for
    `int64`, computed via `MaxOf[T]()`). As a result a value exactly equal
    to the type maximum was incorrectly rejected as "integer value out of
    bounds", even though it is a valid in-range value. This affects every
    integer output type (`int8`/`int16`/`int32`/`int64` and the unsigned
    variants), not just `int64`.
    
    This was noticed while working around the behavior downstream in the
    ADBC Snowflake driver (adbc-drivers/snowflake#161), where it was
    suggested to fix it upstream here.
    
    ### What changes are included in this PR?
    
    - In `decimalToIntImpl`
    (`arrow/compute/internal/kernels/numeric_cast.go`), change the
    upper-bound overflow check from `value >= max` to `value > max` so the
    inclusive maximum is accepted, matching the already-inclusive
    lower-bound check (`value < min`). The `decimal[T]` helper interface now
    requires `Greater` instead of `GreaterEqual`; both `decimal128.Num` and
    `decimal256.Num` already implement it.
    
    ### Are these changes tested?
    
    Yes. Added an `int64 bounds inclusive` regression subtest to
    `TestDecimal128ToInt` and `TestDecimal256ToInt` that assert, with
    `AllowIntOverflow=false`:
    - `math.MaxInt64` (`9223372036854775807`) and `math.MinInt64`
    (`-9223372036854775808`) cast successfully, and
    - values one beyond either bound still fail as overflow.
    
    `go test ./arrow/compute/...` passes and `go vet` is clean.
    
    ### Are there any user-facing changes?
    
    Yes — a bug fix. A safe (non-`AllowIntOverflow`) decimal→integer cast of
    a value equal to the destination type's maximum (e.g. `math.MaxInt64`)
    now succeeds instead of returning an `ErrInvalid` "integer value out of
    bounds" error. Values genuinely outside the range still error as before.
---
 arrow/compute/cast_test.go                     | 32 ++++++++++++++++++++++++++
 arrow/compute/internal/kernels/numeric_cast.go |  7 ++++--
 2 files changed, 37 insertions(+), 2 deletions(-)

diff --git a/arrow/compute/cast_test.go b/arrow/compute/cast_test.go
index 67420098..74d66541 100644
--- a/arrow/compute/cast_test.go
+++ b/arrow/compute/cast_test.go
@@ -721,6 +721,22 @@ func (c *CastSuite) TestDecimal128ToInt() {
                opts.AllowDecimalTruncate = true
                c.checkCastArr(negScale, arrow.PrimitiveTypes.Int64, 
`[1234567890000, -120000]`, *opts)
        })
+
+       c.Run("int64 bounds inclusive", func() {
+               opts.AllowIntOverflow = false
+               opts.AllowDecimalTruncate = false
+
+               atBounds, _, _ := array.FromJSON(c.mem, 
&arrow.Decimal128Type{Precision: 38, Scale: 0},
+                       strings.NewReader(`["9223372036854775807", 
"-9223372036854775808", null]`))
+               defer atBounds.Release()
+               c.checkCastArr(atBounds, arrow.PrimitiveTypes.Int64,
+                       `[9223372036854775807, -9223372036854775808, null]`, 
*opts)
+
+               beyondBounds, _, _ := array.FromJSON(c.mem, 
&arrow.Decimal128Type{Precision: 38, Scale: 0},
+                       strings.NewReader(`["9223372036854775808", 
"-9223372036854775809"]`))
+               defer beyondBounds.Release()
+               checkCastFails(c.T(), beyondBounds, *opts)
+       })
 }
 
 func (c *CastSuite) TestDecimal256ToInt() {
@@ -828,6 +844,22 @@ func (c *CastSuite) TestDecimal256ToInt() {
                opts.AllowDecimalTruncate = true
                c.checkCastArr(negScale, arrow.PrimitiveTypes.Int64, 
`[1234567890000, -120000]`, *opts)
        })
+
+       c.Run("int64 bounds inclusive", func() {
+               opts.AllowIntOverflow = false
+               opts.AllowDecimalTruncate = false
+
+               atBounds, _, _ := array.FromJSON(c.mem, 
&arrow.Decimal256Type{Precision: 40, Scale: 0},
+                       strings.NewReader(`["9223372036854775807", 
"-9223372036854775808", null]`))
+               defer atBounds.Release()
+               c.checkCastArr(atBounds, arrow.PrimitiveTypes.Int64,
+                       `[9223372036854775807, -9223372036854775808, null]`, 
*opts)
+
+               beyondBounds, _, _ := array.FromJSON(c.mem, 
&arrow.Decimal256Type{Precision: 40, Scale: 0},
+                       strings.NewReader(`["9223372036854775808", 
"-9223372036854775809"]`))
+               defer beyondBounds.Release()
+               checkCastFails(c.T(), beyondBounds, *opts)
+       })
 }
 
 func (c *CastSuite) TestIntegerToDecimal() {
diff --git a/arrow/compute/internal/kernels/numeric_cast.go 
b/arrow/compute/internal/kernels/numeric_cast.go
index 7681b02e..9ce451c7 100644
--- a/arrow/compute/internal/kernels/numeric_cast.go
+++ b/arrow/compute/internal/kernels/numeric_cast.go
@@ -72,12 +72,15 @@ func CastIntegerToFloating(ctx *exec.KernelCtx, batch 
*exec.ExecSpan, out *exec.
 
 type decimal[T decimal128.Num | decimal256.Num] interface {
        Less(T) bool
-       GreaterEqual(T) bool
+       Greater(T) bool
        LowBits() uint64
 }
 
 func decimalToIntImpl[InT decimal128.Num | decimal256.Num, OutT arrow.IntType 
| arrow.UintType](allowOverflow bool, min, max InT, v decimal[InT], err *error) 
OutT {
-       if !allowOverflow && (v.Less(min) || v.GreaterEqual(max)) {
+       // min and max are the inclusive bounds of the output integer type, so a
+       // value equal to max (e.g. math.MaxInt64) is in range. Use a strict
+       // greater-than check rather than >= so the endpoints are not rejected.
+       if !allowOverflow && (v.Less(min) || v.Greater(max)) {
                debug.Log("integer value out of bounds from decimal")
                *err = fmt.Errorf("%w: integer value out of bounds", 
arrow.ErrInvalid)
                return OutT(0)

Reply via email to