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 75058671 fix(array): recursively validate nested arrays (#930)
75058671 is described below
commit 750586710988bc18f7af64fd29af8e0acdb252ac
Author: Minh Vu <[email protected]>
AuthorDate: Tue Jul 14 18:06:28 2026 +0200
fix(array): recursively validate nested arrays (#930)
---
arrow/array/fixed_size_list.go | 3 +
arrow/array/list.go | 6 +
arrow/array/validate.go | 394 +++++++++++++++++++++++++++++++++++++++--
arrow/array/validate_test.go | 148 ++++++++++++++++
4 files changed, 540 insertions(+), 11 deletions(-)
diff --git a/arrow/array/fixed_size_list.go b/arrow/array/fixed_size_list.go
index d382ebe9..ca9210ab 100644
--- a/arrow/array/fixed_size_list.go
+++ b/arrow/array/fixed_size_list.go
@@ -37,6 +37,9 @@ type FixedSizeList struct {
var _ ListLike = (*FixedSizeList)(nil)
+func (a *FixedSizeList) Validate() error { return
validateFixedSizeListArray(a) }
+func (a *FixedSizeList) ValidateFull() error { return
validateFixedSizeListArray(a) }
+
// NewFixedSizeListData returns a new List array value, from data.
func NewFixedSizeListData(data arrow.ArrayData) *FixedSizeList {
a := &FixedSizeList{}
diff --git a/arrow/array/list.go b/arrow/array/list.go
index d72887bb..0379c786 100644
--- a/arrow/array/list.go
+++ b/arrow/array/list.go
@@ -112,6 +112,9 @@ func (a *List) GetOneForMarshal(i int) interface{} {
return json.RawMessage(v)
}
+func (a *List) Validate() error { return validateListArray(a, false) }
+func (a *List) ValidateFull() error { return validateListArray(a, true) }
+
func (a *List) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
@@ -244,6 +247,9 @@ func (a *LargeList) GetOneForMarshal(i int) interface{} {
return json.RawMessage(v)
}
+func (a *LargeList) Validate() error { return validateLargeListArray(a,
false) }
+func (a *LargeList) ValidateFull() error { return validateLargeListArray(a,
true) }
+
func (a *LargeList) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
enc := json.NewEncoder(&buf)
diff --git a/arrow/array/validate.go b/arrow/array/validate.go
index 70b7f669..e33202e3 100644
--- a/arrow/array/validate.go
+++ b/arrow/array/validate.go
@@ -20,10 +20,13 @@ import (
"fmt"
"github.com/apache/arrow-go/v18/arrow"
+ "github.com/apache/arrow-go/v18/arrow/bitutil"
+ "github.com/apache/arrow-go/v18/arrow/memory"
)
-// Validator is implemented by array types that can validate their internal
-// consistency. See Validate and ValidateFull for top-level dispatch.
+// Validator is implemented by array types that provide type-specific
+// consistency checks. Validate and ValidateFull also validate generic layout
+// and nested child data for every array type.
type Validator interface {
arrow.Array
// Validate performs a basic O(1) consistency check.
@@ -33,32 +36,401 @@ type Validator interface {
}
// Validate performs a basic O(1) consistency check on arr, returning an error
-// if the array's internal buffers are inconsistent. For array types that do
not
-// implement Validator, nil is returned.
+// if the array's internal buffers or nested child data are inconsistent.
//
// Use this to detect corrupted data from untrusted sources such as Arrow
Flight
// or Flight SQL servers before accessing values, which may otherwise panic.
func Validate(arr arrow.Array) error {
- if v, ok := arr.(Validator); ok {
- return v.Validate()
- }
- return nil
+ return validateArray(arr, false, "")
}
// ValidateFull performs a thorough O(n) consistency check on arr, returning an
-// error if the array's internal buffers are inconsistent. For array types that
-// do not implement Validator, nil is returned.
+// error if the array's internal buffers or nested child data are inconsistent.
//
// Unlike Validate, this checks every element and is therefore O(n). Use this
// when receiving data from untrusted sources where subtle corruption (e.g.
// non-monotonic offsets) may not be detected by Validate alone.
func ValidateFull(arr arrow.Array) error {
+ return validateArray(arr, true, "")
+}
+
+func validateArray(arr arrow.Array, full bool, path string) error {
+ if arr == nil {
+ return nil
+ }
+
+ data, ok := arr.Data().(*Data)
+ if !ok || data == nil {
+ return validationError(path, fmt.Errorf("arrow/array: array
does not expose internal data"))
+ }
+
+ if err := validateArrayData(data); err != nil {
+ return validationError(path, err)
+ }
+ if err := validateArrayStructure(data); err != nil {
+ return validationError(path, err)
+ }
+
if v, ok := arr.(Validator); ok {
- return v.ValidateFull()
+ var err error
+ if full {
+ err = v.ValidateFull()
+ } else {
+ err = v.Validate()
+ }
+ if err != nil {
+ return validationError(path, err)
+ }
+ }
+
+ if full && data.DataType().Layout().HasDict {
+ dictData := data.dictionary
+ if dictData != nil {
+ dt := data.DataType().(*arrow.DictionaryType)
+ indexData := NewData(dt.IndexType, data.length,
data.buffers, nil, data.nulls, data.offset)
+ err := checkIndexBounds(indexData,
uint64(dictData.Len()))
+ indexData.Release()
+ if err != nil {
+ return validationError(joinValidationPath(path,
"dictionary indices"), err)
+ }
+ }
+ }
+
+ if ext, ok := arr.(ExtensionArray); ok {
+ return validateArray(ext.Storage(), full,
joinValidationPath(path, "storage"))
+ }
+
+ for i, childData := range data.Children() {
+ child, err := makeArrayFromData(childData)
+ if err != nil {
+ return validationError(joinValidationPath(path,
validationChildPath(data.DataType(), i)), err)
+ }
+
+ childPath := joinValidationPath(path,
validationChildPath(data.DataType(), i))
+ err = validateArray(child, full, childPath)
+ child.Release()
+ if err != nil {
+ return err
+ }
+ }
+
+ if dictData := data.dictionary; dictData != nil {
+ dict, err := makeArrayFromData(dictData)
+ if err != nil {
+ return validationError(joinValidationPath(path,
"dictionary"), err)
+ }
+
+ err = validateArray(dict, full, joinValidationPath(path,
"dictionary"))
+ dict.Release()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func validateArrayData(data *Data) error {
+ if data == nil || data.dtype == nil {
+ return fmt.Errorf("arrow/array: array data has no data type")
+ }
+ if data.offset < 0 {
+ return fmt.Errorf("arrow/array: array offset is negative: %d",
data.offset)
+ }
+ if data.length < 0 {
+ return fmt.Errorf("arrow/array: array length is negative: %d",
data.length)
+ }
+ if data.nulls < UnknownNullCount || data.nulls > data.length {
+ return fmt.Errorf("arrow/array: invalid null count %d for
length %d", data.nulls, data.length)
+ }
+
+ end := int64(data.offset) + int64(data.length)
+ if end < int64(data.offset) {
+ return fmt.Errorf("arrow/array: array offset and length
overflow")
+ }
+
+ layout := data.dtype.Layout()
+ // Union arrays reserve the first buffer slot for a validity bitmap,
even
+ // though that slot must be nil and is not part of their type layout.
+ bufferOffset := 0
+ if data.dtype.ID() == arrow.SPARSE_UNION || data.dtype.ID() ==
arrow.DENSE_UNION {
+ bufferOffset = 1
+ }
+ bufferCount := len(data.buffers) - bufferOffset
+ if bufferCount < 0 {
+ bufferCount = 0
+ }
+ if bufferCount < len(layout.Buffers) {
+ return fmt.Errorf("arrow/array: expected at least %d buffers
for %s, got %d",
+ len(layout.Buffers), data.dtype, bufferCount)
+ }
+ if layout.VariadicSpec == nil && bufferCount > len(layout.Buffers) {
+ return fmt.Errorf("arrow/array: expected at most %d buffers for
%s, got %d",
+ len(layout.Buffers), data.dtype, bufferCount)
+ }
+
+ for i, spec := range layout.Buffers {
+ if err := validateBuffer(data.buffers[i+bufferOffset], spec,
end, data.length, i+bufferOffset); err != nil {
+ return err
+ }
+ }
+ if layout.VariadicSpec != nil {
+ for i := len(layout.Buffers); i < bufferCount; i++ {
+ if err := validateBuffer(data.buffers[i+bufferOffset],
*layout.VariadicSpec, end, data.length, i+bufferOffset); err != nil {
+ return err
+ }
+ }
}
return nil
}
+func validateBuffer(buf *memory.Buffer, spec arrow.BufferSpec, end int64,
length, index int) error {
+ if buf == nil {
+ if spec.Kind == arrow.KindFixedWidth {
+ if length > 0 {
+ return fmt.Errorf("arrow/array: buffer %d is
nil for non-empty layout kind %v", index, spec.Kind)
+ }
+ }
+ return nil
+ }
+
+ bufferLen := int64(buf.Len())
+ switch spec.Kind {
+ case arrow.KindFixedWidth:
+ if spec.ByteWidth <= 0 {
+ return fmt.Errorf("arrow/array: buffer %d has invalid
fixed width %d", index, spec.ByteWidth)
+ }
+ if end > bufferLen/int64(spec.ByteWidth) {
+ return fmt.Errorf("arrow/array: buffer %d is too small
for offset %d and length %d", index, end-int64(length), length)
+ }
+ case arrow.KindBitmap:
+ if int64(bitutil.BytesForBits(end)) > bufferLen {
+ return fmt.Errorf("arrow/array: bitmap buffer %d is too
small for offset %d and length %d", index, end-int64(length), length)
+ }
+ case arrow.KindVarWidth:
+ return nil
+ case arrow.KindAlwaysNull:
+ return fmt.Errorf("arrow/array: buffer %d must be nil for an
always-null layout", index)
+ }
+ return nil
+}
+
+func validateArrayStructure(data *Data) error {
+ children := data.Children()
+ if nested, ok := data.DataType().(arrow.NestedType); ok {
+ fields := nested.Fields()
+ if len(children) != len(fields) {
+ return fmt.Errorf("arrow/array: %s expects %d child
arrays, got %d", data.DataType(), len(fields), len(children))
+ }
+ for i, child := range children {
+ if child == nil {
+ return fmt.Errorf("arrow/array: child %d of %s
is nil", i, data.DataType())
+ }
+ if !arrow.TypeEqual(fields[i].Type, child.DataType()) {
+ return fmt.Errorf("arrow/array: child %d of %s
has type %s, want %s",
+ i, data.DataType(), child.DataType(),
fields[i].Type)
+ }
+ }
+ } else if len(children) != 0 {
+ return fmt.Errorf("arrow/array: %s must not have child arrays",
data.DataType())
+ }
+
+ if data.DataType().Layout().HasDict {
+ dt, ok := data.DataType().(*arrow.DictionaryType)
+ if !ok {
+ return fmt.Errorf("arrow/array: datatype %s declares
dictionary storage but is not a dictionary type", data.DataType())
+ }
+ if dict := data.dictionary; dict != nil &&
!arrow.TypeEqual(dt.ValueType, dict.DataType()) {
+ return fmt.Errorf("arrow/array: dictionary has type %s,
want %s", dict.DataType(), dt.ValueType)
+ }
+ if data.Len() > 0 && data.dictionary == nil {
+ return fmt.Errorf("arrow/array: non-empty dictionary
array has no dictionary")
+ }
+ }
+ return nil
+}
+
+func validateListArray(a *List, full bool) error {
+ if err := validateArrayData(a.data); err != nil {
+ return err
+ }
+ if len(a.data.childData) != 1 || a.data.childData[0] == nil {
+ return fmt.Errorf("arrow/array: list array must have one
non-nil child array")
+ }
+ // MAP uses the list-like entry layout; large-list variants have
dedicated validators.
+ dt, ok := a.data.dtype.(arrow.ListLikeType)
+ if !ok || dt.ID() == arrow.LARGE_LIST || dt.ID() ==
arrow.LARGE_LIST_VIEW {
+ return fmt.Errorf("arrow/array: invalid datatype %s for list
array", a.data.dtype)
+ }
+ if !arrow.TypeEqual(dt.Elem(), a.data.childData[0].DataType()) {
+ return fmt.Errorf("arrow/array: list values have type %s, want
%s", a.data.childData[0].DataType(), dt.Elem())
+ }
+ if len(a.offsets) == 0 && a.data.length > 0 {
+ return fmt.Errorf("arrow/array: non-empty list array has no
offsets")
+ }
+ return validateListOffsets(a.offsets, a.data,
a.data.childData[0].Len(), full)
+}
+
+func validateLargeListArray(a *LargeList, full bool) error {
+ if err := validateArrayData(a.data); err != nil {
+ return err
+ }
+ if len(a.data.childData) != 1 || a.data.childData[0] == nil {
+ return fmt.Errorf("arrow/array: large list array must have one
non-nil child array")
+ }
+ dt, ok := a.data.dtype.(arrow.ListLikeType)
+ if !ok || dt.ID() != arrow.LARGE_LIST {
+ return fmt.Errorf("arrow/array: invalid datatype %s for large
list array", a.data.dtype)
+ }
+ if !arrow.TypeEqual(dt.Elem(), a.data.childData[0].DataType()) {
+ return fmt.Errorf("arrow/array: large list values have type %s,
want %s", a.data.childData[0].DataType(), dt.Elem())
+ }
+ if len(a.offsets) == 0 && a.data.length > 0 {
+ return fmt.Errorf("arrow/array: non-empty large list array has
no offsets")
+ }
+ return validateListOffsets(a.offsets, a.data,
a.data.childData[0].Len(), full)
+}
+
+func validateListOffsets(offsets interface{}, data *Data, valueLength int,
full bool) error {
+ if data.length == 0 {
+ return nil
+ }
+ start := int64(data.offset)
+ required := start + int64(data.length) + 1
+ if required < start || required > int64(offsetLength(offsets)) {
+ return fmt.Errorf("arrow/array: list offsets buffer is too
small for offset %d and length %d", data.offset, data.length)
+ }
+ get := func(i int64) int64 {
+ switch v := offsets.(type) {
+ case []int32:
+ return int64(v[int(i)])
+ case []int64:
+ return v[int(i)]
+ default:
+ return 0
+ }
+ }
+
+ first := get(start)
+ last := get(required - 1)
+ if err := validateListOffset(first, start, valueLength); err != nil {
+ return err
+ }
+ if err := validateListOffset(last, required-1, valueLength); err != nil
{
+ return err
+ }
+ if !full {
+ return nil
+ }
+
+ previous := first
+ for i := start + 1; i < required; i++ {
+ current := get(i)
+ if current < 0 || current > int64(valueLength) {
+ return fmt.Errorf("arrow/array: list offset at index %d
out of bounds: %d", i, current)
+ }
+ if current < previous {
+ return fmt.Errorf("arrow/array: list offsets are not
monotonically non-decreasing at index %d: %d < %d",
+ i, current, previous)
+ }
+ previous = current
+ }
+ return nil
+}
+
+func offsetLength(offsets interface{}) int {
+ switch v := offsets.(type) {
+ case []int32:
+ return len(v)
+ case []int64:
+ return len(v)
+ default:
+ return 0
+ }
+}
+
+func validateListOffset(offset int64, index int64, valueLength int) error {
+ if offset < 0 {
+ return fmt.Errorf("arrow/array: list offset at index %d is
negative: %d", index, offset)
+ }
+ if offset > int64(valueLength) {
+ return fmt.Errorf("arrow/array: list offset at index %d out of
bounds: %d > %d", index, offset, valueLength)
+ }
+ return nil
+}
+
+func validateFixedSizeListArray(a *FixedSizeList) error {
+ if err := validateArrayData(a.data); err != nil {
+ return err
+ }
+ if len(a.data.childData) != 1 || a.data.childData[0] == nil {
+ return fmt.Errorf("arrow/array: fixed-size list array must have
one non-nil child array")
+ }
+ dt, ok := a.data.dtype.(*arrow.FixedSizeListType)
+ if !ok {
+ return fmt.Errorf("arrow/array: invalid datatype %s for
fixed-size list array", a.data.dtype)
+ }
+ if !arrow.TypeEqual(dt.Elem(), a.data.childData[0].DataType()) {
+ return fmt.Errorf("arrow/array: fixed-size list values have
type %s, want %s", a.data.childData[0].DataType(), dt.Elem())
+ }
+ childLength := int64(a.data.offset) + int64(a.data.length)
+ itemCount := int64(dt.Len())
+ if itemCount <= 0 {
+ return fmt.Errorf("arrow/array: fixed-size list has invalid
item count %d", itemCount)
+ }
+ if childLength > int64(a.data.childData[0].Len())/itemCount {
+ return fmt.Errorf("arrow/array: fixed-size list child length %d
is too small for offset %d and length %d",
+ a.data.childData[0].Len(), a.data.offset, a.data.length)
+ }
+ return nil
+}
+
+func makeArrayFromData(data arrow.ArrayData) (arr arrow.Array, err error) {
+ defer func() {
+ if r := recover(); r != nil {
+ err = fmt.Errorf("arrow/array: failed to construct
child array: %v", r)
+ }
+ }()
+ return MakeFromData(data), nil
+}
+
+func validationChildPath(dt arrow.DataType, index int) string {
+ if nested, ok := dt.(arrow.NestedType); ok {
+ fields := nested.Fields()
+ if index >= 0 && index < len(fields) {
+ field := fields[index]
+ if dt.ID() == arrow.STRUCT || dt.ID() ==
arrow.SPARSE_UNION || dt.ID() == arrow.DENSE_UNION || dt.ID() ==
arrow.RUN_END_ENCODED {
+ return fmt.Sprintf("field %q", field.Name)
+ }
+ }
+ }
+
+ switch dt.ID() {
+ case arrow.LIST, arrow.LARGE_LIST, arrow.FIXED_SIZE_LIST:
+ return "list values"
+ case arrow.LIST_VIEW, arrow.LARGE_LIST_VIEW:
+ return "list view values"
+ case arrow.MAP:
+ return "map entries"
+ default:
+ return fmt.Sprintf("child %d", index)
+ }
+}
+
+func joinValidationPath(path, child string) string {
+ if path == "" {
+ return child
+ }
+ return path + " -> " + child
+}
+
+func validationError(path string, err error) error {
+ if err == nil || path == "" {
+ return err
+ }
+ return fmt.Errorf("%s: %w", path, err)
+}
+
// ValidateRecord validates each column in rec using Validate, returning the
// first error encountered. The error includes the column index and field name.
func ValidateRecord(rec arrow.RecordBatch) error {
diff --git a/arrow/array/validate_test.go b/arrow/array/validate_test.go
index 5d45ce50..53ad126b 100644
--- a/arrow/array/validate_test.go
+++ b/arrow/array/validate_test.go
@@ -17,6 +17,7 @@
package array
import (
+ "strings"
"testing"
"github.com/apache/arrow-go/v18/arrow"
@@ -127,6 +128,18 @@ func TestLargeBinaryValidate(t *testing.T) {
}
func TestStringValidate(t *testing.T) {
+ t.Run("empty values allow a nil data buffer", func(t *testing.T) {
+ offsets :=
memory.NewBufferBytes(arrow.Int32Traits.CastToBytes([]int32{0, 0, 0}))
+ data := NewData(arrow.BinaryTypes.String, 2,
[]*memory.Buffer{nil, offsets, nil}, nil, 0, 0)
+ offsets.Release()
+ arr := NewStringData(data)
+ data.Release()
+ defer arr.Release()
+
+ assert.NoError(t, arr.Validate())
+ assert.NoError(t, arr.ValidateFull())
+ })
+
t.Run("valid array passes", func(t *testing.T) {
arr := makeStringArrayRaw(t, []int32{0, 3, 5, 10},
"abcdeabcde", 3, 0)
assert.NoError(t, arr.Validate())
@@ -209,3 +222,138 @@ func TestTopLevelValidate(t *testing.T) {
assert.Contains(t, err.Error(), "column 1 (bad)")
})
}
+
+func TestTopLevelValidateUnions(t *testing.T) {
+ type unionCase struct {
+ name string
+ arr arrow.Array
+ }
+
+ typeIDs, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int8, strings.NewReader(`[0, 1]`))
+ require.NoError(t, err)
+ defer typeIDs.Release()
+
+ sparseChild0, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int32, strings.NewReader(`[10, 20]`))
+ require.NoError(t, err)
+ defer sparseChild0.Release()
+ sparseChild1, _, err := FromJSON(memory.DefaultAllocator,
arrow.BinaryTypes.String, strings.NewReader(`["one", "two"]`))
+ require.NoError(t, err)
+ defer sparseChild1.Release()
+
+ sparse, err := NewSparseUnionFromArrays(typeIDs,
[]arrow.Array{sparseChild0, sparseChild1})
+ require.NoError(t, err)
+ defer sparse.Release()
+
+ offsets, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int32, strings.NewReader(`[0, 0]`))
+ require.NoError(t, err)
+ defer offsets.Release()
+ denseChild0, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int32, strings.NewReader(`[10]`))
+ require.NoError(t, err)
+ defer denseChild0.Release()
+ denseChild1, _, err := FromJSON(memory.DefaultAllocator,
arrow.BinaryTypes.String, strings.NewReader(`["two"]`))
+ require.NoError(t, err)
+ defer denseChild1.Release()
+
+ dense, err := NewDenseUnionFromArrays(typeIDs, offsets,
[]arrow.Array{denseChild0, denseChild1})
+ require.NoError(t, err)
+ defer dense.Release()
+
+ for _, tc := range []unionCase{{"sparse", sparse}, {"dense", dense}} {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.NoError(t, Validate(tc.arr))
+ assert.NoError(t, ValidateFull(tc.arr))
+
+ nested, err :=
NewStructArrayWithFields([]arrow.Array{tc.arr}, []arrow.Field{
+ {Name: "value", Type: tc.arr.DataType(),
Nullable: true},
+ })
+ require.NoError(t, err)
+ defer nested.Release()
+
+ assert.NoError(t, Validate(nested))
+ assert.NoError(t, ValidateFull(nested))
+ })
+ }
+}
+
+func TestTopLevelValidateFullRecursesThroughNestedChildren(t *testing.T) {
+ badString := makeStringArrayRaw(t, []int32{0, 5, 3, 5}, "hello", 3, 0)
+ defer badString.Release()
+
+ inner, err := NewStructArrayWithFields([]arrow.Array{badString},
[]arrow.Field{
+ {Name: "name", Type: arrow.BinaryTypes.String, Nullable: true},
+ })
+ require.NoError(t, err)
+ defer inner.Release()
+
+ offsets :=
memory.NewBufferBytes(arrow.Int32Traits.CastToBytes([]int32{0, 3}))
+ data := NewData(arrow.ListOf(inner.DataType()), 1,
+ []*memory.Buffer{nil, offsets},
[]arrow.ArrayData{inner.Data()}, 0, 0)
+ offsets.Release()
+ list := NewListData(data)
+ data.Release()
+ defer list.Release()
+
+ outer, err := NewStructArrayWithFields([]arrow.Array{list},
[]arrow.Field{
+ {Name: "orders", Type: list.DataType(), Nullable: true},
+ })
+ require.NoError(t, err)
+ defer outer.Release()
+
+ err = ValidateFull(outer)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), `field "orders" -> list values -> field
"name"`)
+}
+
+func TestListValidateFullChecksOffsets(t *testing.T) {
+ values, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int32, strings.NewReader(`[1, 2]`))
+ require.NoError(t, err)
+ defer values.Release()
+
+ offsets :=
memory.NewBufferBytes(arrow.Int32Traits.CastToBytes([]int32{0, 2, 1}))
+ data := NewData(arrow.ListOf(arrow.PrimitiveTypes.Int32), 2,
+ []*memory.Buffer{nil, offsets},
[]arrow.ArrayData{values.Data()}, 0, 0)
+ offsets.Release()
+ list := NewListData(data)
+ data.Release()
+ defer list.Release()
+
+ assert.NoError(t, list.Validate())
+ err = list.ValidateFull()
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "not monotonically non-decreasing")
+}
+
+func TestTopLevelValidateFullRecursesDictionaryValues(t *testing.T) {
+ badDictionary := makeStringArrayRaw(t, []int32{0, 5, 3, 5}, "hello", 3,
0)
+ defer badDictionary.Release()
+ indices, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int8, strings.NewReader(`[0]`))
+ require.NoError(t, err)
+ defer indices.Release()
+
+ dt := &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Int8,
ValueType: arrow.BinaryTypes.String}
+ dict := NewDictionaryArray(dt, indices, badDictionary)
+ defer dict.Release()
+
+ err = ValidateFull(dict)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "dictionary")
+ assert.Contains(t, err.Error(), "not monotonically non-decreasing")
+}
+
+func TestDictionaryIndexBoundsOnlyRunDuringValidateFull(t *testing.T) {
+ indices, _, err := FromJSON(memory.DefaultAllocator,
arrow.PrimitiveTypes.Int8, strings.NewReader(`[0, 1]`))
+ require.NoError(t, err)
+ defer indices.Release()
+ values, _, err := FromJSON(memory.DefaultAllocator,
arrow.BinaryTypes.String, strings.NewReader(`["only"]`))
+ require.NoError(t, err)
+ defer values.Release()
+
+ dt := &arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Int8,
ValueType: arrow.BinaryTypes.String}
+ dict := NewDictionaryArray(dt, indices, values)
+ defer dict.Release()
+
+ assert.NoError(t, Validate(dict))
+ err = ValidateFull(dict)
+ require.Error(t, err)
+ assert.Contains(t, err.Error(), "dictionary indices")
+}