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)