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 a1416188 perf(arrow/array): compare binary values by valid runs (#1174)
a1416188 is described below

commit a1416188efb6bfc4cd3609f28d3e312b9eef8eda
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 2 19:42:50 2026 +0200

    perf(arrow/array): compare binary values by valid runs (#1174)
    
    ### Rationale for this change
    
    Binary and string equality currently compares every non-null value
    separately. This repeats offset lookups and small byte or string
    comparisons even when a long valid run is stored contiguously.
    
    ### What changes are included in this PR?
    
    - Walk contiguous valid runs for Binary, String, LargeBinary, and
    LargeString.
    - Compare each run payload in one operation.
    - Verify every value length so equal concatenated bytes with different
    boundaries still compare unequal.
    - Keep null payloads ignored.
    - Sample validity fragmentation and retain the scalar path for many tiny
    runs.
    - Add benchmarks and tests for offsets, null patterns, mismatches, and
    value boundaries.
    
    Apple M1 Pro results for 65,536 values of 32 bytes with `-cpu=1`:
    
    | Case | Before | After | Speedup |
    | --- | ---: | ---: | ---: |
    | Binary equal | 644 us | 360 us | 1.79x |
    | Binary 10% null | 582 us | 439 us | 1.33x |
    | Binary mismatch last | 648 us | 350 us | 1.85x |
    | Binary different length | 453 us | 262 us | 1.73x |
    | String equal | 633 us | 390 us | 1.62x |
    | String 10% null | 604 us | 496 us | 1.22x |
    | String mismatch last | 664 us | 406 us | 1.64x |
    | String different length | 426 us | 235 us | 1.81x |
    
    The alternating 50% null case selects the scalar path and remains
    neutral. All benchmark cases stay at zero allocations.
    
    ### Are these changes tested?
    
    Yes.
    
    - `go test ./arrow/...`
    - `go test -race ./arrow/array`
    - `go vet -composites=false ./arrow/array`
    - Cross-compiled array tests for linux/amd64 and linux/s390x.
    
    ### Are there any user-facing changes?
    
    No.
---
 arrow/array/binary.go                         | 140 +++++++-
 arrow/array/binary_equality_benchmark_test.go | 131 ++++++++
 arrow/array/binary_equality_test.go           | 444 ++++++++++++++++++++++++++
 arrow/array/string.go                         |  44 ++-
 4 files changed, 734 insertions(+), 25 deletions(-)

diff --git a/arrow/array/binary.go b/arrow/array/binary.go
index fe986583..c2fde2af 100644
--- a/arrow/array/binary.go
+++ b/arrow/array/binary.go
@@ -24,7 +24,9 @@ import (
        "unsafe"
 
        "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
        "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/internal/bitutils"
        "github.com/apache/arrow-go/v18/internal/json"
 )
 
@@ -231,15 +233,21 @@ func (a *Binary) ValidateFull() error {
 }
 
 func arrayEqualBinary(left, right *Binary) bool {
-       for i := 0; i < left.Len(); i++ {
-               if left.IsNull(i) {
-                       continue
-               }
-               if !bytes.Equal(left.Value(i), right.Value(i)) {
-                       return false
+       if useScalarVariableWidthEquality(left) {
+               for i := range left.Len() {
+                       if !left.IsNull(i) && !bytes.Equal(left.Value(i), 
right.Value(i)) {
+                               return false
+                       }
                }
+               return true
        }
-       return true
+       return arrayEqualVariableWidth(
+               left.valueOffsets, right.valueOffsets,
+               left.valueBytes, right.valueBytes,
+               left.Offset(), right.Offset(), left.Len(),
+               left.NullN(), left.NullBitmapBytes(),
+               bytes.Equal,
+       )
 }
 
 type LargeBinary struct {
@@ -432,17 +440,127 @@ func (a *LargeBinary) ValidateFull() error {
 }
 
 func arrayEqualLargeBinary(left, right *LargeBinary) bool {
-       for i := 0; i < left.Len(); i++ {
-               if left.IsNull(i) {
-                       continue
+       if useScalarVariableWidthEquality(left) {
+               for i := range left.Len() {
+                       if !left.IsNull(i) && !bytes.Equal(left.Value(i), 
right.Value(i)) {
+                               return false
+                       }
+               }
+               return true
+       }
+       return arrayEqualVariableWidth(
+               left.valueOffsets, right.valueOffsets,
+               left.valueBytes, right.valueBytes,
+               left.Offset(), right.Offset(), left.Len(),
+               left.NullN(), left.NullBitmapBytes(),
+               bytes.Equal,
+       )
+}
+
+type binaryOffset interface {
+       ~int32 | ~int64
+}
+
+func useScalarVariableWidthEquality(values arrow.Array) bool {
+       if values.NullN() == 0 {
+               return false
+       }
+       if values.Len() <= 64 || len(values.NullBitmapBytes()) == 0 {
+               return true
+       }
+
+       // Very short validity runs cost more to set up than direct value 
comparisons.
+       // Sample a few runs and retain the scalar path when they average under 
four values.
+       const (
+               sampleRuns          = 8
+               minAverageRunLength = 4
+       )
+       runs := bitutils.NewSetBitRunReader(
+               values.NullBitmapBytes(), int64(values.Data().Offset()), 
int64(values.Len()),
+       )
+       validValues := int64(0)
+       for range sampleRuns {
+               run := runs.NextRun()
+               if run.Length == 0 {
+                       return false
+               }
+               validValues += run.Length
+       }
+       return validValues < sampleRuns*minAverageRunLength
+}
+
+func arrayEqualVariableWidth[T binaryOffset, V ~[]byte | ~string](
+       leftOffsets, rightOffsets []T,
+       leftValues, rightValues V,
+       leftOffset, rightOffset, length, nulls int,
+       validity []byte,
+       equalValues func(V, V) bool,
+) bool {
+       if length == 0 {
+               return true
+       }
+
+       // A declared null count may be inconsistent with the validity bitmap.
+       // Verify zero-null bitmaps before comparing the whole payload.
+       if len(validity) == 0 ||
+               (nulls == 0 && bitutil.CountSetBits(validity, leftOffset, 
length) == length) {
+               return arrayEqualVariableWidthRun(
+                       leftOffsets, rightOffsets,
+                       leftValues, rightValues,
+                       leftOffset, rightOffset, length,
+                       equalValues,
+               )
+       }
+
+       runs := bitutils.NewSetBitRunReader(validity, int64(leftOffset), 
int64(length))
+       for {
+               run := runs.NextRun()
+               if run.Length == 0 {
+                       return true
+               }
+               if !arrayEqualVariableWidthRun(
+                       leftOffsets, rightOffsets,
+                       leftValues, rightValues,
+                       leftOffset+int(run.Pos), rightOffset+int(run.Pos), 
int(run.Length),
+                       equalValues,
+               ) {
+                       return false
                }
-               if !bytes.Equal(left.Value(i), right.Value(i)) {
+       }
+}
+
+func arrayEqualVariableWidthRun[T binaryOffset, V ~[]byte | ~string](
+       leftOffsets, rightOffsets []T,
+       leftValues, rightValues V,
+       leftOffset, rightOffset, length int,
+       equalValues func(V, V) bool,
+) bool {
+       leftStart, leftEnd := leftOffsets[leftOffset], 
leftOffsets[leftOffset+length]
+       rightStart, rightEnd := rightOffsets[rightOffset], 
rightOffsets[rightOffset+length]
+       if leftEnd-leftStart != rightEnd-rightStart ||
+               !equalValues(
+                       sliceBinaryValues(leftValues, leftStart, leftEnd),
+                       sliceBinaryValues(rightValues, rightStart, rightEnd),
+               ) {
+               return false
+       }
+       if length == 1 {
+               return true
+       }
+
+       for i := range length {
+               if leftOffsets[leftOffset+i+1]-leftOffsets[leftOffset+i] !=
+                       
rightOffsets[rightOffset+i+1]-rightOffsets[rightOffset+i] {
                        return false
                }
        }
        return true
 }
 
+func sliceBinaryValues[T binaryOffset, V ~[]byte | ~string](values V, start, 
end T) V {
+       return values[start:end]
+}
+
 type ViewLike interface {
        arrow.Array
        ValueHeader(int) *arrow.ViewHeader
diff --git a/arrow/array/binary_equality_benchmark_test.go 
b/arrow/array/binary_equality_benchmark_test.go
new file mode 100644
index 00000000..79cbaa38
--- /dev/null
+++ b/arrow/array/binary_equality_benchmark_test.go
@@ -0,0 +1,131 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package array_test
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+)
+
+var binaryEqualityResult bool
+
+func BenchmarkBinaryEquality(b *testing.B) {
+       const length = 64 * 1024
+
+       types := []arrow.BinaryDataType{
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.LargeString,
+       }
+
+       for _, dtype := range types {
+               b.Run(dtype.Name(), func(b *testing.B) {
+                       for _, valueLen := range []int{8, 32, 128, 1024} {
+                               benchmarkBinaryEqualityCase(b, dtype, length, 
valueLen, nil, "equal", -1)
+                       }
+
+                       for _, tc := range []struct {
+                               name          string
+                               valid         func(int) bool
+                               mismatchIndex int
+                       }{
+                               {name: "nulls_10_percent", valid: func(i int) 
bool { return i%10 != 0 }, mismatchIndex: -1},
+                               {name: "nulls_50_percent", valid: func(i int) 
bool { return i%2 != 0 }, mismatchIndex: -1},
+                               {name: "nulls_50_percent_clustered", valid: 
func(i int) bool { return i >= length/2 }, mismatchIndex: -1},
+                               {name: "mismatch_first", mismatchIndex: 0},
+                               {name: "mismatch_middle", mismatchIndex: length 
/ 2},
+                               {name: "mismatch_last", mismatchIndex: length - 
1},
+                               {name: "different_length", mismatchIndex: 
length / 2},
+                       } {
+                               benchmarkBinaryEqualityCase(b, dtype, length, 
32, tc.valid, tc.name, tc.mismatchIndex)
+                       }
+               })
+       }
+}
+
+func BenchmarkBinaryEqualityShortArrays(b *testing.B) {
+       for _, length := range []int{4, 8, 16, 32, 64, 128} {
+               length := length
+               b.Run(fmt.Sprintf("length_%d", length), func(b *testing.B) {
+                       benchmarkBinaryEqualityCase(
+                               b,
+                               arrow.BinaryTypes.String,
+                               length,
+                               16,
+                               func(i int) bool { return i%2 == 0 },
+                               "alternating_nulls",
+                               -1,
+                       )
+               })
+       }
+}
+
+func benchmarkBinaryEqualityCase(
+       b *testing.B, dtype arrow.BinaryDataType, length, valueLen int, 
validValue func(int) bool, name string, mismatchIndex int,
+) {
+       b.Helper()
+
+       values := makeBinaryEqualityValues(length, valueLen)
+       rightValues := append([]string(nil), values...)
+       valid := make([]bool, length)
+       for i := range valid {
+               valid[i] = validValue == nil || validValue(i)
+       }
+       if mismatchIndex >= 0 {
+               if name == "different_length" {
+                       rightValues[mismatchIndex] += "x"
+               } else {
+                       value := []byte(rightValues[mismatchIndex])
+                       value[0]++
+                       rightValues[mismatchIndex] = string(value)
+               }
+       }
+
+       mem := memory.NewGoAllocator()
+       left := makeBinaryEqualityArray(mem, dtype, values, valid)
+       right := makeBinaryEqualityArray(mem, dtype, rightValues, valid)
+       b.Cleanup(func() {
+               left.Release()
+               right.Release()
+       })
+
+       b.Run(fmt.Sprintf("%s/value_len_%d", name, valueLen), func(b 
*testing.B) {
+               b.ReportAllocs()
+               b.SetBytes(int64(length * valueLen))
+               b.ResetTimer()
+               for i := 0; i < b.N; i++ {
+                       binaryEqualityResult = array.Equal(left, right)
+               }
+       })
+}
+
+func makeBinaryEqualityValues(length, valueLen int) []string {
+       values := make([]string, length)
+       value := make([]byte, valueLen)
+       for i := range values {
+               for j := range value {
+                       value[j] = byte(i*31 + j*17)
+               }
+               values[i] = string(value)
+       }
+       return values
+}
diff --git a/arrow/array/binary_equality_test.go 
b/arrow/array/binary_equality_test.go
new file mode 100644
index 00000000..05a56581
--- /dev/null
+++ b/arrow/array/binary_equality_test.go
@@ -0,0 +1,444 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package array_test
+
+import (
+       "testing"
+
+       "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/memory"
+       "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
+)
+
+func TestBinaryEqualityByValidRuns(t *testing.T) {
+       types := []arrow.BinaryDataType{
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.LargeString,
+       }
+       tests := []struct {
+               name       string
+               left       []string
+               right      []string
+               valid      []bool
+               want       bool
+               leftSlice  [2]int64
+               rightSlice [2]int64
+       }{
+               {
+                       name:  "equal values",
+                       left:  []string{"alpha", "beta", "gamma"},
+                       right: []string{"alpha", "beta", "gamma"},
+                       valid: []bool{true, true, true},
+                       want:  true,
+               },
+               {
+                       name:  "different payload",
+                       left:  []string{"alpha", "beta", "gamma"},
+                       right: []string{"alpha", "zeta", "gamma"},
+                       valid: []bool{true, true, true},
+               },
+               {
+                       name:  "same payload with different boundaries",
+                       left:  []string{"ab", "c"},
+                       right: []string{"a", "bc"},
+                       valid: []bool{true, true},
+               },
+               {
+                       name:  "null payload ignored",
+                       left:  []string{"alpha", "ignored left", "gamma"},
+                       right: []string{"alpha", "ignored right payload", 
"gamma"},
+                       valid: []bool{true, false, true},
+                       want:  true,
+               },
+               {
+                       name:       "different physical offsets",
+                       left:       []string{"left prefix", "alpha", "beta", 
"left suffix"},
+                       right:      []string{"right prefix 1", "right prefix 
2", "alpha", "beta"},
+                       valid:      []bool{true, true, true, true},
+                       want:       true,
+                       leftSlice:  [2]int64{1, 3},
+                       rightSlice: [2]int64{2, 4},
+               },
+       }
+
+       for _, dtype := range types {
+               t.Run(dtype.Name(), func(t *testing.T) {
+                       for _, test := range tests {
+                               t.Run(test.name, func(t *testing.T) {
+                                       left := 
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, test.left, test.valid)
+                                       defer left.Release()
+                                       right := 
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, test.right, test.valid)
+                                       defer right.Release()
+
+                                       if test.leftSlice != [2]int64{} {
+                                               leftSlice := 
array.NewSlice(left, test.leftSlice[0], test.leftSlice[1])
+                                               defer leftSlice.Release()
+                                               rightSlice := 
array.NewSlice(right, test.rightSlice[0], test.rightSlice[1])
+                                               defer rightSlice.Release()
+                                               left, right = leftSlice, 
rightSlice
+                                       }
+
+                                       assert.Equal(t, test.want, 
array.Equal(left, right))
+                               })
+                       }
+               })
+       }
+}
+
+func TestBinaryEqualityByValidRunsAcrossNulls(t *testing.T) {
+       const length = 256
+
+       types := []arrow.BinaryDataType{
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.LargeString,
+       }
+       valid := makeLongValidRuns(length)
+       tests := []struct {
+               name  string
+               build func(arrow.BinaryDataType) (arrow.Array, arrow.Array)
+               want  bool
+       }{
+               {
+                       name: "equal arrays with long valid runs",
+                       build: func(dtype arrow.BinaryDataType) (arrow.Array, 
arrow.Array) {
+                               values := makeBinaryEqualityValues(length, 16)
+                               return 
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, values, valid),
+                                       
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, values, valid)
+                       },
+                       want: true,
+               },
+               {
+                       name: "different bytes under null slots are ignored",
+                       build: func(dtype arrow.BinaryDataType) (arrow.Array, 
arrow.Array) {
+                               leftValues := makeBinaryEqualityValues(length, 
16)
+                               rightValues := append([]string(nil), 
leftValues...)
+                               for i, isValid := range valid {
+                                       if !isValid {
+                                               rightValues[i] = "different 
ignored payload"
+                                       }
+                               }
+                               return makeRawBinaryEqualityArray(dtype, 
leftValues, valid),
+                                       makeRawBinaryEqualityArray(dtype, 
rightValues, valid)
+                       },
+                       want: true,
+               },
+               {
+                       name: "mismatch in a later valid run",
+                       build: func(dtype arrow.BinaryDataType) (arrow.Array, 
arrow.Array) {
+                               leftValues := makeBinaryEqualityValues(length, 
16)
+                               rightValues := append([]string(nil), 
leftValues...)
+                               value := []byte(rightValues[192])
+                               value[0]++
+                               rightValues[192] = string(value)
+                               return 
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, leftValues, valid),
+                                       
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, rightValues, valid)
+                       },
+               },
+               {
+                       name:  "different physical offsets with nulls",
+                       build: makeSlicedBinaryEqualityPair,
+                       want:  true,
+               },
+               {
+                       name: "shifted value boundary inside a valid run",
+                       build: func(dtype arrow.BinaryDataType) (arrow.Array, 
arrow.Array) {
+                               const length = 192
+                               valid := make([]bool, length)
+                               for i := range valid {
+                                       valid[i] = i < 128
+                               }
+
+                               leftValues := makeBinaryEqualityValues(length, 
16)
+                               rightValues := append([]string(nil), 
leftValues...)
+                               leftValues[64], leftValues[65] = "ab", "c"
+                               rightValues[64], rightValues[65] = "a", "bc"
+                               return 
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, leftValues, valid),
+                                       
makeBinaryEqualityArray(memory.DefaultAllocator, dtype, rightValues, valid)
+                       },
+               },
+       }
+
+       for _, dtype := range types {
+               t.Run(dtype.Name(), func(t *testing.T) {
+                       for _, test := range tests {
+                               t.Run(test.name, func(t *testing.T) {
+                                       left, right := test.build(dtype)
+                                       defer left.Release()
+                                       defer right.Release()
+
+                                       assert.Equal(t, test.want, 
array.Equal(left, right))
+                               })
+                       }
+               })
+       }
+}
+
+func TestBinaryEqualityWithFragmentedValidity(t *testing.T) {
+       const length = 1024
+       leftValues := makeBinaryEqualityValues(length, 16)
+       rightValues := append([]string(nil), leftValues...)
+       valid := make([]bool, length)
+       for i := range valid {
+               valid[i] = i%2 == 0
+               if !valid[i] {
+                       rightValues[i] = "different ignored payload"
+               }
+       }
+
+       left := makeBinaryEqualityArray(memory.DefaultAllocator, 
arrow.BinaryTypes.String, leftValues, valid)
+       defer left.Release()
+       right := makeBinaryEqualityArray(memory.DefaultAllocator, 
arrow.BinaryTypes.String, rightValues, valid)
+       require.True(t, array.Equal(left, right))
+
+       right.Release()
+       rightValues[length-2] = "different valid payload"
+       right = makeBinaryEqualityArray(memory.DefaultAllocator, 
arrow.BinaryTypes.String, rightValues, valid)
+       defer right.Release()
+       require.False(t, array.Equal(left, right))
+}
+
+func TestBinaryEqualityWithDeclaredNullsWithoutBitmap(t *testing.T) {
+       makeArray := func() *array.Binary {
+               const length = 128
+               offsets := make([]int32, length+1)
+               values := make([]byte, length)
+               for i := range values {
+                       offsets[i] = int32(i)
+                       values[i] = byte('a' + i%26)
+               }
+               offsets[length] = int32(length)
+
+               offsetsBuffer := 
memory.NewBufferBytes(arrow.Int32Traits.CastToBytes(offsets))
+               valuesBuffer := memory.NewBufferBytes(values)
+               data := array.NewData(
+                       arrow.BinaryTypes.Binary,
+                       length,
+                       []*memory.Buffer{nil, offsetsBuffer, valuesBuffer},
+                       nil,
+                       1,
+                       0,
+               )
+               offsetsBuffer.Release()
+               valuesBuffer.Release()
+               result := array.NewBinaryData(data)
+               data.Release()
+               return result
+       }
+
+       left := makeArray()
+       defer left.Release()
+       right := makeArray()
+       defer right.Release()
+
+       var equal bool
+       assert.NotPanics(t, func() { equal = array.Equal(left, right) })
+       assert.True(t, equal)
+}
+
+func TestBinaryEqualityWithBitmapAndZeroDeclaredNulls(t *testing.T) {
+       const length = 128
+       valid := makeLongValidRuns(length)
+       leftValues := makeBinaryEqualityValues(length, 16)
+       rightValues := append([]string(nil), leftValues...)
+       for i, isValid := range valid {
+               if !isValid {
+                       rightValues[i] = "different ignored payload"
+               }
+       }
+
+       for _, dtype := range []arrow.BinaryDataType{
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.LargeString,
+       } {
+               t.Run(dtype.Name(), func(t *testing.T) {
+                       left := makeRawBinaryEqualityArrayWithNullCount(dtype, 
leftValues, valid, 0)
+                       defer left.Release()
+                       right := makeRawBinaryEqualityArrayWithNullCount(dtype, 
rightValues, valid, 0)
+                       defer right.Release()
+
+                       assert.True(t, array.Equal(left, right))
+               })
+       }
+}
+
+func TestBinaryEqualityEmptyArraysWithoutOffsets(t *testing.T) {
+       for _, dtype := range []arrow.BinaryDataType{
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.LargeString,
+       } {
+               t.Run(dtype.Name(), func(t *testing.T) {
+                       makeArray := func() arrow.Array {
+                               data := array.NewData(dtype, 0, 
[]*memory.Buffer{nil, nil, nil}, nil, 0, 0)
+                               result := array.MakeFromData(data)
+                               data.Release()
+                               return result
+                       }
+
+                       left := makeArray()
+                       defer left.Release()
+                       right := makeArray()
+                       defer right.Release()
+
+                       assert.NotPanics(t, func() {
+                               assert.True(t, array.Equal(left, right))
+                       })
+               })
+       }
+}
+
+func makeLongValidRuns(length int) []bool {
+       valid := make([]bool, length)
+       for i := range valid {
+               valid[i] = (i/8)%2 == 0
+       }
+       return valid
+}
+
+func makeSlicedBinaryEqualityPair(dtype arrow.BinaryDataType) (arrow.Array, 
arrow.Array) {
+       const (
+               length      = 256
+               leftOffset  = 5
+               rightOffset = 17
+       )
+
+       logicalValues := makeBinaryEqualityValues(length, 16)
+       logicalValid := makeLongValidRuns(length)
+       leftValues := makeBinaryEqualityValues(leftOffset+length+1, 16)
+       rightValues := makeBinaryEqualityValues(rightOffset+length+1, 16)
+       leftValid := make([]bool, len(leftValues))
+       rightValid := make([]bool, len(rightValues))
+       for i := range logicalValues {
+               leftValues[leftOffset+i] = logicalValues[i]
+               rightValues[rightOffset+i] = logicalValues[i]
+               leftValid[leftOffset+i] = logicalValid[i]
+               rightValid[rightOffset+i] = logicalValid[i]
+       }
+
+       leftBase := makeBinaryEqualityArray(memory.DefaultAllocator, dtype, 
leftValues, leftValid)
+       left := array.NewSlice(leftBase, leftOffset, leftOffset+length)
+       leftBase.Release()
+       rightBase := makeBinaryEqualityArray(memory.DefaultAllocator, dtype, 
rightValues, rightValid)
+       right := array.NewSlice(rightBase, rightOffset, rightOffset+length)
+       rightBase.Release()
+       return left, right
+}
+
+func makeRawBinaryEqualityArray(dtype arrow.BinaryDataType, values []string, 
valid []bool) arrow.Array {
+       nulls := 0
+       for _, isValid := range valid {
+               if !isValid {
+                       nulls++
+               }
+       }
+       return makeRawBinaryEqualityArrayWithNullCount(dtype, values, valid, 
nulls)
+}
+
+func makeRawBinaryEqualityArrayWithNullCount(
+       dtype arrow.BinaryDataType, values []string, valid []bool, nulls int,
+) arrow.Array {
+       if len(values) != len(valid) {
+               panic("len(values) != len(valid)")
+       }
+
+       valueBytes := make([]byte, 0)
+       var offsetBytes []byte
+       switch dtype.ID() {
+       case arrow.BINARY, arrow.STRING:
+               offsets := make([]int32, len(values)+1)
+               for i, value := range values {
+                       offsets[i] = int32(len(valueBytes))
+                       valueBytes = append(valueBytes, value...)
+               }
+               offsets[len(values)] = int32(len(valueBytes))
+               offsetBytes = arrow.Int32Traits.CastToBytes(offsets)
+       case arrow.LARGE_BINARY, arrow.LARGE_STRING:
+               offsets := make([]int64, len(values)+1)
+               for i, value := range values {
+                       offsets[i] = int64(len(valueBytes))
+                       valueBytes = append(valueBytes, value...)
+               }
+               offsets[len(values)] = int64(len(valueBytes))
+               offsetBytes = arrow.Int64Traits.CastToBytes(offsets)
+       default:
+               panic("unsupported binary type")
+       }
+
+       validity := make([]byte, (len(valid)+7)/8)
+       for i, isValid := range valid {
+               if isValid {
+                       bitutil.SetBit(validity, i)
+               }
+       }
+
+       validityBuffer := memory.NewBufferBytes(validity)
+       offsetsBuffer := memory.NewBufferBytes(offsetBytes)
+       valuesBuffer := memory.NewBufferBytes(valueBytes)
+       data := array.NewData(
+               dtype,
+               len(values),
+               []*memory.Buffer{validityBuffer, offsetsBuffer, valuesBuffer},
+               nil,
+               nulls,
+               0,
+       )
+       validityBuffer.Release()
+       offsetsBuffer.Release()
+       valuesBuffer.Release()
+       result := array.MakeFromData(data)
+       data.Release()
+       return result
+}
+
+func makeBinaryEqualityArray(
+       mem memory.Allocator, dtype arrow.BinaryDataType, values []string, 
valid []bool,
+) arrow.Array {
+       switch dtype.ID() {
+       case arrow.BINARY:
+               builder := array.NewBinaryBuilder(mem, dtype)
+               defer builder.Release()
+               builder.AppendStringValues(values, valid)
+               return builder.NewBinaryArray()
+       case arrow.STRING:
+               builder := array.NewStringBuilder(mem)
+               defer builder.Release()
+               builder.AppendValues(values, valid)
+               return builder.NewStringArray()
+       case arrow.LARGE_BINARY:
+               builder := array.NewBinaryBuilder(mem, dtype)
+               defer builder.Release()
+               builder.AppendStringValues(values, valid)
+               return builder.NewLargeBinaryArray()
+       case arrow.LARGE_STRING:
+               builder := array.NewLargeStringBuilder(mem)
+               defer builder.Release()
+               builder.AppendValues(values, valid)
+               return builder.NewLargeStringArray()
+       default:
+               panic("unsupported binary type")
+       }
+}
diff --git a/arrow/array/string.go b/arrow/array/string.go
index afd78278..e230fa17 100644
--- a/arrow/array/string.go
+++ b/arrow/array/string.go
@@ -239,15 +239,21 @@ func (a *String) ValidateFull() error {
 }
 
 func arrayEqualString(left, right *String) bool {
-       for i := 0; i < left.Len(); i++ {
-               if left.IsNull(i) {
-                       continue
-               }
-               if left.Value(i) != right.Value(i) {
-                       return false
+       if useScalarVariableWidthEquality(left) {
+               for i := range left.Len() {
+                       if !left.IsNull(i) && left.Value(i) != right.Value(i) {
+                               return false
+                       }
                }
+               return true
        }
-       return true
+       return arrayEqualVariableWidth(
+               left.offsets, right.offsets,
+               left.values, right.values,
+               left.Offset(), right.Offset(), left.Len(),
+               left.NullN(), left.NullBitmapBytes(),
+               equalStrings,
+       )
 }
 
 // String represents an immutable sequence of variable-length UTF-8 strings.
@@ -450,15 +456,25 @@ func (a *LargeString) ValidateFull() error {
 }
 
 func arrayEqualLargeString(left, right *LargeString) bool {
-       for i := 0; i < left.Len(); i++ {
-               if left.IsNull(i) {
-                       continue
-               }
-               if left.Value(i) != right.Value(i) {
-                       return false
+       if useScalarVariableWidthEquality(left) {
+               for i := range left.Len() {
+                       if !left.IsNull(i) && left.Value(i) != right.Value(i) {
+                               return false
+                       }
                }
+               return true
        }
-       return true
+       return arrayEqualVariableWidth(
+               left.offsets, right.offsets,
+               left.values, right.values,
+               left.Offset(), right.Offset(), left.Len(),
+               left.NullN(), left.NullBitmapBytes(),
+               equalStrings,
+       )
+}
+
+func equalStrings(left, right string) bool {
+       return left == right
 }
 
 type StringView struct {

Reply via email to