From b1e00e117ad2e8198eaae3d8dc8079d5148c6fa1 Mon Sep 17 00:00:00 2001 From: Matt Topol Date: Tue, 9 Jun 2026 16:46:06 -0400 Subject: [PATCH] fix(arrow/array): use scale-aware ValueStr in decimal array String() Decimal arrays printed the raw decimal.Num struct (e.g. {1999 0}) from String(), so printing an array or RecordBatch showed unscaled values instead of the logical decimal. Switch to ValueStr, which already formats using the type's scale (matching GetOneForMarshal and JSON output). baseDecimal is generic, so this fixes Decimal32/64/128/256, and RecordBatch printing inherits the corrected output via the column String() path. --- 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 84689c95d..704b1d932 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 4d48a97a7..e642d0374 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 025f3bd3a..b5674253e 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))