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 7886e4e5 perf(arrow/csv): stream full-file CSV reads into builders 
(#1393)
7886e4e5 is described below

commit 7886e4e5e4aa38180b94678fcbbddca2f8000550
Author: Minh Vu <[email protected]>
AuthorDate: Wed Oct 21 23:11:29 2026 +0200

    perf(arrow/csv): stream full-file CSV reads into builders (#1393)
    
    ### What
    - Stream `WithChunk(-1)` CSV rows into Arrow builders as records arrive.
    
    ### Why
    - `ReadAll` keeps every parsed row in memory before building Arrow
    arrays.
    
    ### Implementation
    - Reuse the existing parser and builder path one record at a time.
    - Preserve pending rows and conversion behavior.
    
    ### Benchmark
    `BenchmarkCSVReadAllStreaming`, 100,000 rows and 3 columns; Go 1.26.3,
    Apple M1 Pro, median of 7 runs.
    
    | Metric | Before | After | Change |
    |---|---:|---:|---:|
    | Time (ms/op) | 24.57 | 14.55 | 1.69x faster |
    | Allocated bytes (MB/op) | 28.38 | 8.21 | 71% fewer bytes |
    | Allocations (allocs/op) | 200,166 | 100,138 | 50% fewer |
---
 arrow/csv/reader.go                |  24 +++---
 arrow/csv/reader_streaming_test.go | 171 +++++++++++++++++++++++++++++++++++++
 2 files changed, 182 insertions(+), 13 deletions(-)

diff --git a/arrow/csv/reader.go b/arrow/csv/reader.go
index 555e7f24..59052ee4 100644
--- a/arrow/csv/reader.go
+++ b/arrow/csv/reader.go
@@ -308,20 +308,18 @@ func (r *Reader) nextall() bool {
                r.done = true
        }()
 
-       var recs [][]string
-       if r.pendingRecord != nil {
-               recs = append(recs, r.pendingRecord)
-               r.pendingRecord = nil
-       }
-
-       var remaining [][]string
-       remaining, r.err = r.r.ReadAll()
-       if r.err != nil {
-               return false
-       }
-       recs = append(recs, remaining...)
+       // Build the Arrow arrays as CSV records arrive instead of retaining
+       // every parsed row in memory before conversion.
+       for {
+               rec, err := r.readRecord()
+               if errors.Is(err, io.EOF) {
+                       break
+               }
+               if err != nil {
+                       r.err = err
+                       return false
+               }
 
-       for _, rec := range recs {
                r.validate(rec)
                r.read(rec)
        }
diff --git a/arrow/csv/reader_streaming_test.go 
b/arrow/csv/reader_streaming_test.go
new file mode 100644
index 00000000..8d72eb4f
--- /dev/null
+++ b/arrow/csv/reader_streaming_test.go
@@ -0,0 +1,171 @@
+// 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 csv_test
+
+import (
+       "strconv"
+       "strings"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/csv"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/stretchr/testify/require"
+)
+
+func TestCSVReadAllStreaming(t *testing.T) {
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int64},
+               {Name: "name", Type: arrow.BinaryTypes.String},
+               {Name: "value", Type: arrow.PrimitiveTypes.Int64, Nullable: 
true},
+       }, nil)
+       input := "id,name,value\n1,\"hello, 
world\",7\n2,\"line\nbreak\",NULL\n3,third,9\n"
+       r := csv.NewReader(strings.NewReader(input), schema,
+               csv.WithHeader(true), csv.WithChunk(-1), 
csv.WithNullReader(true, "NULL"))
+       defer r.Release()
+
+       require.True(t, r.Next())
+       require.NoError(t, r.Err())
+       batch := r.RecordBatch()
+       require.Equal(t, int64(3), batch.NumRows())
+
+       ids := batch.Column(0).(*array.Int64)
+       require.Equal(t, int64(1), ids.Value(0))
+       require.Equal(t, int64(3), ids.Value(2))
+       names := batch.Column(1).(*array.String)
+       require.Equal(t, "hello, world", names.Value(0))
+       require.Equal(t, "line\nbreak", names.Value(1))
+       values := batch.Column(2).(*array.Int64)
+       require.True(t, values.IsNull(1))
+       require.Equal(t, int64(9), values.Value(2))
+
+       require.False(t, r.Next())
+       require.NoError(t, r.Err())
+}
+
+func TestCSVReadAllStreamingHeaderlessInference(t *testing.T) {
+       r := csv.NewInferringReader(strings.NewReader("4,alpha\n5,beta\n"),
+               csv.WithHeader(false), csv.WithChunk(-1))
+       defer r.Release()
+
+       require.True(t, r.Next())
+       require.NoError(t, r.Err())
+       require.Equal(t, "f0", r.Schema().Field(0).Name)
+       require.Equal(t, "f1", r.Schema().Field(1).Name)
+       require.Equal(t, int64(2), r.RecordBatch().NumRows())
+       require.Equal(t, int64(4), 
r.RecordBatch().Column(0).(*array.Int64).Value(0))
+       require.Equal(t, "beta", 
r.RecordBatch().Column(1).(*array.String).Value(1))
+       require.False(t, r.Next())
+}
+
+func TestCSVReadAllStreamingEmpty(t *testing.T) {
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int64},
+       }, nil)
+       r := csv.NewReader(strings.NewReader(""), schema, csv.WithChunk(-1))
+       defer r.Release()
+
+       require.True(t, r.Next())
+       require.NoError(t, r.Err())
+       require.Equal(t, int64(0), r.RecordBatch().NumRows())
+       require.False(t, r.Next())
+}
+
+func TestCSVReadAllStreamingMalformedRow(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int64},
+               {Name: "name", Type: arrow.BinaryTypes.String},
+       }, nil)
+       r := csv.NewReader(strings.NewReader("1,ok\n2,\"unterminated\n"), 
schema,
+               csv.WithChunk(-1), csv.WithAllocator(mem))
+       defer func() {
+               r.Release()
+               mem.AssertSize(t, 0)
+       }()
+
+       require.False(t, r.Next())
+       require.Error(t, r.Err())
+       require.Nil(t, r.RecordBatch())
+       require.False(t, r.Next())
+}
+
+func TestCSVReadAllStreamingMalformedInferredRow(t *testing.T) {
+       mem := memory.NewCheckedAllocator(memory.NewGoAllocator())
+       r := 
csv.NewInferringReader(strings.NewReader("1,ok\n2,\"unterminated\n"),
+               csv.WithHeader(false), csv.WithChunk(-1), 
csv.WithAllocator(mem))
+       defer func() {
+               r.Release()
+               mem.AssertSize(t, 0)
+       }()
+
+       require.False(t, r.Next())
+       require.Error(t, r.Err())
+       require.Nil(t, r.RecordBatch())
+       require.False(t, r.Next())
+}
+func TestCSVReadAllStreamingConversionError(t *testing.T) {
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int64, Nullable: true},
+               {Name: "name", Type: arrow.BinaryTypes.String},
+       }, nil)
+       r := 
csv.NewReader(strings.NewReader("1,first\ninvalid,second\n3,third\n"), schema, 
csv.WithChunk(-1))
+       defer r.Release()
+
+       require.True(t, r.Next())
+       require.Error(t, r.Err())
+       batch := r.RecordBatch()
+       require.Equal(t, int64(3), batch.NumRows())
+       require.True(t, batch.Column(0).IsNull(1))
+       require.Equal(t, int64(3), batch.Column(0).(*array.Int64).Value(2))
+       require.Equal(t, "second", batch.Column(1).(*array.String).Value(1))
+       require.False(t, r.Next())
+}
+
+func BenchmarkCSVReadAllStreaming(b *testing.B) {
+       const rows = 100000
+       var input strings.Builder
+       for i := 0; i < rows; i++ {
+               input.WriteString(strconv.Itoa(i))
+               input.WriteString(",value,")
+               input.WriteString(strconv.Itoa(i))
+               input.WriteByte('\n')
+       }
+       data := input.String()
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "id", Type: arrow.PrimitiveTypes.Int64},
+               {Name: "name", Type: arrow.BinaryTypes.String},
+               {Name: "value", Type: arrow.PrimitiveTypes.Int64},
+       }, nil)
+
+       b.SetBytes(int64(len(data)))
+       b.ReportAllocs()
+       for b.Loop() {
+               r := csv.NewReader(strings.NewReader(data), schema, 
csv.WithChunk(-1))
+               if !r.Next() {
+                       err := r.Err()
+                       r.Release()
+                       b.Fatalf("read CSV: %v", err)
+               }
+               if got := r.RecordBatch().NumRows(); got != rows {
+                       r.Release()
+                       b.Fatalf("expected %d rows, got %d", rows, got)
+               }
+               r.Release()
+       }
+}

Reply via email to