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 7090f9ed fix(compute/exec): do not carry a cached null count across 
ArraySpan.SetSlice (#1306)
7090f9ed is described below

commit 7090f9eddcd53a4909c401c7e33b6b567fa3f322
Author: singhpratech <[email protected]>
AuthorDate: Tue Oct 13 08:21:42 2026 -0400

    fix(compute/exec): do not carry a cached null count across 
ArraySpan.SetSlice (#1306)
    
    ### Rationale for this change
    
    `and_kleene` and `or_kleene` return `false` where the answer is null
    when `ExecCtx.ChunkSize` is
    below the input length (#1305). `ArraySpan.SetSlice` kept the cached
    null count for the new
    slice when it was `0` or equal to the old length. That is only right
    while the count describes the
    whole span. The executor reuses one span per argument across slices, and
    a kernel that calls
    `UpdateNullCount()` on it stores the slice's count, so the next
    `SetSlice` treats the following
    slice as all valid or all null.
    
    ### What changes are included in this PR?
    
    `SetSlice` resets the count to `UnknownNullCount` whenever a validity
    bitmap is present, as the C++
    `ArraySpan::SetSlice` does (`cpp/src/arrow/array/data.h`), and keeps it
    unknown when it was already
    non-zero without a bitmap. The null type still gets the slice length; a
    span with no bitmap and a
    zero count keeps zero. Two cases added to `TestArraySpan_SetSlice`, and
    a new test that runs both
    Kleene kernels at chunk sizes 1, 2, 3, 4 and 8 against the default
    result.
    
    Cost: a slice of an all-valid array with a bitmap now reports an unknown
    count, so a kernel that
    asks pays one popcount over the slice instead of reading the cache. That
    is the C++ behaviour.
    
    ### Are these changes tested?
    
    Yes. The new chunk-size test fails on main (four assertions, chunk sizes
    1 and 2 for both kernels)
    and passes with the change; `go test ./arrow/compute/...` passes.
    
    ### Are there any user-facing changes?
    
    Results of `and_kleene`/`or_kleene` no longer depend on
    `ExecCtx.ChunkSize`. `ArraySpan.Nulls` after
    `SetSlice` is `UnknownNullCount` in cases where it was previously `0` or
    the slice length; code that
    read it directly should call `UpdateNullCount()`, which is what the
    package's own kernels do.
    
    Closes #1305.
---
 arrow/compute/exec/span.go                     | 12 ++--
 arrow/compute/exec/span_test.go                |  2 +
 arrow/compute/scalar_boolean_chunksize_test.go | 76 ++++++++++++++++++++++++++
 3 files changed, 84 insertions(+), 6 deletions(-)

diff --git a/arrow/compute/exec/span.go b/arrow/compute/exec/span.go
index 49350eeb..d071c0c6 100644
--- a/arrow/compute/exec/span.go
+++ b/arrow/compute/exec/span.go
@@ -214,13 +214,13 @@ func (a *ArraySpan) SetSlice(off, length int64) {
                return
        }
 
+       // The cached null count describes the span as it was, not the slice.
+       // As in the C++ ArraySpan::SetSlice, a slice of anything with a 
validity
+       // bitmap has an unknown count until someone asks for it; only the null
+       // type and a span without a bitmap can be counted without looking.
        if a.Type.ID() != arrow.NULL {
-               if a.Nulls != 0 {
-                       if a.Nulls == a.Len {
-                               a.Nulls = length
-                       } else {
-                               a.Nulls = array.UnknownNullCount
-                       }
+               if a.Nulls != 0 || len(a.Buffers[0].Buf) != 0 {
+                       a.Nulls = array.UnknownNullCount
                }
        } else {
                a.Nulls = length
diff --git a/arrow/compute/exec/span_test.go b/arrow/compute/exec/span_test.go
index 90d4c93e..3a19b21c 100644
--- a/arrow/compute/exec/span_test.go
+++ b/arrow/compute/exec/span_test.go
@@ -523,6 +523,8 @@ func TestArraySpan_SetSlice(t *testing.T) {
                {"null type", fields{Type: arrow.Null}, args{5, 10}, 10},
                {"not-null type", fields{Type: arrow.PrimitiveTypes.Int8}, 
args{5, 10}, 0},
                {"not-null type with nulls", fields{Type: 
arrow.PrimitiveTypes.Int8, Nulls: -1}, args{5, 10}, array.UnknownNullCount},
+               {"no nulls but a validity bitmap", fields{Type: 
arrow.PrimitiveTypes.Int8, Len: 16, Buffers: [3]exec.BufferSpan{{Buf: 
[]byte{0xff, 0xff}}}}, args{5, 10}, array.UnknownNullCount},
+               {"all null before the slice", fields{Type: 
arrow.PrimitiveTypes.Int8, Len: 16, Nulls: 16, Buffers: 
[3]exec.BufferSpan{{Buf: []byte{0, 0}}}}, args{5, 10}, array.UnknownNullCount},
        }
        for _, tt := range tests {
                t.Run(tt.name, func(t *testing.T) {
diff --git a/arrow/compute/scalar_boolean_chunksize_test.go 
b/arrow/compute/scalar_boolean_chunksize_test.go
new file mode 100644
index 00000000..61c63a1c
--- /dev/null
+++ b/arrow/compute/scalar_boolean_chunksize_test.go
@@ -0,0 +1,76 @@
+// 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 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"
+)
+
+// and_kleene and or_kleene over two boolean arrays with nulls, executed with
+// different ExecCtx.ChunkSize values; the result must not depend on ChunkSize.
+func TestKleeneChunkSizeIndependent(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       // left:  [true, null, true, null, false, true, null, true]
+       // right: [null, true, true, false, null, null, true, true]
+       lb := array.NewBooleanBuilder(mem)
+       lb.AppendValues([]bool{true, false, true, false, false, true, false, 
true}, []bool{true, false, true, false, true, true, false, true})
+       left := lb.NewArray()
+       lb.Release()
+       defer left.Release()
+       rb := array.NewBooleanBuilder(mem)
+       rb.AppendValues([]bool{false, true, true, false, false, false, true, 
true}, []bool{false, true, true, true, false, false, true, true})
+       right := rb.NewArray()
+       rb.Release()
+       defer right.Release()
+
+       for _, fn := range []string{"and_kleene", "or_kleene"} {
+               t.Run(fn, func(t *testing.T) { checkChunkSizeIndependent(t, 
mem, fn, left, right) })
+       }
+}
+
+func checkChunkSizeIndependent(t *testing.T, mem memory.Allocator, fn string, 
left, right arrow.Array) {
+       run := func(chunk int64) string {
+               ectx := compute.DefaultExecCtx()
+               ectx.ChunkSize = chunk
+               ctx := 
compute.SetExecCtx(compute.WithAllocator(context.Background(), mem), ectx)
+               out, err := compute.CallFunction(ctx, fn, nil, 
&compute.ArrayDatum{Value: left.Data()}, &compute.ArrayDatum{Value: 
right.Data()})
+               if err != nil {
+                       t.Fatal(err)
+               }
+               defer out.Release()
+               arr := out.(*compute.ArrayDatum).MakeArray()
+               defer arr.Release()
+               return arr.String()
+       }
+       ref := run(compute.DefaultMaxChunkSize)
+       t.Logf("ChunkSize default: %s", ref)
+       for _, n := range []int64{1, 2, 3, 4, 8} {
+               got := run(n)
+               t.Logf("ChunkSize %d: %s", n, got)
+               if got != ref {
+                       t.Errorf("ChunkSize %d: got %s, want %s", n, got, ref)
+               }
+       }
+}

Reply via email to