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
}