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 fb1040a0 feat(arrow/array): add ValueAsAny for native Go values (#1170)
fb1040a0 is described below

commit fb1040a01a754596623ddb724d442905b4bfbb7b
Author: Digvijay <[email protected]>
AuthorDate: Fri Aug 14 11:13:27 2026 -0500

    feat(arrow/array): add ValueAsAny for native Go values (#1170)
    
    ### Rationale for this change
    
    Downstream code often needs type-agnostic access to Arrow values as
    native Go types. `GetOneForMarshal` is JSON-oriented (e.g. `int8` →
    `float64`, timestamps/decimals as strings), which forces boilerplate
    type switches for generic ingestion.
    
    Fixes #462
    
    ### What changes are included in this PR?
    
    - Add `array.ValueAsAny(arr, i)` plus an optional `ValueAsAnyer`
    interface, without adding a method to `arrow.Array`
    - Implement it across array types and common extensions so nulls return
    `nil` and values stay native (`int8`, `arrow.Timestamp`, decimal nums,
    `[]any` for lists, ordered `[name, value]` pairs for structs)
    - Union slots keep their type id when the selected child is null
    (`[]any{typeID, nil}`)
    - Variant decode errors are returned as the `any` value rather than
    collapsed to `nil`
    - Add unit tests covering primitives, temporal/decimal, nested,
    dictionary, union, duplicate struct fields, and variant decode errors,
    including differences vs `GetOneForMarshal`
    
    ### Are these changes tested?
    
    - `go test ./arrow/ ./arrow/array/ ./arrow/extensions/`
    
    ### Are there any user-facing changes?
    
    Yes — new `array.ValueAsAny` helper for native Go value access.
    
    ---------
    
    Signed-off-by: Digvijay <[email protected]>
    Co-authored-by: Digvijay  Waghela 
<[email protected]>
---
 arrow/array/binary.go                     |  21 ++
 arrow/array/boolean.go                    |   7 +
 arrow/array/decimal.go                    |   7 +
 arrow/array/dictionary.go                 |   7 +
 arrow/array/encoded.go                    |   7 +
 arrow/array/extension.go                  |   4 +
 arrow/array/fixed_size_list.go            |   4 +
 arrow/array/fixedsize_binary.go           |   7 +
 arrow/array/float16.go                    |   7 +
 arrow/array/interval.go                   |  21 ++
 arrow/array/list.go                       |  16 ++
 arrow/array/null.go                       |   4 +
 arrow/array/numeric_generic.go            |  48 +++++
 arrow/array/string.go                     |  21 ++
 arrow/array/struct.go                     |  12 ++
 arrow/array/timestamp.go                  |   7 +
 arrow/array/union.go                      |  15 ++
 arrow/array/value_as_any.go               |  60 ++++++
 arrow/array/value_as_any_test.go          | 333 ++++++++++++++++++++++++++++++
 arrow/extensions/bool8.go                 |   7 +
 arrow/extensions/json.go                  |   7 +
 arrow/extensions/timestamp_with_offset.go |   7 +
 arrow/extensions/uuid.go                  |   7 +
 arrow/extensions/variant.go               |  11 +
 arrow/extensions/variant_test.go          |  31 +++
 25 files changed, 678 insertions(+)

diff --git a/arrow/array/binary.go b/arrow/array/binary.go
index d23e2a0b..fe986583 100644
--- a/arrow/array/binary.go
+++ b/arrow/array/binary.go
@@ -159,6 +159,13 @@ func (a *Binary) GetOneForMarshal(i int) interface{} {
        return a.Value(i)
 }
 
+func (a *Binary) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *Binary) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
@@ -353,6 +360,13 @@ func (a *LargeBinary) GetOneForMarshal(i int) interface{} {
        return a.Value(i)
 }
 
+func (a *LargeBinary) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *LargeBinary) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
@@ -540,6 +554,13 @@ func (a *BinaryView) GetOneForMarshal(i int) interface{} {
        return a.Value(i)
 }
 
+func (a *BinaryView) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *BinaryView) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
diff --git a/arrow/array/boolean.go b/arrow/array/boolean.go
index d579fa0c..80957f19 100644
--- a/arrow/array/boolean.go
+++ b/arrow/array/boolean.go
@@ -97,6 +97,13 @@ func (a *Boolean) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *Boolean) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *Boolean) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
diff --git a/arrow/array/decimal.go b/arrow/array/decimal.go
index 704b1d93..aac075a2 100644
--- a/arrow/array/decimal.go
+++ b/arrow/array/decimal.go
@@ -99,6 +99,13 @@ func (a *baseDecimal[T]) GetOneForMarshal(i int) any {
        return n.ToBigFloat(scale).Text('g', int(typ.GetPrecision()))
 }
 
+func (a *baseDecimal[T]) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *baseDecimal[T]) MarshalJSON() ([]byte, error) {
        vals := make([]any, a.Len())
        for i := 0; i < a.Len(); i++ {
diff --git a/arrow/array/dictionary.go b/arrow/array/dictionary.go
index d151b209..14c234d2 100644
--- a/arrow/array/dictionary.go
+++ b/arrow/array/dictionary.go
@@ -295,6 +295,13 @@ func (d *Dictionary) GetOneForMarshal(i int) interface{} {
        return d.Dictionary().GetOneForMarshal(vidx)
 }
 
+func (d *Dictionary) ValueAsAny(i int) any {
+       if d.IsNull(i) {
+               return nil
+       }
+       return ValueAsAny(d.Dictionary(), d.GetValueIndex(i))
+}
+
 func (d *Dictionary) MarshalJSON() ([]byte, error) {
        vals := make([]any, d.Len())
        for i := range d.Len() {
diff --git a/arrow/array/encoded.go b/arrow/array/encoded.go
index 05d5bda5..219a75cb 100644
--- a/arrow/array/encoded.go
+++ b/arrow/array/encoded.go
@@ -332,6 +332,13 @@ func (r *RunEndEncoded) GetOneForMarshal(i int) 
interface{} {
        return r.values.GetOneForMarshal(r.GetPhysicalIndex(i))
 }
 
+func (r *RunEndEncoded) ValueAsAny(i int) any {
+       if r.IsNull(i) {
+               return nil
+       }
+       return ValueAsAny(r.values, r.GetPhysicalIndex(i))
+}
+
 func (r *RunEndEncoded) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
diff --git a/arrow/array/extension.go b/arrow/array/extension.go
index 5a4d3c86..c82db8e0 100644
--- a/arrow/array/extension.go
+++ b/arrow/array/extension.go
@@ -124,6 +124,10 @@ func (e *ExtensionArrayBase) GetOneForMarshal(i int) 
interface{} {
        return e.storage.GetOneForMarshal(i)
 }
 
+func (e *ExtensionArrayBase) ValueAsAny(i int) any {
+       return ValueAsAny(e.storage, i)
+}
+
 func (e *ExtensionArrayBase) MarshalJSON() ([]byte, error) {
        return json.Marshal(e.storage)
 }
diff --git a/arrow/array/fixed_size_list.go b/arrow/array/fixed_size_list.go
index 6a4f75ca..f13797cc 100644
--- a/arrow/array/fixed_size_list.go
+++ b/arrow/array/fixed_size_list.go
@@ -140,6 +140,10 @@ func (a *FixedSizeList) GetOneForMarshal(i int) 
interface{} {
        return json.RawMessage(v)
 }
 
+func (a *FixedSizeList) ValueAsAny(i int) any {
+       return valueAsAnyFromListLike(a, i)
+}
+
 func (a *FixedSizeList) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
diff --git a/arrow/array/fixedsize_binary.go b/arrow/array/fixedsize_binary.go
index 8530ed6b..567bcba0 100644
--- a/arrow/array/fixedsize_binary.go
+++ b/arrow/array/fixedsize_binary.go
@@ -97,6 +97,13 @@ func (a *FixedSizeBinary) GetOneForMarshal(i int) 
interface{} {
        return a.Value(i)
 }
 
+func (a *FixedSizeBinary) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *FixedSizeBinary) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
diff --git a/arrow/array/float16.go b/arrow/array/float16.go
index 41276803..02802c7d 100644
--- a/arrow/array/float16.go
+++ b/arrow/array/float16.go
@@ -84,6 +84,13 @@ func (a *Float16) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *Float16) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *Float16) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i, v := range a.values {
diff --git a/arrow/array/interval.go b/arrow/array/interval.go
index 2c029c25..5053c3a2 100644
--- a/arrow/array/interval.go
+++ b/arrow/array/interval.go
@@ -101,6 +101,13 @@ func (a *MonthInterval) GetOneForMarshal(i int) 
interface{} {
        return nil
 }
 
+func (a *MonthInterval) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // MarshalJSON will create a json array out of a MonthInterval array,
 // each value will be an object of the form {"months": #} where
 // # is the numeric value of that index
@@ -406,6 +413,13 @@ func (a *DayTimeInterval) GetOneForMarshal(i int) 
interface{} {
        return nil
 }
 
+func (a *DayTimeInterval) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // MarshalJSON will marshal this array to JSON as an array of objects,
 // consisting of the form {"days": #, "milliseconds": #} for each element.
 func (a *DayTimeInterval) MarshalJSON() ([]byte, error) {
@@ -710,6 +724,13 @@ func (a *MonthDayNanoInterval) GetOneForMarshal(i int) 
interface{} {
        return nil
 }
 
+func (a *MonthDayNanoInterval) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // MarshalJSON will marshal this array to a JSON array with elements
 // marshalled to the form {"months": #, "days": #, "nanoseconds": #}
 func (a *MonthDayNanoInterval) MarshalJSON() ([]byte, error) {
diff --git a/arrow/array/list.go b/arrow/array/list.go
index b848192b..dd9d82ef 100644
--- a/arrow/array/list.go
+++ b/arrow/array/list.go
@@ -112,6 +112,10 @@ func (a *List) GetOneForMarshal(i int) interface{} {
        return json.RawMessage(v)
 }
 
+func (a *List) ValueAsAny(i int) any {
+       return valueAsAnyFromListLike(a, i)
+}
+
 func (a *List) Validate() error     { return validateListArray(a, false) }
 func (a *List) ValidateFull() error { return validateListArray(a, true) }
 
@@ -247,6 +251,10 @@ func (a *LargeList) GetOneForMarshal(i int) interface{} {
        return json.RawMessage(v)
 }
 
+func (a *LargeList) ValueAsAny(i int) any {
+       return valueAsAnyFromListLike(a, i)
+}
+
 func (a *LargeList) Validate() error     { return validateLargeListArray(a, 
false) }
 func (a *LargeList) ValidateFull() error { return validateLargeListArray(a, 
true) }
 
@@ -732,6 +740,10 @@ func (a *ListView) GetOneForMarshal(i int) interface{} {
        return json.RawMessage(v)
 }
 
+func (a *ListView) ValueAsAny(i int) any {
+       return valueAsAnyFromListLike(a, i)
+}
+
 func (a *ListView) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
@@ -879,6 +891,10 @@ func (a *LargeListView) GetOneForMarshal(i int) 
interface{} {
        return json.RawMessage(v)
 }
 
+func (a *LargeListView) ValueAsAny(i int) any {
+       return valueAsAnyFromListLike(a, i)
+}
+
 func (a *LargeListView) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
diff --git a/arrow/array/null.go b/arrow/array/null.go
index 8f8f5805..3f7db087 100644
--- a/arrow/array/null.go
+++ b/arrow/array/null.go
@@ -84,6 +84,10 @@ func (a *Null) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *Null) ValueAsAny(i int) any {
+       return nil
+}
+
 func (a *Null) MarshalJSON() ([]byte, error) {
        return json.Marshal(make([]interface{}, a.Len()))
 }
diff --git a/arrow/array/numeric_generic.go b/arrow/array/numeric_generic.go
index 24edd549..2ff88427 100644
--- a/arrow/array/numeric_generic.go
+++ b/arrow/array/numeric_generic.go
@@ -91,6 +91,14 @@ func (a *numericArray[T]) GetOneForMarshal(i int) any {
        return a.values[i]
 }
 
+func (a *numericArray[T]) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       return a.values[i]
+}
+
 func (a *numericArray[T]) MarshalJSON() ([]byte, error) {
        vals := make([]any, a.Len())
        for i := range a.Len() {
@@ -115,6 +123,14 @@ func (a *oneByteArrs[T]) GetOneForMarshal(i int) any {
        return float64(a.values[i]) // prevent uint8/int8 from being seen as 
binary data
 }
 
+func (a *oneByteArrs[T]) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       return a.values[i]
+}
+
 func (a *oneByteArrs[T]) MarshalJSON() ([]byte, error) {
        vals := make([]any, a.Len())
        for i := range a.Len() {
@@ -157,6 +173,14 @@ func (a *floatArray[T]) GetOneForMarshal(i int) any {
        }
 }
 
+func (a *floatArray[T]) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       return a.Value(i)
+}
+
 func (a *floatArray[T]) MarshalJSON() ([]byte, error) {
        vals := make([]any, a.Len())
        for i := range a.values {
@@ -210,6 +234,14 @@ func (d *dateArray[T]) GetOneForMarshal(i int) interface{} 
{
        return d.values[i].FormattedString()
 }
 
+func (d *dateArray[T]) ValueAsAny(i int) any {
+       if d.IsNull(i) {
+               return nil
+       }
+
+       return d.values[i]
+}
+
 type timeType interface {
        TimeUnit() arrow.TimeUnit
 }
@@ -246,6 +278,14 @@ func (a *timeArray[T]) GetOneForMarshal(i int) interface{} 
{
        return 
a.values[i].ToTime(a.DataType().(timeType).TimeUnit()).Format("15:04:05.999999999")
 }
 
+func (a *timeArray[T]) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       return a.values[i]
+}
+
 type Duration struct {
        numericArray[arrow.Duration]
 }
@@ -281,6 +321,14 @@ func (a *Duration) GetOneForMarshal(i int) any {
        return fmt.Sprintf("%d%s", a.values[i], 
a.DataType().(timeType).TimeUnit())
 }
 
+func (a *Duration) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       return a.values[i]
+}
+
 type Int64 struct {
        numericArray[int64]
 }
diff --git a/arrow/array/string.go b/arrow/array/string.go
index 519b2cae..afd78278 100644
--- a/arrow/array/string.go
+++ b/arrow/array/string.go
@@ -161,6 +161,13 @@ func (a *String) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *String) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *String) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
@@ -369,6 +376,13 @@ func (a *LargeString) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *LargeString) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *LargeString) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
@@ -549,6 +563,13 @@ func (a *StringView) GetOneForMarshal(i int) interface{} {
        return a.Value(i)
 }
 
+func (a *StringView) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *StringView) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := 0; i < a.Len(); i++ {
diff --git a/arrow/array/struct.go b/arrow/array/struct.go
index 7a3622bb..49bcc570 100644
--- a/arrow/array/struct.go
+++ b/arrow/array/struct.go
@@ -280,6 +280,18 @@ func (a *Struct) GetOneForMarshal(i int) interface{} {
        return tmp
 }
 
+func (a *Struct) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       fieldList := a.data.dtype.(*arrow.StructType).Fields()
+       out := make([]any, len(a.fields))
+       for j, d := range a.fields {
+               out[j] = []any{fieldList[j].Name, ValueAsAny(d, i)}
+       }
+       return out
+}
+
 func (a *Struct) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
diff --git a/arrow/array/timestamp.go b/arrow/array/timestamp.go
index cf17b31f..523c3a01 100644
--- a/arrow/array/timestamp.go
+++ b/arrow/array/timestamp.go
@@ -116,6 +116,13 @@ func (a *Timestamp) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *Timestamp) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 func (a *Timestamp) MarshalJSON() ([]byte, error) {
        vals := make([]interface{}, a.Len())
        for i := range a.values {
diff --git a/arrow/array/union.go b/arrow/array/union.go
index ed894ab1..afa4e84c 100644
--- a/arrow/array/union.go
+++ b/arrow/array/union.go
@@ -333,6 +333,13 @@ func (a *SparseUnion) GetOneForMarshal(i int) interface{} {
        return []interface{}{typeID, data.GetOneForMarshal(i)}
 }
 
+func (a *SparseUnion) ValueAsAny(i int) any {
+       typeID := a.RawTypeCodes()[i]
+       childID := a.ChildID(i)
+       data := a.Field(childID)
+       return []any{typeID, ValueAsAny(data, i)}
+}
+
 func (a *SparseUnion) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
@@ -627,6 +634,14 @@ func (a *DenseUnion) GetOneForMarshal(i int) interface{} {
        return []interface{}{typeID, data.GetOneForMarshal(offset)}
 }
 
+func (a *DenseUnion) ValueAsAny(i int) any {
+       typeID := a.RawTypeCodes()[i]
+       childID := a.ChildID(i)
+       data := a.Field(childID)
+       offset := int(a.RawValueOffsets()[i])
+       return []any{typeID, ValueAsAny(data, offset)}
+}
+
 func (a *DenseUnion) MarshalJSON() ([]byte, error) {
        var buf bytes.Buffer
        enc := json.NewEncoder(&buf)
diff --git a/arrow/array/value_as_any.go b/arrow/array/value_as_any.go
new file mode 100644
index 00000000..2a5f46fe
--- /dev/null
+++ b/arrow/array/value_as_any.go
@@ -0,0 +1,60 @@
+// 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
+
+import (
+       "fmt"
+
+       "github.com/apache/arrow-go/v18/arrow"
+)
+
+// ValueAsAnyer is an optional interface for arrays that can return a native
+// Go value for a slot. Built-in array types implement it. It is not part of
+// arrow.Array so that adding this capability is not a source-incompatible
+// change for downstream Array implementations.
+type ValueAsAnyer interface {
+       ValueAsAny(i int) any
+}
+
+// ValueAsAny returns the native Go value at index i, or nil if the slot is 
null.
+// Unlike GetOneForMarshal, values are not converted for JSON encoding
+// (for example int8 stays int8, timestamps stay arrow.Timestamp, lists are
+// []any of native values, and structs are []any of [name, value] pairs so
+// field order and duplicate names are preserved).
+//
+// If arr does not implement ValueAsAnyer, ValueAsAny panics.
+func ValueAsAny(arr arrow.Array, i int) any {
+       if v, ok := arr.(ValueAsAnyer); ok {
+               return v.ValueAsAny(i)
+       }
+       panic(fmt.Sprintf("arrow/array: %T does not implement ValueAsAny", arr))
+}
+
+// valueAsAnyFromListLike builds a []any of native child values for one list 
slot.
+func valueAsAnyFromListLike(a ListLike, i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+
+       start, end := a.ValueOffsets(i)
+       vals := a.ListValues()
+       out := make([]any, end-start)
+       for j := start; j < end; j++ {
+               out[j-start] = ValueAsAny(vals, int(j))
+       }
+       return out
+}
diff --git a/arrow/array/value_as_any_test.go b/arrow/array/value_as_any_test.go
new file mode 100644
index 00000000..ae173896
--- /dev/null
+++ b/arrow/array/value_as_any_test.go
@@ -0,0 +1,333 @@
+// 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"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/decimal128"
+       "github.com/apache/arrow-go/v18/arrow/float16"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestValueAsAnyPrimitives(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       t.Run("int8 native vs marshal", func(t *testing.T) {
+               b := array.NewInt8Builder(mem)
+               defer b.Release()
+               b.AppendValues([]int8{1, -2}, []bool{true, true})
+               b.AppendNull()
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, int8(1), array.ValueAsAny(arr, 0))
+               assert.Equal(t, int8(-2), array.ValueAsAny(arr, 1))
+               assert.Nil(t, array.ValueAsAny(arr, 2))
+
+               // GetOneForMarshal widens int8 to float64 for JSON safety.
+               assert.Equal(t, float64(1), arr.GetOneForMarshal(0))
+               assert.NotEqual(t, arr.GetOneForMarshal(0), 
array.ValueAsAny(arr, 0))
+       })
+
+       t.Run("uint8 native vs marshal", func(t *testing.T) {
+               b := array.NewUint8Builder(mem)
+               defer b.Release()
+               b.Append(255)
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, uint8(255), array.ValueAsAny(arr, 0))
+               assert.Equal(t, float64(255), arr.GetOneForMarshal(0))
+       })
+
+       t.Run("int64", func(t *testing.T) {
+               b := array.NewInt64Builder(mem)
+               defer b.Release()
+               b.AppendValues([]int64{42, 0}, []bool{true, false})
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, int64(42), array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+       })
+
+       t.Run("boolean", func(t *testing.T) {
+               b := array.NewBooleanBuilder(mem)
+               defer b.Release()
+               b.AppendValues([]bool{true, false}, []bool{true, false})
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, true, array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+       })
+
+       t.Run("float64 keeps NaN", func(t *testing.T) {
+               b := array.NewFloat64Builder(mem)
+               defer b.Release()
+               b.Append(math.NaN())
+               b.Append(math.Inf(1))
+               arr := b.NewArray()
+               defer arr.Release()
+
+               got := array.ValueAsAny(arr, 0).(float64)
+               assert.True(t, math.IsNaN(got))
+               assert.Equal(t, math.Inf(1), array.ValueAsAny(arr, 1))
+               assert.Equal(t, "NaN", arr.GetOneForMarshal(0))
+               assert.Equal(t, "+Inf", arr.GetOneForMarshal(1))
+       })
+
+       t.Run("string and binary", func(t *testing.T) {
+               sb := array.NewStringBuilder(mem)
+               defer sb.Release()
+               sb.Append("hello")
+               sb.AppendNull()
+               sarr := sb.NewArray()
+               defer sarr.Release()
+               assert.Equal(t, "hello", array.ValueAsAny(sarr, 0))
+               assert.Nil(t, array.ValueAsAny(sarr, 1))
+
+               bb := array.NewBinaryBuilder(mem, arrow.BinaryTypes.Binary)
+               defer bb.Release()
+               bb.Append([]byte{0x01, 0x02})
+               barr := bb.NewArray()
+               defer barr.Release()
+               assert.Equal(t, []byte{0x01, 0x02}, array.ValueAsAny(barr, 0))
+       })
+
+       t.Run("float16", func(t *testing.T) {
+               b := array.NewFloat16Builder(mem)
+               defer b.Release()
+               b.Append(float16.New(1.5))
+               arr := b.NewArray()
+               defer arr.Release()
+
+               got, ok := array.ValueAsAny(arr, 0).(float16.Num)
+               require.True(t, ok)
+               assert.Equal(t, float32(1.5), got.Float32())
+               assert.Equal(t, float32(1.5), arr.GetOneForMarshal(0))
+       })
+}
+
+func TestValueAsAnyTemporalAndDecimal(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       t.Run("timestamp", func(t *testing.T) {
+               dt := &arrow.TimestampType{Unit: arrow.Second, TimeZone: "UTC"}
+               b := array.NewTimestampBuilder(mem, dt)
+               defer b.Release()
+               b.Append(arrow.Timestamp(1_700_000_000))
+               b.AppendNull()
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, arrow.Timestamp(1_700_000_000), 
array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+               _, isString := arr.GetOneForMarshal(0).(string)
+               assert.True(t, isString)
+       })
+
+       t.Run("date32", func(t *testing.T) {
+               b := array.NewDate32Builder(mem)
+               defer b.Release()
+               b.Append(arrow.Date32(10))
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, arrow.Date32(10), array.ValueAsAny(arr, 0))
+               _, isString := arr.GetOneForMarshal(0).(string)
+               assert.True(t, isString)
+       })
+
+       t.Run("duration", func(t *testing.T) {
+               b := array.NewDurationBuilder(mem, &arrow.DurationType{Unit: 
arrow.Millisecond})
+               defer b.Release()
+               b.Append(arrow.Duration(250))
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, arrow.Duration(250), array.ValueAsAny(arr, 0))
+               assert.Equal(t, "250ms", arr.GetOneForMarshal(0))
+       })
+
+       t.Run("decimal128", func(t *testing.T) {
+               dtype := &arrow.Decimal128Type{Precision: 10, Scale: 2}
+               b := array.NewDecimal128Builder(mem, dtype)
+               defer b.Release()
+               n, err := decimal128.FromString("12.34", dtype.Precision, 
dtype.Scale)
+               require.NoError(t, err)
+               b.Append(n)
+               b.AppendNull()
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, n, array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+               _, isString := arr.GetOneForMarshal(0).(string)
+               assert.True(t, isString)
+       })
+}
+
+func TestValueAsAnyNested(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       t.Run("list", func(t *testing.T) {
+               b := array.NewListBuilder(mem, arrow.PrimitiveTypes.Int8)
+               defer b.Release()
+               vb := b.ValueBuilder().(*array.Int8Builder)
+
+               b.Append(true)
+               vb.AppendValues([]int8{1, 2}, nil)
+               b.AppendNull()
+               b.Append(true)
+               vb.Append(3)
+
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, []any{int8(1), int8(2)}, array.ValueAsAny(arr, 
0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+               assert.Equal(t, []any{int8(3)}, array.ValueAsAny(arr, 2))
+       })
+
+       t.Run("struct", func(t *testing.T) {
+               fields := []arrow.Field{
+                       {Name: "n", Type: arrow.PrimitiveTypes.Int8, Nullable: 
true},
+                       {Name: "s", Type: arrow.BinaryTypes.String, Nullable: 
true},
+               }
+               b := array.NewStructBuilder(mem, arrow.StructOf(fields...))
+               defer b.Release()
+               nb := b.FieldBuilder(0).(*array.Int8Builder)
+               sb := b.FieldBuilder(1).(*array.StringBuilder)
+
+               b.Append(true)
+               nb.Append(7)
+               sb.Append("x")
+               b.AppendNull()
+               nb.AppendNull()
+               sb.AppendNull()
+
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, []any{[]any{"n", int8(7)}, []any{"s", "x"}}, 
array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 1))
+       })
+
+       t.Run("struct duplicate field names", func(t *testing.T) {
+               fields := []arrow.Field{
+                       {Name: "dup", Type: arrow.PrimitiveTypes.Int32, 
Nullable: true},
+                       {Name: "dup", Type: arrow.PrimitiveTypes.Int64, 
Nullable: true},
+               }
+               b := array.NewStructBuilder(mem, arrow.StructOf(fields...))
+               defer b.Release()
+               i32 := b.FieldBuilder(0).(*array.Int32Builder)
+               i64 := b.FieldBuilder(1).(*array.Int64Builder)
+
+               b.Append(true)
+               i32.Append(11)
+               i64.Append(22)
+
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, []any{[]any{"dup", int32(11)}, []any{"dup", 
int64(22)}}, array.ValueAsAny(arr, 0))
+       })
+
+       t.Run("null array", func(t *testing.T) {
+               arr := array.NewNull(3)
+               defer arr.Release()
+               assert.Nil(t, array.ValueAsAny(arr, 0))
+               assert.Nil(t, array.ValueAsAny(arr, 2))
+       })
+
+       t.Run("dictionary", func(t *testing.T) {
+               b := array.NewDictionaryBuilder(mem, &arrow.DictionaryType{
+                       IndexType: arrow.PrimitiveTypes.Int8,
+                       ValueType: arrow.BinaryTypes.String,
+               })
+               defer b.Release()
+               db := b.(*array.BinaryDictionaryBuilder)
+               require.NoError(t, db.Append([]byte("a")))
+               require.NoError(t, db.Append([]byte("b")))
+               db.AppendNull()
+               require.NoError(t, db.Append([]byte("a")))
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, "a", array.ValueAsAny(arr, 0))
+               assert.Equal(t, "b", array.ValueAsAny(arr, 1))
+               assert.Nil(t, array.ValueAsAny(arr, 2))
+               assert.Equal(t, "a", array.ValueAsAny(arr, 3))
+       })
+}
+
+func TestValueAsAnyUnionKeepsTypeIDWhenChildNull(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       defer mem.AssertSize(t, 0)
+
+       fields := []arrow.Field{
+               {Name: "i8", Type: arrow.PrimitiveTypes.Int8, Nullable: true},
+               {Name: "str", Type: arrow.BinaryTypes.String, Nullable: true},
+       }
+       codes := []arrow.UnionTypeCode{0, 1}
+
+       t.Run("sparse", func(t *testing.T) {
+               b := array.NewSparseUnionBuilder(mem, 
arrow.SparseUnionOf(fields, codes))
+               defer b.Release()
+               i8b := b.Child(0).(*array.Int8Builder)
+               strb := b.Child(1).(*array.StringBuilder)
+
+               b.Append(0)
+               i8b.Append(5)
+               strb.AppendEmptyValue()
+
+               b.AppendNull()
+
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, []any{arrow.UnionTypeCode(0), int8(5)}, 
array.ValueAsAny(arr, 0))
+               assert.Equal(t, []any{arrow.UnionTypeCode(0), nil}, 
array.ValueAsAny(arr, 1))
+       })
+
+       t.Run("dense", func(t *testing.T) {
+               b := array.NewDenseUnionBuilder(mem, arrow.DenseUnionOf(fields, 
codes))
+               defer b.Release()
+               i8b := b.Child(0).(*array.Int8Builder)
+
+               b.Append(0)
+               i8b.Append(5)
+
+               b.AppendNull()
+
+               arr := b.NewArray()
+               defer arr.Release()
+
+               assert.Equal(t, []any{arrow.UnionTypeCode(0), int8(5)}, 
array.ValueAsAny(arr, 0))
+               assert.Equal(t, []any{arrow.UnionTypeCode(0), nil}, 
array.ValueAsAny(arr, 1))
+       })
+}
diff --git a/arrow/extensions/bool8.go b/arrow/extensions/bool8.go
index 97038a1b..43171983 100644
--- a/arrow/extensions/bool8.go
+++ b/arrow/extensions/bool8.go
@@ -121,6 +121,13 @@ func (a *Bool8Array) GetOneForMarshal(i int) interface{} {
        return a.Value(i)
 }
 
+func (a *Bool8Array) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // boolToInt8 performs the simple scalar conversion of bool to the canonical 
int8
 // value for the Bool8Type.
 func boolToInt8(v bool) int8 {
diff --git a/arrow/extensions/json.go b/arrow/extensions/json.go
index e74ba7f6..b9b1bf3b 100644
--- a/arrow/extensions/json.go
+++ b/arrow/extensions/json.go
@@ -148,6 +148,13 @@ func (a *JSONArray) GetOneForMarshal(i int) interface{} {
        return a.ValueJSON(i)
 }
 
+func (a *JSONArray) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 var (
        _ arrow.ExtensionType  = (*JSONType)(nil)
        _ array.ExtensionArray = (*JSONArray)(nil)
diff --git a/arrow/extensions/timestamp_with_offset.go 
b/arrow/extensions/timestamp_with_offset.go
index 5cc17ed8..257707a0 100644
--- a/arrow/extensions/timestamp_with_offset.go
+++ b/arrow/extensions/timestamp_with_offset.go
@@ -395,6 +395,13 @@ func (a *TimestampWithOffsetArray) GetOneForMarshal(i int) 
interface{} {
        return a.Value(i)
 }
 
+func (a *TimestampWithOffsetArray) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // noLastOffset is the sentinel value for TimestampWithOffsetBuilder.lastOffset
 // indicating that no run-end-encoded run has been started yet. It is 
deliberately
 // outside the range of valid timezone offsets in minutes (roughly [-720, 
840]) so
diff --git a/arrow/extensions/uuid.go b/arrow/extensions/uuid.go
index 9aac0225..e03f2214 100644
--- a/arrow/extensions/uuid.go
+++ b/arrow/extensions/uuid.go
@@ -202,6 +202,13 @@ func (a *UUIDArray) GetOneForMarshal(i int) interface{} {
        return nil
 }
 
+func (a *UUIDArray) ValueAsAny(i int) any {
+       if a.IsNull(i) {
+               return nil
+       }
+       return a.Value(i)
+}
+
 // UUIDType is a simple extension type that represents a FixedSizeBinary(16)
 // to be used for representing UUIDs
 type UUIDType struct {
diff --git a/arrow/extensions/variant.go b/arrow/extensions/variant.go
index 379822c4..fee2e046 100644
--- a/arrow/extensions/variant.go
+++ b/arrow/extensions/variant.go
@@ -612,6 +612,17 @@ func (v *VariantArray) GetOneForMarshal(i int) any {
        return val.Value()
 }
 
+func (v *VariantArray) ValueAsAny(i int) any {
+       if v.IsNull(i) {
+               return nil
+       }
+       val, err := v.Value(i)
+       if err != nil {
+               return err
+       }
+       return val.Value()
+}
+
 type variantReader interface {
        IsNull(i int) bool
        Value(i int) (variant.Value, error)
diff --git a/arrow/extensions/variant_test.go b/arrow/extensions/variant_test.go
index 3580123d..a39fd513 100644
--- a/arrow/extensions/variant_test.go
+++ b/arrow/extensions/variant_test.go
@@ -1772,3 +1772,34 @@ func TestUnshredVariant(t *testing.T) {
                assert.False(t, out.IsShredded())
        })
 }
+
+func TestVariantValueAsAnyInvalidMetadata(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       vt := extensions.NewDefaultVariantType()
+       bldr := array.NewStructBuilder(mem, 
vt.StorageType().(*arrow.StructType))
+       defer bldr.Release()
+       metaBldr := bldr.FieldBuilder(0).(*array.BinaryBuilder)
+       valueBldr := bldr.FieldBuilder(1).(*array.BinaryBuilder)
+
+       bldr.Append(true)
+       metaBldr.Append([]byte{0x01}) // too short to be valid variant metadata
+       vbytes, err := variant.Encode(int8(1))
+       require.NoError(t, err)
+       valueBldr.Append(vbytes)
+
+       storage := bldr.NewArray()
+       defer storage.Release()
+
+       variantArr := array.NewExtensionArrayWithStorage(vt, storage)
+       defer variantArr.Release()
+       varr := variantArr.(*extensions.VariantArray)
+
+       require.False(t, varr.IsNull(0))
+       got := varr.ValueAsAny(0)
+       require.NotNil(t, got)
+       gotErr, ok := got.(error)
+       require.True(t, ok)
+       assert.ErrorIs(t, gotErr, variant.ErrInvalidMetadata)
+}

Reply via email to