From a132ab70369f250b5ffd2f534a9bd326d05d6f8e Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 23 Jun 2026 12:31:15 -0400 Subject: [PATCH] fix(arrow/compute): accept type max value in safe decimal-to-int cast The safe decimal->integer cast kernel checked `value >= max`, where max is the inclusive maximum of the output integer type (e.g. math.MaxInt64). This rejected the maximum value itself as out of bounds for every integer output type, even though it is a valid in-range value. Use a strict `value > max` check so the inclusive upper bound is accepted, matching the already inclusive lower bound. Adds boundary regression tests covering math.MinInt64/math.MaxInt64 for both decimal128 and decimal256 inputs. --- arrow/compute/cast_test.go | 32 +++++++++++++++++++ .../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 674200989..74d66541a 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 7681b02e2..9ce451c7f 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)