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 0ffa1b63 fix(arrow/array): validate bufferless run-end encoded arrays 
(#1421)
0ffa1b63 is described below

commit 0ffa1b63337b10272f8a4009003e7280223922e6
Author: Matt Topol <[email protected]>
AuthorDate: Mon Oct 26 16:08:03 2026 +0000

    fix(arrow/array): validate bufferless run-end encoded arrays (#1421)
    
    ### Rationale for this change
    
    Split out of #1409.
    
    `ValidateFull` rejects run-end-encoded arrays from the builder. A REE
    array from `NewRunEndEncodedBuilder` (or a `Concatenate` of such arrays)
    fails with `expected at least 1 buffers for run_end_encoded<...>, got
    0`, on every platform. The REE layout declares one always-null
    placeholder buffer (`datatype_encoded.go:40`); `NewRunEndEncodedArray`
    and the IPC reader supply a `nil` placeholder, but the builder and C
    Data import produce zero buffers. The Arrow spec says REE arrays have no
    physical buffers, so both representations are legitimate.
    
    ### What changes are included in this PR?
    
    `arrow/array/validate.go` accepts a REE array (including extension types
    with REE storage) that has no buffers. Arrays with a `nil` placeholder
    go through the existing checks as before, and non-nil or extra parent
    buffers are still rejected. Constructors, layouts, IPC and C Data are
    unchanged.
    
    ### Are these changes tested?
    
    Yes. New `TestRunEndEncodedValidateConstructedArrays` checks that
    builder-produced, empty, sliced, constructor-built, `Concatenate`d and
    REE-extension arrays pass `ValidateFull`. The builder, `Concatenate`,
    empty, sliced and extension cases fail before the fix; constructor cases
    passed already. Existing rejection cases (bad run ends, non-nil/extra
    parent buffers) still fail validation. `go vet` and `gofmt -l` are clean
    on `arrow/array`.
    
    ### Are there any user-facing changes?
    
    Yes: `ValidateFull` accepts run-end-encoded arrays produced by the
    builder, `Concatenate` and C Data import. No API changes.
---
 arrow/array/encoded_test.go | 93 +++++++++++++++++++++++++++++++++++++++++----
 arrow/array/validate.go     | 14 +++++++
 2 files changed, 100 insertions(+), 7 deletions(-)

diff --git a/arrow/array/encoded_test.go b/arrow/array/encoded_test.go
index c8a5c4e2..8d02ed9e 100644
--- a/arrow/array/encoded_test.go
+++ b/arrow/array/encoded_test.go
@@ -22,6 +22,7 @@ import (
 
        "github.com/apache/arrow-go/v18/arrow"
        "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/extensions"
        "github.com/apache/arrow-go/v18/arrow/memory"
        "github.com/apache/arrow-go/v18/internal/json"
 
@@ -85,6 +86,55 @@ func TestRLEFromRunEndsAndValues(t *testing.T) {
        })
 }
 
+func TestRunEndEncodedValidateConstructedArrays(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       bldr := array.NewRunEndEncodedBuilder(mem, arrow.PrimitiveTypes.Int64, 
arrow.PrimitiveTypes.Int64)
+       defer bldr.Release()
+
+       bldr.Append(2)
+       bldr.ValueBuilder().(*array.Int64Builder).Append(42)
+       bldr.AppendNull()
+       bldr.Append(3)
+       bldr.ValueBuilder().(*array.Int64Builder).Append(7)
+
+       built := bldr.NewArray().(*array.RunEndEncoded)
+       defer built.Release()
+       constructed := array.NewRunEndEncodedArray(built.RunEndsArr(), 
built.Values(), built.Len(), 0)
+       defer constructed.Release()
+       concatenated, err := array.Concatenate([]arrow.Array{built, 
constructed}, mem)
+       require.NoError(t, err)
+       defer concatenated.Release()
+       concatenatedWithPlaceholder, err := 
array.Concatenate([]arrow.Array{constructed, built}, mem)
+       require.NoError(t, err)
+       defer concatenatedWithPlaceholder.Release()
+       empty := bldr.NewArray()
+       defer empty.Release()
+       sliced := array.NewSlice(built, 1, 5)
+       defer sliced.Release()
+       ext := 
array.NewExtensionArrayWithStorage(extensions.NewOpaqueType(built.DataType(), 
"ree", "test"), built)
+       defer ext.Release()
+
+       for _, tt := range []struct {
+               name string
+               arr  arrow.Array
+       }{
+               {"builder", built},
+               {"constructor", constructed},
+               {"concatenate", concatenated},
+               {"concatenate with placeholder", concatenatedWithPlaceholder},
+               {"empty builder", empty},
+               {"sliced builder", sliced},
+               {"extension", ext},
+       } {
+               t.Run(tt.name, func(t *testing.T) {
+                       assert.NoError(t, array.Validate(tt.arr))
+                       assert.NoError(t, array.ValidateFull(tt.arr))
+               })
+       }
+}
+
 func TestRunEndEncodedValidate(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)
@@ -103,6 +153,33 @@ func TestRunEndEncodedValidate(t *testing.T) {
                assert.NoError(t, arr.ValidateFull())
        })
 
+       t.Run("invalid parent buffers", func(t *testing.T) {
+               runEnds, _, err := array.FromJSON(mem, 
arrow.PrimitiveTypes.Int64, strings.NewReader(`[1, 3]`))
+               require.NoError(t, err)
+               defer runEnds.Release()
+               values, _, err := array.FromJSON(mem, 
arrow.PrimitiveTypes.Int64, strings.NewReader(`[42, 7]`))
+               require.NoError(t, err)
+               defer values.Release()
+
+               for _, tt := range []struct {
+                       name    string
+                       buffers []*memory.Buffer
+                       wantErr string
+               }{
+                       {"non-nil buffer", 
[]*memory.Buffer{memory.NewBufferBytes([]byte{0xff})}, "must be nil"},
+                       {"extra buffer", []*memory.Buffer{nil, nil}, "expected 
at most 1 buffers"},
+               } {
+                       t.Run(tt.name, func(t *testing.T) {
+                               arr := makeRunEndEncodedArrayRaw(t, 
arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int64, arrow.PrimitiveTypes.Int64), 
3, 0, 0,
+                                       tt.buffers, 
[]arrow.ArrayData{runEnds.Data(), values.Data()})
+                               defer arr.Release()
+
+                               assert.ErrorContains(t, array.Validate(arr), 
tt.wantErr)
+                               assert.ErrorContains(t, 
array.ValidateFull(arr), tt.wantErr)
+                       })
+               }
+       })
+
        t.Run("last run end shorter than logical length fails Validate", func(t 
*testing.T) {
                runEnds, _, _ := array.FromJSON(mem, 
arrow.PrimitiveTypes.Int32, strings.NewReader(`[1, 2]`))
                values, _, _ := array.FromJSON(mem, arrow.BinaryTypes.String, 
strings.NewReader(`["a", "b"]`))
@@ -140,14 +217,16 @@ func TestRunEndEncodedValidate(t *testing.T) {
                defer runEnds.Release()
                defer values.Release()
 
-               arr := makeRunEndEncodedArrayRaw(t, 
arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.BinaryTypes.String), 2, 
0, 0,
-                       []*memory.Buffer{nil}, 
[]arrow.ArrayData{runEnds.Data(), values.Data()})
-               defer arr.Release()
+               for _, buffers := range [][]*memory.Buffer{nil, {nil}} {
+                       arr := makeRunEndEncodedArrayRaw(t, 
arrow.RunEndEncodedOf(arrow.PrimitiveTypes.Int32, arrow.BinaryTypes.String), 2, 
0, 0,
+                               buffers, []arrow.ArrayData{runEnds.Data(), 
values.Data()})
+                       defer arr.Release()
 
-               assert.NoError(t, array.Validate(arr))
-               err := array.ValidateFull(arr)
-               require.Error(t, err)
-               assert.Contains(t, err.Error(), "strictly greater")
+                       assert.NoError(t, array.Validate(arr))
+                       err := array.ValidateFull(arr)
+                       require.Error(t, err)
+                       assert.Contains(t, err.Error(), "strictly greater")
+               }
        })
 
        t.Run("unknown null counts without bitmaps still validate", func(t 
*testing.T) {
diff --git a/arrow/array/validate.go b/arrow/array/validate.go
index c1605d44..f627b6ee 100644
--- a/arrow/array/validate.go
+++ b/arrow/array/validate.go
@@ -37,6 +37,8 @@ type Validator interface {
 
 // Validate performs a basic O(1) consistency check on arr, returning an error
 // if the array's internal buffers or nested child data are inconsistent.
+// Run-end encoded arrays may omit the always-nil buffer placeholder used by
+// some constructors.
 //
 // Use this to detect corrupted data from untrusted sources such as Arrow 
Flight
 // or Flight SQL servers before accessing values, which may otherwise panic.
@@ -177,6 +179,18 @@ func validateArrayData(data *Data) error {
                return fmt.Errorf("arrow/array: array offset and length 
overflow")
        }
 
+       // REE arrays have no physical buffers. Constructors and IPC may 
include a
+       // nil placeholder, but builders and C Data imports omit it.
+       if len(data.buffers) == 0 {
+               dt := data.dtype
+               if ext, ok := dt.(arrow.ExtensionType); ok {
+                       dt = ext.StorageType()
+               }
+               if dt.ID() == arrow.RUN_END_ENCODED {
+                       return nil
+               }
+       }
+
        layout := data.dtype.Layout()
        // Union arrays reserve the first buffer slot for a validity bitmap, 
even
        // though that slot must be nil and is not part of their type layout.

Reply via email to