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 df6bddc4 fix(arrow): support zero-length fixed-size lists (#1308)
df6bddc4 is described below

commit df6bddc456b13dd916dba83496d35c8aaa1528be
Author: Jake Thomas <[email protected]>
AuthorDate: Tue Sep 15 14:58:13 2026 -0400

    fix(arrow): support zero-length fixed-size lists (#1308)
    
    ### Rationale for this change
    
    DataFusion can return `FixedSizeList(0, Null)` for an empty list. Arrow
    Go currently rejects the C data schema `+w:0`, and its fixed-size list
    constructors also panic for size zero. This prevents importing results
    such as:
    
    ```sql
    select arrow_cast(a, 'FixedSizeList(0, Null)')
    from values ([]), (NULL) t(a);
    ```
    
    A zero-size list has no child values, but its parent array still has
    rows and a validity bitmap that distinguishes empty lists from null
    lists. This fixes the two failing cases in DataFusion's
    
[`arrow_typeof.slt`](https://github.com/apache/datafusion/blob/d5552342012888b7d1a3ab88d92e3d292fc0cde0/datafusion/sqllogictest/test_files/arrow_typeof.slt#L400).
    
    ### What changes are included in this PR?
    
    - Accept zero in the three fixed-size list type constructors and in C
    data schema imports.
    - Allow zero-size fixed-size list arrays to validate without dividing by
    zero. Negative sizes, invalid child schemas, and overflowing C schema
    sizes remain rejected.
    - Add constructor, array, C data, and IPC regression tests covering
    empty and null rows, bulk appends, nonzero slice offsets, and
    checked-allocator cleanup with null, integer, and nested-list element
    types.
    
    ### Are these changes tested?
    
    Local verification on macOS ARM64 with Go 1.25.2:
    
    - `pre-commit run --all-files --show-diff-on-failure`
    - `ci/scripts/build.sh "$PWD"`
    - `ci/scripts/test.sh "$PWD"` (race detector, `assert,test`, and `noasm`
    variants)
    - `go test -race -short -tags=assert,test ./internal/...`
    
    The datafusion-go SQLLogicTest corpus also passes **24,958/24,958
    assertions across 365 executable SQL files** on macOS when an isolated
    Go module file replaces Arrow Go with this checkout. That includes all
    62 records in `arrow_typeof.slt`; the released Arrow Go dependency fails
    two of those records. This downstream run used DataFusion 55.0.0 and
    datafusion-go commit `8fbd0218b6c5f3d9d64ec798ae466f66725be325`.
    
    ### Are there any user-facing changes?
    
    Zero-size fixed-size lists can now be constructed, validated, and
    imported through the C data interface. Existing constructor and import
    checks for negative sizes are preserved.
---
 arrow/array/fixed_size_list_test.go | 44 +++++++++++++++++++++++++++++++++++++
 arrow/array/validate.go             |  4 ++--
 arrow/cdata/cdata.go                |  4 ++--
 arrow/cdata/cdata_test.go           | 34 +++++++++++++++++++++++++++-
 arrow/datatype_nested.go            |  8 +++----
 arrow/datatype_nested_test.go       | 14 ++++++++++++
 arrow/ipc/ipc_test.go               | 36 ++++++++++++++++++++++++++++++
 7 files changed, 135 insertions(+), 9 deletions(-)

diff --git a/arrow/array/fixed_size_list_test.go 
b/arrow/array/fixed_size_list_test.go
index 6326d9c2..b6b7dbd1 100644
--- a/arrow/array/fixed_size_list_test.go
+++ b/arrow/array/fixed_size_list_test.go
@@ -25,6 +25,7 @@ import (
        "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/memory"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
 func TestFixedSizeListArray(t *testing.T) {
@@ -92,6 +93,49 @@ func TestFixedSizeListArrayEmpty(t *testing.T) {
        }
 }
 
+func TestZeroSizeFixedSizeListArray(t *testing.T) {
+       for _, element := range []arrow.DataType{arrow.Null, 
arrow.PrimitiveTypes.Int64, arrow.ListOf(arrow.BinaryTypes.String)} {
+               t.Run(element.String(), func(t *testing.T) {
+                       pool := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       defer pool.AssertSize(t, 0)
+                       builder := array.NewFixedSizeListBuilder(pool, 0, 
element)
+                       defer builder.Release()
+
+                       empty := builder.NewListArray()
+                       defer empty.Release()
+                       require.NoError(t, empty.Validate())
+                       require.NoError(t, empty.ValidateFull())
+                       assert.Zero(t, empty.Len())
+                       assert.Zero(t, empty.ListValues().Len())
+
+                       builder.AppendEmptyValue()
+                       builder.AppendNull()
+                       builder.AppendNulls(2)
+                       builder.AppendEmptyValues(2)
+                       arr := builder.NewListArray()
+                       defer arr.Release()
+                       require.NoError(t, arr.Validate())
+                       require.NoError(t, arr.ValidateFull())
+                       assert.Equal(t, 6, arr.Len())
+                       assert.Equal(t, 3, arr.NullN())
+                       assert.Zero(t, arr.ListValues().Len())
+                       encoded, err := arr.MarshalJSON()
+                       require.NoError(t, err)
+                       assert.JSONEq(t, `[[], null, null, null, [], []]`, 
string(encoded))
+
+                       sliced := array.NewSlice(arr, 1, 
5).(*array.FixedSizeList)
+                       defer sliced.Release()
+                       require.NoError(t, sliced.ValidateFull())
+                       assert.Equal(t, 4, sliced.Len())
+                       assert.Equal(t, 3, sliced.NullN())
+                       assert.Zero(t, sliced.ListValues().Len())
+                       start, end := sliced.ValueOffsets(3)
+                       assert.Zero(t, start)
+                       assert.Zero(t, end)
+               })
+       }
+}
+
 func TestFixedSizeListArrayBulkAppend(t *testing.T) {
        pool := memory.NewCheckedAllocator(memory.NewGoAllocator())
        defer pool.AssertSize(t, 0)
diff --git a/arrow/array/validate.go b/arrow/array/validate.go
index 2a50412a..c1605d44 100644
--- a/arrow/array/validate.go
+++ b/arrow/array/validate.go
@@ -403,10 +403,10 @@ func validateFixedSizeListArray(a *FixedSizeList) error {
        }
        childLength := int64(a.data.offset) + int64(a.data.length)
        itemCount := int64(dt.Len())
-       if itemCount <= 0 {
+       if itemCount < 0 {
                return fmt.Errorf("arrow/array: fixed-size list has invalid 
item count %d", itemCount)
        }
-       if childLength > int64(a.data.childData[0].Len())/itemCount {
+       if itemCount > 0 && childLength > 
int64(a.data.childData[0].Len())/itemCount {
                return fmt.Errorf("arrow/array: fixed-size list child length %d 
is too small for offset %d and length %d",
                        a.data.childData[0].Len(), a.data.offset, a.data.length)
        }
diff --git a/arrow/cdata/cdata.go b/arrow/cdata/cdata.go
index 161440e4..de17b0ed 100644
--- a/arrow/cdata/cdata.go
+++ b/arrow/cdata/cdata.go
@@ -329,8 +329,8 @@ func importSchema(schema *CArrowSchema) (ret arrow.Field, 
err error) {
                        if err != nil {
                                return ret, fmt.Errorf("%w: invalid fixed-size 
list format %q: %v", arrow.ErrInvalid, f, err)
                        }
-                       if listSize <= 0 || int64(listSize) > 1<<31-1 {
-                               return ret, fmt.Errorf("%w: fixed-size list 
size must be in the range [1, %d]: %d", arrow.ErrInvalid, 1<<31-1, listSize)
+                       if listSize < 0 || int64(listSize) > 1<<31-1 {
+                               return ret, fmt.Errorf("%w: fixed-size list 
size must be in the range [0, %d]: %d", arrow.ErrInvalid, 1<<31-1, listSize)
                        }
 
                        dt = arrow.FixedSizeListOfField(int32(listSize), 
childFields[0])
diff --git a/arrow/cdata/cdata_test.go b/arrow/cdata/cdata_test.go
index 164a837a..dcc2851f 100644
--- a/arrow/cdata/cdata_test.go
+++ b/arrow/cdata/cdata_test.go
@@ -119,7 +119,7 @@ func TestImportSchemaRejectsMalformedFormats(t *testing.T) {
 }
 
 func TestImportSchemaRejectsInvalidNestedFormats(t *testing.T) {
-       for _, format := range []string{"+vx", "+vlx", "+vLx", "+lx", "+Lx", 
"+w:0", "+w:-1", "+w:2147483648"} {
+       for _, format := range []string{"+vx", "+vlx", "+vLx", "+lx", "+Lx", 
"+w:-1", "+w:2147483648"} {
                t.Run(format, func(t *testing.T) {
                        schemas := testNested([]string{format, "i"}, 
[]string{"", "item"}, []bool{true})
                        defer freeMallocedSchemas(schemas)
@@ -1074,6 +1074,38 @@ func TestEmptyListExport(t *testing.T) {
        assert.NotNil(t, out.children)
 }
 
+func TestZeroSizeFixedSizeListRoundTrip(t *testing.T) {
+       for _, element := range []arrow.DataType{arrow.Null, 
arrow.PrimitiveTypes.Int64, arrow.ListOf(arrow.BinaryTypes.String)} {
+               t.Run(element.String(), func(t *testing.T) {
+                       allocator := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       defer allocator.AssertSize(t, 0)
+                       builder := array.NewFixedSizeListBuilder(allocator, 0, 
element)
+                       defer builder.Release()
+                       builder.AppendEmptyValue()
+                       builder.AppendNull()
+                       builder.AppendEmptyValues(2)
+                       original := builder.NewListArray()
+                       defer original.Release()
+                       require.NoError(t, original.ValidateFull())
+                       sliced := array.NewSlice(original, 1, 4)
+                       defer sliced.Release()
+                       var data CArrowArray
+                       var schema CArrowSchema
+                       ExportArrowArray(sliced, &data, &schema)
+                       defer ReleaseCArrowArray(&data)
+                       defer ReleaseCArrowSchema(&schema)
+                       _, imported, err := ImportCArray(&data, &schema)
+                       require.NoError(t, err)
+                       defer imported.Release()
+                       require.NoError(t, 
imported.(*array.FixedSizeList).ValidateFull())
+                       assert.True(t, array.Equal(sliced, imported))
+                       assert.Equal(t, 3, imported.Len())
+                       assert.Equal(t, 1, imported.NullN())
+                       assert.EqualValues(t, 0, 
imported.DataType().(*arrow.FixedSizeListType).Len())
+               })
+       }
+}
+
 func TestEmptyDictExport(t *testing.T) {
        bldr := array.NewBuilder(memory.DefaultAllocator, 
&arrow.DictionaryType{IndexType: arrow.PrimitiveTypes.Int8, ValueType: 
arrow.BinaryTypes.String, Ordered: true})
        defer bldr.Release()
diff --git a/arrow/datatype_nested.go b/arrow/datatype_nested.go
index cf8ca377..1f104e7e 100644
--- a/arrow/datatype_nested.go
+++ b/arrow/datatype_nested.go
@@ -182,7 +182,7 @@ func FixedSizeListOfField(n int32, f Field) 
*FixedSizeListType {
        if f.Type == nil {
                panic("arrow: nil DataType")
        }
-       if n <= 0 {
+       if n < 0 {
                panic("arrow: invalid size")
        }
        return &FixedSizeListType{n: n, elem: f}
@@ -192,13 +192,13 @@ func FixedSizeListOfField(n int32, f Field) 
*FixedSizeListType {
 // For example, if t represents int32, FixedSizeListOf(10, t) represents 
[10]int32.
 //
 // FixedSizeListOf panics if t is nil or invalid.
-// FixedSizeListOf panics if n is <= 0.
+// FixedSizeListOf panics if n is negative.
 // NullableElem defaults to true
 func FixedSizeListOf(n int32, t DataType) *FixedSizeListType {
        if t == nil {
                panic("arrow: nil DataType")
        }
-       if n <= 0 {
+       if n < 0 {
                panic("arrow: invalid size")
        }
        return &FixedSizeListType{n: n, elem: Field{Name: "item", Type: t, 
Nullable: true}}
@@ -210,7 +210,7 @@ func FixedSizeListOfNonNullable(n int32, t DataType) 
*FixedSizeListType {
        if t == nil {
                panic("arrow: nil DataType")
        }
-       if n <= 0 {
+       if n < 0 {
                panic("arrow: invalid size")
        }
        return &FixedSizeListType{n: n, elem: Field{Name: "item", Type: t, 
Nullable: false}}
diff --git a/arrow/datatype_nested_test.go b/arrow/datatype_nested_test.go
index 1845d171..3af1d40e 100644
--- a/arrow/datatype_nested_test.go
+++ b/arrow/datatype_nested_test.go
@@ -81,6 +81,20 @@ func TestListOf(t *testing.T) {
        }
 }
 
+func TestZeroSizeFixedSizeListType(t *testing.T) {
+       field := Field{Name: "values", Type: Null, Nullable: true}
+       for _, dt := range []*FixedSizeListType{
+               FixedSizeListOf(0, Null), FixedSizeListOfField(0, field), 
FixedSizeListOfNonNullable(0, Null),
+       } {
+               assert.Zero(t, dt.Len())
+               assert.Equal(t, Null, dt.Elem())
+       }
+       assert.Equal(t, field, FixedSizeListOfField(0, field).ElemField())
+       assert.Panics(t, func() { FixedSizeListOf(-1, Null) })
+       assert.Panics(t, func() { FixedSizeListOfField(-1, field) })
+       assert.Panics(t, func() { FixedSizeListOfNonNullable(-1, Null) })
+}
+
 func TestStructOf(t *testing.T) {
        for _, tc := range []struct {
                fields []Field
diff --git a/arrow/ipc/ipc_test.go b/arrow/ipc/ipc_test.go
index f1f310bd..dd635226 100644
--- a/arrow/ipc/ipc_test.go
+++ b/arrow/ipc/ipc_test.go
@@ -747,6 +747,42 @@ func TestArrowBinaryIPCWriterTruncatedVOffsets(t 
*testing.T) {
        require.False(t, reader.Next())
 }
 
+func TestZeroSizeFixedSizeListRoundTrip(t *testing.T) {
+       for _, element := range []arrow.DataType{arrow.Null, 
arrow.PrimitiveTypes.Int64, arrow.ListOf(arrow.BinaryTypes.String)} {
+               t.Run(element.String(), func(t *testing.T) {
+                       mem := 
memory.NewCheckedAllocator(memory.DefaultAllocator)
+                       defer mem.AssertSize(t, 0)
+                       schema := arrow.NewSchema([]arrow.Field{{Name: "lists", 
Type: arrow.FixedSizeListOf(0, element), Nullable: true}}, nil)
+                       builder := array.NewRecordBuilder(mem, schema)
+                       defer builder.Release()
+                       lists := builder.Field(0).(*array.FixedSizeListBuilder)
+                       lists.AppendEmptyValue()
+                       lists.AppendNull()
+                       lists.AppendEmptyValues(2)
+                       record := builder.NewRecordBatch()
+                       defer record.Release()
+                       sliced := record.NewSlice(1, 4)
+                       defer sliced.Release()
+
+                       var buf bytes.Buffer
+                       writer := ipc.NewWriter(&buf, ipc.WithSchema(schema), 
ipc.WithAllocator(mem))
+                       defer writer.Close()
+                       require.NoError(t, writer.Write(sliced))
+                       require.NoError(t, writer.Close())
+
+                       reader, err := ipc.NewReader(&buf, 
ipc.WithAllocator(mem))
+                       require.NoError(t, err)
+                       defer reader.Release()
+                       require.True(t, reader.Next())
+                       got := reader.RecordBatch()
+                       require.NoError(t, 
got.Column(0).(*array.FixedSizeList).ValidateFull())
+                       assert.True(t, array.RecordEqual(sliced, got))
+                       require.False(t, reader.Next())
+                       require.NoError(t, reader.Err())
+               })
+       }
+}
+
 func TestRecordBatchCustomMetadataRoundtrip(t *testing.T) {
        mem := memory.NewGoAllocator()
        schema := arrow.NewSchema(

Reply via email to