Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ dist

# Local output of the e2e conformance suite
/test/e2e/.out/

# Local reproduction scripts and logs
/repro/

# Local development log
devlog.md
9 changes: 8 additions & 1 deletion internal/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,16 @@ func (c *TableCell) AppendValueToARROWBuilder(builder array.Builder) error {
if !ok {
return fmt.Errorf("failed to convert to list builder from %T", builder)
}
// BigQuery REPEATED fields are always non-null: a REPEATED column is
// either empty or populated, never null. Append(true) marks this list
// slot as valid (non-null) and opens it for elements. The slot is closed
// implicitly when Append is next called (for the following row). A nil
// []*TableCell is therefore treated identically to an empty slice.
// The original bug (issue #399) placed this call inside the element
// loop, opening N slots instead of 1 and causing a NewRecord panic.
listBuilder.Append(true)
b := listBuilder.ValueBuilder()
for _, vv := range v {
listBuilder.Append(true)
if err := vv.AppendValueToARROWBuilder(b); err != nil {
return err
}
Expand Down
86 changes: 86 additions & 0 deletions internal/types/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package types

import (
"testing"

"github.com/apache/arrow-go/v18/arrow"
"github.com/apache/arrow-go/v18/arrow/array"
"github.com/apache/arrow-go/v18/arrow/memory"
)

// TestAppendValueToARROWBuilder_List is a regression test for issue #399.
// AppendValueToARROWBuilder was calling listBuilder.Append(true) inside the
// per-element loop instead of once per row, producing N list slots instead of
// 1 and causing a row-count mismatch panic in array.RecordBuilder.NewRecord.
func TestAppendValueToARROWBuilder_List(t *testing.T) {
mem := memory.NewGoAllocator()
schema := arrow.NewSchema([]arrow.Field{
{Name: "items", Type: arrow.ListOf(arrow.PrimitiveTypes.Int64), Nullable: true},
}, nil)
rb := array.NewRecordBuilder(mem, schema)
defer rb.Release()

listBldr := rb.Field(0).(*array.ListBuilder)

rows := []struct {
cells []*TableCell
wantLen int
}{
{
cells: []*TableCell{{V: "1"}, {V: "2"}, {V: "3"}},
wantLen: 3,
},
{
cells: []*TableCell{},
wantLen: 0,
},
{
cells: []*TableCell{{V: "4"}},
wantLen: 1,
},
{
// A nil []*TableCell is a typed nil stored in the interface V field.
// BigQuery has no null-array concept for REPEATED columns, so nil
// must behave identically to an empty slice: a valid, non-null,
// zero-length list slot.
cells: nil,
wantLen: 0,
},
}
Comment on lines +25 to +49

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both gaps are addressed in commit \35660fc:

  1. Nil vs empty: Added a fourth row with \cells: nil\ (typed-nil []*TableCell\ stored in \interface{}). It asserts \wantLen: 0\ and is annotated to explain that in BigQuery REPEATED fields are always non-null — nil means empty, not null.

  2. Element value assertions: After the per-row length loop, the test now extracts the flat \ListValues()\ array as *array.Int64\ and verifies row 0's elements are exactly [1, 2, 3]\ using \ValueOffsets\ to bound the slice.


for _, row := range rows {
cell := &TableCell{V: row.cells}
if err := cell.AppendValueToARROWBuilder(listBldr); err != nil {
t.Fatalf("AppendValueToARROWBuilder: %v", err)
}
}

// NewRecord panics (row-count mismatch) when the bug is present.
rec := rb.NewRecord()
defer rec.Release()

if got := rec.NumRows(); got != int64(len(rows)) {
t.Fatalf("NumRows = %d, want %d", got, len(rows))
}

col := rec.Column(0).(*array.List)
for i, row := range rows {
start, end := col.ValueOffsets(i)
if got := int(end - start); got != row.wantLen {
t.Errorf("row %d: list length = %d, want %d", i, got, row.wantLen)
}
}

// Verify actual element values in the first row (cells "1","2","3" → 1,2,3).
valCol := col.ListValues().(*array.Int64)
start, end := col.ValueOffsets(0)
wantVals := []int64{1, 2, 3}
if got := int(end - start); got != len(wantVals) {
t.Fatalf("row 0 element count = %d, want %d", got, len(wantVals))
}
for i, wv := range wantVals {
if got := valCol.Value(int(start) + i); got != wv {
t.Errorf("row 0 element %d = %d, want %d", i, got, wv)
}
}
}