Skip to content

Commit d17f687

Browse files
authored
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.
1 parent 4a540f5 commit d17f687

2 files changed

Lines changed: 37 additions & 2 deletions

File tree

arrow/compute/cast_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,22 @@ func (c *CastSuite) TestDecimal128ToInt() {
721721
opts.AllowDecimalTruncate = true
722722
c.checkCastArr(negScale, arrow.PrimitiveTypes.Int64, `[1234567890000, -120000]`, *opts)
723723
})
724+
725+
c.Run("int64 bounds inclusive", func() {
726+
opts.AllowIntOverflow = false
727+
opts.AllowDecimalTruncate = false
728+
729+
atBounds, _, _ := array.FromJSON(c.mem, &arrow.Decimal128Type{Precision: 38, Scale: 0},
730+
strings.NewReader(`["9223372036854775807", "-9223372036854775808", null]`))
731+
defer atBounds.Release()
732+
c.checkCastArr(atBounds, arrow.PrimitiveTypes.Int64,
733+
`[9223372036854775807, -9223372036854775808, null]`, *opts)
734+
735+
beyondBounds, _, _ := array.FromJSON(c.mem, &arrow.Decimal128Type{Precision: 38, Scale: 0},
736+
strings.NewReader(`["9223372036854775808", "-9223372036854775809"]`))
737+
defer beyondBounds.Release()
738+
checkCastFails(c.T(), beyondBounds, *opts)
739+
})
724740
}
725741

726742
func (c *CastSuite) TestDecimal256ToInt() {
@@ -828,6 +844,22 @@ func (c *CastSuite) TestDecimal256ToInt() {
828844
opts.AllowDecimalTruncate = true
829845
c.checkCastArr(negScale, arrow.PrimitiveTypes.Int64, `[1234567890000, -120000]`, *opts)
830846
})
847+
848+
c.Run("int64 bounds inclusive", func() {
849+
opts.AllowIntOverflow = false
850+
opts.AllowDecimalTruncate = false
851+
852+
atBounds, _, _ := array.FromJSON(c.mem, &arrow.Decimal256Type{Precision: 40, Scale: 0},
853+
strings.NewReader(`["9223372036854775807", "-9223372036854775808", null]`))
854+
defer atBounds.Release()
855+
c.checkCastArr(atBounds, arrow.PrimitiveTypes.Int64,
856+
`[9223372036854775807, -9223372036854775808, null]`, *opts)
857+
858+
beyondBounds, _, _ := array.FromJSON(c.mem, &arrow.Decimal256Type{Precision: 40, Scale: 0},
859+
strings.NewReader(`["9223372036854775808", "-9223372036854775809"]`))
860+
defer beyondBounds.Release()
861+
checkCastFails(c.T(), beyondBounds, *opts)
862+
})
831863
}
832864

833865
func (c *CastSuite) TestIntegerToDecimal() {

arrow/compute/internal/kernels/numeric_cast.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,15 @@ func CastIntegerToFloating(ctx *exec.KernelCtx, batch *exec.ExecSpan, out *exec.
7272

7373
type decimal[T decimal128.Num | decimal256.Num] interface {
7474
Less(T) bool
75-
GreaterEqual(T) bool
75+
Greater(T) bool
7676
LowBits() uint64
7777
}
7878

7979
func decimalToIntImpl[InT decimal128.Num | decimal256.Num, OutT arrow.IntType | arrow.UintType](allowOverflow bool, min, max InT, v decimal[InT], err *error) OutT {
80-
if !allowOverflow && (v.Less(min) || v.GreaterEqual(max)) {
80+
// min and max are the inclusive bounds of the output integer type, so a
81+
// value equal to max (e.g. math.MaxInt64) is in range. Use a strict
82+
// greater-than check rather than >= so the endpoints are not rejected.
83+
if !allowOverflow && (v.Less(min) || v.Greater(max)) {
8184
debug.Log("integer value out of bounds from decimal")
8285
*err = fmt.Errorf("%w: integer value out of bounds", arrow.ErrInvalid)
8386
return OutT(0)

0 commit comments

Comments
 (0)