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 55f6fd07 fix(arrow/array): use scale-aware ValueStr in decimal array
String() (#849)
55f6fd07 is described below
commit 55f6fd07654248e7eea1bf90981b0c358aeb630e
Author: Matt Topol <[email protected]>
AuthorDate: Wed Jun 10 15:57:12 2026 -0400
fix(arrow/array): use scale-aware ValueStr in decimal array String() (#849)
### Rationale for this change
Decimal arrays stringify the raw `decimal.Num` struct instead of the
scaled logical value. `fmt.Println` on a decimal array — or on a
`Record`/`RecordBatch` containing one — prints e.g. `{1999 0}` instead
of `19.99`, even though `ValueStr` already formats the value correctly
using the type's scale. This is inconsistent with `ValueStr`/JSON output
and with other Arrow implementations (e.g. PyArrow prints `19.99`).
### What changes are included in this PR?
- `baseDecimal[T].String()` now formats each non-null value via
`ValueStr` (which applies the type's scale through `GetOneForMarshal`)
instead of `fmt.Sprintf("%v", Value(i))`. Because `baseDecimal` is
generic, this corrects `Decimal32/64/128/256` at once, and `RecordBatch`
printing inherits the fix through the column `String()` path
(`record.go`).
- Updated two existing slice-test assertions in `decimal128_test.go` /
`decimal256_test.go` that pinned the old raw-struct output (they were
already inconsistent with the adjacent `ValueStr` expectations on the
next lines).
- Added `TestDecimal128StringScaled`, reproducing the issue
(`decimal128(5, 2)` = `19.99`) and asserting both the array `String()`
and the `RecordBatch` output.
### Are these changes tested?
Yes. `go test ./arrow/array/` passes, including the new regression test
and the updated slice tests. `go build ./...` and `go vet`/golangci-lint
(via pre-commit) pass.
### Are there any user-facing changes?
Yes — the textual output of `String()` on decimal arrays, and of any
`Record`/`RecordBatch` containing decimal columns, now shows scaled
decimal values (e.g. `19.99`) instead of the internal struct (e.g.
`{1999 0}`). This only affects human-readable stringification;
binary/IPC/JSON representations are unchanged.
Closes #848
---
arrow/array/decimal.go | 2 +-
arrow/array/decimal128_test.go | 38 +++++++++++++++++++++++++++++++++++++-
arrow/array/decimal256_test.go | 2 +-
3 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/arrow/array/decimal.go b/arrow/array/decimal.go
index 84689c95..704b1d93 100644
--- a/arrow/array/decimal.go
+++ b/arrow/array/decimal.go
@@ -71,7 +71,7 @@ func (a *baseDecimal[T]) String() string {
case a.IsNull(i):
o.WriteString(NullValueStr)
default:
- fmt.Fprintf(o, "%v", a.Value(i))
+ fmt.Fprintf(o, "%v", a.ValueStr(i))
}
}
o.WriteString("]")
diff --git a/arrow/array/decimal128_test.go b/arrow/array/decimal128_test.go
index 4d48a97a..e642d037 100644
--- a/arrow/array/decimal128_test.go
+++ b/arrow/array/decimal128_test.go
@@ -17,6 +17,7 @@
package array_test
import (
+ "fmt"
"testing"
"github.com/apache/arrow-go/v18/arrow"
@@ -168,7 +169,7 @@ func TestDecimal128Slice(t *testing.T) {
t.Fatalf("could not type-assert to array.String")
}
- if got, want := v.String(), `[(null) {4 -4}]`; got != want {
+ if got, want := v.String(), `[(null) -7.378697629e+18]`; got != want {
t.Fatalf("got=%q, want=%q", got, want)
}
assert.Equal(t, array.NullValueStr, v.ValueStr(0))
@@ -281,3 +282,38 @@ func TestDecimal128GetOneForMarshal(t *testing.T) {
assert.Equalf(t, cases[i].want, arr.GetOneForMarshal(i),
"unexpected value at index %d", i)
}
}
+
+// TestDecimal128StringScaled is a regression test for apache/arrow-go#848:
+// Array.String() (and RecordBatch printing) must use the type's scale like
+// ValueStr, not the raw unscaled decimal128.Num struct.
+func TestDecimal128StringScaled(t *testing.T) {
+ mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+ defer mem.AssertSize(t, 0)
+
+ typ := &arrow.Decimal128Type{Precision: 5, Scale: 2}
+
+ b := array.NewDecimal128Builder(mem, typ)
+ defer b.Release()
+
+ value, err := decimal128.FromString("19.99", typ.Precision, typ.Scale)
+ if err != nil {
+ t.Fatal(err)
+ }
+ b.Append(value)
+ b.AppendNull()
+
+ arr := b.NewDecimal128Array()
+ defer arr.Release()
+
+ assert.Equal(t, "19.99", arr.ValueStr(0))
+ assert.Equal(t, "[19.99 (null)]", arr.String())
+
+ schema := arrow.NewSchema([]arrow.Field{{Name: "price", Type: typ}},
nil)
+ rec := array.NewRecordBatch(schema, []arrow.Array{arr},
int64(arr.Len()))
+ defer rec.Release()
+
+ out := fmt.Sprintf("%v", rec)
+ assert.Contains(t, out, "[19.99 (null)]")
+ // 1999 is the raw unscaled value that must never leak into output.
+ assert.NotContains(t, out, "1999")
+}
diff --git a/arrow/array/decimal256_test.go b/arrow/array/decimal256_test.go
index 025f3bd3..b5674253 100644
--- a/arrow/array/decimal256_test.go
+++ b/arrow/array/decimal256_test.go
@@ -169,7 +169,7 @@ func TestDecimal256Slice(t *testing.T) {
t.Fatalf("could not type-assert to array.String")
}
- if got, want := v.String(), `[(null) {[4 4 4 4]}]`; got != want {
+ if got, want := v.String(), `[(null) 2.510840694e+57]`; got != want {
t.Fatalf("got=%q, want=%q", got, want)
}
assert.Equal(t, array.NullValueStr, v.ValueStr(0))