blob: 74fda5039baf023fae842b1f2accc50b80fc15f6 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package array_test
import (
"math"
"reflect"
"testing"
"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/apache/arrow-go/v18/arrow/memory"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestStructArray(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
f1s = []byte{'j', 'o', 'e', 'b', 'o', 'b', 'm', 'a', 'r', 'k'}
f2s = []int32{1, 2, 3, 4}
f1Lengths = []int{3, 0, 3, 4}
f1Offsets = []int32{0, 3, 3, 6, 10}
f1Valids = []bool{true, false, true, true}
isValid = []bool{true, true, true, true}
fields = []arrow.Field{
{Name: "f1", Type: arrow.ListOf(arrow.PrimitiveTypes.Uint8)},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
for i := 0; i < 10; i++ {
f1b := sb.FieldBuilder(0).(*array.ListBuilder)
f1vb := f1b.ValueBuilder().(*array.Uint8Builder)
f2b := sb.FieldBuilder(1).(*array.Int32Builder)
if got, want := sb.NumField(), 2; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
sb.Resize(len(f1Lengths))
f1vb.Resize(len(f1s))
f2b.Resize(len(f2s))
pos := 0
for i, length := range f1Lengths {
f1b.Append(f1Valids[i])
for j := 0; j < length; j++ {
f1vb.Append(f1s[pos])
pos++
}
f2b.Append(f2s[i])
}
for _, valid := range isValid {
sb.Append(valid)
}
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
arr.Retain()
arr.Release()
if got, want := arr.DataType().ID(), arrow.STRUCT; got != want {
t.Fatalf("got=%v, want=%v", got, want)
}
if got, want := arr.Len(), len(isValid); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i, valid := range isValid {
if got, want := arr.IsValid(i), valid; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
}
{
f1arr := arr.Field(0).(*array.List)
if got, want := f1arr.Len(), len(f1Lengths); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i := range f1Lengths {
if got, want := f1arr.IsValid(i), f1Valids[i]; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
if got, want := f1arr.IsNull(i), f1Lengths[i] == 0; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
}
if got, want := f1arr.Offsets(), f1Offsets; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%v, want=%v", got, want)
}
varr := f1arr.ListValues().(*array.Uint8)
if got, want := varr.Uint8Values(), f1s; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%v, want=%v", got, want)
}
}
{
f2arr := arr.Field(1).(*array.Int32)
if got, want := f2arr.Len(), len(f2s); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
if got, want := f2arr.Int32Values(), f2s; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%d, want=%d", got, want)
}
}
}
}
func TestStructValidateFullRejectsShortField(t *testing.T) {
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
dt := arrow.StructOf(
arrow.Field{Name: "a", Type: arrow.PrimitiveTypes.Int32},
arrow.Field{Name: "b", Type: arrow.PrimitiveTypes.Int32},
)
b := array.NewStructBuilder(mem, dt)
defer b.Release()
b.Append(true)
b.Append(true)
b.FieldBuilder(0).(*array.Int32Builder).Append(1)
b.FieldBuilder(1).(*array.Int32Builder).AppendValues([]int32{2, 3}, nil)
arr := b.NewStructArray()
defer arr.Release()
assert.ErrorIs(t, arr.Validate(), arrow.ErrInvalid)
assert.ErrorIs(t, arr.ValidateFull(), arrow.ErrInvalid)
assert.ErrorIs(t, array.ValidateFull(arr), arrow.ErrInvalid)
sliced := array.NewSlice(arr, 1, 2).(*array.Struct)
defer sliced.Release()
assert.Zero(t, sliced.Field(0).Len())
assert.ErrorIs(t, sliced.Validate(), arrow.ErrInvalid)
assert.ErrorIs(t, sliced.ValidateFull(), arrow.ErrInvalid)
}
func TestStructValidateRejectsOffsetLengthOverflow(t *testing.T) {
childData := array.NewData(arrow.PrimitiveTypes.Int32, 0, []*memory.Buffer{nil, nil}, nil, 0, 0)
defer childData.Release()
data := array.NewData(arrow.StructOf(arrow.Field{Name: "value", Type: arrow.PrimitiveTypes.Int32}),
math.MaxInt, nil, []arrow.ArrayData{childData}, 0, 1)
defer data.Release()
arr := array.NewStructData(data)
defer arr.Release()
require.ErrorIs(t, arr.Validate(), arrow.ErrInvalid)
}
func TestStructStringRoundTrip(t *testing.T) {
// 1. create array
mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer mem.AssertSize(t, 0)
dt := arrow.StructOf(
arrow.Field{Name: "nullable_bool", Type: new(arrow.BooleanType), Nullable: true},
arrow.Field{Name: "non_nullable_bool", Type: new(arrow.BooleanType)},
)
builder := array.NewStructBuilder(memory.DefaultAllocator, dt)
nullableBld := builder.FieldBuilder(0).(*array.BooleanBuilder)
nonNullableBld := builder.FieldBuilder(1).(*array.BooleanBuilder)
builder.Append(true)
nullableBld.Append(true)
nonNullableBld.Append(true)
builder.Append(true)
nullableBld.AppendNull()
nonNullableBld.Append(true)
builder.AppendNull()
arr := builder.NewArray().(*array.Struct)
// 2. create array via AppendValueFromString
b1 := array.NewStructBuilder(mem, dt)
defer b1.Release()
for i := 0; i < arr.Len(); i++ {
assert.NoError(t, b1.AppendValueFromString(arr.ValueStr(i)))
}
arr1 := b1.NewArray().(*array.Struct)
defer arr1.Release()
assert.True(t, array.Equal(arr, arr1))
}
func TestStructArrayEmpty(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
sb := array.NewStructBuilder(pool, arrow.StructOf())
defer sb.Release()
if got, want := sb.NumField(), 0; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
arr := sb.NewArray().(*array.Struct)
if got, want := arr.Len(), 0; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
if got, want := arr.NumField(), 0; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
}
func TestStructArrayBulkAppend(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
f1s = []byte{'j', 'o', 'e', 'b', 'o', 'b', 'm', 'a', 'r', 'k'}
f2s = []int32{1, 2, 3, 4}
f1Lengths = []int{3, 0, 3, 4}
f1Offsets = []int32{0, 3, 3, 6, 10}
f1Valids = []bool{true, false, true, true}
isValid = []bool{true, true, true, true}
fields = []arrow.Field{
{Name: "f1", Type: arrow.ListOf(arrow.PrimitiveTypes.Uint8)},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
for i := 0; i < 10; i++ {
f1b := sb.FieldBuilder(0).(*array.ListBuilder)
f1vb := f1b.ValueBuilder().(*array.Uint8Builder)
f2b := sb.FieldBuilder(1).(*array.Int32Builder)
if got, want := sb.NumField(), 2; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
sb.Resize(len(f1Lengths))
f1vb.Resize(len(f1s))
f2b.Resize(len(f2s))
sb.AppendValues(isValid)
f1b.AppendValues(f1Offsets, f1Valids)
f1vb.AppendValues(f1s, nil)
f2b.AppendValues(f2s, nil)
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
if got, want := arr.DataType().ID(), arrow.STRUCT; got != want {
t.Fatalf("got=%v, want=%v", got, want)
}
if got, want := arr.Len(), len(isValid); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i, valid := range isValid {
if got, want := arr.IsValid(i), valid; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
}
{
f1arr := arr.Field(0).(*array.List)
if got, want := f1arr.Len(), len(f1Lengths); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i := range f1Lengths {
if got, want := f1arr.IsValid(i), f1Valids[i]; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
if got, want := f1arr.IsNull(i), f1Lengths[i] == 0; got != want {
t.Fatalf("got[%d]=%v, want[%d]=%v", i, got, i, want)
}
}
if got, want := f1arr.Offsets(), f1Offsets; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%v, want=%v", got, want)
}
varr := f1arr.ListValues().(*array.Uint8)
if got, want := varr.Uint8Values(), f1s; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%v, want=%v", got, want)
}
}
{
f2arr := arr.Field(1).(*array.Int32)
if got, want := f2arr.Len(), len(f2s); got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
if got, want := f2arr.Int32Values(), f2s; !reflect.DeepEqual(got, want) {
t.Fatalf("got=%d, want=%d", got, want)
}
}
}
}
func TestStructArrayStringer(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
f1s = []float64{1.1, 1.2, 1.3, 1.4}
f2s = []int32{1, 2, 3, 4}
fields = []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Float64},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
f1b := sb.FieldBuilder(0).(*array.Float64Builder)
f2b := sb.FieldBuilder(1).(*array.Int32Builder)
if got, want := sb.NumField(), 2; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i := range f1s {
sb.Append(true)
switch i {
case 1:
f1b.AppendNull()
f2b.Append(f2s[i])
case 2:
f1b.Append(f1s[i])
f2b.AppendNull()
default:
f1b.Append(f1s[i])
f2b.Append(f2s[i])
}
}
assert.NoError(t, sb.AppendValueFromString(`{"f1": 1.1, "f2": 1}`))
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
assert.Equal(t, `{"f1":1.1,"f2":1}`, arr.ValueStr(4))
want := "{[1.1 (null) 1.3 1.4 1.1] [1 2 (null) 4 1]}"
got := arr.String()
if got != want {
t.Fatalf("invalid string representation:\ngot = %q\nwant= %q", got, want)
}
}
func TestStructArraySlice(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
f1s = []float64{1.1, 1.2, 1.3, 1.4}
f2s = []int32{1, 2, 3, 4}
valids = []bool{true, true, true, true}
fields = []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Float64},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
f1b := sb.FieldBuilder(0).(*array.Float64Builder)
f2b := sb.FieldBuilder(1).(*array.Int32Builder)
if got, want := sb.NumField(), 2; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
for i := range f1s {
sb.Append(valids[i])
switch i {
case 1:
f1b.AppendNull()
f2b.Append(f2s[i])
case 2:
f1b.Append(f1s[i])
f2b.AppendNull()
default:
f1b.Append(f1s[i])
f2b.Append(f2s[i])
}
}
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
// Slice
arrSlice := array.NewSlice(arr, 2, 4).(*array.Struct)
defer arrSlice.Release()
want := "{[1.3 1.4] [(null) 4]}"
got := arrSlice.String()
if got != want {
t.Fatalf("invalid string representation:\ngot = %q\nwant= %q", got, want)
}
}
func TestStructArrayNullBitmap(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
f1s = []float64{1.1, 1.2, 1.3, 1.4}
f2s = []int32{1, 2, 3, 4}
valids = []bool{true, true, true, false}
fields = []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Float64},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
f1b := sb.FieldBuilder(0).(*array.Float64Builder)
f2b := sb.FieldBuilder(1).(*array.Int32Builder)
if got, want := sb.NumField(), 2; got != want {
t.Fatalf("got=%d, want=%d", got, want)
}
sb.AppendValues(valids)
for i := range f1s {
f1b.Append(f1s[i])
switch i {
case 1:
f2b.AppendNull()
default:
f2b.Append(f2s[i])
}
}
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
want := "{[1.1 1.2 1.3 (null)] [1 (null) 3 (null)]}"
got := arr.String()
if got != want {
t.Fatalf("invalid string representation:\ngot = %q\nwant= %q", got, want)
}
}
func TestStructArrayStringerMasksRequiredChildWithoutNullBitmap(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
childData := array.NewData(arrow.PrimitiveTypes.Int32, 3, []*memory.Buffer{
nil,
memory.NewBufferBytes(arrow.Int32Traits.CastToBytes([]int32{1, 2, 3})),
}, nil, 0, 0)
defer childData.Release()
child := array.MakeFromData(childData)
defer child.Release()
require.Empty(t, child.NullBitmapBytes())
fields := []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
}
nullBitmap := memory.NewBufferBytes([]byte{0x05})
arr, err := array.NewStructArrayWithFieldsAndNulls([]arrow.Array{child}, fields, nullBitmap, 1, 0)
require.NoError(t, err)
defer arr.Release()
assert.NotPanics(t, func() {
assert.Equal(t, "{[1 (null) 3]}", arr.String())
})
}
func TestNewStructArrayWithNullsValidatesOffset(t *testing.T) {
builder := array.NewInt32Builder(memory.DefaultAllocator)
defer builder.Release()
builder.AppendValues([]int32{1, 2, 3}, nil)
child := builder.NewArray()
defer child.Release()
for _, test := range []struct {
name string
offset int
length int
}{
{name: "zero", offset: 0, length: 3},
{name: "sliced", offset: 1, length: 2},
{name: "empty", offset: 3, length: 0},
} {
t.Run(test.name, func(t *testing.T) {
arr, err := array.NewStructArrayWithNulls(
[]arrow.Array{child}, []string{"value"}, nil, 0, test.offset)
require.NoError(t, err)
defer arr.Release()
assert.Equal(t, test.length, arr.Len())
assert.Equal(t, test.length, arr.Field(0).Len())
})
}
for _, offset := range []int{-1, 4} {
arr, err := array.NewStructArrayWithNulls(
[]arrow.Array{child}, []string{"value"}, nil, 0, offset)
require.ErrorIs(t, err, arrow.ErrInvalid)
require.Nil(t, arr)
}
_, err := array.NewStructArrayWithNulls(
[]arrow.Array{child}, []string{"value"}, nil, 1, 0)
require.ErrorIs(t, err, arrow.ErrInvalid)
fields := []arrow.Field{{Name: "value", Type: arrow.PrimitiveTypes.Int32}}
for _, offset := range []int{-1, 4} {
arr, err := array.NewStructArrayWithFieldsAndNulls(
[]arrow.Array{child}, fields, nil, 0, offset)
require.ErrorIs(t, err, arrow.ErrInvalid)
require.Nil(t, arr)
}
}
func TestStructStringMasksParentValidityAtPhysicalOffset(t *testing.T) {
fields := []arrow.Field{{Name: "value", Type: arrow.PrimitiveTypes.Int32}}
parentNulls := memory.NewBufferBytes([]byte{0x05})
t.Run("child validity bitmap", func(t *testing.T) {
values := make([]int32, 11)
for i := range values {
values[i] = int32(i)
}
validity := memory.NewBufferBytes([]byte{0xff, 0x07})
valueBuffer := memory.NewBufferBytes(arrow.Int32Traits.CastToBytes(values))
childData := array.NewData(arrow.PrimitiveTypes.Int32, len(values), []*memory.Buffer{
validity, valueBuffer,
}, nil, 0, 0)
defer childData.Release()
child := array.MakeFromData(childData)
defer child.Release()
childSlice := array.NewSlice(child, 8, 11)
defer childSlice.Release()
arr, err := array.NewStructArrayWithFieldsAndNulls(
[]arrow.Array{childSlice}, fields, parentNulls, 1, 1)
require.NoError(t, err)
defer arr.Release()
assert.Equal(t, "{[(null) 10]}", arr.String())
})
t.Run("child without validity bitmap", func(t *testing.T) {
builder := array.NewInt32Builder(memory.DefaultAllocator)
defer builder.Release()
values := make([]int32, 11)
for i := range values {
values[i] = int32(i)
}
builder.AppendValues(values, nil)
child := builder.NewArray()
defer child.Release()
childSlice := array.NewSlice(child, 8, 11)
defer childSlice.Release()
arr, err := array.NewStructArrayWithFieldsAndNulls(
[]arrow.Array{childSlice}, fields, parentNulls, 1, 1)
require.NoError(t, err)
defer arr.Release()
assert.Equal(t, "{[(null) 10]}", arr.String())
})
t.Run("identical bitmap with different physical offsets", func(t *testing.T) {
values := []int32{10, 11, 12, 13}
validity := memory.NewBufferBytes([]byte{0x0d})
valueBuffer := memory.NewBufferBytes(arrow.Int32Traits.CastToBytes(values))
childData := array.NewData(arrow.PrimitiveTypes.Int32, len(values), []*memory.Buffer{
validity, valueBuffer,
}, nil, 0, 0)
defer childData.Release()
child := array.MakeFromData(childData)
defer child.Release()
childSlice := array.NewSlice(child, 1, 4)
defer childSlice.Release()
parentNulls := memory.NewBufferBytes([]byte{0x0d})
defer parentNulls.Release()
arr, err := array.NewStructArrayWithFieldsAndNulls(
[]arrow.Array{childSlice}, fields, parentNulls, 1, 1)
require.NoError(t, err)
defer arr.Release()
assert.Equal(t, "{[(null) 13]}", arr.String())
})
}
func TestStructArrayUnmarshalJSONMissingFields(t *testing.T) {
pool := memory.NewGoAllocator()
var (
fields = []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Float64, Nullable: true},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32},
{
Name: "f3", Type: arrow.StructOf(
[]arrow.Field{
{Name: "f3_1", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "f3_2", Type: arrow.BinaryTypes.String, Nullable: true},
{Name: "f3_3", Type: arrow.BinaryTypes.String, Nullable: false},
}...,
),
},
}
dtype = arrow.StructOf(fields...)
)
tests := []struct {
name string
jsonInput string
want string
invalid bool
}{
{
name: "missing required field",
jsonInput: `[{"f2": 3, "f3": {"f3_1": "test"}}]`,
invalid: true,
want: "",
},
{
name: "missing optional fields",
jsonInput: `[{"f2": 3, "f3": {"f3_3": "test"}}]`,
invalid: false,
want: `{[(null)] [3] {[(null)] [(null)] ["test"]}}`,
},
}
for _, tc := range tests {
t.Run(
tc.name, func(t *testing.T) {
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
err := sb.UnmarshalJSON([]byte(tc.jsonInput))
if err != nil {
t.Fatal(err)
}
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
if tc.invalid {
require.ErrorIs(t, array.ValidateFull(arr), arrow.ErrInvalid)
return
}
require.NoError(t, array.ValidateFull(arr))
got := arr.String()
if got != tc.want {
t.Fatalf("invalid string representation:\ngot = %q\nwant= %q", got, tc.want)
}
},
)
}
}
func TestCreateStructWithNulls(t *testing.T) {
pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
defer pool.AssertSize(t, 0)
var (
fields = []arrow.Field{
{Name: "f1", Type: arrow.PrimitiveTypes.Float64, Nullable: true},
{Name: "f2", Type: arrow.PrimitiveTypes.Int32, Nullable: false},
}
dtype = arrow.StructOf(fields...)
)
sb := array.NewStructBuilder(pool, dtype)
defer sb.Release()
sb.AppendNulls(100)
arr := sb.NewArray().(*array.Struct)
defer arr.Release()
assert.EqualValues(t, 100, arr.Len())
assert.EqualValues(t, 100, arr.NullN())
arr2, err := array.NewStructArrayWithFieldsAndNulls(
[]arrow.Array{arr.Field(0), arr.Field(1)}, fields, arr.Data().Buffers()[0], arr.NullN(), 0)
require.NoError(t, err)
defer arr2.Release()
assert.True(t, array.Equal(arr, arr2))
}