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 2950f2ed fix(arrow): validate precision in NewDecimalType (#1162)
2950f2ed is described below
commit 2950f2edac8f1e783ed143b7f827be5402195bd3
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 26 21:42:32 2026 +0200
fix(arrow): validate precision in NewDecimalType (#1162)
### Rationale for this change
NewDecimalType only checks maximum precision with debug assertions and
does not reject zero precision. Normal builds can therefore create
invalid decimal types.
### What changes are included in this PR?
Validate that precision is between 1 and the maximum supported by the
requested decimal type, and return arrow.ErrInvalid otherwise.
### Are these changes tested?
Yes. The tests cover the lower and upper valid boundaries plus zero and
overflow precision for all four decimal widths. The full arrow package
suite passes.
### Are there any user-facing changes?
NewDecimalType now returns an error for invalid precision instead of
constructing an invalid type.
---
arrow/avro/schema.go | 6 +++++-
arrow/avro/schema_test.go | 23 +++++++++++++++++++++++
arrow/datatype_fixedwidth.go | 27 ++++++++++++++++++---------
arrow/datatype_fixedwidth_test.go | 29 +++++++++++++++++++++++++++++
4 files changed, 75 insertions(+), 10 deletions(-)
diff --git a/arrow/avro/schema.go b/arrow/avro/schema.go
index 39053a4c..96508df6 100644
--- a/arrow/avro/schema.go
+++ b/arrow/avro/schema.go
@@ -472,7 +472,11 @@ func avroLogicalToArrowField(n *schemaNode) {
if n.node.Precision > decimal128.MaxPrecision {
id = arrow.DECIMAL256
}
- dt, _ = arrow.NewDecimalType(id, int32(n.node.Precision),
int32(n.node.Scale))
+ var err error
+ dt, err = arrow.NewDecimalType(id, int32(n.node.Precision),
int32(n.node.Scale))
+ if err != nil {
+ panic(err)
+ }
// The uuid logical type represents a random generated universally
unique identifier (UUID).
// A uuid logical type annotates an Avro string. The string has to
conform with RFC-4122
diff --git a/arrow/avro/schema_test.go b/arrow/avro/schema_test.go
index fe5dea36..8b208a34 100644
--- a/arrow/avro/schema_test.go
+++ b/arrow/avro/schema_test.go
@@ -17,6 +17,7 @@
package avro
import (
+ "errors"
"fmt"
"strings"
"testing"
@@ -268,6 +269,28 @@ func TestArrowSchemaFromAvroJSON_RejectsInvalidRoot(t
*testing.T) {
}
}
+func TestArrowSchemaFromAvroJSON_RejectsInvalidDecimalPrecision(t *testing.T) {
+ const schemaJSON = `{
+ "type": "record",
+ "name": "Root",
+ "fields": [{
+ "name": "value",
+ "type": {"type": "bytes", "logicalType": "decimal",
"precision": 77, "scale": 2}
+ }]
+ }`
+
+ _, err := ArrowSchemaFromAvroJSON(schemaJSON)
+ if err == nil {
+ t.Fatal("expected invalid decimal precision error")
+ }
+ if !errors.Is(err, arrow.ErrInvalid) {
+ t.Fatalf("expected errors.Is(err, arrow.ErrInvalid), got %v",
err)
+ }
+ if !strings.Contains(err.Error(), "precision") {
+ t.Fatalf("expected precision in error, got %v", err)
+ }
+}
+
// A named record referenced again by name resolves back to the same
definition.
func TestArrowSchemaFromAvroJSON_ReusedNamedReference(t *testing.T) {
const schemaJSON = `{"type":"record","name":"Root","fields":[
diff --git a/arrow/datatype_fixedwidth.go b/arrow/datatype_fixedwidth.go
index 663f3474..5cdca735 100644
--- a/arrow/datatype_fixedwidth.go
+++ b/arrow/datatype_fixedwidth.go
@@ -26,7 +26,6 @@ import (
"time"
"github.com/apache/arrow-go/v18/arrow/decimal"
- "github.com/apache/arrow-go/v18/arrow/internal/debug"
"github.com/apache/arrow-go/v18/internal/json"
)
@@ -590,22 +589,32 @@ func NarrowestDecimalType(prec, scale int32)
(DecimalType, error) {
}
func NewDecimalType(id Type, prec, scale int32) (DecimalType, error) {
+ var (
+ dtype DecimalType
+ maxPrecision int32
+ )
switch id {
case DECIMAL32:
- debug.Assert(prec <=
int32(decimal.MaxPrecision[decimal.Decimal32]()), "invalid precision for
decimal32")
- return &Decimal32Type{Precision: prec, Scale: scale}, nil
+ dtype = &Decimal32Type{Precision: prec, Scale: scale}
+ maxPrecision = int32(decimal.MaxPrecision[decimal.Decimal32]())
case DECIMAL64:
- debug.Assert(prec <=
int32(decimal.MaxPrecision[decimal.Decimal64]()), "invalid precision for
decimal64")
- return &Decimal64Type{Precision: prec, Scale: scale}, nil
+ dtype = &Decimal64Type{Precision: prec, Scale: scale}
+ maxPrecision = int32(decimal.MaxPrecision[decimal.Decimal64]())
case DECIMAL128:
- debug.Assert(prec <=
int32(decimal.MaxPrecision[decimal.Decimal128]()), "invalid precision for
decimal128")
- return &Decimal128Type{Precision: prec, Scale: scale}, nil
+ dtype = &Decimal128Type{Precision: prec, Scale: scale}
+ maxPrecision = int32(decimal.MaxPrecision[decimal.Decimal128]())
case DECIMAL256:
- debug.Assert(prec <=
int32(decimal.MaxPrecision[decimal.Decimal256]()), "invalid precision for
decimal256")
- return &Decimal256Type{Precision: prec, Scale: scale}, nil
+ dtype = &Decimal256Type{Precision: prec, Scale: scale}
+ maxPrecision = int32(decimal.MaxPrecision[decimal.Decimal256]())
default:
return nil, fmt.Errorf("%w: must use one of the DECIMAL IDs to
create a DecimalType", ErrInvalid)
}
+
+ if prec <= 0 || prec > maxPrecision {
+ return nil, fmt.Errorf("%w: precision for %s must be between 1
and %d, got %d",
+ ErrInvalid, id, maxPrecision, prec)
+ }
+ return dtype, nil
}
// Decimal32Type represents a fixed-size 32-bit decimal type.
diff --git a/arrow/datatype_fixedwidth_test.go
b/arrow/datatype_fixedwidth_test.go
index 05afa334..0dc9a844 100644
--- a/arrow/datatype_fixedwidth_test.go
+++ b/arrow/datatype_fixedwidth_test.go
@@ -604,3 +604,32 @@ func TestNarrowestDecimalType(t *testing.T) {
assert.Error(t, err)
assert.ErrorIs(t, err, arrow.ErrInvalid)
}
+
+func TestNewDecimalTypeValidatesPrecision(t *testing.T) {
+ tests := []struct {
+ id arrow.Type
+ maxPrecision int32
+ }{
+ {arrow.DECIMAL32, 9},
+ {arrow.DECIMAL64, 18},
+ {arrow.DECIMAL128, 38},
+ {arrow.DECIMAL256, 76},
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.id.String(), func(t *testing.T) {
+ for _, precision := range []int32{1, tt.maxPrecision} {
+ typ, err := arrow.NewDecimalType(tt.id,
precision, 2)
+ require.NoError(t, err)
+ assert.Equal(t, precision, typ.GetPrecision())
+ assert.Equal(t, int32(2), typ.GetScale())
+ }
+
+ for _, precision := range []int32{-1, 0,
tt.maxPrecision + 1} {
+ typ, err := arrow.NewDecimalType(tt.id,
precision, 2)
+ assert.Nil(t, typ)
+ assert.ErrorIs(t, err, arrow.ErrInvalid)
+ }
+ })
+ }
+}