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 28ffdf09 fix(arrow/array): propagate embedded pointer nullability
(#1038)
28ffdf09 is described below
commit 28ffdf090a9028d1a4188c3d6ed4206275fe2f09
Author: Minh Vu <[email protected]>
AuthorDate: Tue Jul 28 17:07:38 2026 +0200
fix(arrow/array): propagate embedded pointer nullability (#1038)
## What changed
Carry nullability through anonymous embedded pointer paths while
collecting reflected struct fields.
## Why
A promoted non-pointer field beneath an embedded pointer can be absent
when that pointer is nil. The array builder already emits a null in that
case, but schema inference marked the field non-nullable because it only
inspected the leaf field type.
This change makes the inferred schema agree with the values produced by
the builder, including for multi-level embedded pointer paths.
## Validation
`go test ./arrow/array/arreflect`
---
arrow/array/arreflect/reflect.go | 17 ++++++++++-------
arrow/array/arreflect/reflect_go_to_arrow_test.go | 2 ++
arrow/array/arreflect/reflect_infer_test.go | 17 +++++++++++++++++
3 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/arrow/array/arreflect/reflect.go b/arrow/array/arreflect/reflect.go
index 248350b5..54d2084f 100644
--- a/arrow/array/arreflect/reflect.go
+++ b/arrow/array/arreflect/reflect.go
@@ -143,9 +143,10 @@ func parseDecimalOpt(opts *tagOpts, token string) {
}
type bfsEntry struct {
- t reflect.Type
- index []int
- depth int
+ t reflect.Type
+ index []int
+ depth int
+ nullable bool
}
type candidate struct {
@@ -214,20 +215,22 @@ func collectFieldCandidates(t reflect.Type)
map[string][]candidate {
if sf.Anonymous && !hasTag {
ft := sf.Type
+ pathNullable := entry.nullable || ft.Kind() ==
reflect.Ptr
for ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct {
queue = append(queue, bfsEntry{
- t: ft,
- index: fullIndex,
- depth: entry.depth + 1,
+ t: ft,
+ index: fullIndex,
+ depth: entry.depth + 1,
+ nullable: pathNullable,
})
continue
}
}
- nullable := sf.Type.Kind() == reflect.Ptr
+ nullable := entry.nullable || sf.Type.Kind() ==
reflect.Ptr
tagged := hasTag && opts.Name != ""
meta := fieldMeta{
diff --git a/arrow/array/arreflect/reflect_go_to_arrow_test.go
b/arrow/array/arreflect/reflect_go_to_arrow_test.go
index 7f098874..4992ae77 100644
--- a/arrow/array/arreflect/reflect_go_to_arrow_test.go
+++ b/arrow/array/arreflect/reflect_go_to_arrow_test.go
@@ -312,6 +312,8 @@ func TestBuildStructArray(t *testing.T) {
sa := arr.(*array.Struct)
require.Equal(t, 3, sa.Len())
require.Equal(t, 3, sa.NumField(), "expected 3 promoted fields
(Name, City, Zip)")
+ assert.True(t,
sa.DataType().(*arrow.StructType).Field(1).Nullable)
+ assert.True(t,
sa.DataType().(*arrow.StructType).Field(2).Nullable)
nameArr := sa.Field(0).(*array.String)
cityArr := sa.Field(1).(*array.String)
diff --git a/arrow/array/arreflect/reflect_infer_test.go
b/arrow/array/arreflect/reflect_infer_test.go
index 744d296f..aa94422e 100644
--- a/arrow/array/arreflect/reflect_infer_test.go
+++ b/arrow/array/arreflect/reflect_infer_test.go
@@ -154,6 +154,23 @@ func TestInferStructType(t *testing.T) {
assert.True(t, st.Field(1).Nullable, "Label should be nullable")
})
+ t.Run("fields promoted through embedded pointers are nullable", func(t
*testing.T) {
+ type Inner struct {
+ Value int32
+ }
+ type Middle struct {
+ *Inner
+ }
+ type Outer struct {
+ *Middle
+ }
+
+ st, err := inferStructType(reflect.TypeOf(Outer{}))
+ require.NoError(t, err)
+ require.Equal(t, 1, st.NumFields())
+ assert.True(t, st.Field(0).Nullable)
+ })
+
t.Run("arrow:\"-\" tagged field is excluded", func(t *testing.T) {
type S struct {
Keep string