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 05fa1adb perf(parquet/pqarrow): write record ranges directly (#1248)
05fa1adb is described below

commit 05fa1adb6c37a6d48b4c296900fe84a319a3812e
Author: Minh Vu <[email protected]>
AuthorDate: Mon Aug 31 17:42:07 2026 +0200

    perf(parquet/pqarrow): write record ranges directly (#1248)
    
    ## Summary
    
    - **Write row-group ranges directly from a record batch.**
    - Reuse one `*arrow.Chunked` wrapper per column when a batch spans row
    groups.
    - Keep the existing path when a batch fits in one row group.
    - Preserve buffered row-group boundaries, including full and zero-row
    cases.
    - Add nested/null round-trip coverage and benchmarks.
    
    ## Benchmark
    
    Local Apple M1 Pro. `-benchtime=500ms -count=3`. 8 int64 columns, 8,192
    rows, row-group size 256.
    
    | Method | main | change | allocs/op |
    | --- | ---: | ---: | ---: |
    | `Write` | ~2.31 ms, 4.95 MB | ~2.11 ms, 4.87 MB | 21,905 -> 20,761 |
    | `WriteBuffered` | ~2.54 ms, 6.69 MB | ~2.45 ms, 6.61 MB | 25,407 ->
    24,263 |
    
    The wall-time result varies a bit between runs. The allocation reduction
    is consistent.
    
    ## Tests
    
    - `go test ./parquet/pqarrow -run
    
'^(TestFileWriter|TestWriteOnClosedFileWriter|TestBufferedRecWrite|TestByteArrayStatisticsStreamingReleaseBetweenBatches)$'`
    - `go test -race ./parquet/pqarrow -run
    '^(TestFileWriterRangeWritesPreserveData|TestFileWriterZeroRowRecord)$'`
    - `go vet -composites=false ./parquet/pqarrow`
    - `go test ./... -run '^$'`
    - `GOOS=linux GOARCH=386 go test -c -o /dev/null ./parquet/pqarrow`
---
 parquet/pqarrow/file_writer.go                  | 105 +++++++++++++-------
 parquet/pqarrow/file_writer_range_bench_test.go | 102 +++++++++++++++++++
 parquet/pqarrow/file_writer_test.go             | 125 ++++++++++++++++++++++++
 3 files changed, 297 insertions(+), 35 deletions(-)

diff --git a/parquet/pqarrow/file_writer.go b/parquet/pqarrow/file_writer.go
index 31967b6f..08bf351d 100644
--- a/parquet/pqarrow/file_writer.go
+++ b/parquet/pqarrow/file_writer.go
@@ -315,7 +315,6 @@ func (fw *FileWriter) WriteBuffered(rec arrow.RecordBatch) 
error {
        }
 
        var (
-               recList []arrow.RecordBatch
                maxRows = fw.wr.Properties().MaxRowGroupLength()
                curRows int
                err     error
@@ -329,31 +328,33 @@ func (fw *FileWriter) WriteBuffered(rec 
arrow.RecordBatch) error {
                        return err
                }
        }
-
        if int64(curRows)+rec.NumRows() <= maxRows {
-               recList = []arrow.RecordBatch{rec}
-       } else {
-               recList = []arrow.RecordBatch{rec.NewSlice(0, 
maxRows-int64(curRows))}
-               defer recList[0].Release()
-               for offset := maxRows - int64(curRows); offset < rec.NumRows(); 
offset += maxRows {
-                       s := rec.NewSlice(offset, offset+utils.Min(maxRows, 
rec.NumRows()-offset))
-                       defer s.Release()
-                       recList = append(recList, s)
+               if err := fw.writeRecordBatchColumns(rec); err != nil {
+                       fw.Close()
+                       return err
                }
+               fw.colIdx = 0
+               return nil
        }
 
-       for idx, r := range recList {
-               if idx > 0 {
-                       if err := fw.NewBufferedRowGroupChecked(); err != nil {
-                               return err
-                       }
+       columns := newChunkedRecordColumns(rec)
+       defer releaseChunkedRecordColumns(columns)
+
+       firstSize := maxRows - int64(curRows)
+
+       for offset, size := int64(0), firstSize; ; {
+               if err := fw.writeRecordBatchRange(columns, offset, size); err 
!= nil {
+                       fw.Close()
+                       return err
                }
-               for i := 0; i < int(r.NumCols()); i++ {
-                       if err := fw.WriteColumnData(r.Column(i)); err != nil {
-                               fw.Close()
-                               return err
-                       }
+               if offset+size >= rec.NumRows() {
+                       break
+               }
+               if err := fw.NewBufferedRowGroupChecked(); err != nil {
+                       return err
                }
+               offset += size
+               size = utils.Min(maxRows, rec.NumRows()-offset)
        }
        fw.colIdx = 0
        return nil
@@ -375,34 +376,68 @@ func (fw *FileWriter) Write(rec arrow.RecordBatch) error {
                return fmt.Errorf("record schema does not match writer's. 
\nrecord: %s\nwriter: %s", rec.Schema(), fw.schema)
        }
 
-       var recList []arrow.RecordBatch
        rowgroupLen := fw.wr.Properties().MaxRowGroupLength()
-       if rec.NumRows() > rowgroupLen {
-               recList = make([]arrow.RecordBatch, 0)
-               for offset := int64(0); offset < rec.NumRows(); offset += 
rowgroupLen {
-                       s := rec.NewSlice(offset, offset+utils.Min(rowgroupLen, 
rec.NumRows()-offset))
-                       defer s.Release()
-                       recList = append(recList, s)
+       if rec.NumRows() <= rowgroupLen {
+               if err := fw.NewRowGroupChecked(); err != nil {
+                       return err
                }
-       } else {
-               recList = []arrow.RecordBatch{rec}
+               if err := fw.writeRecordBatchColumns(rec); err != nil {
+                       fw.Close()
+                       return err
+               }
+               fw.colIdx = 0
+               return fw.rgw.Close()
        }
 
-       for _, r := range recList {
+       columns := newChunkedRecordColumns(rec)
+       defer releaseChunkedRecordColumns(columns)
+
+       for offset := int64(0); offset < rec.NumRows(); offset += rowgroupLen {
                if err := fw.NewRowGroupChecked(); err != nil {
                        return err
                }
-               for i := 0; i < int(r.NumCols()); i++ {
-                       if err := fw.WriteColumnData(r.Column(i)); err != nil {
-                               fw.Close()
-                               return err
-                       }
+               size := utils.Min(rowgroupLen, rec.NumRows()-offset)
+               if err := fw.writeRecordBatchRange(columns, offset, size); err 
!= nil {
+                       fw.Close()
+                       return err
                }
        }
        fw.colIdx = 0
        return fw.rgw.Close()
 }
 
+func newChunkedRecordColumns(rec arrow.RecordBatch) []*arrow.Chunked {
+       columns := make([]*arrow.Chunked, int(rec.NumCols()))
+       for i, column := range rec.Columns() {
+               columns[i] = arrow.NewChunked(column.DataType(), 
[]arrow.Array{column})
+       }
+       return columns
+}
+
+func releaseChunkedRecordColumns(columns []*arrow.Chunked) {
+       for _, column := range columns {
+               column.Release()
+       }
+}
+
+func (fw *FileWriter) writeRecordBatchColumns(rec arrow.RecordBatch) error {
+       for i := 0; i < int(rec.NumCols()); i++ {
+               if err := fw.WriteColumnData(rec.Column(i)); err != nil {
+                       return err
+               }
+       }
+       return nil
+}
+
+func (fw *FileWriter) writeRecordBatchRange(columns []*arrow.Chunked, offset, 
size int64) error {
+       for _, column := range columns {
+               if err := fw.WriteColumnChunked(column, offset, size); err != 
nil {
+                       return err
+               }
+       }
+       return nil
+}
+
 // WriteTable writes an arrow table to the underlying file using chunkSize to 
determine
 // the size to break at for making row groups. Writing a table will always 
create a new
 // row group for each chunk of chunkSize rows in the table. Calling this with 
0 rows will
diff --git a/parquet/pqarrow/file_writer_range_bench_test.go 
b/parquet/pqarrow/file_writer_range_bench_test.go
new file mode 100644
index 00000000..e20d375a
--- /dev/null
+++ b/parquet/pqarrow/file_writer_range_bench_test.go
@@ -0,0 +1,102 @@
+// 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 pqarrow_test
+
+import (
+       "bytes"
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow"
+       "github.com/apache/arrow-go/v18/arrow/array"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/pqarrow"
+)
+
+func BenchmarkFileWriterRecordBatchRanges(b *testing.B) {
+       tests := []struct {
+               name     string
+               numCols  int
+               numRows  int
+               rowGroup int64
+       }{
+               {name: "1col_16rows_rg16", numCols: 1, numRows: 16, rowGroup: 
16},
+               {name: "8cols_8192rows_rg256", numCols: 8, numRows: 8192, 
rowGroup: 256},
+               {name: "32cols_4096rows_rg64", numCols: 32, numRows: 4096, 
rowGroup: 64},
+       }
+
+       for _, test := range tests {
+               schema, record := makeRangeWriteRecord(test.numCols, 
test.numRows)
+               b.Run(test.name, func(b *testing.B) {
+                       for _, method := range []struct {
+                               name  string
+                               write func(*pqarrow.FileWriter, 
arrow.RecordBatch) error
+                       }{
+                               {name: "Write", write: 
(*pqarrow.FileWriter).Write},
+                               {name: "WriteBuffered", write: 
(*pqarrow.FileWriter).WriteBuffered},
+                       } {
+                               b.Run(method.name, func(b *testing.B) {
+                                       props := parquet.NewWriterProperties(
+                                               
parquet.WithDictionaryDefault(false),
+                                               
parquet.WithMaxRowGroupLength(test.rowGroup),
+                                       )
+                                       arrProps := pqarrow.DefaultWriterProps()
+                                       var output bytes.Buffer
+                                       output.Grow(test.numCols * test.numRows 
* 8)
+
+                                       b.ReportAllocs()
+                                       b.SetBytes(int64(test.numCols * 
test.numRows * 8))
+                                       b.ResetTimer()
+                                       for i := 0; i < b.N; i++ {
+                                               output.Reset()
+                                               writer, err := 
pqarrow.NewFileWriter(schema, &output, props, arrProps)
+                                               if err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               if err := method.write(writer, 
record); err != nil {
+                                                       b.Fatal(err)
+                                               }
+                                               if err := writer.Close(); err 
!= nil {
+                                                       b.Fatal(err)
+                                               }
+                                       }
+                               })
+                       }
+               })
+               record.Release()
+       }
+}
+
+func makeRangeWriteRecord(numCols, numRows int) (*arrow.Schema, 
arrow.RecordBatch) {
+       fields := make([]arrow.Field, numCols)
+       for i := range fields {
+               fields[i] = arrow.Field{Name: fmt.Sprintf("column%d", i), Type: 
arrow.PrimitiveTypes.Int64}
+       }
+       schema := arrow.NewSchema(fields, nil)
+       builder := array.NewRecordBuilder(memory.DefaultAllocator, schema)
+       defer builder.Release()
+
+       values := make([]int64, numRows)
+       for col := 0; col < numCols; col++ {
+               for row := range values {
+                       values[row] = int64(row*numCols + col)
+               }
+               builder.Field(col).(*array.Int64Builder).AppendValues(values, 
nil)
+       }
+       return schema, builder.NewRecordBatch()
+}
diff --git a/parquet/pqarrow/file_writer_test.go 
b/parquet/pqarrow/file_writer_test.go
index b3672b0d..96b10f7f 100644
--- a/parquet/pqarrow/file_writer_test.go
+++ b/parquet/pqarrow/file_writer_test.go
@@ -18,6 +18,7 @@ package pqarrow_test
 
 import (
        "bytes"
+       "context"
        "math"
        "strings"
        "testing"
@@ -26,6 +27,7 @@ import (
        "github.com/apache/arrow-go/v18/arrow/array"
        "github.com/apache/arrow-go/v18/arrow/memory"
        "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/file"
        "github.com/apache/arrow-go/v18/parquet/pqarrow"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
@@ -209,6 +211,129 @@ func TestFileWriterTotalBytesBuffered(t *testing.T) {
        assert.Equal(t, int64(1120), writer.TotalBytesWritten())
 }
 
+func TestFileWriterRangeWritesPreserveData(t *testing.T) {
+       schema := arrow.NewSchema([]arrow.Field{
+               {Name: "number", Type: arrow.PrimitiveTypes.Int64, Nullable: 
true},
+               {Name: "text", Type: arrow.BinaryTypes.String, Nullable: true},
+               {Name: "values", Type: 
arrow.ListOf(arrow.PrimitiveTypes.Int32), Nullable: true},
+       }, nil)
+       record, _, err := array.RecordFromJSON(memory.DefaultAllocator, schema, 
strings.NewReader(`[
+               {"number": 1, "text": "one", "values": [1, 2]},
+               {"number": null, "text": "two", "values": []},
+               {"number": 3, "text": null, "values": null},
+               {"number": 4, "text": "four", "values": [4]},
+               {"number": 5, "text": "five", "values": [5, 6, 7]}
+       ]`))
+       require.NoError(t, err)
+       defer record.Release()
+
+       writeAndRead := func(t *testing.T, write func(*pqarrow.FileWriter) 
error) {
+               t.Helper()
+
+               var output bytes.Buffer
+               writer, err := pqarrow.NewFileWriter(
+                       schema,
+                       &output,
+                       
parquet.NewWriterProperties(parquet.WithMaxRowGroupLength(2)),
+                       pqarrow.DefaultWriterProps(),
+               )
+               require.NoError(t, err)
+               require.NoError(t, write(writer))
+               require.NoError(t, writer.Close())
+
+               reader, err := 
file.NewParquetReader(bytes.NewReader(output.Bytes()))
+               require.NoError(t, err)
+               require.Equal(t, 3, reader.NumRowGroups())
+               require.Equal(t, int64(5), reader.NumRows())
+               require.NoError(t, reader.Close())
+
+               got, err := pqarrow.ReadTable(context.Background(), 
bytes.NewReader(output.Bytes()), nil, pqarrow.ArrowReadProperties{}, 
memory.DefaultAllocator)
+               require.NoError(t, err)
+               defer got.Release()
+               require.Equal(t, int64(5), got.NumRows())
+               for i := 0; i < int(record.NumCols()); i++ {
+                       expected := 
arrow.NewChunked(record.Column(i).DataType(), []arrow.Array{record.Column(i)})
+                       require.Truef(t, array.ChunkedEqual(expected, 
got.Column(i).Data()), "column %d differs", i)
+                       expected.Release()
+               }
+       }
+
+       t.Run("Write", func(t *testing.T) {
+               writeAndRead(t, func(writer *pqarrow.FileWriter) error {
+                       return writer.Write(record)
+               })
+       })
+
+       t.Run("WriteBuffered", func(t *testing.T) {
+               writeAndRead(t, func(writer *pqarrow.FileWriter) error {
+                       return writer.WriteBuffered(record)
+               })
+       })
+
+       t.Run("WriteBufferedAcrossCalls", func(t *testing.T) {
+               first := record.NewSlice(0, 1)
+               defer first.Release()
+               second := record.NewSlice(1, record.NumRows())
+               defer second.Release()
+
+               writeAndRead(t, func(writer *pqarrow.FileWriter) error {
+                       if err := writer.WriteBuffered(first); err != nil {
+                               return err
+                       }
+                       return writer.WriteBuffered(second)
+               })
+       })
+
+       t.Run("WriteBufferedAtFullBoundary", func(t *testing.T) {
+               first := record.NewSlice(0, 2)
+               defer first.Release()
+               second := record.NewSlice(2, record.NumRows())
+               defer second.Release()
+
+               writeAndRead(t, func(writer *pqarrow.FileWriter) error {
+                       if err := writer.WriteBuffered(first); err != nil {
+                               return err
+                       }
+                       return writer.WriteBuffered(second)
+               })
+       })
+}
+
+func TestFileWriterZeroRowRecord(t *testing.T) {
+       schema := arrow.NewSchema([]arrow.Field{{Name: "value", Type: 
arrow.PrimitiveTypes.Int64}}, nil)
+       builder := array.NewRecordBuilder(memory.DefaultAllocator, schema)
+       record := builder.NewRecordBatch()
+       builder.Release()
+       defer record.Release()
+
+       for _, test := range []struct {
+               name  string
+               write func(*pqarrow.FileWriter, arrow.RecordBatch) error
+       }{
+               {name: "Write", write: (*pqarrow.FileWriter).Write},
+               {name: "WriteBuffered", write: 
(*pqarrow.FileWriter).WriteBuffered},
+       } {
+               t.Run(test.name, func(t *testing.T) {
+                       var output bytes.Buffer
+                       writer, err := pqarrow.NewFileWriter(
+                               schema,
+                               &output,
+                               
parquet.NewWriterProperties(parquet.WithMaxRowGroupLength(2)),
+                               pqarrow.DefaultWriterProps(),
+                       )
+                       require.NoError(t, err)
+                       require.NoError(t, test.write(writer, record))
+                       require.NoError(t, writer.Close())
+
+                       reader, err := 
file.NewParquetReader(bytes.NewReader(output.Bytes()))
+                       require.NoError(t, err)
+                       require.Equal(t, 1, reader.NumRowGroups())
+                       require.Equal(t, int64(0), reader.NumRows())
+                       require.NoError(t, reader.Close())
+               })
+       }
+}
+
 func TestWriteOnClosedFileWriter(t *testing.T) {
        schema := arrow.NewSchema([]arrow.Field{
                {Name: "one", Nullable: true, Type: 
arrow.PrimitiveTypes.Float64},

Reply via email to