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 d3b223f1 perf(arrow/compute): take chunked binary values without 
concatenation (#1199)
d3b223f1 is described below

commit d3b223f1c613bcb7b28fd62c257dca4fd4b93959
Author: Minh Vu <[email protected]>
AuthorDate: Wed Aug 19 18:28:05 2026 +0200

    perf(arrow/compute): take chunked binary values without concatenation 
(#1199)
    
    ## What
    
    Chunked string and binary `take` currently concatenates all value chunks
    before selecting rows.
    
    This adds a chunk-aware path for:
    
    - STRING
    - BINARY
    - LARGE_STRING
    - LARGE_BINARY
    
    The new path resolves global indices to source chunks and copies only
    selected values. It keeps one output chunk per index chunk and preserves
    null and bounds behavior.
    
    ## Benchmark
    
    64 chunks x 4096 rows, 32-byte values, random indices, Apple M1 Pro:
    
    - 1% selected: 0.87 ms / 9.6 MB -> 0.08 ms / 0.11 MB
    - 10% selected: 2.36 ms / 10.4 MB -> 1.43 ms / 0.96 MB
    
    The benchmark covers 8 and 64 chunks, 1% / 10% / 50% / 100% selection,
    and all four types.
    
    ## Tests
    
    - `go test ./arrow/compute/... -count=1`
    
    ---------
    
    Co-authored-by: Matt Topol <[email protected]>
---
 .github/workflows/test.yml                         |   1 +
 arrow/compute/chunked_take_bench_test.go           | 193 ++++++++++++++
 arrow/compute/chunked_take_test.go                 | 247 ++++++++++++++++++
 arrow/compute/executor.go                          |   6 +-
 arrow/compute/executor_test.go                     |  27 ++
 .../compute/internal/kernels/chunked_take_test.go  |  42 +++
 arrow/compute/internal/kernels/vector_selection.go | 282 ++++++++++++++++++++-
 7 files changed, 793 insertions(+), 5 deletions(-)

diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 567b34bb..6b7e713b 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -406,6 +406,7 @@ jobs:
           archery docker run \
             -e ARCHERY_DEFAULT_BRANCH=${{ 
github.event.repository.default_branch }} \
             -e ARCHERY_INTEGRATION_TARGET_IMPLEMENTATIONS=go \
+            -e GTest_SOURCE=SYSTEM \
             -e ARCHERY_INTEGRATION_WITH_DOTNET=1 \
             -e ARCHERY_INTEGRATION_WITH_GO=1 \
             -e ARCHERY_INTEGRATION_WITH_JAVA=1 \
diff --git a/arrow/compute/chunked_take_bench_test.go 
b/arrow/compute/chunked_take_bench_test.go
new file mode 100644
index 00000000..4ec255e4
--- /dev/null
+++ b/arrow/compute/chunked_take_bench_test.go
@@ -0,0 +1,193 @@
+// 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.
+
+//go:build go1.18
+
+package compute_test
+
+import (
+       "context"
+       "fmt"
+       "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/memory"
+       "github.com/stretchr/testify/require"
+)
+
+func TestChunkedBinaryTake(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       ctx := compute.WithAllocator(context.Background(), mem)
+       for _, typ := range []arrow.DataType{
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.LargeString,
+               arrow.BinaryTypes.LargeBinary,
+       } {
+               t.Run(typ.String(), func(t *testing.T) {
+                       chunk0Full := newTestBinaryArray(mem, typ,
+                               [][]byte{[]byte("prefix"), []byte("hello"), 
[]byte("world"), []byte("suffix")},
+                               []bool{true, true, true, true})
+                       chunk0 := array.NewSlice(chunk0Full, 1, 3)
+                       chunk1 := newTestBinaryArray(mem, typ,
+                               [][]byte{[]byte("unused"), []byte("foo"), 
[]byte("bar"), []byte("baz")},
+                               []bool{false, true, true, true})
+                       empty := newTestBinaryArray(mem, typ, nil, nil)
+                       values := arrow.NewChunked(typ, []arrow.Array{empty, 
chunk0, empty, chunk1})
+                       defer values.Release()
+                       empty.Release()
+                       chunk0.Release()
+                       chunk0Full.Release()
+                       chunk1.Release()
+
+                       indices := newTestInt64Array(mem, []int64{4, 0, 2, 0, 
5, 1}, []bool{true, true, true, false, true, true})
+                       defer indices.Release()
+                       result, err := compute.Take(ctx, 
*compute.DefaultTakeOptions(),
+                               &compute.ChunkedDatum{Value: values}, 
&compute.ArrayDatum{Value: indices.Data()})
+                       require.NoError(t, err)
+                       actual := result.(*compute.ChunkedDatum).Value
+                       expectedArray := newTestBinaryArray(mem, typ,
+                               [][]byte{[]byte("bar"), []byte("hello"), nil, 
nil, []byte("baz"), []byte("world")},
+                               []bool{true, true, false, false, true, true})
+                       expected := arrow.NewChunked(typ, 
[]arrow.Array{expectedArray})
+                       require.True(t, array.ChunkedEqual(expected, actual))
+                       result.Release()
+
+                       invalid := newTestInt64Array(mem, []int64{6}, nil)
+                       _, err = compute.Take(ctx, 
*compute.DefaultTakeOptions(),
+                               &compute.ChunkedDatum{Value: values}, 
&compute.ArrayDatum{Value: invalid.Data()})
+                       require.ErrorIs(t, err, arrow.ErrIndex)
+                       invalid.Release()
+
+                       indices0 := newTestInt64Array(mem, []int64{4, 0, 2}, 
nil)
+                       indices1 := newTestInt64Array(mem, []int64{0, 5, 1}, 
[]bool{false, true, true})
+                       chunkedIndices := 
arrow.NewChunked(arrow.PrimitiveTypes.Int64, []arrow.Array{indices0, indices1})
+                       defer chunkedIndices.Release()
+                       indices0.Release()
+                       indices1.Release()
+
+                       result, err = compute.Take(ctx, 
*compute.DefaultTakeOptions(),
+                               &compute.ChunkedDatum{Value: values}, 
&compute.ChunkedDatum{Value: chunkedIndices})
+                       require.NoError(t, err)
+                       actual = result.(*compute.ChunkedDatum).Value
+                       require.True(t, array.ChunkedEqual(expected, actual))
+                       result.Release()
+                       expected.Release()
+                       expectedArray.Release()
+               })
+       }
+       mem.AssertSize(t, 0)
+}
+
+func newTestBinaryArray(mem memory.Allocator, typ arrow.DataType, values 
[][]byte, valid []bool) arrow.Array {
+       bldr := array.NewBinaryBuilder(mem, typ.(arrow.BinaryDataType))
+       bldr.Reserve(len(values))
+       for i, value := range values {
+               if len(valid) != 0 && !valid[i] {
+                       bldr.AppendNull()
+               } else {
+                       bldr.Append(value)
+               }
+       }
+       result := bldr.NewArray()
+       bldr.Release()
+       return result
+}
+
+func newTestInt64Array(mem memory.Allocator, values []int64, valid []bool) 
arrow.Array {
+       bldr := array.NewInt64Builder(mem)
+       bldr.Reserve(len(values))
+       for i, value := range values {
+               if len(valid) != 0 && !valid[i] {
+                       bldr.AppendNull()
+               } else {
+                       bldr.Append(value)
+               }
+       }
+       result := bldr.NewArray()
+       bldr.Release()
+       return result
+}
+
+func BenchmarkTakeChunkedBinary(b *testing.B) {
+       for _, typ := range []arrow.DataType{
+               arrow.BinaryTypes.String,
+               arrow.BinaryTypes.Binary,
+               arrow.BinaryTypes.LargeString,
+               arrow.BinaryTypes.LargeBinary,
+       } {
+               for _, numChunks := range []int{8, 64} {
+                       for _, selectivity := range []int{1, 10, 50, 100} {
+                               name := 
fmt.Sprintf("%s/chunks=%d/selectivity=%d%%", typ, numChunks, selectivity)
+                               b.Run(name, func(b *testing.B) {
+                                       mem := memory.DefaultAllocator
+                                       ctx := 
compute.WithAllocator(context.Background(), mem)
+
+                                       const rowsPerChunk = 4096
+                                       values, indices := 
makeChunkedBinaryTakeInputs(mem, typ, numChunks, rowsPerChunk, selectivity)
+                                       defer values.Release()
+                                       defer indices.Release()
+
+                                       b.ReportAllocs()
+                                       b.ResetTimer()
+                                       for i := 0; i < b.N; i++ {
+                                               result, err := 
compute.Take(ctx, *compute.DefaultTakeOptions(),
+                                                       
&compute.ChunkedDatum{Value: values},
+                                                       
&compute.ArrayDatum{Value: indices.Data()})
+                                               if err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               result.Release()
+                                       }
+                               })
+                       }
+               }
+       }
+}
+
+func makeChunkedBinaryTakeInputs(mem memory.Allocator, typ arrow.DataType, 
numChunks, rowsPerChunk, selectivity int) (*arrow.Chunked, arrow.Array) {
+       chunks := make([]arrow.Array, numChunks)
+       value := []byte("0123456789abcdefghijklmnopqrstuv")
+       for i := range chunks {
+               bldr := array.NewBinaryBuilder(mem, typ.(arrow.BinaryDataType))
+               bldr.Reserve(rowsPerChunk)
+               bldr.ReserveData(rowsPerChunk * len(value))
+               for j := 0; j < rowsPerChunk; j++ {
+                       bldr.Append(value)
+               }
+               chunks[i] = bldr.NewArray()
+               bldr.Release()
+       }
+
+       values := arrow.NewChunked(typ, chunks)
+       for _, chunk := range chunks {
+               chunk.Release()
+       }
+
+       totalRows := numChunks * rowsPerChunk
+       numIndices := totalRows * selectivity / 100
+       indicesBldr := array.NewInt64Builder(mem)
+       indicesBldr.Reserve(numIndices)
+       for i := 0; i < numIndices; i++ {
+               idx := (int64(i)*1103515245 + 12345) % int64(totalRows)
+               indicesBldr.Append(idx)
+       }
+       indices := indicesBldr.NewArray()
+       indicesBldr.Release()
+       return values, indices
+}
diff --git a/arrow/compute/chunked_take_test.go 
b/arrow/compute/chunked_take_test.go
new file mode 100644
index 00000000..e6e0784f
--- /dev/null
+++ b/arrow/compute/chunked_take_test.go
@@ -0,0 +1,247 @@
+// 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.
+
+//go:build go1.18
+
+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/memory"
+       "github.com/stretchr/testify/require"
+)
+
+var chunkedTakeIndexTypes = []arrow.DataType{
+       arrow.PrimitiveTypes.Int8,
+       arrow.PrimitiveTypes.Uint8,
+       arrow.PrimitiveTypes.Int16,
+       arrow.PrimitiveTypes.Uint16,
+       arrow.PrimitiveTypes.Int32,
+       arrow.PrimitiveTypes.Uint32,
+       arrow.PrimitiveTypes.Int64,
+       arrow.PrimitiveTypes.Uint64,
+}
+
+var chunkedTakeBinaryTypes = []arrow.DataType{
+       arrow.BinaryTypes.String,
+       arrow.BinaryTypes.Binary,
+       arrow.BinaryTypes.LargeString,
+       arrow.BinaryTypes.LargeBinary,
+}
+
+func TestChunkedBinaryTakeIndexTypes(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       for _, typ := range chunkedTakeBinaryTypes {
+               t.Run(typ.String(), func(t *testing.T) {
+                       values := makeChunkedTakeValues(mem, typ)
+                       defer values.Release()
+
+                       for _, indexType := range chunkedTakeIndexTypes {
+                               t.Run(indexType.String(), func(t *testing.T) {
+                                       indices0Full := 
makeChunkedTakeIndexArray(t, mem, indexType,
+                                               []string{"77", "0", "5", "99", 
"3", "77"},
+                                               []bool{true, true, true, false, 
true, true})
+                                       indices0 := 
array.NewSlice(indices0Full, 1, 5)
+                                       indices1Full := 
makeChunkedTakeIndexArray(t, mem, indexType,
+                                               []string{"77", "2", "1", "77"}, 
nil)
+                                       indices1 := 
array.NewSlice(indices1Full, 1, 3)
+
+                                       indices := arrow.NewChunked(indexType, 
[]arrow.Array{indices0, indices1})
+                                       indices0Full.Release()
+                                       indices0.Release()
+                                       indices1Full.Release()
+                                       indices1.Release()
+                                       defer indices.Release()
+
+                                       result, err := compute.Take(ctx, 
*compute.DefaultTakeOptions(),
+                                               &compute.ChunkedDatum{Value: 
values},
+                                               &compute.ChunkedDatum{Value: 
indices})
+                                       require.NoError(t, err)
+                                       defer result.Release()
+
+                                       actual := 
result.(*compute.ChunkedDatum).Value
+                                       require.Equal(t, 6, actual.Len())
+                                       require.Len(t, actual.Chunks(), 2)
+                                       require.Equal(t, 4, 
actual.Chunk(0).Len())
+                                       require.Equal(t, 2, 
actual.Chunk(1).Len())
+
+                                       expected0 := newTestBinaryArray(mem, 
typ,
+                                               [][]byte{[]byte("zero"), 
[]byte("five"), nil, []byte("three")},
+                                               []bool{true, true, false, true})
+                                       expected1 := newTestBinaryArray(mem, 
typ,
+                                               [][]byte{[]byte("two"), 
[]byte("one")}, nil)
+                                       expected := arrow.NewChunked(typ, 
[]arrow.Array{expected0, expected1})
+                                       expected0.Release()
+                                       expected1.Release()
+                                       defer expected.Release()
+                                       require.True(t, 
array.ChunkedEqual(expected, actual))
+                               })
+                       }
+               })
+       }
+
+       mem.AssertSize(t, 0)
+}
+
+func TestChunkedBinaryTakeBoundsChecks(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       for _, typ := range chunkedTakeBinaryTypes {
+               t.Run(typ.String(), func(t *testing.T) {
+                       values := makeChunkedTakeValues(mem, typ)
+                       defer values.Release()
+
+                       for _, indexType := range chunkedTakeIndexTypes {
+                               cases := []struct {
+                                       name  string
+                                       value string
+                               }{
+                                       {name: "upper_bound", value: "6"},
+                               }
+                               if arrow.IsSignedInteger(indexType.ID()) {
+                                       cases = append(cases, struct {
+                                               name  string
+                                               value string
+                                       }{name: "negative", value: "-1"})
+                               }
+
+                               for _, tc := range cases {
+                                       t.Run(indexType.String()+"/"+tc.name, 
func(t *testing.T) {
+                                               indices := 
makeChunkedTakeIndexArray(t, mem, indexType, []string{tc.value}, nil)
+                                               defer indices.Release()
+
+                                               _, err := compute.Take(ctx, 
*compute.DefaultTakeOptions(),
+                                                       
&compute.ChunkedDatum{Value: values},
+                                                       
&compute.ArrayDatum{Value: indices.Data()})
+                                               require.ErrorIs(t, err, 
arrow.ErrIndex)
+                                       })
+                               }
+                       }
+               })
+       }
+
+       mem.AssertSize(t, 0)
+}
+
+func TestChunkedBinaryTakeEmptyIndexChunks(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       ctx := compute.WithAllocator(context.Background(), mem)
+
+       for _, typ := range chunkedTakeBinaryTypes {
+               t.Run(typ.String(), func(t *testing.T) {
+                       values := makeChunkedTakeValues(mem, typ)
+                       defer values.Release()
+
+                       for _, indexType := range chunkedTakeIndexTypes {
+                               t.Run(indexType.String(), func(t *testing.T) {
+                                       t.Run("zero_chunks", func(t *testing.T) 
{
+                                               indices := 
arrow.NewChunked(indexType, nil)
+                                               defer indices.Release()
+
+                                               result, err := 
compute.Take(ctx, *compute.DefaultTakeOptions(),
+                                                       
&compute.ChunkedDatum{Value: values},
+                                                       
&compute.ChunkedDatum{Value: indices})
+                                               require.NoError(t, err)
+                                               defer result.Release()
+
+                                               actual := 
result.(*compute.ChunkedDatum).Value
+                                               require.Equal(t, 0, 
actual.Len())
+                                               require.Empty(t, 
actual.Chunks())
+                                       })
+
+                                       for _, tc := range []struct {
+                                               name          string
+                                               chunkedValues [][]string
+                                               wantLen       int
+                                               wantChunks    int
+                                       }{
+                                               {name: "empty_chunk", 
chunkedValues: [][]string{nil}, wantLen: 0, wantChunks: 0},
+                                               {name: "leading_empty", 
chunkedValues: [][]string{nil, {"0", "5"}}, wantLen: 2, wantChunks: 1},
+                                               {name: "trailing_empty", 
chunkedValues: [][]string{{"0", "5"}, nil}, wantLen: 2, wantChunks: 1},
+                                       } {
+                                               t.Run(tc.name, func(t 
*testing.T) {
+                                                       chunks := 
make([]arrow.Array, len(tc.chunkedValues))
+                                                       for i, chunkValues := 
range tc.chunkedValues {
+                                                               chunks[i] = 
makeChunkedTakeIndexArray(t, mem, indexType, chunkValues, nil)
+                                                       }
+                                                       indices := 
arrow.NewChunked(indexType, chunks)
+                                                       for _, chunk := range 
chunks {
+                                                               chunk.Release()
+                                                       }
+                                                       defer indices.Release()
+
+                                                       result, err := 
compute.Take(ctx, *compute.DefaultTakeOptions(),
+                                                               
&compute.ChunkedDatum{Value: values},
+                                                               
&compute.ChunkedDatum{Value: indices})
+                                                       require.NoError(t, err)
+                                                       defer result.Release()
+
+                                                       actual := 
result.(*compute.ChunkedDatum).Value
+                                                       require.Equal(t, 
tc.wantLen, actual.Len())
+                                                       require.Len(t, 
actual.Chunks(), tc.wantChunks)
+                                                       if tc.wantChunks == 1 {
+                                                               
require.Equal(t, 2, actual.Chunk(0).Len())
+                                                       }
+                                               })
+                                       }
+                               })
+                       }
+               })
+       }
+
+       mem.AssertSize(t, 0)
+}
+
+func makeChunkedTakeValues(mem memory.Allocator, typ arrow.DataType) 
*arrow.Chunked {
+       full := newTestBinaryArray(mem, typ,
+               [][]byte{[]byte("prefix"), []byte("zero"), []byte("one"), 
[]byte("two"), []byte("three"), []byte("suffix")}, nil)
+       middle := array.NewSlice(full, 1, 5)
+       tail := newTestBinaryArray(mem, typ, [][]byte{[]byte("four"), 
[]byte("five")}, nil)
+       empty0 := newTestBinaryArray(mem, typ, nil, nil)
+       empty1 := newTestBinaryArray(mem, typ, nil, nil)
+
+       values := arrow.NewChunked(typ, []arrow.Array{empty0, middle, tail, 
empty1})
+       full.Release()
+       middle.Release()
+       tail.Release()
+       empty0.Release()
+       empty1.Release()
+       return values
+}
+
+func makeChunkedTakeIndexArray(t testing.TB, mem memory.Allocator, typ 
arrow.DataType, values []string, valid []bool) arrow.Array {
+       t.Helper()
+       bldr := array.NewBuilder(mem, typ)
+       defer bldr.Release()
+       bldr.Reserve(len(values))
+       for i, value := range values {
+               if len(valid) != 0 && !valid[i] {
+                       bldr.AppendNull()
+                       continue
+               }
+               require.NoError(t, bldr.AppendValueFromString(value))
+       }
+       result := bldr.NewArray()
+       return result
+}
diff --git a/arrow/compute/executor.go b/arrow/compute/executor.go
index 1728e49e..3468d640 100644
--- a/arrow/compute/executor.go
+++ b/arrow/compute/executor.go
@@ -1137,9 +1137,11 @@ func (v *vectorExecutor) execChunked(batch *ExecBatch, 
out chan<- Datum) error {
        }
 
        if len(result) == 0 {
-               empty := output.MakeArray()
+               outType := output.Type
+               empty := array.MakeArrayOfNull(exec.GetAllocator(v.ctx.Ctx), 
outType, 0)
                defer empty.Release()
-               out <- &ChunkedDatum{Value: arrow.NewChunked(output.Type, 
[]arrow.Array{empty})}
+               output.Release()
+               out <- &ChunkedDatum{Value: arrow.NewChunked(outType, 
[]arrow.Array{empty})}
                return nil
        }
 
diff --git a/arrow/compute/executor_test.go b/arrow/compute/executor_test.go
index a8194108..dbc2312b 100644
--- a/arrow/compute/executor_test.go
+++ b/arrow/compute/executor_test.go
@@ -39,6 +39,33 @@ func (d *signalChunkedDatum) Chunks() []arrow.Array {
        return d.Value.Chunks()
 }
 
+func TestVectorExecutorWrapResultsReleasesEmptyArrayOutput(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       builder := array.NewBinaryBuilder(mem, arrow.BinaryTypes.String)
+       empty := builder.NewArray()
+       builder.Release()
+
+       output := make(chan Datum, 1)
+       output <- NewDatum(empty)
+       close(output)
+
+       executor := &vectorExecutor{
+               nonAggExecImpl: nonAggExecImpl{
+                       kernel:  &exec.VectorKernel{OutputChunked: true},
+                       outType: arrow.BinaryTypes.String,
+               },
+       }
+
+       result := executor.WrapResults(context.Background(), output, true)
+       require.NotNil(t, result)
+       require.Equal(t, KindChunked, result.Kind())
+       require.Empty(t, result.(*ChunkedDatum).Value.Chunks())
+       result.Release()
+       empty.Release()
+}
+
 func TestVectorExecutorWrapResultsReleasesEmptyChunkedOutput(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)
diff --git a/arrow/compute/internal/kernels/chunked_take_test.go 
b/arrow/compute/internal/kernels/chunked_take_test.go
new file mode 100644
index 00000000..64b5ee30
--- /dev/null
+++ b/arrow/compute/internal/kernels/chunked_take_test.go
@@ -0,0 +1,42 @@
+// 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.
+
+//go:build go1.18
+
+package kernels
+
+import (
+       "math"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/stretchr/testify/require"
+)
+
+func TestTakeChunkedBinaryOffsetLimits(t *testing.T) {
+       maxOffset, offsetBytes := binaryTakeOffsetLimits[int32]()
+       require.Equal(t, int64(math.MaxInt32), maxOffset)
+       require.Equal(t, 4, offsetBytes)
+       require.NoError(t, checkBinaryTakeOffset[int32](0, math.MaxInt32))
+       require.ErrorIs(t,
+               checkBinaryTakeOffset[int32](0, int64(math.MaxInt32)+1),
+               arrow.ErrInvalid)
+
+       maxOffset, offsetBytes = binaryTakeOffsetLimits[int64]()
+       require.Equal(t, int64(math.MaxInt64), maxOffset)
+       require.Equal(t, 8, offsetBytes)
+       require.NoError(t, checkBinaryTakeOffset[int64](0, 
int64(math.MaxInt32)+1))
+}
diff --git a/arrow/compute/internal/kernels/vector_selection.go 
b/arrow/compute/internal/kernels/vector_selection.go
index f08b53f7..00bc141a 100644
--- a/arrow/compute/internal/kernels/vector_selection.go
+++ b/arrow/compute/internal/kernels/vector_selection.go
@@ -662,6 +662,72 @@ func (c *chunkedPrimitiveGetter[T]) GetValue(i int64) T {
 func (c *chunkedPrimitiveGetter[T]) NullCount() int64 { return c.nulls }
 func (c *chunkedPrimitiveGetter[T]) Len() int64       { return c.len }
 
+type binaryGetter interface {
+       IsValid(int64) bool
+       GetValue(int64) []byte
+       NullCount() int64
+       Len() int64
+       DataLen() int64
+}
+
+type chunkedBinaryGetter[OffsetT int32 | int64] struct {
+       resolver      *exec.ChunkResolver
+       offsets       [][]OffsetT
+       values        [][]byte
+       valuesIsValid [][]byte
+       valuesOffset  []int64
+       nulls         int64
+       length        int64
+       dataLen       int64
+}
+
+func newChunkedBinaryGetter[OffsetT int32 | int64](arr *arrow.Chunked) 
*chunkedBinaryGetter[OffsetT] {
+       chunks := make([]arrow.Array, 0, len(arr.Chunks()))
+       for _, chunk := range arr.Chunks() {
+               if chunk.Len() > 0 {
+                       chunks = append(chunks, chunk)
+               }
+       }
+       getter := &chunkedBinaryGetter[OffsetT]{
+               resolver:      exec.NewChunkResolver(chunks),
+               offsets:       make([][]OffsetT, len(chunks)),
+               values:        make([][]byte, len(chunks)),
+               valuesIsValid: make([][]byte, len(chunks)),
+               valuesOffset:  make([]int64, len(chunks)),
+               nulls:         int64(arr.NullN()),
+               length:        int64(arr.Len()),
+       }
+
+       var span exec.ArraySpan
+       for i, chunk := range chunks {
+               span.SetMembers(chunk.Data())
+               getter.values[i] = span.Buffers[2].Buf
+               getter.valuesIsValid[i] = span.Buffers[0].Buf
+               getter.valuesOffset[i] = span.Offset
+               if span.Len > 0 {
+                       getter.offsets[i] = exec.GetSpanOffsets[OffsetT](&span, 
1)
+                       getter.dataLen += int64(getter.offsets[i][span.Len] - 
getter.offsets[i][0])
+               }
+       }
+       return getter
+}
+
+func (c *chunkedBinaryGetter[OffsetT]) IsValid(i int64) bool {
+       chunk, index := c.resolver.Resolve(i)
+       bitmap := c.valuesIsValid[chunk]
+       return bitmap == nil || bitutil.BitIsSet(bitmap, 
int(c.valuesOffset[chunk]+index))
+}
+
+func (c *chunkedBinaryGetter[OffsetT]) GetValue(i int64) []byte {
+       chunk, index := c.resolver.Resolve(i)
+       offsets := c.offsets[chunk]
+       return c.values[chunk][offsets[index]:offsets[index+1]]
+}
+
+func (c *chunkedBinaryGetter[OffsetT]) NullCount() int64 { return c.nulls }
+func (c *chunkedBinaryGetter[OffsetT]) Len() int64       { return c.length }
+func (c *chunkedBinaryGetter[OffsetT]) DataLen() int64   { return c.dataLen }
+
 // isSorted checks if indices are monotonically increasing (sorted)
 // Returns true if sorted, false otherwise
 // Uses sampling for large arrays to avoid full scan
@@ -1161,6 +1227,216 @@ func ChunkedPrimitiveTake(ctx *exec.KernelCtx, batch 
[]*arrow.Chunked, out *exec
        }
 }
 
+func binaryTakeOffsetLimits[OffsetT int32 | int64]() (int64, int) {
+       var zero OffsetT
+       switch any(zero).(type) {
+       case int32:
+               return math.MaxInt32, 4
+       case int64:
+               return math.MaxInt64, 8
+       default:
+               panic("unsupported binary offset type")
+       }
+}
+
+func checkBinaryTakeOffset[OffsetT int32 | int64](offset OffsetT, valueLen 
int64) error {
+       maxOffset, _ := binaryTakeOffsetLimits[OffsetT]()
+       if offset < 0 || valueLen < 0 || valueLen > maxOffset-int64(offset) {
+               return fmt.Errorf("%w: binary output offset overflow", 
arrow.ErrInvalid)
+       }
+       return nil
+}
+
+func takeChunkedBinaryImpl[IdxT arrow.UintType, OffsetT int32 | int64](ctx 
*exec.KernelCtx, indices *exec.ArraySpan, values binaryGetter, out 
*exec.ExecResult) error {
+       var (
+               indicesValues   = exec.GetSpanValues[IdxT](indices, 1)
+               indicesIsValid  = bitutil.OptionalBitIndexer{Bitmap: 
indices.Buffers[0].Buf, Offset: int(indices.Offset)}
+               bitCounter      = 
bitutils.NewOptionalBitBlockCounter(indices.Buffers[0].Buf, indices.Offset, 
indices.Len)
+               validityBuilder = validityBuilder{mem: 
exec.GetAllocator(ctx.Ctx)}
+               offsetBuilder   = 
newBufferBuilder[OffsetT](exec.GetAllocator(ctx.Ctx))
+               dataBuilder     = 
newBufferBuilder[uint8](exec.GetAllocator(ctx.Ctx))
+               valuesHaveNulls = values.NullCount() != 0
+               pos             int64
+               offset          OffsetT
+       )
+
+       maxInt := int(^uint(0) >> 1)
+       if indices.Len >= int64(maxInt) || indices.Len > math.MaxInt64-7 {
+               return fmt.Errorf("%w: binary take input length exceeds 
capacity", arrow.ErrInvalid)
+       }
+       offsetElements := int(indices.Len) + 1
+       _, offsetSize := binaryTakeOffsetLimits[OffsetT]()
+       if offsetElements > maxInt/offsetSize {
+               return fmt.Errorf("%w: binary take offset buffer exceeds 
capacity", arrow.ErrInvalid)
+       }
+
+       defer func() {
+               if validityBuilder.buffer != nil {
+                       validityBuilder.buffer.Release()
+               }
+               if offsetBuilder.buffer != nil {
+                       offsetBuilder.buffer.Release()
+               }
+               if dataBuilder.buffer != nil {
+                       dataBuilder.buffer.Release()
+               }
+       }()
+
+       validityBuilder.Reserve(indices.Len)
+       offsetBuilder.reserve(offsetElements)
+       if values.Len() > 0 && values.DataLen() > 0 && indices.Len > 0 {
+               const maxPrealloc = int64(16777216)
+               meanValueLen := values.DataLen() / values.Len()
+               estimatedTotalSize := int64(0)
+               if meanValueLen > 0 {
+                       if meanValueLen >= maxPrealloc || indices.Len > 
maxPrealloc/meanValueLen {
+                               estimatedTotalSize = maxPrealloc
+                       } else {
+                               estimatedTotalSize = meanValueLen * indices.Len
+                       }
+               }
+               dataBuilder.reserve(int(estimatedTotalSize))
+       }
+
+       spaceAvail := dataBuilder.cap()
+       appendValue := func(idx int64) error {
+               value := values.GetValue(idx)
+               valueLen := int64(len(value))
+               if err := checkBinaryTakeOffset(offset, valueLen); err != nil {
+                       return err
+               }
+
+               dataLen := dataBuilder.len()
+               if len(value) > maxInt-dataLen {
+                       return fmt.Errorf("%w: binary output size exceeds 
capacity", arrow.ErrInvalid)
+               }
+
+               offsetBuilder.unsafeAppend(offset)
+               if len(value) > spaceAvail {
+                       needed := dataLen + len(value)
+                       newCap := dataBuilder.cap()
+                       if newCap < dataLen {
+                               newCap = dataLen
+                       }
+                       if newCap == 0 {
+                               newCap = len(value)
+                       }
+                       for newCap < needed {
+                               if newCap > maxInt/2 {
+                                       newCap = needed
+                                       break
+                               }
+                               newCap *= 2
+                       }
+                       dataBuilder.reserve(newCap - dataLen)
+                       spaceAvail = dataBuilder.cap() - dataBuilder.len()
+               }
+               dataBuilder.unsafeAppendSlice(value)
+               spaceAvail -= len(value)
+               offset += OffsetT(valueLen)
+               return nil
+       }
+       appendNull := func() {
+               offsetBuilder.unsafeAppend(offset)
+       }
+
+       for pos < indices.Len {
+               block := bitCounter.NextBlock()
+               indicesHaveNulls := block.Popcnt < block.Len
+               switch {
+               case !indicesHaveNulls && !valuesHaveNulls:
+                       validityBuilder.UnsafeAppendN(int64(block.Len), true)
+                       for i := 0; i < int(block.Len); i++ {
+                               if err := 
appendValue(int64(indicesValues[pos])); err != nil {
+                                       return err
+                               }
+                               pos++
+                       }
+               case block.Popcnt > 0:
+                       for i := 0; i < int(block.Len); i++ {
+                               idxValid := !indicesHaveNulls || 
indicesIsValid.GetBit(int(pos))
+                               if idxValid && (!valuesHaveNulls || 
values.IsValid(int64(indicesValues[pos]))) {
+                                       validityBuilder.UnsafeAppend(true)
+                                       if err := 
appendValue(int64(indicesValues[pos])); err != nil {
+                                               return err
+                                       }
+                               } else {
+                                       validityBuilder.UnsafeAppend(false)
+                                       appendNull()
+                               }
+                               pos++
+                       }
+               default:
+                       validityBuilder.UnsafeAppendN(int64(block.Len), false)
+                       for i := 0; i < int(block.Len); i++ {
+                               appendNull()
+                       }
+                       pos += int64(block.Len)
+               }
+       }
+
+       offsetBuilder.unsafeAppend(offset)
+       out.Len = indices.Len
+       out.Nulls = int64(validityBuilder.falseCount)
+       out.Buffers[0].WrapBuffer(validityBuilder.Finish())
+       out.Buffers[1].WrapBuffer(offsetBuilder.finish())
+       out.Buffers[2].WrapBuffer(dataBuilder.finish())
+       return nil
+}
+
+func takeChunkedBinaryDispatch[IdxT arrow.UintType, OffsetT int32 | int64](ctx 
*exec.KernelCtx, values binaryGetter, indices *arrow.Chunked, out 
[]*exec.ExecResult) error {
+       var span exec.ArraySpan
+       for i, chunk := range indices.Chunks() {
+               span.SetMembers(chunk.Data())
+               if err := takeChunkedBinaryImpl[IdxT, OffsetT](ctx, &span, 
values, out[i]); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func ChunkedVarBinaryTake[OffsetT int32 | int64](ctx *exec.KernelCtx, batch 
[]*arrow.Chunked, out *exec.ExecResult) ([]*exec.ExecResult, error) {
+       values, indices := batch[0], batch[1]
+       if ctx.State.(TakeState).BoundsCheck {
+               if err := checkIndexBoundsChunked(indices, 
uint64(values.Len())); err != nil {
+                       return nil, err
+               }
+       }
+
+       outData := make([]*exec.ExecResult, len(indices.Chunks()))
+       for i := range outData {
+               outData[i] = &exec.ExecResult{Type: out.Type}
+       }
+
+       cleanup := func() {
+               for _, result := range outData {
+                       if result != nil {
+                               result.Release()
+                       }
+               }
+       }
+
+       valuesGetter := newChunkedBinaryGetter[OffsetT](values)
+       var err error
+       switch indices.DataType().(arrow.FixedWidthDataType).Bytes() {
+       case 1:
+               err = takeChunkedBinaryDispatch[uint8, OffsetT](ctx, 
valuesGetter, indices, outData)
+       case 2:
+               err = takeChunkedBinaryDispatch[uint16, OffsetT](ctx, 
valuesGetter, indices, outData)
+       case 4:
+               err = takeChunkedBinaryDispatch[uint32, OffsetT](ctx, 
valuesGetter, indices, outData)
+       case 8:
+               err = takeChunkedBinaryDispatch[uint64, OffsetT](ctx, 
valuesGetter, indices, outData)
+       default:
+               err = fmt.Errorf("%w: invalid byte width for indices", 
arrow.ErrIndex)
+       }
+       if err != nil {
+               cleanup()
+               return nil, err
+       }
+       return outData, nil
+}
+
 func NullTake(ctx *exec.KernelCtx, batch *exec.ExecSpan, out *exec.ExecResult) 
error {
        if ctx.State.(TakeState).BoundsCheck {
                if err := checkIndexBounds(&batch.Values[1].Array, 
uint64(batch.Values[0].Array.Len)); err != nil {
@@ -2058,7 +2334,7 @@ type SelectionKernelData struct {
 }
 
 func ChunkedTakeSupported(dt arrow.DataType) bool {
-       return arrow.IsPrimitive(dt.ID())
+       return arrow.IsPrimitive(dt.ID()) || arrow.IsBaseBinary(dt.ID())
 }
 
 func GetVectorSelectionKernels() (filterkernels, takeKernels 
[]SelectionKernelData) {
@@ -2078,8 +2354,8 @@ func GetVectorSelectionKernels() (filterkernels, 
takeKernels []SelectionKernelDa
                {In: exec.NewIDInput(arrow.DECIMAL128), Exec: 
TakeExec(FSBImpl)},
                {In: exec.NewIDInput(arrow.DECIMAL256), Exec: 
TakeExec(FSBImpl)},
                {In: exec.NewIDInput(arrow.FIXED_SIZE_BINARY), Exec: 
TakeExec(FSBImpl)},
-               {In: exec.NewMatchedInput(exec.BinaryLike()), Exec: 
TakeExec(VarBinaryImpl[int32])},
-               {In: exec.NewMatchedInput(exec.LargeBinaryLike()), Exec: 
TakeExec(VarBinaryImpl[int64])},
+               {In: exec.NewMatchedInput(exec.BinaryLike()), Exec: 
TakeExec(VarBinaryImpl[int32]), Chunked: ChunkedVarBinaryTake[int32]},
+               {In: exec.NewMatchedInput(exec.LargeBinaryLike()), Exec: 
TakeExec(VarBinaryImpl[int64]), Chunked: ChunkedVarBinaryTake[int64]},
        }
        return
 }

Reply via email to