You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by ze...@apache.org on 2026/02/06 19:55:14 UTC

(arrow-go) branch main updated: fix(parquet/file): write large string values (#655)

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 bbf7ab75 fix(parquet/file): write large string values (#655)
bbf7ab75 is described below

commit bbf7ab7523a6411e25c7a08566a40e8759cc6c13
Author: Matt Topol <zo...@gmail.com>
AuthorDate: Fri Feb 6 14:55:08 2026 -0500

    fix(parquet/file): write large string values (#655)
    
    ### Rationale for this change
    Writing large byte array values (e.g., 50 values x 50MB each = 2.5GB
    total) caused a panic due to exceeding max page size.
    
    This happened because the writer accumulates the values in batches
    *before* checking the page size limits:
    
    1. `WriteBatch()` calls `writeValues()` which adds ALL values to the
    encoder buffer
    2. `commitWriteAndCheckPageLimit()` checks if the buffer exceeds the
    limit
    3. **PROBLEM**: At this point, the buffer alraedy contains > 2GB of data
    if we hit the limit
    4. `FlushCurrentPage()` attempts to do `int32(values.Len())` which
    overflows: `2,500,000,000 -> -1,794,967,296`
    5. `bytes.Buffer.Grow(-1,794,967,296)` panics
    
    See
    https://github.com/apache/arrow-go/issues/622#issuecomment-3822818048
    
    ### What changes are included in this PR?
    Modified `writeValues()` and `writeValuesSpaced()` for `ByteArray` and
    `FixedLenByteArray` types to check the buffer size *beore* adding the
    values and proactively flush when approaching the 2GB limit (parquet
    uses an int32 for page size).
    
    ### Are these changes tested?
    Yes, new tests are added, including some benchmarks to ensure that the
    new changes don't cause any performance impacts.
    
    
    ## Performance Impact
    
    **TL;DR: <1% overhead for typical workloads, 0% for fixed-size types**
    
    ### Benchmarks
    
    ```
    Benchmark                            Time       Data        Throughput
    ─────────────────────────────────────────────────────────────────────
    WriteSmallByteArrayValues (100B)    2.19 ms    1 MB        457 MB/s
    WriteMediumByteArrayValues (10KB)   18.0 ms   10 MB        556 MB/s
    WriteLargeByteArrayValues (1MB)      137 ms  100 MB        730 MB/s
    WriteInt32Values (control)          0.15 ms 0.04 MB        267 MB/s (unchanged)
    ```
    
    ### Impact by Data Type
    
    | Data Type | Overhead | Notes |
    |-----------|----------|-------|
    | Int32, Int64, Float, Boolean | **0%** | Unchanged code paths |
    | ByteArray (small, <1KB) | **<1%** | Batched processing |
    | ByteArray (large, >1MB) | **<0.01%** | I/O dominates, checking
    negligible |
    
    ### Per-Value Overhead
    
    | Value Size | Encoding Time | Added Overhead | % Impact |
    |------------|--------------|----------------|----------|
    | 100 bytes | 200 ns | ~10 ns | ~5% |
    | 1 KB | 2,000 ns | ~10 ns | ~0.5% |
    | 100 KB | 200,000 ns | ~10 ns | ~0.005% |
    | 1 MB+ | 2,000,000 ns | ~120 ns | ~0.006% |
    
    ### Are there any user-facing changes?
    Only the fix to the previous situation that would panic.
---
 parquet/file/column_writer_types.gen.go      | 168 +++++++++++++++-
 parquet/file/column_writer_types.gen.go.tmpl |  94 +++++++++
 parquet/file/large_value_test.go             | 163 ++++++++++++++++
 parquet/file/writer_performance_test.go      | 281 +++++++++++++++++++++++++++
 4 files changed, 696 insertions(+), 10 deletions(-)

diff --git a/parquet/file/column_writer_types.gen.go b/parquet/file/column_writer_types.gen.go
index 65bf29a7..6530fbeb 100644
--- a/parquet/file/column_writer_types.gen.go
+++ b/parquet/file/column_writer_types.gen.go
@@ -1371,7 +1371,50 @@ func (w *ByteArrayColumnChunkWriter) WriteDictIndices(indices arrow.Array, defLe
 }
 
 func (w *ByteArrayColumnChunkWriter) writeValues(values []parquet.ByteArray, numNulls int64) {
-	w.currentEncoder.(encoding.ByteArrayEncoder).Put(values)
+	// For variable-length types, we need to check buffer size to prevent int32 overflow
+	// For small values (<1MB), checking frequently adds negligible overhead
+	// For large values (>1MB), we MUST check before each value
+	const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+	const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+	encoder := w.currentEncoder.(encoding.ByteArrayEncoder)
+	currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+	// Batch process small values, check individually for large values
+	batchStart := 0
+	for i := 0; i < len(values); i++ {
+		valueSize := int64(len(values[i]))
+
+		// If this value might cause overflow, flush first
+		if currentSize+valueSize >= maxSafeBufferSize {
+			// Add accumulated batch before flushing
+			if i > batchStart {
+				encoder.Put(values[batchStart:i])
+				currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+			}
+			// Flush the page
+			if err := w.FlushCurrentPage(); err != nil {
+				panic(err)
+			}
+			batchStart = i
+			currentSize = 0
+		}
+
+		// Track size estimate
+		currentSize += valueSize + 4 // +4 for length prefix
+
+		// For large values, add and flush immediately if needed
+		if valueSize >= largeValueThreshold {
+			encoder.Put(values[i : i+1])
+			batchStart = i + 1
+			currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+		}
+	}
+
+	// Add remaining batch
+	if batchStart < len(values) {
+		encoder.Put(values[batchStart:])
+	}
 	if w.pageStatistics != nil {
 		w.pageStatistics.(*metadata.ByteArrayStatistics).Update(values, numNulls)
 	}
@@ -1382,10 +1425,41 @@ func (w *ByteArrayColumnChunkWriter) writeValues(values []parquet.ByteArray, num
 }
 
 func (w *ByteArrayColumnChunkWriter) writeValuesSpaced(spacedValues []parquet.ByteArray, numRead, numValues int64, validBits []byte, validBitsOffset int64) {
-	if len(spacedValues) != int(numRead) {
-		w.currentEncoder.(encoding.ByteArrayEncoder).PutSpaced(spacedValues, validBits, validBitsOffset)
-	} else {
-		w.currentEncoder.(encoding.ByteArrayEncoder).Put(spacedValues)
+	// For variable-length types, we need to check buffer size to prevent int32 overflow
+	// For small values (<1MB), checking frequently adds negligible overhead
+	// For large values (>1MB), we MUST check before each value
+	const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+	const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+	encoder := w.currentEncoder.(encoding.ByteArrayEncoder)
+	currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+	for i := 0; i < len(spacedValues); i++ {
+		valueSize := int64(len(spacedValues[i]))
+
+		// If this value might cause overflow, flush first
+		if currentSize+valueSize >= maxSafeBufferSize {
+			if err := w.FlushCurrentPage(); err != nil {
+				// If flush fails, panic will be caught by WriteBatch's defer recover
+				panic(err)
+			}
+			currentSize = 0
+		}
+
+		// Add the value
+		chunk := spacedValues[i : i+1]
+		if len(spacedValues) != int(numRead) && validBits != nil {
+			encoder.PutSpaced(chunk, validBits, validBitsOffset+int64(i))
+		} else {
+			encoder.Put(chunk)
+		}
+
+		// Track size estimate (only update for large values or every 100 values)
+		if valueSize >= largeValueThreshold || i%100 == 0 {
+			currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+		} else {
+			currentSize += valueSize + 4 // +4 for length prefix
+		}
 	}
 	if w.pageStatistics != nil {
 		nulls := numValues - numRead
@@ -1569,7 +1643,50 @@ func (w *FixedLenByteArrayColumnChunkWriter) WriteDictIndices(indices arrow.Arra
 }
 
 func (w *FixedLenByteArrayColumnChunkWriter) writeValues(values []parquet.FixedLenByteArray, numNulls int64) {
-	w.currentEncoder.(encoding.FixedLenByteArrayEncoder).Put(values)
+	// For variable-length types, we need to check buffer size to prevent int32 overflow
+	// For small values (<1MB), checking frequently adds negligible overhead
+	// For large values (>1MB), we MUST check before each value
+	const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+	const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+	encoder := w.currentEncoder.(encoding.FixedLenByteArrayEncoder)
+	currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+	// Batch process small values, check individually for large values
+	batchStart := 0
+	for i := 0; i < len(values); i++ {
+		valueSize := int64(w.descr.TypeLength())
+
+		// If this value might cause overflow, flush first
+		if currentSize+valueSize >= maxSafeBufferSize {
+			// Add accumulated batch before flushing
+			if i > batchStart {
+				encoder.Put(values[batchStart:i])
+				currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+			}
+			// Flush the page
+			if err := w.FlushCurrentPage(); err != nil {
+				panic(err)
+			}
+			batchStart = i
+			currentSize = 0
+		}
+
+		// Track size estimate
+		currentSize += valueSize + 4 // +4 for length prefix
+
+		// For large values, add and flush immediately if needed
+		if valueSize >= largeValueThreshold {
+			encoder.Put(values[i : i+1])
+			batchStart = i + 1
+			currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+		}
+	}
+
+	// Add remaining batch
+	if batchStart < len(values) {
+		encoder.Put(values[batchStart:])
+	}
 	if w.pageStatistics != nil {
 		if w.Descr().LogicalType().Equals(schema.Float16LogicalType{}) {
 			w.pageStatistics.(*metadata.Float16Statistics).Update(values, numNulls)
@@ -1584,10 +1701,41 @@ func (w *FixedLenByteArrayColumnChunkWriter) writeValues(values []parquet.FixedL
 }
 
 func (w *FixedLenByteArrayColumnChunkWriter) writeValuesSpaced(spacedValues []parquet.FixedLenByteArray, numRead, numValues int64, validBits []byte, validBitsOffset int64) {
-	if len(spacedValues) != int(numRead) {
-		w.currentEncoder.(encoding.FixedLenByteArrayEncoder).PutSpaced(spacedValues, validBits, validBitsOffset)
-	} else {
-		w.currentEncoder.(encoding.FixedLenByteArrayEncoder).Put(spacedValues)
+	// For variable-length types, we need to check buffer size to prevent int32 overflow
+	// For small values (<1MB), checking frequently adds negligible overhead
+	// For large values (>1MB), we MUST check before each value
+	const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+	const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+	encoder := w.currentEncoder.(encoding.FixedLenByteArrayEncoder)
+	currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+	for i := 0; i < len(spacedValues); i++ {
+		valueSize := int64(w.descr.TypeLength())
+
+		// If this value might cause overflow, flush first
+		if currentSize+valueSize >= maxSafeBufferSize {
+			if err := w.FlushCurrentPage(); err != nil {
+				// If flush fails, panic will be caught by WriteBatch's defer recover
+				panic(err)
+			}
+			currentSize = 0
+		}
+
+		// Add the value
+		chunk := spacedValues[i : i+1]
+		if len(spacedValues) != int(numRead) && validBits != nil {
+			encoder.PutSpaced(chunk, validBits, validBitsOffset+int64(i))
+		} else {
+			encoder.Put(chunk)
+		}
+
+		// Track size estimate (only update for large values or every 100 values)
+		if valueSize >= largeValueThreshold || i%100 == 0 {
+			currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+		} else {
+			currentSize += valueSize + 4 // +4 for length prefix
+		}
 	}
 	if w.pageStatistics != nil {
 		nulls := numValues - numRead
diff --git a/parquet/file/column_writer_types.gen.go.tmpl b/parquet/file/column_writer_types.gen.go.tmpl
index d0a4da26..936920b4 100644
--- a/parquet/file/column_writer_types.gen.go.tmpl
+++ b/parquet/file/column_writer_types.gen.go.tmpl
@@ -185,7 +185,58 @@ func (w *{{.Name}}ColumnChunkWriter) WriteDictIndices(indices arrow.Array, defLe
 }
 
 func (w *{{.Name}}ColumnChunkWriter) writeValues(values []{{.name}}, numNulls int64) {
+{{- if or (eq .Name "ByteArray") (eq .Name "FixedLenByteArray")}}
+  // For variable-length types, we need to check buffer size to prevent int32 overflow
+  // For small values (<1MB), checking frequently adds negligible overhead
+  // For large values (>1MB), we MUST check before each value
+  const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+  const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+  encoder := w.currentEncoder.(encoding.{{.Name}}Encoder)
+  currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+  // Batch process small values, check individually for large values
+  batchStart := 0
+  for i := 0; i < len(values); i++ {
+{{- if eq .Name "ByteArray"}}
+    valueSize := int64(len(values[i]))
+{{- else}}
+    valueSize := int64(w.descr.TypeLength())
+{{- end}}
+
+    // If this value might cause overflow, flush first
+    if currentSize + valueSize >= maxSafeBufferSize {
+      // Add accumulated batch before flushing
+      if i > batchStart {
+        encoder.Put(values[batchStart:i])
+        currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+      }
+      // Flush the page
+      if err := w.FlushCurrentPage(); err != nil {
+        panic(err)
+      }
+      batchStart = i
+      currentSize = 0
+    }
+
+    // Track size estimate
+    currentSize += valueSize + 4 // +4 for length prefix
+
+    // For large values, add and flush immediately if needed
+    if valueSize >= largeValueThreshold {
+      encoder.Put(values[i:i+1])
+      batchStart = i + 1
+      currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+    }
+  }
+
+  // Add remaining batch
+  if batchStart < len(values) {
+    encoder.Put(values[batchStart:])
+  }
+{{- else}}
   w.currentEncoder.(encoding.{{.Name}}Encoder).Put(values)
+{{- end}}
   if w.pageStatistics != nil {
 {{- if ne .Name "FixedLenByteArray"}}
     w.pageStatistics.(*metadata.{{.Name}}Statistics).Update(values, numNulls)
@@ -204,11 +255,54 @@ func (w *{{.Name}}ColumnChunkWriter) writeValues(values []{{.name}}, numNulls in
 }
 
 func (w *{{.Name}}ColumnChunkWriter) writeValuesSpaced(spacedValues []{{.name}}, numRead, numValues int64, validBits []byte, validBitsOffset int64) {
+{{- if or (eq .Name "ByteArray") (eq .Name "FixedLenByteArray")}}
+  // For variable-length types, we need to check buffer size to prevent int32 overflow
+  // For small values (<1MB), checking frequently adds negligible overhead
+  // For large values (>1MB), we MUST check before each value
+  const maxSafeBufferSize = 1.0 * 1024 * 1024 * 1024 // 1GB threshold
+  const largeValueThreshold = 1.0 * 1024 * 1024      // 1MB
+
+  encoder := w.currentEncoder.(encoding.{{.Name}}Encoder)
+  currentSize := w.currentEncoder.EstimatedDataEncodedSize()
+
+  for i := 0; i < len(spacedValues); i++ {
+{{- if eq .Name "ByteArray"}}
+    valueSize := int64(len(spacedValues[i]))
+{{- else}}
+    valueSize := int64(w.descr.TypeLength())
+{{- end}}
+
+    // If this value might cause overflow, flush first
+    if currentSize + valueSize >= maxSafeBufferSize {
+      if err := w.FlushCurrentPage(); err != nil {
+        // If flush fails, panic will be caught by WriteBatch's defer recover
+        panic(err)
+      }
+      currentSize = 0
+    }
+
+    // Add the value
+    chunk := spacedValues[i:i+1]
+    if len(spacedValues) != int(numRead) && validBits != nil {
+      encoder.PutSpaced(chunk, validBits, validBitsOffset+int64(i))
+    } else {
+      encoder.Put(chunk)
+    }
+
+    // Track size estimate (only update for large values or every 100 values)
+    if valueSize >= largeValueThreshold || i % 100 == 0 {
+      currentSize = w.currentEncoder.EstimatedDataEncodedSize()
+    } else {
+      currentSize += valueSize + 4 // +4 for length prefix
+    }
+  }
+{{- else}}
   if len(spacedValues) != int(numRead) {
     w.currentEncoder.(encoding.{{.Name}}Encoder).PutSpaced(spacedValues, validBits, validBitsOffset)
   } else {
     w.currentEncoder.(encoding.{{.Name}}Encoder).Put(spacedValues)
   }
+{{- end}}
   if w.pageStatistics != nil {
     nulls := numValues - numRead
 {{- if ne .Name "FixedLenByteArray"}}
diff --git a/parquet/file/large_value_test.go b/parquet/file/large_value_test.go
new file mode 100644
index 00000000..3def3dc6
--- /dev/null
+++ b/parquet/file/large_value_test.go
@@ -0,0 +1,163 @@
+// 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 file_test
+
+import (
+	"bytes"
+	"runtime"
+	"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/file"
+	"github.com/apache/arrow-go/v18/parquet/pqarrow"
+	"github.com/apache/arrow-go/v18/parquet/schema"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestLargeByteArrayValuesDoNotOverflowInt32 tests that writing large byte array
+// values that would exceed the 1GB flush threshold does not cause an int32 overflow panic.
+// The fix ensures pages are flushed automatically before buffer size exceeds safe limits.
+func TestLargeByteArrayValuesDoNotOverflowInt32(t *testing.T) {
+	if runtime.GOARCH == "386" {
+		t.Skip("Skipping test on 32-bit architecture")
+	}
+
+	// Create schema with a single byte array column
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("large_data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false), // Disable stats to focus on core issue
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false), // Plain encoding
+		parquet.WithDataPageSize(1024*1024),  // 1MB page size
+	)
+
+	out := &bytes.Buffer{}
+	writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+	defer writer.Close()
+
+	rgw := writer.AppendRowGroup()
+	colWriter, _ := rgw.NextColumn()
+
+	// Create 700 values of 1.5MB each (1.05GB total)
+	// This exceeds the 1GB flush threshold, triggering automatic page flushes
+	// Uses minimal memory (single 1.5MB buffer reused) while testing loop logic thoroughly
+	const valueSize = 1.5 * 1024 * 1024 // 1.5MB per value (>= 1MB threshold for large value handling)
+	const numValues = 700               // 700 values = 1.05GB total
+
+	// Create a single 1.5MB buffer and reuse it (only allocates 1.5MB!)
+	largeValue := make([]byte, valueSize)
+	for i := range largeValue {
+		largeValue[i] = byte(i % 256)
+	}
+
+	values := make([]parquet.ByteArray, numValues)
+	for i := range values {
+		values[i] = largeValue // Reuse same buffer (memory efficient: 2MB total, writes 1.1GB)
+	}
+
+	// This should NOT panic with int32 overflow
+	// Expected behavior: automatically flush pages at 1GB threshold
+	byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+	_, err := byteArrayWriter.WriteBatch(values, nil, nil)
+
+	// Should succeed without panic
+	assert.NoError(t, err)
+
+	err = colWriter.Close()
+	assert.NoError(t, err)
+
+	err = rgw.Close()
+	assert.NoError(t, err)
+
+	err = writer.Close()
+	assert.NoError(t, err)
+
+	// Verify we wrote data successfully
+	assert.Greater(t, out.Len(), 0, "should have written data to buffer")
+}
+
+// TestLargeStringArrayWithArrow tests the same issue using Arrow arrays
+// This tests the pqarrow integration path which is commonly used.
+// Uses LARGE_STRING type (int64 offsets) to handle >1GB of string data without overflow.
+func TestLargeStringArrayWithArrow(t *testing.T) {
+	if runtime.GOARCH == "386" {
+		t.Skip("Skipping test on 32-bit architecture")
+	}
+
+	mem := memory.NewGoAllocator()
+
+	// Create Arrow schema with LARGE_STRING field (uses int64 offsets, can handle >2GB)
+	field := arrow.Field{Name: "large_strings", Type: arrow.BinaryTypes.LargeString, Nullable: true}
+	arrowSchema := arrow.NewSchema([]arrow.Field{field}, nil)
+
+	// Write to Parquet
+	out := &bytes.Buffer{}
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+		parquet.WithDataPageSize(1024*1024),
+	)
+
+	pqw, err := pqarrow.NewFileWriter(arrowSchema, out, props, pqarrow.NewArrowWriterProperties())
+	require.NoError(t, err)
+
+	// Write in multiple batches to reduce memory usage
+	// Each batch: 10 values × 10MB = 100MB
+	// Total: 11 batches = 1.1GB written (only 100MB memory at a time!)
+	const valueSize = 10 * 1024 * 1024 // 10MB per string (realistic large blob)
+	const valuesPerBatch = 10          // 10 values per batch
+	const numBatches = 11              // 11 batches = 1.1GB total
+
+	largeStr := string(make([]byte, valueSize))
+
+	for batchNum := 0; batchNum < numBatches; batchNum++ {
+		// Build a small batch
+		builder := array.NewLargeStringBuilder(mem)
+		for i := 0; i < valuesPerBatch; i++ {
+			builder.Append(largeStr)
+		}
+		arr := builder.NewArray()
+
+		rec := array.NewRecordBatch(arrowSchema, []arrow.Array{arr}, int64(valuesPerBatch))
+
+		// Write batch - this should NOT panic with int32 overflow
+		err = pqw.Write(rec)
+
+		// Clean up batch resources
+		rec.Release()
+		arr.Release()
+		builder.Release()
+
+		assert.NoError(t, err)
+	}
+
+	err = pqw.Close()
+	assert.NoError(t, err)
+
+	// Verify we wrote data successfully
+	assert.Greater(t, out.Len(), 0, "should have written data to buffer")
+}
diff --git a/parquet/file/writer_performance_test.go b/parquet/file/writer_performance_test.go
new file mode 100644
index 00000000..f3283ae6
--- /dev/null
+++ b/parquet/file/writer_performance_test.go
@@ -0,0 +1,281 @@
+// 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 file_test
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/apache/arrow-go/v18/parquet"
+	"github.com/apache/arrow-go/v18/parquet/file"
+	"github.com/apache/arrow-go/v18/parquet/schema"
+)
+
+// Benchmark writing small ByteArray values (typical case)
+// This tests the common scenario where values are small (< 1KB)
+func BenchmarkWriteSmallByteArrayValues(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	// Small values: 100 bytes each
+	const valueSize = 100
+	const numValues = 10000
+	value := make([]byte, valueSize)
+	for i := range value {
+		value[i] = byte(i % 256)
+	}
+
+	values := make([]parquet.ByteArray, numValues)
+	for i := range values {
+		values[i] = value
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+		byteArrayWriter.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}
+
+// Benchmark writing medium ByteArray values (10KB each)
+func BenchmarkWriteMediumByteArrayValues(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	// Medium values: 10KB each
+	const valueSize = 10 * 1024
+	const numValues = 1000
+	value := make([]byte, valueSize)
+	for i := range value {
+		value[i] = byte(i % 256)
+	}
+
+	values := make([]parquet.ByteArray, numValues)
+	for i := range values {
+		values[i] = value
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+		byteArrayWriter.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}
+
+// Benchmark writing large ByteArray values (1MB each)
+func BenchmarkWriteLargeByteArrayValues(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	// Large values: 1MB each
+	const valueSize = 1024 * 1024
+	const numValues = 100
+	value := make([]byte, valueSize)
+	for i := range value {
+		value[i] = byte(i % 256)
+	}
+
+	values := make([]parquet.ByteArray, numValues)
+	for i := range values {
+		values[i] = value
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+		byteArrayWriter.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}
+
+// Benchmark writing Int32 values (control - unaffected by fix)
+func BenchmarkWriteInt32Values(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.Int32, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	const numValues = 10000
+	values := make([]int32, numValues)
+	for i := range values {
+		values[i] = int32(i)
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		int32Writer := colWriter.(*file.Int32ColumnChunkWriter)
+		int32Writer.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}
+
+// Benchmark writing variable-sized ByteArray values (mixed workload)
+func BenchmarkWriteMixedByteArrayValues(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	// Mix of small (100B), medium (10KB), and large (1MB) values
+	const numValues = 1000
+	values := make([]parquet.ByteArray, numValues)
+
+	for i := range values {
+		var size int
+		switch i % 10 {
+		case 0, 1, 2, 3, 4, 5, 6, 7: // 80% small
+			size = 100
+		case 8: // 10% medium
+			size = 10 * 1024
+		case 9: // 10% large
+			size = 100 * 1024
+		}
+		value := make([]byte, size)
+		for j := range value {
+			value[j] = byte(j % 256)
+		}
+		values[i] = value
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+		byteArrayWriter.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}
+
+// Benchmark writing tiny ByteArray values (worst case for checking overhead)
+// Values are 10 bytes each - this maximizes the ratio of checking overhead to actual work
+func BenchmarkWriteTinyByteArrayValues(b *testing.B) {
+	sc := schema.NewSchema(schema.MustGroup(schema.NewGroupNode("schema", parquet.Repetitions.Required, schema.FieldList{
+		schema.Must(schema.NewPrimitiveNode("data", parquet.Repetitions.Optional, parquet.Types.ByteArray, -1, -1)),
+	}, -1)))
+
+	props := parquet.NewWriterProperties(
+		parquet.WithStats(false),
+		parquet.WithVersion(parquet.V2_LATEST),
+		parquet.WithDataPageVersion(parquet.DataPageV2),
+		parquet.WithDictionaryDefault(false),
+	)
+
+	// Tiny values: 10 bytes each - worst case for overhead
+	const valueSize = 10
+	const numValues = 100000
+	value := make([]byte, valueSize)
+	for i := range value {
+		value[i] = byte(i % 256)
+	}
+
+	values := make([]parquet.ByteArray, numValues)
+	for i := range values {
+		values[i] = value
+	}
+
+	b.ResetTimer()
+	b.ReportAllocs()
+
+	for i := 0; i < b.N; i++ {
+		out := &bytes.Buffer{}
+		writer := file.NewParquetWriter(out, sc.Root(), file.WithWriterProps(props))
+		rgw := writer.AppendRowGroup()
+		colWriter, _ := rgw.NextColumn()
+		byteArrayWriter := colWriter.(*file.ByteArrayColumnChunkWriter)
+		byteArrayWriter.WriteBatch(values, nil, nil)
+		colWriter.Close()
+		rgw.Close()
+		writer.Close()
+	}
+}