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 bea104d0 fix(arrow/compute): release sort scratch buffers (#1431)
bea104d0 is described below

commit bea104d01941d9672629bf700f6e7a099e7a6399
Author: Matt Topol <[email protected]>
AuthorDate: Wed Oct 28 19:41:10 2026 +0000

    fix(arrow/compute): release sort scratch buffers (#1431)
    
    ### Rationale for this change
    
    SortIndices allocates temporary index buffers for single-column chunked
    sorting, null/NaN partitioning, and multi-key sorting without releasing
    them. A checked allocator reports outstanding memory even after callers
    release the returned indices.
    
    ### What changes are included in this PR?
    
    - Defer Release immediately after each of the three previously
    unreleased scratch allocations, matching the existing integer radix-sort
    pattern.
    - Preserve ownership of the output buffer, which is transferred to the
    returned result.
    - Add checked-allocator regression coverage for an array with nulls, a
    float64 array with NaN, a multi-chunk array, a multi-key record batch,
    and a multi-key multi-chunk table. Each case also checks the resulting
    permutation.
    - Document temporary-buffer lifetime and result ownership on
    SortIndices.
    
    ### Are these changes tested?
    
    Yes. Before the fix, `go test ./arrow/compute -run
    '^TestSortIndicesReleasesScratchBuffers$' -count=1` failed on
    upstream/main with the new test added: all five subtests reported a
    64-byte leak from `exec.(*KernelCtx).Allocate`.
    
    After the fix, `go test ./arrow/compute/... -count=1` passed for all
    four packages, including the new regression test.
    
    ### Are there any user-facing changes?
    
    Sort operations no longer leak temporary allocator-backed scratch
    buffers. Sort order and API signatures are unchanged; callers still own
    and must release returned results.
---
 arrow/compute/internal/kernels/vector_sort.go |  3 ++
 arrow/compute/vector_sort.go                  | 20 ++++++++
 arrow/compute/vector_sort_test.go             | 68 +++++++++++++++++++++++++++
 3 files changed, 91 insertions(+)

diff --git a/arrow/compute/internal/kernels/vector_sort.go 
b/arrow/compute/internal/kernels/vector_sort.go
index fc09c5ba..025c3dd3 100644
--- a/arrow/compute/internal/kernels/vector_sort.go
+++ b/arrow/compute/internal/kernels/vector_sort.go
@@ -448,6 +448,7 @@ func SortIndices(ctx *exec.KernelCtx, columns 
[]*arrow.Chunked, keys []SortKey)
                chunks := columns[0].Chunks()
                if len(chunks) > 1 {
                        tmpBuf := ctx.Allocate(nRows * arrow.Uint64SizeBytes)
+                       defer tmpBuf.Release()
                        tmp := arrow.GetData[uint64](tmpBuf.Buf())[:nRows]
                        spanScratch := make([]chunkIndexSpan, len(chunks))
                        sortIndicesSingleColumnChunked(indices, chunks, 
comparators[0], keys[0], tmp, spanScratch)
@@ -463,6 +464,7 @@ func SortIndices(ctx *exec.KernelCtx, columns 
[]*arrow.Chunked, keys []SortKey)
                                slices.SortStableFunc(indices, func(a, b 
uint64) int { return c0.compareRowsForKey(a, b, k0) })
                        } else {
                                tmpBuf := ctx.Allocate(nRows * 
arrow.Uint64SizeBytes)
+                               defer tmpBuf.Release()
                                tmp := 
arrow.GetData[uint64](tmpBuf.Buf())[:nRows]
                                arraySortOneColumnRange(indices, tmp, c0, k0, 
0, nRows)
                        }
@@ -477,6 +479,7 @@ func SortIndices(ctx *exec.KernelCtx, columns 
[]*arrow.Chunked, keys []SortKey)
                multiChunkMerge := aligned && nSeg > 1
 
                tmpBuf := ctx.Allocate(nRows * arrow.Uint64SizeBytes)
+               defer tmpBuf.Release()
                tmp := arrow.GetData[uint64](tmpBuf.Buf())[:nRows]
 
                var spanScratch []chunkIndexSpan
diff --git a/arrow/compute/vector_sort.go b/arrow/compute/vector_sort.go
index 2baaa95e..bad14e4f 100644
--- a/arrow/compute/vector_sort.go
+++ b/arrow/compute/vector_sort.go
@@ -206,11 +206,15 @@ func sortIndicesImpl(ctx context.Context, opts 
FunctionOptions, input Datum) (Da
 // SortIndices computes the indices that would sort the input.
 // For arrays and chunked arrays, pass a single SortKey (ColumnIndex is 
ignored).
 // For record batches and tables, pass []SortKey to specify multi-column sort 
order.
+// Temporary sorting buffers use the context's allocator and are released 
before returning.
+// The caller must release the returned datum.
 func SortIndices(ctx context.Context, input Datum, keys SortOptions) (Datum, 
error) {
        return CallFunction(ctx, "sort_indices", keys, input)
 }
 
 // SortIndicesArray computes the indices that would sort the input array.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
array.
 func SortIndicesArray(ctx context.Context, input arrow.Array, key SortKey) 
(arrow.Array, error) {
        v := NewDatumWithoutOwning(input)
 
@@ -224,6 +228,8 @@ func SortIndicesArray(ctx context.Context, input 
arrow.Array, key SortKey) (arro
 }
 
 // SortIndicesChunked computes the indices that would sort the input chunked 
array.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
array.
 func SortIndicesChunked(ctx context.Context, input *arrow.Chunked, key 
SortKey) (arrow.Array, error) {
        v := NewDatumWithoutOwning(input)
 
@@ -237,6 +243,8 @@ func SortIndicesChunked(ctx context.Context, input 
*arrow.Chunked, key SortKey)
 }
 
 // SortIndicesRecordBatch computes the indices that would sort the record 
batch (stable, lexicographic by keys).
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
array.
 func SortIndicesRecordBatch(ctx context.Context, batch arrow.RecordBatch, keys 
[]SortKey) (arrow.Array, error) {
        if len(keys) == 0 {
                return nil, fmt.Errorf("%w: at least one sort key is required", 
arrow.ErrInvalid)
@@ -254,6 +262,8 @@ func SortIndicesRecordBatch(ctx context.Context, batch 
arrow.RecordBatch, keys [
 }
 
 // SortIndicesTable computes the indices that would sort the table (stable, 
lexicographic by keys).
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
array.
 func SortIndicesTable(ctx context.Context, tbl arrow.Table, keys []SortKey) 
(arrow.Array, error) {
        if len(keys) == 0 {
                return nil, fmt.Errorf("%w: at least one sort key is required", 
arrow.ErrInvalid)
@@ -272,11 +282,15 @@ func SortIndicesTable(ctx context.Context, tbl 
arrow.Table, keys []SortKey) (arr
 
 // Sort returns a sorted copy of the input datum by calling the registered 
"sort" function
 // ([SortIndices] then [Take]). Supported kinds are [KindArray], 
[KindChunked], [KindRecord], and [KindTable].
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
datum.
 func Sort(ctx context.Context, input Datum, keys SortOptions) (Datum, error) {
        return CallFunction(ctx, "sort", keys, input)
 }
 
 // SortArray returns a sorted copy of the input array.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
array.
 func SortArray(ctx context.Context, input arrow.Array, key SortKey) 
(arrow.Array, error) {
        indicesArr, err := SortIndicesArray(ctx, input, key)
        if err != nil {
@@ -288,6 +302,8 @@ func SortArray(ctx context.Context, input arrow.Array, key 
SortKey) (arrow.Array
 }
 
 // SortChunked returns a sorted copy of the input chunked array.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
chunked array.
 func SortChunked(ctx context.Context, input *arrow.Chunked, key SortKey) 
(*arrow.Chunked, error) {
        inputDatum := NewDatumWithoutOwning(input)
 
@@ -311,6 +327,8 @@ func SortChunked(ctx context.Context, input *arrow.Chunked, 
key SortKey) (*arrow
 // SortRecordBatch returns a sorted copy of the record batch using 
lexicographic ordering across the specified columns.
 // Each SortKey specifies a column index and its sort order and null placement.
 // When multiple keys are provided, ties in earlier columns are broken by 
later columns.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
record batch.
 //
 // Example:
 //
@@ -347,6 +365,8 @@ func SortRecordBatch(ctx context.Context, batch 
arrow.RecordBatch, keys []kernel
 // SortTable returns a sorted copy of the table using lexicographic ordering 
across the specified columns.
 // Each SortKey specifies a column index and its sort order and null placement.
 // When multiple keys are provided, ties in earlier columns are broken by 
later columns.
+// As with [SortIndices], temporary sorting buffers use the context's allocator
+// and are released before returning. The caller must release the returned 
table.
 //
 // Example:
 //
diff --git a/arrow/compute/vector_sort_test.go 
b/arrow/compute/vector_sort_test.go
index 39bf5e95..a4922baa 100644
--- a/arrow/compute/vector_sort_test.go
+++ b/arrow/compute/vector_sort_test.go
@@ -323,6 +323,74 @@ func TestSortIndices(t *testing.T) {
        }
 }
 
+func TestSortIndicesReleasesScratchBuffers(t *testing.T) {
+       for _, input := range []string{"ArrayNulls", "ArrayNaN", "Chunked", 
"RecordBatch", "Table"} {
+               t.Run(input, func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.NewGoAllocator())
+                       defer mem.AssertSize(t, 0)
+                       ctx := compute.WithAllocator(context.Background(), mem)
+
+                       bldr := array.NewFloat64Builder(mem)
+                       defer bldr.Release()
+                       valid := []bool{true, true, true, false}
+                       if input == "ArrayNaN" {
+                               valid = nil
+                       }
+                       bldr.AppendValues([]float64{2, 1, 1, math.NaN()}, valid)
+                       arr := bldr.NewArray()
+                       defer arr.Release()
+
+                       key := compute.DefaultSortKey()
+                       want := []uint64{1, 2, 0, 3}
+                       var (
+                               indices arrow.Array
+                               err     error
+                       )
+                       switch input {
+                       case "ArrayNulls", "ArrayNaN":
+                               indices, err = compute.SortIndicesArray(ctx, 
arr, key)
+                       case "Chunked":
+                               first := array.NewSlice(arr, 0, 2)
+                               defer first.Release()
+                               second := array.NewSlice(arr, 2, 4)
+                               defer second.Release()
+                               chunked := arrow.NewChunked(arr.DataType(), 
[]arrow.Array{first, second})
+                               defer chunked.Release()
+                               indices, err = compute.SortIndicesChunked(ctx, 
chunked, key)
+                       case "RecordBatch", "Table":
+                               secondary, _, buildErr := array.FromJSON(mem, 
arrow.PrimitiveTypes.Int32,
+                                       strings.NewReader("[0, 2, 1, 0]"))
+                               require.NoError(t, buildErr)
+                               defer secondary.Release()
+                               schema := arrow.NewSchema([]arrow.Field{
+                                       {Name: "primary", Type: arr.DataType(), 
Nullable: true},
+                                       {Name: "secondary", Type: 
secondary.DataType()},
+                               }, nil)
+                               batch := array.NewRecordBatch(schema, 
[]arrow.Array{arr, secondary}, 4)
+                               defer batch.Release()
+                               secondKey := compute.DefaultSortKey()
+                               secondKey.ColumnIndex = 1
+                               keys := []compute.SortKey{key, secondKey}
+                               want = []uint64{2, 1, 0, 3}
+                               if input == "RecordBatch" {
+                                       indices, err = 
compute.SortIndicesRecordBatch(ctx, batch, keys)
+                               } else {
+                                       first := batch.NewSlice(0, 2)
+                                       defer first.Release()
+                                       second := batch.NewSlice(2, 4)
+                                       defer second.Release()
+                                       tbl := 
array.NewTableFromRecords(schema, []arrow.RecordBatch{first, second})
+                                       defer tbl.Release()
+                                       indices, err = 
compute.SortIndicesTable(ctx, tbl, keys)
+                               }
+                       }
+                       require.NoError(t, err)
+                       defer indices.Release()
+                       assert.Equal(t, want, 
indices.(*array.Uint64).Uint64Values())
+               })
+       }
+}
+
 func TestSortArray(t *testing.T) {
        mem := memory.NewGoAllocator()
        ctx := context.Background()

Reply via email to