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 84e4e99d fix(arrow/array): preserve run-end encoded empty values 
(#1156)
84e4e99d is described below

commit 84e4e99da0adafd34cf5dbd38f438dcfc63c9f97
Author: Minh Vu <[email protected]>
AuthorDate: Tue Aug 25 17:39:56 2026 +0200

    fix(arrow/array): preserve run-end encoded empty values (#1156)
    
    ## What
    
    Make RunEndEncodedBuilder.AppendEmptyValue and AppendEmptyValues append
    the value builder empty value instead of a null.
    
    ## Test
    
    - go test ./arrow/array -run ^TestRunEndEncoded -count=1
---
 arrow/array/dictionary.go      | 22 +++++++++--
 arrow/array/dictionary_test.go | 29 ++++++++++++++
 arrow/array/encoded.go         | 19 ++++++---
 arrow/array/encoded_test.go    | 89 ++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 149 insertions(+), 10 deletions(-)

diff --git a/arrow/array/dictionary.go b/arrow/array/dictionary.go
index 14c234d2..a03fa457 100644
--- a/arrow/array/dictionary.go
+++ b/arrow/array/dictionary.go
@@ -668,13 +668,27 @@ func (b *dictionaryBuilder) AppendNulls(n int) {
 }
 
 func (b *dictionaryBuilder) AppendEmptyValue() {
-       b.length += 1
-       b.idxBuilder.AppendEmptyValue()
+       b.AppendEmptyValues(1)
 }
 
 func (b *dictionaryBuilder) AppendEmptyValues(n int) {
-       for i := 0; i < n; i++ {
-               b.AppendEmptyValue()
+       if n <= 0 {
+               return
+       }
+
+       if b.dt.ValueType.ID() == arrow.NULL {
+               b.AppendNulls(n)
+               return
+       }
+
+       valueBuilder := NewBuilder(b.mem, b.dt.ValueType)
+       defer valueBuilder.Release()
+       valueBuilder.AppendEmptyValues(n)
+
+       values := valueBuilder.NewArray()
+       defer values.Release()
+       if err := b.AppendArray(values); err != nil {
+               panic(err)
        }
 }
 
diff --git a/arrow/array/dictionary_test.go b/arrow/array/dictionary_test.go
index 90f3c0bd..2324b743 100644
--- a/arrow/array/dictionary_test.go
+++ b/arrow/array/dictionary_test.go
@@ -564,6 +564,35 @@ func TestStringDictionaryBuilderInit(t *testing.T) {
        assert.True(t, array.Equal(expected, result))
 }
 
+func TestStringDictionaryBuilderAppendEmptyValue(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       dictArr, _, err := array.FromJSON(mem, arrow.BinaryTypes.String, 
strings.NewReader(`["existing"]`))
+       require.NoError(t, err)
+       defer dictArr.Release()
+
+       dictType := &arrow.DictionaryType{IndexType: &arrow.Int8Type{}, 
ValueType: arrow.BinaryTypes.String}
+       bldr := array.NewDictionaryBuilderWithDict(mem, dictType, dictArr)
+       defer bldr.Release()
+
+       bldr.AppendEmptyValue()
+       bldr.AppendEmptyValues(2)
+
+       result := bldr.NewDictionaryArray()
+       defer result.Release()
+
+       dict := result.Dictionary().(*array.String)
+       assert.Equal(t, 2, dict.Len())
+       assert.Equal(t, "existing", dict.Value(0))
+       assert.Empty(t, dict.Value(1))
+       for i := 0; i < result.Len(); i++ {
+               assert.False(t, result.IsNull(i))
+               assert.Equal(t, 1, result.GetValueIndex(i))
+               assert.Empty(t, result.GetOneForMarshal(i))
+       }
+}
+
 func TestStringDictionaryBuilderOnlyNull(t *testing.T) {
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
        defer mem.AssertSize(t, 0)
diff --git a/arrow/array/encoded.go b/arrow/array/encoded.go
index 219a75cb..bd9a6fb5 100644
--- a/arrow/array/encoded.go
+++ b/arrow/array/encoded.go
@@ -392,9 +392,10 @@ type RunEndEncodedBuilder struct {
        maxRunEnd uint64
 
        // currently, mixing AppendValueFromString & UnmarshalOne is unsupported
-       lastUnmarshalled interface{}
-       unmarshalled     bool // tracks if Unmarshal was called (in case 
lastUnmarshalled is nil)
-       lastStr          *string
+       lastUnmarshalled  interface{}
+       unmarshalled      bool // tracks if Unmarshal was called (in case 
lastUnmarshalled is nil)
+       lastValueWasEmpty bool
+       lastStr           *string
 }
 
 func NewRunEndEncodedBuilder(mem memory.Allocator, runEnds, encoded 
arrow.DataType) *RunEndEncodedBuilder {
@@ -449,6 +450,7 @@ func (b *RunEndEncodedBuilder) finishRun() {
        b.lastUnmarshalled = nil
        b.lastStr = nil
        b.unmarshalled = false
+       b.lastValueWasEmpty = false
        if b.length == 0 {
                return
        }
@@ -502,11 +504,16 @@ func (b *RunEndEncodedBuilder) NullN() int {
 }
 
 func (b *RunEndEncodedBuilder) AppendEmptyValue() {
-       b.AppendNull()
+       b.finishRun()
+       b.values.AppendEmptyValue()
+       b.addLength(1)
+       b.lastValueWasEmpty = true
 }
 
 func (b *RunEndEncodedBuilder) AppendEmptyValues(n int) {
-       b.AppendNulls(n)
+       for i := 0; i < n; i++ {
+               b.AppendEmptyValue()
+       }
 }
 
 func (b *RunEndEncodedBuilder) Reserve(n int) {
@@ -585,7 +592,7 @@ func (b *RunEndEncodedBuilder) UnmarshalOne(dec 
*json.Decoder) error {
        // make sure we add a new run instead. We can detect that case by
        // checking that the number of runEnds matches the number of values
        // we have, which means no matter what we have to start a new run
-       if reflect.DeepEqual(value, b.lastUnmarshalled) && (value != nil || 
b.runEnds.Len() != b.values.Len()) {
+       if !b.lastValueWasEmpty && reflect.DeepEqual(value, b.lastUnmarshalled) 
&& (value != nil || b.runEnds.Len() != b.values.Len()) {
                b.ContinueRun(1)
                return nil
        }
diff --git a/arrow/array/encoded_test.go b/arrow/array/encoded_test.go
index 7269f4fd..512adf85 100644
--- a/arrow/array/encoded_test.go
+++ b/arrow/array/encoded_test.go
@@ -367,6 +367,95 @@ func TestRunEndEncodedBuilder(t *testing.T) {
        assert.Equal(t, "Hello", strValues.ValueStr(0))
 }
 
+func TestRunEndEncodedBuilderEmptyValues(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       bldr := array.NewRunEndEncodedBuilder(mem, arrow.PrimitiveTypes.Int16, 
arrow.BinaryTypes.String)
+       defer bldr.Release()
+
+       bldr.AppendEmptyValue()
+       bldr.AppendEmptyValues(2)
+
+       arr := bldr.NewRunEndEncodedArray()
+       defer arr.Release()
+
+       values := arr.Values().(*array.String)
+       assert.Equal(t, 3, values.Len())
+       for i := 0; i < values.Len(); i++ {
+               assert.False(t, values.IsNull(i))
+               assert.Empty(t, values.Value(i))
+       }
+}
+
+func TestRunEndEncodedBuilderEmptyValueBeforeUnmarshalNull(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       bldr := array.NewRunEndEncodedBuilder(mem, arrow.PrimitiveTypes.Int16, 
arrow.BinaryTypes.String)
+       defer bldr.Release()
+
+       bldr.AppendEmptyValue()
+       dec := json.NewDecoder(strings.NewReader("null"))
+       require.NoError(t, bldr.UnmarshalOne(dec))
+
+       arr := bldr.NewRunEndEncodedArray()
+       defer arr.Release()
+
+       assert.Equal(t, []int16{1, 2}, 
arr.RunEndsArr().(*array.Int16).Int16Values())
+       values := arr.Values().(*array.String)
+       assert.False(t, values.IsNull(0))
+       assert.Empty(t, values.Value(0))
+       assert.True(t, values.IsNull(1))
+}
+
+func TestRunEndEncodedBuilderEmptyValuesBeforeUnmarshalNulls(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       bldr := array.NewRunEndEncodedBuilder(mem, arrow.PrimitiveTypes.Int16, 
arrow.BinaryTypes.String)
+       defer bldr.Release()
+
+       bldr.AppendEmptyValues(2)
+       dec := json.NewDecoder(strings.NewReader("null null"))
+       require.NoError(t, bldr.UnmarshalOne(dec))
+       require.NoError(t, bldr.UnmarshalOne(dec))
+
+       arr := bldr.NewRunEndEncodedArray()
+       defer arr.Release()
+
+       assert.Equal(t, []int16{1, 2, 4}, 
arr.RunEndsArr().(*array.Int16).Int16Values())
+       values := arr.Values().(*array.String)
+       assert.Equal(t, 3, values.Len())
+       assert.False(t, values.IsNull(0))
+       assert.Empty(t, values.Value(0))
+       assert.False(t, values.IsNull(1))
+       assert.Empty(t, values.Value(1))
+       assert.True(t, values.IsNull(2))
+}
+
+func TestRunEndEncodedBuilderDictionaryEmptyValue(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.DefaultAllocator)
+       defer mem.AssertSize(t, 0)
+
+       encoded := &arrow.DictionaryType{
+               IndexType: arrow.PrimitiveTypes.Int8,
+               ValueType: arrow.BinaryTypes.String,
+       }
+       bldr := array.NewRunEndEncodedBuilder(mem, arrow.PrimitiveTypes.Int16, 
encoded)
+       defer bldr.Release()
+
+       bldr.AppendEmptyValue()
+
+       arr := bldr.NewRunEndEncodedArray()
+       defer arr.Release()
+
+       values := arr.Values().(*array.Dictionary)
+       assert.Equal(t, 1, values.Dictionary().Len())
+       assert.Equal(t, 0, values.GetValueIndex(0))
+       assert.Equal(t, "", arr.GetOneForMarshal(0))
+}
+
 func TestRunEndEncodedStringRoundTrip(t *testing.T) {
        // 1. create array
        mem := memory.NewCheckedAllocator(memory.DefaultAllocator)

Reply via email to