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 2e264f0b feat(extensions): add VariantGet for path extraction from 
variant arrays (#1206)
2e264f0b is described below

commit 2e264f0bfb1f3cfc788c4208bdb4c8fb40542847
Author: Neelesh Salian <[email protected]>
AuthorDate: Tue Sep 1 14:16:53 2026 -0700

    feat(extensions): add VariantGet for path extraction from variant arrays 
(#1206)
    
    ### Rationale for this change
    
    Reading one field from a variant column today reassembles the whole
    value per row (`VariantArray.Value`) and then navigates to the field in
    Go. When the variant is shredded, that field is often already a typed
    column, so the full reassembly is wasted work.
    
    ### What changes are included in this PR?
    
    - Adds `compute.VariantGet(ctx, *extensions.VariantArray,
    VariantGetOptions{Path, AsType, Strict})`, mirroring the arrow-rs
    
[variant_get](https://github.com/apache/arrow-rs/blob/main/parquet-variant-compute/src/variant_get.rs).
    It follows the shredded `typed_value` columns as far as the path allows
    (stepping into struct fields directly, gathering list elements with the
    `take` kernel) and reassembles the residual `value` per-row only for
    what remains.
    - `AsType == nil` returns a `VariantArray` pointing at the path; set it
    to get a typed array. Typed output is produced by grouping leaves by
    their natural Arrow type and casting each group with the existing
    `compute` cast kernels, so per-element values convert to the requested
    target independent of row order.
    - `Strict` controls cast strictness: the default allows
    overflow/truncation via the cast kernels, `Strict` makes a lossy or
    impossible cast error (arrow-go has no per-value null-on-failure mode).
    - Adds `variant.VariantPath` (an opaque
    `Field`/`Index`/`Join`/`Len`/`StepAt` builder) and
    `variant.Value.GetByPath`, plus a `VariantArray.VariantType()` accessor.
    
    ### Follow-ups (out of scope here):
    
    - Nested `AsType` output: struct/list target types return
    `arrow.ErrNotImplemented`; materializing them would need a recursive
    per-field builder.
    
    ### Notes on cross-implementation behavior
    
    Path navigation follows arrow-rs rather than Spark's `variant_get` in
    two places: a field access on a non-object errors (Spark returns null),
    and `Strict` treats in-range fractional truncation as an error (Spark
    truncates). Absent paths, out-of-range indices, and
    object/array-to-primitive casts match Spark (null, or error under
    `Strict`).
    
    ### Are these changes tested?
    
    - `arrow/compute/variant_get_test.go` covers typed and variant output,
    nested field and list-index paths, residual/mixed-shredded rows, missing
    field, null rows, dictionary-encoded metadata, mixed-width/type leaves,
    order independence, the perfect-shredding fast path, `Strict` vs default
    cast outcomes, nested-type rejection, field-on-scalar errors, and
    `CheckedAllocator` leak checks.
    - `parquet/variant/path_test.go` covers the `VariantPath` builder and
    `GetByPath` navigation, including the empty-string object key.
    
    ### Are there any user-facing changes?
    
    Yes - new exported API: `compute.VariantGet`,
    `compute.VariantGetOptions`, `variant.VariantPath`,
    `variant.Value.GetByPath`, and `extensions.VariantArray.VariantType()`.
    Additive only.
---
 arrow/compute/variant_get.go      | 750 ++++++++++++++++++++++++++++++++
 arrow/compute/variant_get_test.go | 875 ++++++++++++++++++++++++++++++++++++++
 arrow/extensions/variant.go       |  10 +
 parquet/variant/path.go           | 109 +++++
 parquet/variant/path_test.go      | 116 +++++
 5 files changed, 1860 insertions(+)

diff --git a/arrow/compute/variant_get.go b/arrow/compute/variant_get.go
new file mode 100644
index 00000000..8f5a2e2a
--- /dev/null
+++ b/arrow/compute/variant_get.go
@@ -0,0 +1,750 @@
+// 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 compute
+
+import (
+       "context"
+       "fmt"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
+       "github.com/apache/arrow-go/v18/arrow/decimal"
+       "github.com/apache/arrow-go/v18/arrow/decimal128"
+       "github.com/apache/arrow-go/v18/arrow/extensions"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet/variant"
+       "github.com/google/uuid"
+)
+
+// VariantGetOptions controls VariantGet.
+type VariantGetOptions struct {
+       // Path is the path to extract from every variant value.
+       Path variant.VariantPath
+       // AsType, when nil, makes VariantGet return a VariantArray pointing at 
the path;
+       // when set, the extracted values are cast to it via the cast kernels.
+       AsType arrow.DataType
+       // Strict makes a lossy cast fail; otherwise a value that cannot 
convert to AsType
+       // nulls only that row, not the rest of its natural-type group.
+       Strict bool
+}
+
+// VariantGet extracts opts.Path from every value of input. It follows the 
shredded
+// typed_value columns as far as the path allows - stepping into struct fields
+// directly and gathering list elements with the take kernel - then 
reassembles only
+// the residual for any remaining path. With AsType nil it returns a 
VariantArray of
+// the extracted values; otherwise it casts them to AsType with the cast 
kernels.
+func VariantGet(ctx context.Context, input *extensions.VariantArray, opts 
VariantGetOptions) (arrow.Array, error) {
+       if input == nil {
+               return nil, fmt.Errorf("%w: VariantGet requires a non-nil 
VariantArray", arrow.ErrInvalid)
+       }
+
+       // Reject nested target storage. Unwrap extension types first: they 
embed
+       // ExtensionBase and so satisfy arrow.NestedType even when backed by 
e.g. UUID.
+       nestedCheck := opts.AsType
+       if ext, ok := nestedCheck.(arrow.ExtensionType); ok {
+               nestedCheck = ext.StorageType()
+       }
+       if _, ok := nestedCheck.(arrow.NestedType); ok {
+               return nil, fmt.Errorf("%w: VariantGet cast to nested type %s", 
arrow.ErrNotImplemented, opts.AsType)
+       }
+
+       // Empty path, no cast: the values are returned unchanged.
+       if opts.Path.Len() == 0 && opts.AsType == nil {
+               input.Retain()
+
+               return input, nil
+       }
+
+       return shreddedGetPath(ctx, input, opts)
+}
+
+// shreddingState is a (value?, typed_value?) column pair at one level of a 
shredded
+// variant, mirroring arrow-rs ShreddingState.
+type shreddingState struct {
+       value      arrow.TypedArray[[]byte]
+       typedValue arrow.Array
+       length     int
+}
+
+func stateFromInput(input *extensions.VariantArray) shreddingState {
+       return shreddingState{
+               value:      input.UntypedValues(),
+               typedValue: input.Shredded(),
+               length:     input.Len(),
+       }
+}
+
+func stateFromFieldStruct(child *array.Struct) shreddingState {
+       ct := child.DataType().(*arrow.StructType)
+
+       var value arrow.TypedArray[[]byte]
+       if idx, ok := ct.FieldIdx("value"); ok {
+               value = child.Field(idx).(arrow.TypedArray[[]byte])
+       }
+
+       var typed arrow.Array
+       if idx, ok := ct.FieldIdx("typed_value"); ok {
+               typed = child.Field(idx)
+       }
+
+       return shreddingState{value: value, typedValue: typed, length: 
child.Len()}
+}
+
+type pathStepKind int
+
+const (
+       stepSuccess pathStepKind = iota
+       stepMissing
+)
+
+type pathStep struct {
+       kind  pathStepKind
+       state shreddingState
+       owned []arrow.Array // intermediate take results the caller must release
+}
+
+// missingStep marks a path step whose typed field is absent. The descent loop 
breaks
+// on any residual before stepping, so a step that reaches here is provably 
missing.
+func (s shreddingState) missingStep() pathStep {
+       return pathStep{kind: stepMissing}
+}
+
+// hasResidual reports whether any row carries a value in this level's value 
column.
+func (s shreddingState) hasResidual() bool {
+       return s.value != nil && s.value.NullN() != s.value.Len()
+}
+
+func fieldStep(s shreddingState, name string) (pathStep, error) {
+       if s.typedValue == nil {
+               return s.missingStep(), nil
+       }
+       st, ok := s.typedValue.(*array.Struct)
+       if !ok {
+               // A field step into a non-object shredded value is a type 
error, matching the
+               // per-row GetByPath path. Any residual was already diverted 
before this runs.
+               return pathStep{}, fmt.Errorf("%w: variant path field %q 
applied to non-object %s",
+                       arrow.ErrInvalid, name, s.typedValue.DataType())
+       }
+       idx, ok := st.DataType().(*arrow.StructType).FieldIdx(name)
+       if !ok {
+               return s.missingStep(), nil
+       }
+       child, ok := st.Field(idx).(*array.Struct)
+       if !ok {
+               return pathStep{}, fmt.Errorf("%w: expected struct field %q 
while following path, got %s",
+                       arrow.ErrInvalid, name, st.Field(idx).DataType())
+       }
+
+       return pathStep{kind: stepSuccess, state: stateFromFieldStruct(child)}, 
nil
+}
+
+// indexStep gathers element index from every row of a shredded list with the 
take
+// kernel, producing the shredding state one level deeper.
+func indexStep(ctx context.Context, mem memory.Allocator, s shreddingState, 
index int) (pathStep, error) {
+       if s.typedValue == nil {
+               return s.missingStep(), nil
+       }
+       list, ok := s.typedValue.(array.ListLike)
+       if !ok {
+               return s.missingStep(), nil
+       }
+       elems, ok := list.ListValues().(*array.Struct)
+       if !ok {
+               return s.missingStep(), nil
+       }
+
+       ib := array.NewUint64Builder(mem)
+       defer ib.Release()
+       ib.Reserve(s.length)
+       for row := 0; row < s.length; row++ {
+               start, end := list.ValueOffsets(row)
+               if list.IsValid(row) && index >= 0 && int64(index) < end-start {
+                       ib.Append(uint64(start + int64(index)))
+               } else {
+                       ib.AppendNull()
+               }
+       }
+       indices := ib.NewArray()
+       defer indices.Release()
+
+       et := elems.DataType().(*arrow.StructType)
+       var owned []arrow.Array
+       var next shreddingState
+       next.length = s.length
+
+       if vi, ok := et.FieldIdx("value"); ok {
+               taken, err := TakeArray(ctx, elems.Field(vi), indices)
+               if err != nil {
+                       return pathStep{}, err
+               }
+               owned = append(owned, taken)
+               next.value = taken.(arrow.TypedArray[[]byte])
+       }
+       if ti, ok := et.FieldIdx("typed_value"); ok {
+               taken, err := TakeArray(ctx, elems.Field(ti), indices)
+               if err != nil {
+                       releaseAll(owned)
+
+                       return pathStep{}, err
+               }
+               owned = append(owned, taken)
+               next.typedValue = taken
+       }
+
+       return pathStep{kind: stepSuccess, state: next, owned: owned}, nil
+}
+
+func releaseAll(arrs []arrow.Array) {
+       for _, a := range arrs {
+               a.Release()
+       }
+}
+
+func shreddedGetPath(ctx context.Context, input *extensions.VariantArray, opts 
VariantGetOptions) (arrow.Array, error) {
+       mem := GetAllocator(ctx)
+       state := stateFromInput(input)
+       nulls := newNullTracker(input.Len(), mem)
+       defer nulls.release()
+       nulls.merge(input.Storage())
+
+       var owned []arrow.Array
+       defer func() { releaseAll(owned) }()
+
+       idx := 0
+       for idx < opts.Path.Len() {
+               // residual-backed rows live in the value column, unreachable 
by the typed_value descent; reassemble per-row.
+               if state.hasResidual() {
+                       break
+               }
+
+               name, index, isField := opts.Path.StepAt(idx)
+               var (
+                       step pathStep
+                       err  error
+               )
+               if isField {
+                       step, err = fieldStep(state, name)
+               } else {
+                       step, err = indexStep(ctx, mem, state, index)
+               }
+               if err != nil {
+                       return nil, err
+               }
+
+               if step.kind == stepSuccess {
+                       nulls.merge(state.typedValue)
+                       state = step.state
+                       owned = append(owned, step.owned...)
+                       idx++
+
+                       continue
+               }
+
+               // stepMissing: the typed field is provably absent (no 
residual, checked above).
+               return allNullResult(mem, input.Len(), opts.AsType), nil
+       }
+
+       remaining := subPath(opts.Path, idx)
+
+       // Try to return the typed column directly before building the target 
array,
+       // so a perfect shredding does not allocate a struct and bitmap it 
discards.
+       if remaining.Len() == 0 && opts.AsType != nil {
+               if col := perfectShredded(state, nulls, opts.AsType); col != 
nil {
+                       defer col.Release()
+
+                       return CastArray(ctx, col, NewCastOptions(opts.AsType, 
opts.Strict))
+               }
+       }
+
+       target, err := buildTargetVariant(input, state, nulls, mem)
+       if err != nil {
+               return nil, err
+       }
+       defer target.Release()
+
+       if remaining.Len() == 0 && opts.AsType == nil {
+               target.Retain()
+
+               return target, nil
+       }
+
+       leaves, err := extractLeaves(target, remaining)
+       if err != nil {
+               return nil, err
+       }
+       if opts.AsType == nil {
+               return buildLeafVariantArray(mem, leaves), nil
+       }
+
+       return castLeaves(ctx, mem, leaves, opts.AsType, opts.Strict)
+}
+
+// perfectShredded returns the typed_value column when the path landed on a 
fully
+// shredded value of exactly AsType and no ancestor nulls need merging; 
otherwise
+// the caller's reassembly path produces the same values.
+func perfectShredded(s shreddingState, nulls *nullTracker, asType 
arrow.DataType) arrow.Array {
+       if s.typedValue == nil || !nulls.allValid() {
+               return nil
+       }
+       if !arrow.TypeEqual(s.typedValue.DataType(), asType) {
+               return nil
+       }
+       if s.value != nil && s.value.NullN() != s.value.Len() {
+               return nil
+       }
+
+       s.typedValue.Retain()
+
+       return s.typedValue
+}
+
+func buildTargetVariant(input *extensions.VariantArray, s shreddingState, 
nulls *nullTracker, mem memory.Allocator) (*extensions.VariantArray, error) {
+       // Read the raw metadata column rather than input.Metadata(), which 
asserts plain
+       // binary and panics on dictionary-encoded metadata; the raw column 
preserves
+       // dictionary/large-binary encoding and is decoded by the target's own 
reader.
+       storage := input.Storage().(*array.Struct)
+       mdIdx, ok := storage.DataType().(*arrow.StructType).FieldIdx("metadata")
+       if !ok {
+               return nil, fmt.Errorf("%w: variant storage is missing its 
metadata field", arrow.ErrInvalid)
+       }
+       metadata := storage.Field(mdIdx)
+
+       fields := []arrow.Field{{Name: "metadata", Type: metadata.DataType(), 
Nullable: false}}
+       cols := []arrow.Array{metadata}
+       if s.value != nil {
+               fields = append(fields, arrow.Field{Name: "value", Type: 
s.value.DataType(), Nullable: true})
+               cols = append(cols, s.value)
+       }
+       if s.typedValue != nil {
+               fields = append(fields, arrow.Field{Name: "typed_value", Type: 
s.typedValue.DataType(), Nullable: true})
+               cols = append(cols, s.typedValue)
+       }
+
+       bitmap, nullCount := nulls.validityBitmap()
+       st, err := array.NewStructArrayWithFieldsAndNulls(cols, fields, bitmap, 
nullCount, 0)
+       if err != nil {
+               return nil, err
+       }
+       defer st.Release()
+
+       vt, err := extensions.NewVariantType(st.DataType())
+       if err != nil {
+               return nil, err
+       }
+
+       return array.NewExtensionArrayWithStorage(vt, 
st).(*extensions.VariantArray), nil
+}
+
+// subPath returns the suffix of p starting at from, rebuilt through the 
opaque API.
+func subPath(p variant.VariantPath, from int) variant.VariantPath {
+       var out variant.VariantPath
+       for i := from; i < p.Len(); i++ {
+               if name, index, isField := p.StepAt(i); isField {
+                       out = out.Field(name)
+               } else {
+                       out = out.Index(index)
+               }
+       }
+
+       return out
+}
+
+// variantLeaf is one row's extracted value; present is false when the path is
+// absent for that row (or the row is null).
+type variantLeaf struct {
+       value   variant.Value
+       present bool
+}
+
+func extractLeaves(target *extensions.VariantArray, path variant.VariantPath) 
([]variantLeaf, error) {
+       leaves := make([]variantLeaf, target.Len())
+       for i := range leaves {
+               if target.IsNull(i) {
+                       continue
+               }
+               v, err := target.Value(i)
+               if err != nil {
+                       return nil, fmt.Errorf("variant: reassembling row %d: 
%w", i, err)
+               }
+               leaf, found, err := v.GetByPath(path)
+               if err != nil {
+                       return nil, err
+               }
+               leaves[i] = variantLeaf{value: leaf, present: found}
+       }
+
+       return leaves, nil
+}
+
+func buildLeafVariantArray(mem memory.Allocator, leaves []variantLeaf) 
arrow.Array {
+       bldr := extensions.NewVariantBuilder(mem, 
extensions.NewDefaultVariantType())
+       defer bldr.Release()
+       bldr.Reserve(len(leaves))
+       for _, l := range leaves {
+               if !l.present {
+                       bldr.AppendNull()
+
+                       continue
+               }
+               bldr.Append(l.value)
+       }
+
+       return bldr.NewArray()
+}
+
+// castLeaves converts each leaf to asType (arrow-rs variant_get parity): 
leaves are
+// grouped by natural type, each group cast with the cast kernels, then 
scattered back
+// so the result is order-independent. Strict errors on a lossy cast, else 
null.
+func castLeaves(ctx context.Context, mem memory.Allocator, leaves 
[]variantLeaf, asType arrow.DataType, strict bool) (arrow.Array, error) {
+       type leafGroup struct {
+               dt   arrow.DataType
+               rows []int
+       }
+       groups := make(map[string]*leafGroup)
+       var order []string
+       for i, l := range leaves {
+               if !l.present || l.value.Type() == variant.Null {
+                       continue
+               }
+               dt := naturalArrowType(l.value)
+               if dt == nil {
+                       // Object/array leaves have no primitive natural type. 
Under Strict this is an
+                       // impossible cast (errors like any other); otherwise 
the row stays null.
+                       if strict {
+                               return nil, fmt.Errorf("%w: cannot cast 
non-primitive variant leaf to %s", arrow.ErrInvalid, asType)
+                       }
+
+                       continue
+               }
+               key := dt.String()
+               g := groups[key]
+               if g == nil {
+                       g = &leafGroup{dt: dt}
+                       groups[key] = g
+                       order = append(order, key)
+               }
+               g.rows = append(g.rows, i)
+       }
+
+       perm := make([]uint64, len(leaves))
+       valid := make([]bool, len(leaves))
+       var casted []arrow.Array
+       defer func() { releaseAll(casted) }()
+
+       var pos uint64
+       for _, key := range order {
+               g := groups[key]
+               col := buildTypedColumn(mem, g.dt, leaves, g.rows)
+               cast, err := CastArray(ctx, col, NewCastOptions(asType, strict))
+               col.Release()
+               if err == nil {
+                       casted = append(casted, cast)
+                       for _, row := range g.rows {
+                               perm[row] = pos
+                               valid[row] = true
+                               pos++
+                       }
+
+                       continue
+               }
+               if strict {
+                       return nil, err
+               }
+               // Non-strict: retry each row alone so only the inconvertible 
rows null, not the whole group.
+               for _, row := range g.rows {
+                       rc := buildTypedColumn(mem, g.dt, leaves, []int{row})
+                       one, cerr := CastArray(ctx, rc, NewCastOptions(asType, 
strict))
+                       rc.Release()
+                       if cerr != nil {
+                               continue // this row alone is inconvertible; it 
stays null
+                       }
+                       casted = append(casted, one)
+                       perm[row] = pos
+                       valid[row] = true
+                       pos++
+               }
+       }
+
+       if len(casted) == 0 {
+               return allNullResult(mem, len(leaves), asType), nil
+       }
+
+       // One natural type covering every row already sits in original order.
+       if len(casted) == 1 && pos == uint64(len(leaves)) {
+               casted[0].Retain()
+
+               return casted[0], nil
+       }
+
+       combined, err := array.Concatenate(casted, mem)
+       if err != nil {
+               return nil, err
+       }
+       defer combined.Release()
+
+       ib := array.NewUint64Builder(mem)
+       defer ib.Release()
+       ib.AppendValues(perm, valid)
+       indices := ib.NewArray()
+       defer indices.Release()
+
+       return TakeArray(ctx, combined, indices)
+}
+
+// buildTypedColumn materializes the given leaf rows, all of natural type dt, 
into a
+// homogeneous Arrow array the cast kernels can consume.
+func buildTypedColumn(mem memory.Allocator, dt arrow.DataType, leaves 
[]variantLeaf, rows []int) arrow.Array {
+       bldr := array.NewBuilder(mem, dt)
+       defer bldr.Release()
+       bldr.Reserve(len(rows))
+       for _, row := range rows {
+               if !appendNatural(bldr, leaves[row].value) {
+                       bldr.AppendNull()
+               }
+       }
+
+       return bldr.NewArray()
+}
+
+// allNullResult builds the all-null output for a provably missing path.
+func allNullResult(mem memory.Allocator, n int, asType arrow.DataType) 
arrow.Array {
+       if asType != nil {
+               return array.MakeArrayOfNull(mem, asType, n)
+       }
+
+       // MakeArrayOfNull cannot build the variant extension type (its storage 
struct's
+       // metadata/value are non-nullable), so append encoded variant nulls 
instead.
+       bldr := extensions.NewVariantBuilder(mem, 
extensions.NewDefaultVariantType())
+       defer bldr.Release()
+       for range n {
+               bldr.AppendNull()
+       }
+
+       return bldr.NewArray()
+}
+
+// nullTracker accumulates ancestor validity bitmaps with a bitmap AND.
+type nullTracker struct {
+       length int
+       mem    memory.Allocator
+       buf    *memory.Buffer // validity bitmap (1 = valid); nil means all 
valid
+}
+
+func newNullTracker(length int, mem memory.Allocator) *nullTracker {
+       return &nullTracker{length: length, mem: mem}
+}
+
+// merge folds arr's validity into the accumulated mask. Arrow validity bits 
are
+// 1=valid, so accumulating ancestor nulls is a bitmap AND (a row is null in 
the
+// result when it is null at any level) - the validity-space equivalent of 
OR-ing
+// null masks.
+func (n *nullTracker) merge(arr arrow.Array) {
+       if arr == nil {
+               return
+       }
+       vb := arr.Data().Buffers()[0]
+       if vb == nil {
+               return // all valid
+       }
+       off := int64(arr.Data().Offset())
+       if n.buf == nil {
+               n.buf = bitutil.BitmapAndAlloc(n.mem, vb.Bytes(), vb.Bytes(), 
off, off, int64(n.length), 0)
+
+               return
+       }
+       merged := bitutil.BitmapAndAlloc(n.mem, n.buf.Bytes(), vb.Bytes(), 0, 
off, int64(n.length), 0)
+       n.buf.Release()
+       n.buf = merged
+}
+
+func (n *nullTracker) allValid() bool { return n.buf == nil }
+
+func (n *nullTracker) validityBitmap() (*memory.Buffer, int) {
+       if n.buf == nil {
+               return nil, 0
+       }
+
+       return n.buf, n.length - bitutil.CountSetBits(n.buf.Bytes(), 0, 
n.length)
+}
+
+func (n *nullTracker) release() {
+       if n.buf != nil {
+               n.buf.Release()
+               n.buf = nil
+       }
+}
+
+func naturalArrowType(v variant.Value) arrow.DataType {
+       switch v.Type() {
+       case variant.Bool:
+               return arrow.FixedWidthTypes.Boolean
+       case variant.Int8:
+               return arrow.PrimitiveTypes.Int8
+       case variant.Int16:
+               return arrow.PrimitiveTypes.Int16
+       case variant.Int32:
+               return arrow.PrimitiveTypes.Int32
+       case variant.Int64:
+               return arrow.PrimitiveTypes.Int64
+       case variant.Float:
+               return arrow.PrimitiveTypes.Float32
+       case variant.Double:
+               return arrow.PrimitiveTypes.Float64
+       case variant.String:
+               return arrow.BinaryTypes.String
+       case variant.Binary:
+               return arrow.BinaryTypes.Binary
+       case variant.Date:
+               return arrow.FixedWidthTypes.Date32
+       case variant.Time:
+               return arrow.FixedWidthTypes.Time64us
+       case variant.TimestampMicros:
+               return &arrow.TimestampType{Unit: arrow.Microsecond, TimeZone: 
"UTC"}
+       case variant.TimestampMicrosNTZ:
+               return &arrow.TimestampType{Unit: arrow.Microsecond}
+       case variant.TimestampNanos:
+               return &arrow.TimestampType{Unit: arrow.Nanosecond, TimeZone: 
"UTC"}
+       case variant.TimestampNanosNTZ:
+               return &arrow.TimestampType{Unit: arrow.Nanosecond}
+       case variant.UUID:
+               return extensions.NewUUIDType()
+       case variant.Decimal4, variant.Decimal8, variant.Decimal16:
+               return &arrow.Decimal128Type{Precision: 38, Scale: 
int32(decimalScale(v))}
+       }
+
+       return nil
+}
+
+// appendNatural appends v to bldr when v's value matches bldr's element type,
+// reporting whether it did; callers group leaves by natural type first, so it 
matches.
+func appendNatural(bldr array.Builder, v variant.Value) bool {
+       switch b := bldr.(type) {
+       case *array.BooleanBuilder:
+               if x, ok := v.Value().(bool); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Int8Builder:
+               if x, ok := v.Value().(int8); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Int16Builder:
+               if x, ok := v.Value().(int16); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Int32Builder:
+               if x, ok := v.Value().(int32); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Int64Builder:
+               if x, ok := v.Value().(int64); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Float32Builder:
+               if x, ok := v.Value().(float32); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Float64Builder:
+               if x, ok := v.Value().(float64); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.StringBuilder:
+               if x, ok := v.Value().(string); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.BinaryBuilder:
+               if x, ok := v.Value().([]byte); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Date32Builder:
+               if x, ok := v.Value().(arrow.Date32); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Time64Builder:
+               if x, ok := v.Value().(arrow.Time64); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.TimestampBuilder:
+               if x, ok := v.Value().(arrow.Timestamp); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *extensions.UUIDBuilder:
+               if x, ok := v.Value().(uuid.UUID); ok {
+                       b.Append(x)
+
+                       return true
+               }
+       case *array.Decimal128Builder:
+               if num, ok := decimalAsNum128(v); ok && int32(decimalScale(v)) 
== b.Type().(*arrow.Decimal128Type).Scale {
+                       b.Append(num)
+
+                       return true
+               }
+       }
+
+       return false
+}
+
+func decimalScale(v variant.Value) uint8 {
+       switch d := v.Value().(type) {
+       case variant.DecimalValue[decimal.Decimal32]:
+               return d.Scale
+       case variant.DecimalValue[decimal.Decimal64]:
+               return d.Scale
+       case variant.DecimalValue[decimal.Decimal128]:
+               return d.Scale
+       }
+
+       return 0
+}
+
+func decimalAsNum128(v variant.Value) (decimal128.Num, bool) {
+       switch d := v.Value().(type) {
+       case variant.DecimalValue[decimal.Decimal32]:
+               return decimal128.FromI64(int64(d.Value.(decimal.Decimal32))), 
true
+       case variant.DecimalValue[decimal.Decimal64]:
+               return decimal128.FromI64(int64(d.Value.(decimal.Decimal64))), 
true
+       case variant.DecimalValue[decimal.Decimal128]:
+               return d.Value.(decimal.Decimal128), true
+       }
+
+       return decimal128.Num{}, false
+}
diff --git a/arrow/compute/variant_get_test.go 
b/arrow/compute/variant_get_test.go
new file mode 100644
index 00000000..cf841ba2
--- /dev/null
+++ b/arrow/compute/variant_get_test.go
@@ -0,0 +1,875 @@
+// 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 compute_test
+
+import (
+       "context"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/compute"
+       "github.com/apache/arrow-go/v18/arrow/compute/exec"
+       "github.com/apache/arrow-go/v18/arrow/decimal"
+       "github.com/apache/arrow-go/v18/arrow/extensions"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet/variant"
+       "github.com/google/uuid"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func vgVariant(t *testing.T, v any) variant.Value {
+       t.Helper()
+       var b variant.Builder
+       require.NoError(t, b.Append(v))
+       val, err := b.Build()
+       require.NoError(t, err)
+
+       return val
+}
+
+func vgNonShredded(t *testing.T, mem memory.Allocator, vals ...any) 
*extensions.VariantArray {
+       t.Helper()
+       bldr := extensions.NewVariantBuilder(mem, 
extensions.NewDefaultVariantType())
+       defer bldr.Release()
+       for _, v := range vals {
+               if v == nil {
+                       bldr.AppendNull()
+
+                       continue
+               }
+               bldr.Append(vgVariant(t, v))
+       }
+
+       return bldr.NewArray().(*extensions.VariantArray)
+}
+
+func vgShreddedInt(t *testing.T, mem memory.Allocator, vals ...int64) 
*extensions.VariantArray {
+       t.Helper()
+       vt := extensions.NewShreddedVariantType(arrow.PrimitiveTypes.Int64)
+       bldr := extensions.NewVariantBuilder(mem, vt)
+       defer bldr.Release()
+       for _, v := range vals {
+               bldr.Append(vgVariant(t, v))
+       }
+
+       return bldr.NewArray().(*extensions.VariantArray)
+}
+
+func field(name string) variant.VariantPath { return 
variant.VariantPath{}.Field(name) }
+
+func TestVariantGetTyped(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem,
+               map[string]any{"a": int64(1)},
+               map[string]any{"a": int64(2)},
+               nil,
+       )
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+
+       ints := out.(*array.Int64)
+       require.Equal(t, 3, ints.Len())
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.EqualValues(t, 2, ints.Value(1))
+       assert.True(t, ints.IsNull(2))
+}
+
+func TestVariantGetVariantOutput(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, map[string]any{"a": int64(7)}, 
map[string]any{"b": int64(9)})
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a")})
+       require.NoError(t, err)
+       defer out.Release()
+
+       varr := out.(*extensions.VariantArray)
+       v, err := varr.Value(0)
+       require.NoError(t, err)
+       assert.EqualValues(t, 7, v.Value())
+       assert.True(t, varr.IsNull(1))
+}
+
+func TestVariantGetNestedAndIndex(t *testing.T) {
+       mem := memory.DefaultAllocator
+       nested := vgNonShredded(t, mem, map[string]any{"a": map[string]any{"b": 
int64(5)}})
+       defer nested.Release()
+       out, err := compute.VariantGet(context.Background(), nested, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Field("a").Field("b"), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       assert.EqualValues(t, 5, out.(*array.Int64).Value(0))
+
+       arrs := vgNonShredded(t, mem, []any{int64(10), int64(20), int64(30)})
+       defer arrs.Release()
+       got, err := compute.VariantGet(context.Background(), arrs, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(1), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer got.Release()
+       assert.EqualValues(t, 20, got.(*array.Int64).Value(0))
+
+       oob, err := compute.VariantGet(context.Background(), arrs, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(9), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer oob.Release()
+       assert.True(t, oob.(*array.Int64).IsNull(0))
+}
+
+// TestVariantGetMixedShreddedRows reproduces zeroshade's [1,2] case: row 0 is 
in
+// typed_value, row 1 is in the residual value. Both must come back, not 
[1,null].
+func TestVariantGetMixedShreddedRows(t *testing.T) {
+       mem := memory.DefaultAllocator
+       s := arrow.StructOf(
+               arrow.Field{Name: "metadata", Type: arrow.BinaryTypes.Binary},
+               arrow.Field{Name: "value", Type: arrow.BinaryTypes.Binary, 
Nullable: true},
+               arrow.Field{Name: "typed_value", Type: 
arrow.PrimitiveTypes.Int64, Nullable: true})
+       b := array.NewStructBuilder(mem, s)
+       defer b.Release()
+       mb := b.FieldBuilder(0).(*array.BinaryBuilder)
+       vb := b.FieldBuilder(1).(*array.BinaryBuilder)
+       tb := b.FieldBuilder(2).(*array.Int64Builder)
+
+       b.Append(true)
+       mb.Append(variant.EmptyMetadataBytes[:])
+       vb.AppendNull()
+       tb.Append(1)
+
+       b.Append(true)
+       mb.Append(variant.EmptyMetadataBytes[:])
+       enc, err := variant.Encode(int64(2))
+       require.NoError(t, err)
+       vb.Append(enc)
+       tb.AppendNull()
+
+       st := b.NewArray()
+       defer st.Release()
+       vt, err := extensions.NewVariantType(s)
+       require.NoError(t, err)
+       arr := array.NewExtensionArrayWithStorage(vt, 
st).(*extensions.VariantArray)
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       require.Equal(t, 2, ints.Len())
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.EqualValues(t, 2, ints.Value(1), "residual-value row must be 
reconstructed, not null")
+       assert.False(t, ints.IsNull(1))
+}
+
+// TestVariantGetLenientCast covers zeroshade's :269 examples: ordinary 
widening
+// casts succeed under the default (non-strict) mode.
+func TestVariantGetLenientCast(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgShreddedInt(t, mem, 3, 5)
+       defer arr.Release()
+
+       f64, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64})
+       require.NoError(t, err)
+       defer f64.Release()
+       assert.EqualValues(t, 3, f64.(*array.Float64).Value(0))
+       assert.EqualValues(t, 5, f64.(*array.Float64).Value(1))
+
+       dec, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: &arrow.Decimal128Type{Precision: 10, Scale: 
0}})
+       require.NoError(t, err)
+       defer dec.Release()
+       assert.Equal(t, 2, dec.Len())
+}
+
+func TestVariantGetStrictCastErrors(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgShreddedInt(t, mem, 5_000_000_000) // overflows int8
+       defer arr.Release()
+
+       _, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               AsType: arrow.PrimitiveTypes.Int8, Strict: true,
+       })
+       require.Error(t, err, "strict cast of an overflowing value must error")
+}
+
+// TestVariantGetFieldOnScalarErrors covers :323: a field step into a scalar 
is a
+// type error, not a silent null.
+func TestVariantGetFieldOnScalarErrors(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, int64(1))
+       defer arr.Release()
+
+       _, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a")})
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+// TestVariantGetHugeIndex covers :307: a huge index must not wrap to a valid 
one.
+func TestVariantGetHugeIndex(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, []any{int64(10), int64(20)})
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(1 << 40), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       assert.True(t, out.(*array.Int64).IsNull(0))
+}
+
+func TestVariantGetEmptyPath(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, int64(1), int64(2))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{})
+       require.NoError(t, err)
+       defer out.Release()
+       assert.Equal(t, 2, out.Len())
+}
+
+// TestVariantGetShreddedFieldPushdown drives nested field steps through the 
shredded
+// typed_value columns and the perfect-shredding fast path.
+func TestVariantGetShreddedFieldPushdown(t *testing.T) {
+       mem := memory.DefaultAllocator
+       vt := extensions.NewShreddedVariantType(arrow.StructOf(
+               arrow.Field{Name: "a", Type: arrow.StructOf(
+                       arrow.Field{Name: "b", Type: 
arrow.PrimitiveTypes.Int64})}))
+       bldr := extensions.NewVariantBuilder(mem, vt)
+       defer bldr.Release()
+       bldr.Append(vgVariant(t, map[string]any{"a": map[string]any{"b": 
int64(5)}}))
+       bldr.Append(vgVariant(t, map[string]any{"a": map[string]any{"b": 
int64(6)}}))
+       arr := bldr.NewArray().(*extensions.VariantArray)
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Field("a").Field("b"), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       assert.EqualValues(t, 5, ints.Value(0))
+       assert.EqualValues(t, 6, ints.Value(1))
+}
+
+// TestVariantGetShreddedListIndex drives an index step over a shredded list, 
which
+// gathers elements with the take kernel.
+func TestVariantGetShreddedListIndex(t *testing.T) {
+       mem := memory.DefaultAllocator
+       vt := 
extensions.NewShreddedVariantType(arrow.ListOf(arrow.PrimitiveTypes.Int64))
+       bldr := extensions.NewVariantBuilder(mem, vt)
+       defer bldr.Release()
+       bldr.Append(vgVariant(t, []any{int64(10), int64(20), int64(30)}))
+       bldr.Append(vgVariant(t, []any{int64(40), int64(50)}))
+       arr := bldr.NewArray().(*extensions.VariantArray)
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(1), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       assert.EqualValues(t, 20, ints.Value(0))
+       assert.EqualValues(t, 50, ints.Value(1))
+}
+
+func TestVariantGetNoLeak(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := exec.WithAllocator(context.Background(), mem)
+
+       vt := 
extensions.NewShreddedVariantType(arrow.ListOf(arrow.PrimitiveTypes.Int64))
+       bldr := extensions.NewVariantBuilder(mem, vt)
+       bldr.Append(vgVariant(t, []any{int64(10), int64(20)}))
+       bldr.AppendNull()
+       arr := bldr.NewArray().(*extensions.VariantArray)
+       bldr.Release()
+
+       idx, err := compute.VariantGet(ctx, arr, compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(0), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       idx.Release()
+       arr.Release()
+
+       // A missing field on an object-shredded variant exercises the all-null 
path.
+       objVT := 
extensions.NewShreddedVariantType(arrow.StructOf(arrow.Field{Name: "a", Type: 
arrow.PrimitiveTypes.Int64}))
+       ob := extensions.NewVariantBuilder(mem, objVT)
+       ob.Append(vgVariant(t, map[string]any{"a": int64(1)}))
+       objArr := ob.NewArray().(*extensions.VariantArray)
+       ob.Release()
+
+       missing, err := compute.VariantGet(ctx, objArr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Field("nope"), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       missing.Release()
+       objArr.Release()
+}
+
+// TestVariantGetDictMetadata guards against a panic when the metadata column 
is
+// dictionary-encoded (spec-legal): buildTargetVariant must read the raw 
column,
+// not the plain-binary accessor.
+func TestVariantGetDictMetadata(t *testing.T) {
+       mem := memory.DefaultAllocator
+       s := arrow.StructOf(
+               arrow.Field{Name: "metadata", Type: 
&arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Uint8, ValueType: 
arrow.BinaryTypes.Binary}},
+               arrow.Field{Name: "value", Type: arrow.BinaryTypes.Binary, 
Nullable: true},
+               arrow.Field{Name: "typed_value", Type: arrow.StructOf(
+                       arrow.Field{Name: "a", Type: arrow.StructOf(
+                               arrow.Field{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true},
+                               arrow.Field{Name: "typed_value", Type: 
arrow.PrimitiveTypes.Int64, Nullable: true},
+                       )},
+               ), Nullable: true})
+       vt, err := extensions.NewVariantType(s)
+       require.NoError(t, err)
+       bldr := vt.NewBuilder(mem).(*extensions.VariantBuilder)
+       defer bldr.Release()
+       bldr.Append(vgVariant(t, map[string]any{"a": int64(5), "b": "resid"}))
+       arr := bldr.NewArray().(*extensions.VariantArray)
+       defer arr.Release()
+
+       // "b" is not shredded, so this takes the NotShredded -> 
buildTargetVariant path.
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: variant.VariantPath{}.Field("b")})
+       require.NoError(t, err)
+       defer out.Release()
+       v, err := out.(*extensions.VariantArray).Value(0)
+       require.NoError(t, err)
+       assert.Equal(t, "resid", v.Value())
+}
+
+// vgMixedResidual builds a two-row shredded array: row 0 shredded (fill 
populates typed_value, top value null),
+// row 1 residual (top typed_value null, top value = resid). Storage comes 
from NewShreddedVariantType.
+func vgMixedResidual(t *testing.T, mem memory.Allocator, shredType 
arrow.DataType, fill func(b array.Builder), resid variant.Value) 
*extensions.VariantArray {
+       t.Helper()
+       vt := extensions.NewShreddedVariantType(shredType)
+       s := vt.StorageType().(*arrow.StructType)
+       mIdx, _ := s.FieldIdx("metadata")
+       vIdx, _ := s.FieldIdx("value")
+       tIdx, _ := s.FieldIdx("typed_value")
+
+       b := array.NewStructBuilder(mem, s)
+       defer b.Release()
+       mb := b.FieldBuilder(mIdx).(*array.BinaryBuilder)
+       vb := b.FieldBuilder(vIdx).(*array.BinaryBuilder)
+
+       b.Append(true)
+       mb.Append(variant.EmptyMetadataBytes[:])
+       vb.AppendNull()
+       fill(b.FieldBuilder(tIdx))
+
+       b.Append(true)
+       mb.Append(resid.Metadata().Bytes())
+       vb.Append(resid.Bytes())
+       b.FieldBuilder(tIdx).AppendNull()
+
+       st := b.NewArray()
+       defer st.Release()
+
+       return array.NewExtensionArrayWithStorage(vt, 
st).(*extensions.VariantArray)
+}
+
+// TestVariantGetResidualBackedField covers zeroshade's blocking :233 case for 
a root
+// field path: row 1's {"a":2} lives in the top residual, so $.a must return 2 
not null.
+func TestVariantGetResidualBackedField(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgMixedResidual(t, mem, arrow.StructOf(arrow.Field{Name: "a", 
Type: arrow.PrimitiveTypes.Int64}),
+               func(b array.Builder) {
+                       tv := b.(*array.StructBuilder)
+                       tv.Append(true)
+                       a := tv.FieldBuilder(0).(*array.StructBuilder)
+                       a.Append(true)
+                       a.FieldBuilder(0).(*array.BinaryBuilder).AppendNull()
+                       a.FieldBuilder(1).(*array.Int64Builder).Append(1)
+               }, vgVariant(t, map[string]any{"a": int64(2)}))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       require.Equal(t, 2, ints.Len())
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.EqualValues(t, 2, ints.Value(1), "residual-backed row must be 
reassembled, not nulled")
+}
+
+// TestVariantGetResidualBackedNestedField covers the nested-field case: $.a.b 
where
+// row 1's whole {"a":{"b":6}} lives in the top residual.
+func TestVariantGetResidualBackedNestedField(t *testing.T) {
+       mem := memory.DefaultAllocator
+       shred := arrow.StructOf(arrow.Field{Name: "a", Type: 
arrow.StructOf(arrow.Field{Name: "b", Type: arrow.PrimitiveTypes.Int64})})
+       arr := vgMixedResidual(t, mem, shred,
+               func(b array.Builder) {
+                       tv := b.(*array.StructBuilder)
+                       tv.Append(true)
+                       a := tv.FieldBuilder(0).(*array.StructBuilder)
+                       a.Append(true)
+                       a.FieldBuilder(0).(*array.BinaryBuilder).AppendNull()
+                       aTV := a.FieldBuilder(1).(*array.StructBuilder)
+                       aTV.Append(true)
+                       bf := aTV.FieldBuilder(0).(*array.StructBuilder)
+                       bf.Append(true)
+                       bf.FieldBuilder(0).(*array.BinaryBuilder).AppendNull()
+                       bf.FieldBuilder(1).(*array.Int64Builder).Append(5)
+               }, vgVariant(t, map[string]any{"a": map[string]any{"b": 
int64(6)}}))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Field("a").Field("b"), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       assert.EqualValues(t, 5, ints.Value(0))
+       assert.EqualValues(t, 6, ints.Value(1), "residual-backed row must be 
reassembled, not nulled")
+}
+
+// TestVariantGetResidualBackedListIndex covers the list-index case: [0] where 
row 1's
+// whole [30,40] lives in the top residual.
+func TestVariantGetResidualBackedListIndex(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgMixedResidual(t, mem, arrow.ListOf(arrow.PrimitiveTypes.Int64),
+               func(b array.Builder) {
+                       lb := b.(*array.ListBuilder)
+                       lb.Append(true)
+                       el := lb.ValueBuilder().(*array.StructBuilder)
+                       for _, v := range []int64{10, 20} {
+                               el.Append(true)
+                               
el.FieldBuilder(0).(*array.BinaryBuilder).AppendNull()
+                               
el.FieldBuilder(1).(*array.Int64Builder).Append(v)
+                       }
+               }, vgVariant(t, []any{int64(30), int64(40)}))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(0), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       assert.EqualValues(t, 10, ints.Value(0))
+       assert.EqualValues(t, 30, ints.Value(1), "residual-backed row must be 
reassembled, not nulled")
+}
+
+// TestVariantGetMixedWidthIntegers covers zeroshade :411: variant ints encode 
at their
+// natural width, so a wider AsType must not drop rows whose leaf shredded 
narrower.
+func TestVariantGetMixedWidthIntegers(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem,
+               map[string]any{"a": int64(1)},                // int8
+               map[string]any{"a": int64(1000)},             // int16
+               map[string]any{"a": int64(5_000_000_000)},    // int64
+               map[string]any{"a": int64(9007199254740993)}) // int64 > 2^53, 
must stay exact (no float intermediate)
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       require.Equal(t, 4, ints.Len())
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.EqualValues(t, 1000, ints.Value(1), "narrower-width leaf must 
not be dropped")
+       assert.EqualValues(t, 5_000_000_000, ints.Value(2))
+       assert.EqualValues(t, 9007199254740993, ints.Value(3), "value > 2^53 
must be exact, not routed through float64")
+}
+
+// TestVariantGetHeterogeneousLeaves (arrow-rs parity): a valid int64 survives 
a
+// narrower-typed sibling; only a non-numeric string nulls.
+func TestVariantGetHeterogeneousLeaves(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem,
+               map[string]any{"a": int64(1)},             // int8 encoding
+               map[string]any{"a": int64(5_000_000_000)}, // int64, does not 
fit int8
+               map[string]any{"a": "x"})                  // non-numeric -> 
null
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.EqualValues(t, 5_000_000_000, ints.Value(1), "valid int64 must 
survive a narrower-typed sibling")
+       assert.True(t, ints.IsNull(2), "non-numeric string must be null")
+}
+
+// TestVariantGetEmptyKey covers zeroshade's blocking path.go:42 case: an 
empty-string
+// object key is a field step, not array index 0.
+func TestVariantGetEmptyKey(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, map[string]any{"": int64(42)})
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field(""), AsType: arrow.PrimitiveTypes.Int64})
+       require.NoError(t, err)
+       defer out.Release()
+       assert.EqualValues(t, 42, out.(*array.Int64).Value(0))
+}
+
+// TestVariantGetResidualBackedDeepField exercises the residual break after a 
successful
+// columnar descent: row 0 is shredded through a.b, row 1's {"b":6} sits in 
a's residual.
+func TestVariantGetResidualBackedDeepField(t *testing.T) {
+       mem := memory.DefaultAllocator
+       shred := arrow.StructOf(arrow.Field{Name: "a", Type: 
arrow.StructOf(arrow.Field{Name: "b", Type: arrow.PrimitiveTypes.Int64})})
+       vt := extensions.NewShreddedVariantType(shred)
+       s := vt.StorageType().(*arrow.StructType)
+       mIdx, _ := s.FieldIdx("metadata")
+       vIdx, _ := s.FieldIdx("value")
+       tIdx, _ := s.FieldIdx("typed_value")
+
+       b := array.NewStructBuilder(mem, s)
+       defer b.Release()
+       mb := b.FieldBuilder(mIdx).(*array.BinaryBuilder)
+       vb := b.FieldBuilder(vIdx).(*array.BinaryBuilder)
+       tvb := b.FieldBuilder(tIdx).(*array.StructBuilder) // struct{a}
+       aField := tvb.FieldBuilder(0).(*array.StructBuilder)
+       aVal := aField.FieldBuilder(0).(*array.BinaryBuilder)
+       aTyped := aField.FieldBuilder(1).(*array.StructBuilder) // struct{b}
+       bField := aTyped.FieldBuilder(0).(*array.StructBuilder)
+       bVal := bField.FieldBuilder(0).(*array.BinaryBuilder)
+       bTyped := bField.FieldBuilder(1).(*array.Int64Builder)
+
+       // row 0: fully shredded a.b = 5
+       b.Append(true)
+       mb.Append(variant.EmptyMetadataBytes[:])
+       vb.AppendNull()
+       tvb.Append(true)
+       aField.Append(true)
+       aVal.AppendNull()
+       aTyped.Append(true)
+       bField.Append(true)
+       bVal.AppendNull()
+       bTyped.Append(5)
+
+       // row 1: a is residual-backed with {"b":6} (top typed_value present, 
a.value set)
+       resid := vgVariant(t, map[string]any{"b": int64(6)})
+       b.Append(true)
+       mb.Append(resid.Metadata().Bytes())
+       vb.AppendNull()
+       tvb.Append(true)
+       aField.Append(true)
+       aVal.Append(resid.Bytes())
+       aTyped.AppendNull() // a.typed_value null -> recurses 
bField/bVal/bTyped null
+
+       st := b.NewArray()
+       defer st.Release()
+       arr := array.NewExtensionArrayWithStorage(vt, 
st).(*extensions.VariantArray)
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Field("a").Field("b"), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ints := out.(*array.Int64)
+       require.Equal(t, 2, ints.Len())
+       assert.EqualValues(t, 5, ints.Value(0))
+       assert.EqualValues(t, 6, ints.Value(1), "mid-level residual row must be 
reassembled, not nulled")
+}
+
+// TestVariantGetResidualNoLeak guards the residual break path 
(buildTargetVariant +
+// per-row reassembly) against leaks, which TestVariantGetNoLeak does not 
reach.
+func TestVariantGetResidualNoLeak(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := exec.WithAllocator(context.Background(), mem)
+
+       arr := vgMixedResidual(t, mem, arrow.ListOf(arrow.PrimitiveTypes.Int64),
+               func(bld array.Builder) {
+                       lb := bld.(*array.ListBuilder)
+                       lb.Append(true)
+                       el := lb.ValueBuilder().(*array.StructBuilder)
+                       for _, v := range []int64{10, 20} {
+                               el.Append(true)
+                               
el.FieldBuilder(0).(*array.BinaryBuilder).AppendNull()
+                               
el.FieldBuilder(1).(*array.Int64Builder).Append(v)
+                       }
+               }, vgVariant(t, []any{int64(30), int64(40)}))
+
+       out, err := compute.VariantGet(ctx, arr, compute.VariantGetOptions{
+               Path: variant.VariantPath{}.Index(0), AsType: 
arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+       out.Release()
+       arr.Release()
+}
+
+// vgNonShreddedVals builds a non-shredded array from pre-built values, so a 
test can
+// control per-value encoding (e.g. timestamp unit) that vgNonShredded cannot.
+func vgNonShreddedVals(t *testing.T, mem memory.Allocator, vals 
...variant.Value) *extensions.VariantArray {
+       t.Helper()
+       bldr := extensions.NewVariantBuilder(mem, 
extensions.NewDefaultVariantType())
+       defer bldr.Release()
+       for _, v := range vals {
+               bldr.Append(v)
+       }
+
+       return bldr.NewArray().(*extensions.VariantArray)
+}
+
+func vgTimestamp(t *testing.T, ts arrow.Timestamp, nano bool) variant.Value {
+       t.Helper()
+       var b variant.Builder
+       opts := []variant.AppendOpt{variant.OptTimestampUTC}
+       if nano {
+               opts = append(opts, variant.OptTimestampNano)
+       }
+       require.NoError(t, b.Append(ts, opts...))
+       val, err := b.Build()
+       require.NoError(t, err)
+
+       return val
+}
+
+// TestVariantGetMixedFloatWidths (:411, floats): a Float(32) and a Double(64) 
leaf
+// both survive a Float64 request, independent of order.
+func TestVariantGetMixedFloatWidths(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, float32(1.5), float64(2.5))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64})
+       require.NoError(t, err)
+       defer out.Release()
+       f := out.(*array.Float64)
+       require.Equal(t, 2, f.Len())
+       assert.InDelta(t, 1.5, f.Value(0), 1e-9)
+       assert.InDelta(t, 2.5, f.Value(1), 1e-9, "Double leaf must not be 
dropped by a Float first leaf")
+}
+
+// TestVariantGetIntPlusFloat: a mixed int/float column cast to Float64 widens 
the int
+// through the cast kernels rather than nulling it.
+func TestVariantGetIntPlusFloat(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, int64(3), float64(2.5))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64})
+       require.NoError(t, err)
+       defer out.Release()
+       f := out.(*array.Float64)
+       assert.InDelta(t, 3.0, f.Value(0), 1e-9)
+       assert.InDelta(t, 2.5, f.Value(1), 1e-9)
+}
+
+// TestVariantGetTypeOrderIndependent (:411): the same two values give the 
same result
+// regardless of which row comes first.
+func TestVariantGetTypeOrderIndependent(t *testing.T) {
+       mem := memory.DefaultAllocator
+       forward := vgNonShredded(t, mem, int64(3), float64(2.5))
+       defer forward.Release()
+       reverse := vgNonShredded(t, mem, float64(2.5), int64(3))
+       defer reverse.Release()
+
+       optsF := compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64}
+       fwd, err := compute.VariantGet(context.Background(), forward, optsF)
+       require.NoError(t, err)
+       defer fwd.Release()
+       rev, err := compute.VariantGet(context.Background(), reverse, optsF)
+       require.NoError(t, err)
+       defer rev.Release()
+
+       fa, ra := fwd.(*array.Float64), rev.(*array.Float64)
+       assert.InDelta(t, fa.Value(0), ra.Value(1), 1e-9)
+       assert.InDelta(t, fa.Value(1), ra.Value(0), 1e-9)
+       assert.False(t, fa.IsNull(0) || fa.IsNull(1) || ra.IsNull(0) || 
ra.IsNull(1), "no leaf dropped in either order")
+}
+
+// TestVariantGetMixedTimestampUnits: a micros leaf and a nanos leaf of the 
same instant
+// both land on it when cast to a nanos target (units are converted, not 
reinterpreted).
+func TestVariantGetMixedTimestampUnits(t *testing.T) {
+       mem := memory.DefaultAllocator
+       const micros = arrow.Timestamp(1_600_000_000_000_000)
+       const nanos = arrow.Timestamp(1_600_000_000_000_000_000)
+       arr := vgNonShreddedVals(t, mem, vgTimestamp(t, micros, false), 
vgTimestamp(t, nanos, true))
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               AsType: &arrow.TimestampType{Unit: arrow.Nanosecond, TimeZone: 
"UTC"},
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       ts := out.(*array.Timestamp)
+       require.Equal(t, 2, ts.Len())
+       assert.EqualValues(t, nanos, ts.Value(0), "micros leaf must be scaled 
to nanos, not copied raw")
+       assert.EqualValues(t, nanos, ts.Value(1))
+}
+
+// TestVariantGetMixedDecimalScales: leaves shredded at different scales both 
rescale to
+// the requested target scale.
+func TestVariantGetMixedDecimalScales(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShreddedVals(t, mem,
+               vgVariant(t, variant.DecimalValue[decimal.Decimal32]{Scale: 1, 
Value: decimal.Decimal32(15)}),  // 1.5
+               vgVariant(t, variant.DecimalValue[decimal.Decimal32]{Scale: 2, 
Value: decimal.Decimal32(225)})) // 2.25
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               AsType: &arrow.Decimal128Type{Precision: 38, Scale: 2},
+       })
+       require.NoError(t, err)
+       defer out.Release()
+       d := out.(*array.Decimal128)
+       require.Equal(t, 2, d.Len())
+       assert.InDelta(t, 1.5, d.Value(0).ToFloat64(2), 1e-9, "scale-1 leaf 
must rescale to scale-2, not drop")
+       assert.InDelta(t, 2.25, d.Value(1).ToFloat64(2), 1e-9)
+}
+
+// TestVariantGetStrictSlowPathErrors (:411/:269): on the reassembly path a 
lossy cast
+// errors under Strict rather than nulling.
+func TestVariantGetStrictSlowPathErrors(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, map[string]any{"a": int64(5_000_000_000)}) 
// overflows int32
+       defer arr.Release()
+
+       _, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: field("a"), AsType: arrow.PrimitiveTypes.Int32, Strict: 
true,
+       })
+       require.Error(t, err, "Strict must error on an overflowing cast, not 
null it")
+}
+
+// TestVariantGetMixedTypeNoLeak guards the multi-group scatter path (cast + 
Concatenate
+// + Take), which the single-type leak tests do not reach.
+// TestVariantGetNestedTypeNotImplemented pins that a nested AsType is 
rejected with
+// ErrNotImplemented rather than silently producing an all-null array.
+func TestVariantGetNestedTypeNotImplemented(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, map[string]any{"a": map[string]any{"x": 
int64(1)}})
+       defer arr.Release()
+
+       for _, nested := range []arrow.DataType{
+               arrow.StructOf(arrow.Field{Name: "x", Type: 
arrow.PrimitiveTypes.Int64}),
+               arrow.ListOf(arrow.PrimitiveTypes.Int64),
+       } {
+               out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: nested})
+               if out != nil {
+                       out.Release()
+               }
+               require.ErrorIs(t, err, arrow.ErrNotImplemented, "nested AsType 
%s must error, not null", nested)
+       }
+}
+
+func TestVariantGetMixedTypeNoLeak(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := exec.WithAllocator(context.Background(), mem)
+
+       arr := vgNonShredded(t, mem, int64(3), float64(2.5), nil, "x")
+       out, err := compute.VariantGet(ctx, arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64})
+       require.NoError(t, err)
+       out.Release()
+       arr.Release()
+}
+
+// TestVariantGetInterleavedScatter exercises the multi-group scatter 
(Concatenate +
+// TakeArray) with a NON-IDENTITY permutation: two same-typed leaves straddle a
+// differently-typed one, so group order [3,7,2.5] must scatter back to row 
order
+// [3,2.5,7]. Every other multi-group test lands in identity order.
+func TestVariantGetInterleavedScatter(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgNonShredded(t, mem, int64(3), float64(2.5), int64(7)) // 
Int8{0,2}, Float64{1}
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{AsType: arrow.PrimitiveTypes.Float64})
+       require.NoError(t, err)
+       defer out.Release()
+       f := out.(*array.Float64)
+       require.Equal(t, 3, f.Len())
+       assert.InDelta(t, 3.0, f.Value(0), 1e-9)
+       assert.InDelta(t, 2.5, f.Value(1), 1e-9, "interleaved leaf must scatter 
back to its row, not stay in group order")
+       assert.InDelta(t, 7.0, f.Value(2), 1e-9)
+}
+
+// TestVariantGetStrictObjectLeafErrors pins that under Strict an object/array 
leaf cast
+// to a primitive errors (impossible cast) rather than silently nulling.
+func TestVariantGetStrictObjectLeafErrors(t *testing.T) {
+       mem := memory.DefaultAllocator
+       for _, v := range []any{
+               map[string]any{"a": map[string]any{"x": int64(1)}}, // object 
leaf at $.a
+               map[string]any{"a": []any{int64(1), int64(2)}},     // array 
leaf at $.a
+       } {
+               arr := vgNonShredded(t, mem, v)
+               out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+                       Path: field("a"), AsType: arrow.PrimitiveTypes.Int64, 
Strict: true,
+               })
+               if out != nil {
+                       out.Release()
+               }
+               arr.Release()
+               require.ErrorIs(t, err, arrow.ErrInvalid, "strict cast of a 
non-primitive leaf to Int64 must error")
+       }
+}
+
+// TestVariantGetShreddedFieldOnScalarErrors pins that a field step into a 
shredded
+// scalar column errors on the columnar path, matching the per-row GetByPath 
path.
+func TestVariantGetShreddedFieldOnScalarErrors(t *testing.T) {
+       mem := memory.DefaultAllocator
+       arr := vgShreddedInt(t, mem, 1, 2, 3) // typed_value is a scalar Int64 
column
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{Path: field("a"), AsType: arrow.PrimitiveTypes.Int64})
+       if out != nil {
+               out.Release()
+       }
+       require.ErrorIs(t, err, arrow.ErrInvalid, "field access on a shredded 
scalar must error, not return null")
+}
+
+// TestVariantGetUUIDTarget pins that a UUID AsType reaches the UUID cast, not 
the nested-type reject.
+func TestVariantGetUUIDTarget(t *testing.T) {
+       mem := memory.DefaultAllocator
+       u := uuid.MustParse("00112233-4455-6677-8899-aabbccddeeff")
+       arr := vgNonShredded(t, mem, map[string]any{"id": u})
+       defer arr.Release()
+
+       out, err := compute.VariantGet(context.Background(), arr, 
compute.VariantGetOptions{
+               Path: field("id"), AsType: extensions.NewUUIDType(),
+       })
+       require.NoError(t, err, "UUID target must not be rejected as a nested 
type")
+       defer out.Release()
+
+       uarr, ok := out.(*extensions.UUIDArray)
+       require.True(t, ok, "expected *extensions.UUIDArray, got %T", out)
+       require.Equal(t, 1, uarr.Len())
+       require.False(t, uarr.IsNull(0))
+       assert.Equal(t, u, uarr.Value(0))
+}
+
+// TestVariantGetPartialCastFailure pins that non-strict nulls only the 
inconvertible row: ["1","bad"]->int64 is [1,null].
+func TestVariantGetPartialCastFailure(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+       ctx := exec.WithAllocator(context.Background(), mem)
+       arr := vgNonShredded(t, mem, "1", "bad")
+
+       out, err := compute.VariantGet(ctx, arr, compute.VariantGetOptions{
+               AsType: arrow.PrimitiveTypes.Int64,
+       })
+       require.NoError(t, err)
+
+       ints := out.(*array.Int64)
+       require.Equal(t, 2, ints.Len())
+       assert.False(t, ints.IsNull(0), "valid row must survive a sibling row's 
cast failure")
+       assert.EqualValues(t, 1, ints.Value(0))
+       assert.True(t, ints.IsNull(1), "only the inconvertible row is null")
+
+       out.Release()
+       arr.Release()
+}
diff --git a/arrow/extensions/variant.go b/arrow/extensions/variant.go
index fee2e046..a0cc74ed 100644
--- a/arrow/extensions/variant.go
+++ b/arrow/extensions/variant.go
@@ -458,6 +458,11 @@ func (v *VariantArray) IsShredded() bool {
        return v.ExtensionType().(*VariantType).typedValueFieldIdx != -1
 }
 
+// VariantType returns the array's extension type without the ExtensionType 
cast.
+func (v *VariantArray) VariantType() *VariantType {
+       return v.ExtensionType().(*VariantType)
+}
+
 // UnshredVariant returns an equivalent VariantArray in the non-shredded layout
 // (a struct of metadata and value), reassembling each row's value from the
 // shredded typed_value and value columns. If the array is already non-shredded
@@ -513,6 +518,11 @@ func (v *VariantArray) IsNull(i int) bool {
                }
        }
 
+       if vt.valueFieldIdx == -1 {
+               // No residual value column: a null typed_value means the value 
is missing.
+               return true
+       }
+
        valArr := v.Storage().(*array.Struct).Field(vt.valueFieldIdx)
        b := valArr.(arrow.TypedArray[[]byte]).Value(i)
        return len(b) == 1 && b[0] == 0 // variant null
diff --git a/parquet/variant/path.go b/parquet/variant/path.go
new file mode 100644
index 00000000..bc46da77
--- /dev/null
+++ b/parquet/variant/path.go
@@ -0,0 +1,109 @@
+// 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 variant
+
+import (
+       "errors"
+       "fmt"
+
+       "github.com/apache/arrow-go/v18/arrow"
+)
+
+// pathElem is one step of a VariantPath: an object field when isField is set, 
else an array index.
+type pathElem struct {
+       name    string
+       index   int
+       isField bool
+}
+
+// VariantPath is an ordered list of steps to navigate into a variant value. 
The
+// zero value is the root path; extend it with Field and Index.
+type VariantPath struct {
+       elems []pathElem
+}
+
+// Field returns a copy of the path with an object-field step appended.
+func (p VariantPath) Field(name string) VariantPath {
+       return VariantPath{elems: append(p.grow(), pathElem{name: name, 
isField: true})}
+}
+
+// Index returns a copy of the path with an array-index step appended.
+func (p VariantPath) Index(i int) VariantPath {
+       return VariantPath{elems: append(p.grow(), pathElem{index: i})}
+}
+
+// Join returns a copy of the path with other's steps appended.
+func (p VariantPath) Join(other VariantPath) VariantPath {
+       return VariantPath{elems: append(p.grow(), other.elems...)}
+}
+
+func (p VariantPath) grow() []pathElem {
+       return append(make([]pathElem, 0, len(p.elems)+1), p.elems...)
+}
+
+// Len returns the number of steps in the path.
+func (p VariantPath) Len() int { return len(p.elems) }
+
+// StepAt returns the i-th step's name and index, with isField true for an 
object-field step.
+func (p VariantPath) StepAt(i int) (name string, index int, isField bool) {
+       e := p.elems[i]
+
+       return e.name, e.index, e.isField
+}
+
+// GetByPath navigates path into v and returns the leaf value. found is false 
when
+// the path is cleanly absent (a missing object field, or an out-of-range or
+// non-array index). It returns an error for a type error (a field step into a
+// non-object) or corrupt data (a field id not present in the metadata).
+func (v Value) GetByPath(path VariantPath) (leaf Value, found bool, err error) 
{
+       cur := v
+       for _, e := range path.elems {
+               if e.isField {
+                       obj, ok := cur.Value().(ObjectValue)
+                       if !ok {
+                               return Value{}, false, fmt.Errorf("%w: variant 
path field %q applied to non-object", arrow.ErrInvalid, e.name)
+                       }
+                       field, ferr := obj.ValueByKey(e.name)
+                       if ferr != nil {
+                               if errors.Is(ferr, arrow.ErrNotFound) {
+                                       return Value{}, false, nil
+                               }
+
+                               return Value{}, false, ferr
+                       }
+                       cur = field.Value
+
+                       continue
+               }
+
+               arr, ok := cur.Value().(ArrayValue)
+               if !ok {
+                       return Value{}, false, nil
+               }
+               if e.index < 0 || uint64(e.index) >= uint64(arr.Len()) {
+                       return Value{}, false, nil
+               }
+               el, aerr := arr.Value(uint32(e.index))
+               if aerr != nil {
+                       return Value{}, false, nil
+               }
+               cur = el
+       }
+
+       return cur, true, nil
+}
diff --git a/parquet/variant/path_test.go b/parquet/variant/path_test.go
new file mode 100644
index 00000000..9de8b84a
--- /dev/null
+++ b/parquet/variant/path_test.go
@@ -0,0 +1,116 @@
+// 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 variant_test
+
+import (
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/parquet/variant"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func buildVar(t *testing.T, v any) variant.Value {
+       t.Helper()
+       var b variant.Builder
+       require.NoError(t, b.Append(v))
+       val, err := b.Build()
+       require.NoError(t, err)
+
+       return val
+}
+
+func TestGetByPathFieldAndIndex(t *testing.T) {
+       v := buildVar(t, map[string]any{"a": map[string]any{"b": int64(5)}, 
"arr": []any{int64(10), int64(20)}})
+
+       leaf, found, err := 
v.GetByPath(variant.VariantPath{}.Field("a").Field("b"))
+       require.NoError(t, err)
+       require.True(t, found)
+       assert.EqualValues(t, 5, leaf.Value())
+
+       leaf, found, err = 
v.GetByPath(variant.VariantPath{}.Field("arr").Index(1))
+       require.NoError(t, err)
+       require.True(t, found)
+       assert.EqualValues(t, 20, leaf.Value())
+}
+
+func TestGetByPathAbsent(t *testing.T) {
+       v := buildVar(t, map[string]any{"a": int64(1), "arr": []any{int64(10)}})
+
+       for _, p := range []variant.VariantPath{
+               variant.VariantPath{}.Field("missing"),      // absent object 
field
+               variant.VariantPath{}.Field("arr").Index(9), // out-of-range 
index
+               variant.VariantPath{}.Field("a").Index(0),   // index into a 
scalar
+               variant.VariantPath{}.Index(0),              // index into an 
object
+       } {
+               _, found, err := v.GetByPath(p)
+               require.NoError(t, err)
+               assert.False(t, found)
+       }
+}
+
+// TestGetByPathFieldOnScalarErrors: a field step into a non-object is a type 
error.
+func TestGetByPathFieldOnScalarErrors(t *testing.T) {
+       v := buildVar(t, int64(1))
+       _, _, err := v.GetByPath(variant.VariantPath{}.Field("a"))
+       require.ErrorIs(t, err, arrow.ErrInvalid)
+}
+
+// TestGetByPathHugeIndex: a huge index must not wrap; it is simply absent.
+func TestGetByPathHugeIndex(t *testing.T) {
+       v := buildVar(t, []any{int64(10), int64(20)})
+       _, found, err := v.GetByPath(variant.VariantPath{}.Index(1 << 40))
+       require.NoError(t, err)
+       assert.False(t, found)
+}
+
+func TestVariantPathJoinAndStepAt(t *testing.T) {
+       p := 
variant.VariantPath{}.Field("a").Join(variant.VariantPath{}.Index(2).Field("b"))
+       require.Equal(t, 3, p.Len())
+
+       name, _, isField := p.StepAt(0)
+       assert.Equal(t, "a", name)
+       assert.True(t, isField)
+       name, idx, isField := p.StepAt(1)
+       assert.Equal(t, "", name)
+       assert.Equal(t, 2, idx)
+       assert.False(t, isField)
+       name, _, isField = p.StepAt(2)
+       assert.Equal(t, "b", name)
+       assert.True(t, isField)
+}
+
+// TestGetByPathEmptyKey covers the empty-string object key: Field("") is a 
field
+// step distinct from Index(0), so it must resolve the "" key rather than 
index 0.
+func TestGetByPathEmptyKey(t *testing.T) {
+       v := buildVar(t, map[string]any{"": int64(42)})
+
+       _, _, isField := variant.VariantPath{}.Field("").StepAt(0)
+       assert.True(t, isField, `Field("") must be a field step, not an index 
step`)
+
+       leaf, found, err := v.GetByPath(variant.VariantPath{}.Field(""))
+       require.NoError(t, err)
+       require.True(t, found)
+       assert.EqualValues(t, 42, leaf.Value())
+
+       // Index(0) on the object must not match the "" key.
+       _, found, err = v.GetByPath(variant.VariantPath{}.Index(0))
+       require.NoError(t, err)
+       assert.False(t, found)
+}

Reply via email to