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 066b7916 perf(parquet/pqarrow): write binary values from Arrow offsets 
(#1262)
066b7916 is described below

commit 066b79161f09ae9055ce4439e4e1ab7b9b43cbdc
Author: Minh Vu <[email protected]>
AuthorDate: Wed Sep 2 20:24:13 2026 +0200

    perf(parquet/pqarrow): write binary values from Arrow offsets (#1262)
    
    ## Summary
    
    - **Writes Binary, String, LargeBinary, and LargeString values from
    Arrow buffers and offsets.**
    - Removes the full `[]parquet.ByteArray` allocation from non-dictionary
    writes.
    - Keeps nullable writes, page statistics, bloom filters, page limits,
    and dictionary fallback behavior.
    - Adds coverage for 32-bit and 64-bit offsets, nulls, slices, small
    batches, and all byte-array encodings.
    
    ## Benchmark
    
    64K String rows, uncompressed, dictionary disabled, Apple M1 Pro. Median
    of 3 runs with 3 seconds per run.
    
    | stats | before | after | heap/op |
    | --- | ---: | ---: | ---: |
    | false | 1.798 ms | 1.260 ms | 10.62 MB -> 9.04 MB |
    | true | 2.343 ms | 1.861 ms | 10.62 MB -> 9.06 MB |
    
    Benchmark command: `go test ./parquet/pqarrow -run '^$' -bench
    '^BenchmarkWriteArrowBinaryOffsets$' -benchmem -benchtime=3s -count=3`
    
    ## Tests
    
    - `go test ./parquet/...`
    - `go vet ./parquet/internal/encoding ./parquet/metadata ./parquet/file
    ./parquet/pqarrow`
---
 parquet/file/byte_array_column_writer_arrow.go     | 345 +++++++++++++++++++++
 .../internal/encoding/byte_array_arrow_encoder.go  | 232 ++++++++++++++
 .../encoding/byte_array_arrow_encoder_test.go      | 190 ++++++++++++
 parquet/metadata/bloom_filter_arrow_offsets.go     | 100 ++++++
 parquet/metadata/byte_array_arrow_offsets.go       |  89 ++++++
 parquet/metadata/byte_array_arrow_offsets_test.go  | 114 +++++++
 parquet/pqarrow/binary_offsets_bench_test.go       |  75 +++++
 parquet/pqarrow/binary_offsets_test.go             | 234 ++++++++++++++
 parquet/pqarrow/encode_arrow.go                    |  24 ++
 9 files changed, 1403 insertions(+)

diff --git a/parquet/file/byte_array_column_writer_arrow.go 
b/parquet/file/byte_array_column_writer_arrow.go
new file mode 100644
index 00000000..ce99540c
--- /dev/null
+++ b/parquet/file/byte_array_column_writer_arrow.go
@@ -0,0 +1,345 @@
+// 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
+
+import (
+       "fmt"
+
+       "github.com/apache/arrow-go/v18/internal/utils"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/metadata"
+)
+
+type byteArrayArrowOffset interface {
+       ~int32 | ~int64
+}
+
+type byteArrayArrowEncoder32 interface {
+       PutArrow([]byte, []int32)
+       PutArrowSpaced([]byte, []int32, []byte, int64)
+}
+
+type byteArrayArrowEncoder64 interface {
+       PutArrow64([]byte, []int64)
+       PutArrowSpaced64([]byte, []int64, []byte, int64)
+}
+
+func arrowOffsetsForBatch[T byteArrayArrowOffset](offsets []T, offset, count 
int64) []T {
+       if offset < 0 || offset > int64(len(offsets)) {
+               panic("parquet: Arrow offset index is out of bounds")
+       }
+       if count == 0 {
+               if offset == int64(len(offsets)) {
+                       return nil
+               }
+               return offsets[offset : offset+1]
+       }
+
+       end := offset + count + 1
+       if end > int64(len(offsets)) {
+               panic("parquet: Arrow offset range is out of bounds")
+       }
+       return offsets[offset:end]
+}
+
+// SupportsArrowOffsets reports whether the active byte-array encoder can 
consume
+// Arrow's value buffer and offsets without materializing parquet.ByteArray 
values.
+func (w *ByteArrayColumnChunkWriter) SupportsArrowOffsets() bool {
+       _, supports32 := w.currentEncoder.(byteArrayArrowEncoder32)
+       _, supports64 := w.currentEncoder.(byteArrayArrowEncoder64)
+       return supports32 || supports64
+}
+
+func writeArrowValues[T byteArrayArrowOffset](w *ByteArrayColumnChunkWriter, 
values []byte, offsets []T, numNulls int64) {
+       switch offsets := any(offsets).(type) {
+       case []int32:
+               enc, ok := w.currentEncoder.(byteArrayArrowEncoder32)
+               if !ok {
+                       panic("parquet: current byte-array encoder does not 
support Arrow offsets")
+               }
+               enc.PutArrow(values, offsets)
+               if w.pageStatistics != nil {
+                       
w.pageStatistics.(*metadata.ByteArrayStatistics).UpdateFromArrowOffsets(values, 
offsets, numNulls)
+               }
+               if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+                       metadata.InsertArrowOffsetHashes(w.bloomFilter, values, 
offsets)
+               }
+       case []int64:
+               enc, ok := w.currentEncoder.(byteArrayArrowEncoder64)
+               if !ok {
+                       panic("parquet: current byte-array encoder does not 
support Arrow offsets")
+               }
+               enc.PutArrow64(values, offsets)
+               if w.pageStatistics != nil {
+                       
w.pageStatistics.(*metadata.ByteArrayStatistics).UpdateFromArrowOffsets64(values,
 offsets, numNulls)
+               }
+               if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+                       metadata.InsertArrowOffsetHashes64(w.bloomFilter, 
values, offsets)
+               }
+       }
+}
+
+func writeArrowValuesSpaced[T byteArrayArrowOffset](w 
*ByteArrayColumnChunkWriter, values []byte, offsets []T, numRead, numValues 
int64, validBits []byte, validBitsOffset int64) {
+       numSpaced := int64(0)
+       if len(offsets) > 0 {
+               numSpaced = int64(len(offsets) - 1)
+       }
+
+       switch offsets := any(offsets).(type) {
+       case []int32:
+               enc, ok := w.currentEncoder.(byteArrayArrowEncoder32)
+               if !ok {
+                       panic("parquet: current byte-array encoder does not 
support Arrow offsets")
+               }
+               if numSpaced != numRead {
+                       enc.PutArrowSpaced(values, offsets, validBits, 
validBitsOffset)
+               } else {
+                       enc.PutArrow(values, offsets)
+               }
+               if w.pageStatistics != nil {
+                       stats := 
w.pageStatistics.(*metadata.ByteArrayStatistics)
+                       stats.UpdateFromArrowOffsetsSpaced(values, offsets, 
validBits, validBitsOffset, numSpaced-numRead)
+                       stats.IncNulls(numValues - numSpaced)
+               }
+               if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+                       metadata.InsertSpacedArrowOffsetHashes(w.bloomFilter, 
numRead, values, offsets, validBits, validBitsOffset)
+               }
+       case []int64:
+               enc, ok := w.currentEncoder.(byteArrayArrowEncoder64)
+               if !ok {
+                       panic("parquet: current byte-array encoder does not 
support Arrow offsets")
+               }
+               if numSpaced != numRead {
+                       enc.PutArrowSpaced64(values, offsets, validBits, 
validBitsOffset)
+               } else {
+                       enc.PutArrow64(values, offsets)
+               }
+               if w.pageStatistics != nil {
+                       stats := 
w.pageStatistics.(*metadata.ByteArrayStatistics)
+                       stats.UpdateFromArrowOffsetsSpaced64(values, offsets, 
validBits, validBitsOffset, numSpaced-numRead)
+                       stats.IncNulls(numValues - numSpaced)
+               }
+               if w.bloomFilter != nil && w.currentEncoder.Encoding() != 
parquet.Encodings.PlainDict {
+                       metadata.InsertSpacedArrowOffsetHashes64(w.bloomFilter, 
numRead, values, offsets, validBits, validBitsOffset)
+               }
+       }
+}
+
+func writeBatchArrow[T byteArrayArrowOffset](w *ByteArrayColumnChunkWriter, 
values []byte, offsets []T, defLevels, repLevels []int16) (valueOffset int64) {
+       var n int64
+       if defLevels != nil {
+               n = int64(len(defLevels))
+       } else if len(offsets) > 0 {
+               n = int64(len(offsets) - 1)
+       }
+       if n == 0 {
+               return 0
+       }
+
+       const maxSafeBatchDataSize int64 = 1 << 30
+       batchSize := w.props.WriteBatchSize()
+       maxDefLevel := w.descr.MaxDefinitionLevel()
+       requiresRowAlignment := (w.props.DataPageVersion() != 
parquet.DataPageV1 ||
+               w.props.PageIndexEnabledFor(w.descr.Path())) &&
+               repLevels != nil && w.descr.MaxRepetitionLevel() > 0
+       levelOffset := int64(0)
+       valueCount := int64(0)
+       if len(offsets) > 0 {
+               valueCount = int64(len(offsets) - 1)
+       }
+
+       if requiresRowAlignment {
+               if int64(len(repLevels)) < n {
+                       panic("columnwriter: not enough repetition levels for 
batch to write")
+               }
+               if repLevels[0] != 0 {
+                       panic("columnwriter: row-aligned batch writing must 
start at a row boundary")
+               }
+               repLevels = repLevels[:n]
+       }
+
+       for levelOffset < n {
+               remaining := n - levelOffset
+               batch := min(remaining, batchSize)
+
+               var cumDataSize int64
+               valueScan := valueOffset
+               for li := int64(0); li < batch; li++ {
+                       isValue := defLevels == nil || maxDefLevel == 0 || 
defLevels[levelOffset+li] == maxDefLevel
+                       if isValue && valueScan < valueCount {
+                               valueSize := int64(offsets[valueScan+1]) - 
int64(offsets[valueScan]) + 4
+                               if cumDataSize+valueSize > maxSafeBatchDataSize 
&& li > 0 {
+                                       batch = li
+                                       break
+                               }
+                               cumDataSize += valueSize
+                               valueScan++
+                       }
+               }
+
+               if requiresRowAlignment {
+                       batch = alignBatchToRowBoundary(repLevels, levelOffset, 
batch)
+               }
+               if batch < 1 {
+                       batch = 1
+               }
+
+               toWrite := w.writeLevels(batch, levelSliceOrNil(defLevels, 
levelOffset, batch),
+                       levelSliceOrNil(repLevels, levelOffset, batch))
+               batchOffsets := arrowOffsetsForBatch(offsets, valueOffset, 
toWrite)
+               writeArrowValues(w, values, batchOffsets, batch-toWrite)
+               if err := w.commitWriteAndCheckPageLimit(batch, toWrite); err 
!= nil {
+                       panic(err)
+               }
+               valueOffset += toWrite
+               w.checkDictionarySizeLimit()
+               levelOffset += batch
+       }
+       return valueOffset
+}
+
+func writeBatchSpacedArrow[T byteArrayArrowOffset](w 
*ByteArrayColumnChunkWriter, values []byte, offsets []T, defLevels, repLevels 
[]int16, validBits []byte, validBitsOffset int64) (valueOffset int64) {
+       length := len(defLevels)
+       if defLevels == nil {
+               if len(offsets) > 0 {
+                       length = len(offsets) - 1
+               } else {
+                       length = 0
+               }
+       }
+       if length == 0 {
+               return 0
+       }
+
+       const maxSafeBatchDataSize int64 = 1 << 30
+       batchSize := w.props.WriteBatchSize()
+       requiresRowAlignment := (w.props.DataPageVersion() != 
parquet.DataPageV1 ||
+               w.props.PageIndexEnabledFor(w.descr.Path())) &&
+               repLevels != nil && w.descr.MaxRepetitionLevel() > 0
+       levelOffset := int64(0)
+       n := int64(length)
+       valueCount := int64(0)
+       if len(offsets) > 0 {
+               valueCount = int64(len(offsets) - 1)
+       }
+
+       if requiresRowAlignment {
+               if int64(len(repLevels)) < n {
+                       panic("columnwriter: not enough repetition levels for 
batch to write")
+               }
+               if repLevels[0] != 0 {
+                       panic("columnwriter: row-aligned batch writing must 
start at a row boundary")
+               }
+               repLevels = repLevels[:n]
+       }
+
+       for levelOffset < n {
+               remaining := n - levelOffset
+               batch := min(remaining, batchSize)
+
+               var cumDataSize int64
+               for vi := int64(0); vi < batch && valueOffset+vi < valueCount; 
vi++ {
+                       valueSize := int64(offsets[valueOffset+vi+1]) - 
int64(offsets[valueOffset+vi]) + 4
+                       if cumDataSize+valueSize > maxSafeBatchDataSize && vi > 
0 {
+                               batch = vi
+                               break
+                       }
+                       cumDataSize += valueSize
+               }
+
+               if requiresRowAlignment {
+                       batch = alignBatchToRowBoundary(repLevels, levelOffset, 
batch)
+               }
+               if batch < 1 {
+                       batch = 1
+               }
+
+               info := w.maybeCalculateValidityBits(levelSliceOrNil(defLevels, 
levelOffset, batch), batch)
+               w.writeLevelsSpaced(batch, levelSliceOrNil(defLevels, 
levelOffset, batch),
+                       levelSliceOrNil(repLevels, levelOffset, batch))
+               batchOffsets := arrowOffsetsForBatch(offsets, valueOffset, 
info.numSpaced())
+
+               writeBits := validBits
+               writeBitsOffset := validBitsOffset + valueOffset
+               if w.bitsBuffer != nil {
+                       writeBits = w.bitsBuffer.Bytes()
+                       writeBitsOffset = 0
+               }
+               writeArrowValuesSpaced(w, values, batchOffsets, info.batchNum, 
batch, writeBits, writeBitsOffset)
+               if err := w.commitWriteAndCheckPageLimit(batch, 
info.numSpaced()); err != nil {
+                       panic(err)
+               }
+               valueOffset += info.numSpaced()
+               w.checkDictionarySizeLimit()
+               levelOffset += batch
+       }
+       return valueOffset
+}
+
+// WriteBatchArrow writes Arrow binary data using its value buffer and 32-bit 
offsets.
+// The active encoder must support Arrow offsets; dictionary encoders use the 
existing
+// parquet.ByteArray path instead.
+func (w *ByteArrayColumnChunkWriter) WriteBatchArrow(values []byte, offsets 
[]int32, defLevels, repLevels []int16) (valueOffset int64, err error) {
+       defer func() {
+               if r := recover(); r != nil {
+                       err = utils.FormatRecoveredError("unknown error type", 
r)
+               }
+       }()
+       if _, ok := w.currentEncoder.(byteArrayArrowEncoder32); !ok {
+               return 0, fmt.Errorf("parquet: current byte-array encoder does 
not support 32-bit Arrow offsets")
+       }
+       return writeBatchArrow(w, values, offsets, defLevels, repLevels), nil
+}
+
+// WriteBatchArrow64 writes Arrow binary data using its value buffer and 
64-bit offsets.
+func (w *ByteArrayColumnChunkWriter) WriteBatchArrow64(values []byte, offsets 
[]int64, defLevels, repLevels []int16) (valueOffset int64, err error) {
+       defer func() {
+               if r := recover(); r != nil {
+                       err = utils.FormatRecoveredError("unknown error type", 
r)
+               }
+       }()
+       if _, ok := w.currentEncoder.(byteArrayArrowEncoder64); !ok {
+               return 0, fmt.Errorf("parquet: current byte-array encoder does 
not support 64-bit Arrow offsets")
+       }
+       return writeBatchArrow(w, values, offsets, defLevels, repLevels), nil
+}
+
+// WriteBatchSpacedArrow writes spaced Arrow binary data using 32-bit offsets.
+func (w *ByteArrayColumnChunkWriter) WriteBatchSpacedArrow(values []byte, 
offsets []int32, defLevels, repLevels []int16, validBits []byte, 
validBitsOffset int64) (valueOffset int64, err error) {
+       defer func() {
+               if r := recover(); r != nil {
+                       err = utils.FormatRecoveredError("unknown error type", 
r)
+               }
+       }()
+       if _, ok := w.currentEncoder.(byteArrayArrowEncoder32); !ok {
+               return 0, fmt.Errorf("parquet: current byte-array encoder does 
not support 32-bit Arrow offsets")
+       }
+       return writeBatchSpacedArrow(w, values, offsets, defLevels, repLevels, 
validBits, validBitsOffset), nil
+}
+
+// WriteBatchSpacedArrow64 writes spaced Arrow binary data using 64-bit 
offsets.
+func (w *ByteArrayColumnChunkWriter) WriteBatchSpacedArrow64(values []byte, 
offsets []int64, defLevels, repLevels []int16, validBits []byte, 
validBitsOffset int64) (valueOffset int64, err error) {
+       defer func() {
+               if r := recover(); r != nil {
+                       err = utils.FormatRecoveredError("unknown error type", 
r)
+               }
+       }()
+       if _, ok := w.currentEncoder.(byteArrayArrowEncoder64); !ok {
+               return 0, fmt.Errorf("parquet: current byte-array encoder does 
not support 64-bit Arrow offsets")
+       }
+       return writeBatchSpacedArrow(w, values, offsets, defLevels, repLevels, 
validBits, validBitsOffset), nil
+}
diff --git a/parquet/internal/encoding/byte_array_arrow_encoder.go 
b/parquet/internal/encoding/byte_array_arrow_encoder.go
new file mode 100644
index 00000000..b5253a55
--- /dev/null
+++ b/parquet/internal/encoding/byte_array_arrow_encoder.go
@@ -0,0 +1,232 @@
+// 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 encoding
+
+import (
+       "encoding/binary"
+
+       "github.com/apache/arrow-go/v18/internal/bitutils"
+       "github.com/apache/arrow-go/v18/parquet"
+)
+
+type arrowByteArrayOffset interface {
+       ~int32 | ~int64
+}
+
+func putArrowPlain[T arrowByteArrayOffset](sink *PooledBufferWriter, values 
[]byte, offsets []T) {
+       if len(offsets) < 2 {
+               return
+       }
+
+       encodedSize := 0
+       for i := 0; i < len(offsets)-1; i++ {
+               encodedSize += int(offsets[i+1]-offsets[i]) + 4
+       }
+
+       sink.Reserve(encodedSize)
+       out := sink.buf.Buf()[sink.pos : sink.pos+encodedSize]
+       for i := 0; i < len(offsets)-1; i++ {
+               start := int(offsets[i])
+               end := int(offsets[i+1])
+               binary.LittleEndian.PutUint32(out, uint32(end-start))
+               copy(out[4:], values[start:end])
+               out = out[4+end-start:]
+       }
+       sink.pos += encodedSize
+}
+
+func putArrowPlainSpaced[T arrowByteArrayOffset](sink *PooledBufferWriter, 
values []byte, offsets []T, validBits []byte, validBitsOffset int64) {
+       if len(offsets) < 2 {
+               return
+       }
+       if validBits == nil {
+               putArrowPlain(sink, values, offsets)
+               return
+       }
+
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), func(pos, length int64) {
+               putArrowPlain(sink, values, offsets[pos:pos+length+1])
+       })
+}
+
+func (enc *PlainByteArrayEncoder) PutArrow(values []byte, offsets []int32) {
+       putArrowPlain(enc.sink, values, offsets)
+}
+
+func (enc *PlainByteArrayEncoder) PutArrow64(values []byte, offsets []int64) {
+       putArrowPlain(enc.sink, values, offsets)
+}
+
+func (enc *PlainByteArrayEncoder) PutArrowSpaced(values []byte, offsets 
[]int32, validBits []byte, validBitsOffset int64) {
+       putArrowPlainSpaced(enc.sink, values, offsets, validBits, 
validBitsOffset)
+}
+
+func (enc *PlainByteArrayEncoder) PutArrowSpaced64(values []byte, offsets 
[]int64, validBits []byte, validBitsOffset int64) {
+       putArrowPlainSpaced(enc.sink, values, offsets, validBits, 
validBitsOffset)
+}
+
+func putArrowDeltaLength[T arrowByteArrayOffset](enc 
*DeltaLengthByteArrayEncoder, values []byte, offsets []T, validBits []byte, 
validBitsOffset int64) {
+       if len(offsets) < 2 {
+               return
+       }
+
+       if validBits == nil {
+               batchSize := 0
+               totalLen := 0
+               for i := 0; i < len(offsets)-1; i++ {
+                       start := int(offsets[i])
+                       end := int(offsets[i+1])
+                       enc.lengths[batchSize] = int32(end - start)
+                       totalLen += end - start
+                       batchSize++
+                       if batchSize == len(enc.lengths) {
+                               enc.lengthEncoder.Put(enc.lengths[:batchSize])
+                               batchSize = 0
+                       }
+               }
+               if batchSize != 0 {
+                       enc.lengthEncoder.Put(enc.lengths[:batchSize])
+               }
+
+               enc.sink.Reserve(totalLen)
+               for i := 0; i < len(offsets)-1; i++ {
+                       
enc.sink.UnsafeWrite(values[int(offsets[i]):int(offsets[i+1])])
+               }
+               return
+       }
+
+       batchSize := 0
+       totalLen := 0
+       visit := func(pos, length int64) {
+               for i := pos; i < pos+length; i++ {
+                       start := int(offsets[i])
+                       end := int(offsets[i+1])
+                       enc.lengths[batchSize] = int32(end - start)
+                       totalLen += end - start
+                       batchSize++
+                       if batchSize == len(enc.lengths) {
+                               enc.lengthEncoder.Put(enc.lengths[:batchSize])
+                               batchSize = 0
+                       }
+               }
+       }
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), visit)
+       if batchSize != 0 {
+               enc.lengthEncoder.Put(enc.lengths[:batchSize])
+       }
+
+       enc.sink.Reserve(totalLen)
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), func(pos, length int64) {
+               for i := pos; i < pos+length; i++ {
+                       
enc.sink.UnsafeWrite(values[int(offsets[i]):int(offsets[i+1])])
+               }
+       })
+}
+
+func (enc *DeltaLengthByteArrayEncoder) PutArrow(values []byte, offsets 
[]int32) {
+       putArrowDeltaLength(enc, values, offsets, nil, 0)
+}
+
+func (enc *DeltaLengthByteArrayEncoder) PutArrow64(values []byte, offsets 
[]int64) {
+       putArrowDeltaLength(enc, values, offsets, nil, 0)
+}
+
+func (enc *DeltaLengthByteArrayEncoder) PutArrowSpaced(values []byte, offsets 
[]int32, validBits []byte, validBitsOffset int64) {
+       putArrowDeltaLength(enc, values, offsets, validBits, validBitsOffset)
+}
+
+func (enc *DeltaLengthByteArrayEncoder) PutArrowSpaced64(values []byte, 
offsets []int64, validBits []byte, validBitsOffset int64) {
+       putArrowDeltaLength(enc, values, offsets, validBits, validBitsOffset)
+}
+
+func putArrowDeltaByte[T arrowByteArrayOffset](enc *DeltaByteArrayEncoder, 
values []byte, offsets []T, validBits []byte, validBitsOffset int64) {
+       if len(offsets) < 2 {
+               return
+       }
+
+       if enc.prefixEncoder == nil {
+               enc.initEncoders()
+       }
+
+       lastVal := enc.lastVal
+       if validBits == nil {
+               batchSize := 0
+               for i := 0; i < len(offsets)-1; i++ {
+                       val := 
parquet.ByteArray(values[int(offsets[i]):int(offsets[i+1])])
+                       prefixLength := commonPrefixLength(lastVal, val)
+                       lastVal = val
+                       enc.prefixLengths[batchSize] = int32(prefixLength)
+                       enc.suffixes[batchSize] = val[prefixLength:]
+                       batchSize++
+                       if batchSize == len(enc.suffixes) {
+                               enc.suffixEncoder.Put(enc.suffixes[:batchSize])
+                               
enc.prefixEncoder.Put(enc.prefixLengths[:batchSize])
+                               clear(enc.suffixes[:batchSize])
+                               batchSize = 0
+                       }
+               }
+               if batchSize != 0 {
+                       enc.suffixEncoder.Put(enc.suffixes[:batchSize])
+                       enc.prefixEncoder.Put(enc.prefixLengths[:batchSize])
+                       clear(enc.suffixes[:batchSize])
+               }
+               enc.lastVal = append(enc.lastVal[:0], lastVal...)
+               return
+       }
+
+       batchSize := 0
+       visit := func(pos, length int64) {
+               for i := pos; i < pos+length; i++ {
+                       val := 
parquet.ByteArray(values[int(offsets[i]):int(offsets[i+1])])
+                       prefixLength := commonPrefixLength(lastVal, val)
+                       lastVal = val
+                       enc.prefixLengths[batchSize] = int32(prefixLength)
+                       enc.suffixes[batchSize] = val[prefixLength:]
+                       batchSize++
+                       if batchSize == len(enc.suffixes) {
+                               enc.suffixEncoder.Put(enc.suffixes[:batchSize])
+                               
enc.prefixEncoder.Put(enc.prefixLengths[:batchSize])
+                               clear(enc.suffixes[:batchSize])
+                               batchSize = 0
+                       }
+               }
+       }
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), visit)
+       if batchSize != 0 {
+               enc.suffixEncoder.Put(enc.suffixes[:batchSize])
+               enc.prefixEncoder.Put(enc.prefixLengths[:batchSize])
+               clear(enc.suffixes[:batchSize])
+       }
+
+       enc.lastVal = append(enc.lastVal[:0], lastVal...)
+}
+
+func (enc *DeltaByteArrayEncoder) PutArrow(values []byte, offsets []int32) {
+       putArrowDeltaByte(enc, values, offsets, nil, 0)
+}
+
+func (enc *DeltaByteArrayEncoder) PutArrow64(values []byte, offsets []int64) {
+       putArrowDeltaByte(enc, values, offsets, nil, 0)
+}
+
+func (enc *DeltaByteArrayEncoder) PutArrowSpaced(values []byte, offsets 
[]int32, validBits []byte, validBitsOffset int64) {
+       putArrowDeltaByte(enc, values, offsets, validBits, validBitsOffset)
+}
+
+func (enc *DeltaByteArrayEncoder) PutArrowSpaced64(values []byte, offsets 
[]int64, validBits []byte, validBitsOffset int64) {
+       putArrowDeltaByte(enc, values, offsets, validBits, validBitsOffset)
+}
diff --git a/parquet/internal/encoding/byte_array_arrow_encoder_test.go 
b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
new file mode 100644
index 00000000..949d5e65
--- /dev/null
+++ b/parquet/internal/encoding/byte_array_arrow_encoder_test.go
@@ -0,0 +1,190 @@
+// 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 encoding
+
+import (
+       "fmt"
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/stretchr/testify/require"
+)
+
+type arrowByteArrayEncoder32Test interface {
+       ByteArrayEncoder
+       PutArrow([]byte, []int32)
+       PutArrowSpaced([]byte, []int32, []byte, int64)
+}
+
+type arrowByteArrayEncoder64Test interface {
+       ByteArrayEncoder
+       PutArrow64([]byte, []int64)
+       PutArrowSpaced64([]byte, []int64, []byte, int64)
+}
+
+func arrowByteArrayInput() ([]parquet.ByteArray, []byte, []int32, []int64) {
+       values := []parquet.ByteArray{
+               []byte("prefix/000"),
+               []byte("prefix/001"),
+               {},
+               []byte("prefix/003"),
+               []byte("other"),
+               {},
+               []byte("other/longer"),
+       }
+       data := make([]byte, 0, 64)
+       offsets32 := []int32{0}
+       offsets64 := []int64{0}
+       for _, value := range values {
+               data = append(data, value...)
+               offsets32 = append(offsets32, int32(len(data)))
+               offsets64 = append(offsets64, int64(len(data)))
+       }
+       return values, data, offsets32, offsets64
+}
+
+func encodedByteArrays(t *testing.T, encoding parquet.Encoding, values 
[]parquet.ByteArray, spaced bool, validBits []byte, validBitsOffset int64) 
[]byte {
+       t.Helper()
+       enc := NewEncoder(parquet.Types.ByteArray, encoding, false, nil, 
memory.DefaultAllocator).(ByteArrayEncoder)
+       defer enc.Release()
+       if spaced {
+               enc.PutSpaced(values, validBits, validBitsOffset)
+       } else {
+               enc.Put(values)
+       }
+       buf, err := enc.FlushValues()
+       require.NoError(t, err)
+       defer buf.Release()
+       return append([]byte(nil), buf.Bytes()...)
+}
+
+func TestByteArrayArrowEncodersMatchByteArrayInput(t *testing.T) {
+       values, data, offsets32, offsets64 := arrowByteArrayInput()
+       validBits := make([]byte, bitutil.BytesForBits(int64(len(values)+5)))
+       validBitsOffset := int64(3)
+       for _, index := range []int{0, 1, 3, 4, 6} {
+               bitutil.SetBit(validBits, int(validBitsOffset)+index)
+       }
+
+       for _, encoding := range []parquet.Encoding{
+               parquet.Encodings.Plain,
+               parquet.Encodings.DeltaLengthByteArray,
+               parquet.Encodings.DeltaByteArray,
+       } {
+               t.Run(encoding.String(), func(t *testing.T) {
+                       want := encodedByteArrays(t, encoding, values, false, 
nil, 0)
+                       wantSpaced := encodedByteArrays(t, encoding, values, 
true, validBits, validBitsOffset)
+
+                       for _, width := range []string{"int32", "int64"} {
+                               t.Run(width, func(t *testing.T) {
+                                       enc := 
NewEncoder(parquet.Types.ByteArray, encoding, false, nil, 
memory.DefaultAllocator).(ByteArrayEncoder)
+                                       defer enc.Release()
+                                       if width == "int32" {
+                                               direct := 
enc.(arrowByteArrayEncoder32Test)
+                                               direct.PutArrow(data, offsets32)
+                                       } else {
+                                               direct := 
enc.(arrowByteArrayEncoder64Test)
+                                               direct.PutArrow64(data, 
offsets64)
+                                       }
+                                       buf, err := enc.FlushValues()
+                                       require.NoError(t, err)
+                                       require.Equal(t, want, buf.Bytes())
+                                       buf.Release()
+
+                                       if width == "int32" {
+                                               direct := 
enc.(arrowByteArrayEncoder32Test)
+                                               direct.PutArrowSpaced(data, 
offsets32, validBits, validBitsOffset)
+                                       } else {
+                                               direct := 
enc.(arrowByteArrayEncoder64Test)
+                                               direct.PutArrowSpaced64(data, 
offsets64, validBits, validBitsOffset)
+                                       }
+                                       buf, err = enc.FlushValues()
+                                       require.NoError(t, err)
+                                       require.Equal(t, wantSpaced, 
buf.Bytes())
+                                       buf.Release()
+                               })
+                       }
+               })
+       }
+}
+
+func TestDeltaByteArrayArrowEncoderPreservesStateAcrossBatches(t *testing.T) {
+       values, data, offsets32, offsets64 := arrowByteArrayInput()
+
+       for _, width := range []string{"int32", "int64"} {
+               t.Run(width, func(t *testing.T) {
+                       enc := NewEncoder(parquet.Types.ByteArray, 
parquet.Encodings.DeltaByteArray, false, nil, 
memory.DefaultAllocator).(ByteArrayEncoder)
+                       defer enc.Release()
+                       direct32, direct64 := 
enc.(arrowByteArrayEncoder32Test), enc.(arrowByteArrayEncoder64Test)
+                       if width == "int32" {
+                               direct32.PutArrow(data, offsets32[:3])
+                               direct32.PutArrow(data, offsets32[2:])
+                       } else {
+                               direct64.PutArrow64(data, offsets64[:3])
+                               direct64.PutArrow64(data, offsets64[2:])
+                       }
+                       got, err := enc.FlushValues()
+                       require.NoError(t, err)
+                       defer got.Release()
+                       want := encodedByteArrays(t, 
parquet.Encodings.DeltaByteArray, values, false, nil, 0)
+                       require.Equal(t, want, got.Bytes())
+               })
+       }
+}
+
+func TestDeltaArrowEncodersAcrossInternalBatches(t *testing.T) {
+       const nvalues = deltaByteArrayBatchSize*2 + 17
+       values := make([]parquet.ByteArray, nvalues)
+       data := make([]byte, 0, nvalues*16)
+       offsets32 := []int32{0}
+       offsets64 := []int64{0}
+       validBits := make([]byte, bitutil.BytesForBits(nvalues+1))
+       validBitsOffset := int64(1)
+       for i := range values {
+               values[i] = 
parquet.ByteArray(fmt.Sprintf("partition/%03d/value", i))
+               data = append(data, values[i]...)
+               offsets32 = append(offsets32, int32(len(data)))
+               offsets64 = append(offsets64, int64(len(data)))
+               if i%5 != 0 {
+                       bitutil.SetBit(validBits, int(validBitsOffset)+i)
+               }
+       }
+
+       for _, encoding := range []parquet.Encoding{
+               parquet.Encodings.DeltaLengthByteArray,
+               parquet.Encodings.DeltaByteArray,
+       } {
+               for _, width := range []string{"int32", "int64"} {
+                       t.Run(encoding.String()+"/"+width, func(t *testing.T) {
+                               want := encodedByteArrays(t, encoding, values, 
true, validBits, validBitsOffset)
+                               enc := NewEncoder(parquet.Types.ByteArray, 
encoding, false, nil, memory.DefaultAllocator).(ByteArrayEncoder)
+                               defer enc.Release()
+                               if width == "int32" {
+                                       
enc.(arrowByteArrayEncoder32Test).PutArrowSpaced(data, offsets32, validBits, 
validBitsOffset)
+                               } else {
+                                       
enc.(arrowByteArrayEncoder64Test).PutArrowSpaced64(data, offsets64, validBits, 
validBitsOffset)
+                               }
+                               got, err := enc.FlushValues()
+                               require.NoError(t, err)
+                               require.Equal(t, want, got.Bytes())
+                               got.Release()
+                       })
+               }
+       }
+}
diff --git a/parquet/metadata/bloom_filter_arrow_offsets.go 
b/parquet/metadata/bloom_filter_arrow_offsets.go
new file mode 100644
index 00000000..1e4bfd8a
--- /dev/null
+++ b/parquet/metadata/bloom_filter_arrow_offsets.go
@@ -0,0 +1,100 @@
+// 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 metadata
+
+import "github.com/apache/arrow-go/v18/internal/bitutils"
+
+type byteArrayArrowHashOffset interface {
+       ~int32 | ~int64
+}
+
+func insertArrowOffsetHashes[T byteArrayArrowHashOffset](b BloomFilterBuilder, 
values []byte, offsets []T, numValid int64, validBits []byte, validBitsOffset 
int64) {
+       if len(offsets) < 2 || numValid == 0 {
+               return
+       }
+
+       h := b.Hasher()
+       var (
+               byteBatch [bloomFilterHashBatchSize][]byte
+               hashBatch [bloomFilterHashBatchSize]uint64
+               batchSize int
+       )
+       flush := func() {
+               b.InsertBulk(sum64s(h, byteBatch[:batchSize], 
hashBatch[:batchSize]))
+               batchSize = 0
+       }
+       if validBits == nil {
+               for i := 0; i < len(offsets)-1; i++ {
+                       byteBatch[batchSize] = 
values[int(offsets[i]):int(offsets[i+1])]
+                       batchSize++
+                       if batchSize == len(byteBatch) {
+                               flush()
+                       }
+               }
+               if batchSize != 0 {
+                       flush()
+               }
+               return
+       }
+
+       visit := func(pos, length int64) {
+               for i := pos; i < pos+length; i++ {
+                       byteBatch[batchSize] = 
values[int(offsets[i]):int(offsets[i+1])]
+                       batchSize++
+                       if batchSize == len(byteBatch) {
+                               flush()
+                       }
+               }
+       }
+
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(len(offsets)-1), visit)
+       if batchSize != 0 {
+               flush()
+       }
+}
+
+// InsertArrowOffsetHashes inserts hashes for values described by 32-bit Arrow
+// offsets without materializing parquet.ByteArray values.
+func InsertArrowOffsetHashes(b BloomFilterBuilder, values []byte, offsets 
[]int32) {
+       numValues := 0
+       if len(offsets) > 0 {
+               numValues = len(offsets) - 1
+       }
+       insertArrowOffsetHashes(b, values, offsets, int64(numValues), nil, 0)
+}
+
+// InsertArrowOffsetHashes64 inserts hashes for values described by 64-bit 
Arrow
+// offsets without materializing parquet.ByteArray values.
+func InsertArrowOffsetHashes64(b BloomFilterBuilder, values []byte, offsets 
[]int64) {
+       numValues := 0
+       if len(offsets) > 0 {
+               numValues = len(offsets) - 1
+       }
+       insertArrowOffsetHashes(b, values, offsets, int64(numValues), nil, 0)
+}
+
+// InsertSpacedArrowOffsetHashes inserts hashes for valid values described by
+// spaced 32-bit Arrow offsets without materializing parquet.ByteArray values.
+func InsertSpacedArrowOffsetHashes(b BloomFilterBuilder, numValid int64, 
values []byte, offsets []int32, validBits []byte, validBitsOffset int64) {
+       insertArrowOffsetHashes(b, values, offsets, numValid, validBits, 
validBitsOffset)
+}
+
+// InsertSpacedArrowOffsetHashes64 inserts hashes for valid values described by
+// spaced 64-bit Arrow offsets without materializing parquet.ByteArray values.
+func InsertSpacedArrowOffsetHashes64(b BloomFilterBuilder, numValid int64, 
values []byte, offsets []int64, validBits []byte, validBitsOffset int64) {
+       insertArrowOffsetHashes(b, values, offsets, numValid, validBits, 
validBitsOffset)
+}
diff --git a/parquet/metadata/byte_array_arrow_offsets.go 
b/parquet/metadata/byte_array_arrow_offsets.go
new file mode 100644
index 00000000..dbb4055d
--- /dev/null
+++ b/parquet/metadata/byte_array_arrow_offsets.go
@@ -0,0 +1,89 @@
+// 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 metadata
+
+import (
+       "github.com/apache/arrow-go/v18/internal/bitutils"
+       "github.com/apache/arrow-go/v18/parquet"
+)
+
+type byteArrayArrowOffset interface {
+       ~int32 | ~int64
+}
+
+func updateByteArrayStatisticsFromArrowOffsets[T byteArrayArrowOffset](s 
*ByteArrayStatistics, values []byte, offsets []T, numNull int64, validBits 
[]byte, validBitsOffset int64, spaced bool) {
+       numValues := 0
+       if len(offsets) > 0 {
+               numValues = len(offsets) - 1
+       }
+
+       s.IncNulls(numNull)
+       if spaced {
+               s.nvalues += int64(numValues) - numNull
+       } else {
+               s.nvalues += int64(numValues)
+       }
+       if numValues == 0 {
+               return
+       }
+
+       min := s.defaultMin()
+       max := s.defaultMax()
+       if validBits == nil {
+               for i := 0; i < numValues; i++ {
+                       value := 
parquet.ByteArray(values[int(offsets[i]):int(offsets[i+1])])
+                       min = s.minval(min, value)
+                       max = s.maxval(max, value)
+               }
+               s.SetMinMax(min, max)
+               return
+       }
+
+       visit := func(pos, length int64) {
+               for i := pos; i < pos+length; i++ {
+                       value := 
parquet.ByteArray(values[int(offsets[i]):int(offsets[i+1])])
+                       min = s.minval(min, value)
+                       max = s.maxval(max, value)
+               }
+       }
+       bitutils.VisitSetBitRunsNoErr(validBits, validBitsOffset, 
int64(numValues), visit)
+       s.SetMinMax(min, max)
+}
+
+// UpdateFromArrowOffsets updates byte-array statistics from an Arrow value 
buffer
+// and 32-bit offsets without materializing parquet.ByteArray values.
+func (s *ByteArrayStatistics) UpdateFromArrowOffsets(values []byte, offsets 
[]int32, numNull int64) {
+       updateByteArrayStatisticsFromArrowOffsets(s, values, offsets, numNull, 
nil, 0, false)
+}
+
+// UpdateFromArrowOffsets64 updates byte-array statistics from an Arrow value 
buffer
+// and 64-bit offsets without materializing parquet.ByteArray values.
+func (s *ByteArrayStatistics) UpdateFromArrowOffsets64(values []byte, offsets 
[]int64, numNull int64) {
+       updateByteArrayStatisticsFromArrowOffsets(s, values, offsets, numNull, 
nil, 0, false)
+}
+
+// UpdateFromArrowOffsetsSpaced updates byte-array statistics from spaced Arrow
+// values using a validity bitmap and 32-bit offsets.
+func (s *ByteArrayStatistics) UpdateFromArrowOffsetsSpaced(values []byte, 
offsets []int32, validBits []byte, validBitsOffset, numNull int64) {
+       updateByteArrayStatisticsFromArrowOffsets(s, values, offsets, numNull, 
validBits, validBitsOffset, true)
+}
+
+// UpdateFromArrowOffsetsSpaced64 updates byte-array statistics from spaced 
Arrow
+// values using a validity bitmap and 64-bit offsets.
+func (s *ByteArrayStatistics) UpdateFromArrowOffsetsSpaced64(values []byte, 
offsets []int64, validBits []byte, validBitsOffset, numNull int64) {
+       updateByteArrayStatisticsFromArrowOffsets(s, values, offsets, numNull, 
validBits, validBitsOffset, true)
+}
diff --git a/parquet/metadata/byte_array_arrow_offsets_test.go 
b/parquet/metadata/byte_array_arrow_offsets_test.go
new file mode 100644
index 00000000..43689178
--- /dev/null
+++ b/parquet/metadata/byte_array_arrow_offsets_test.go
@@ -0,0 +1,114 @@
+// 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 metadata
+
+import (
+       "testing"
+
+       "github.com/apache/arrow-go/v18/arrow/bitutil"
+       "github.com/apache/arrow-go/v18/arrow/memory"
+       "github.com/apache/arrow-go/v18/parquet"
+       "github.com/apache/arrow-go/v18/parquet/schema"
+       "github.com/stretchr/testify/require"
+)
+
+func byteArrayArrowMetadataInput() ([]parquet.ByteArray, []byte, []int32, 
[]int64) {
+       values := []parquet.ByteArray{
+               []byte("alpha"),
+               []byte("beta"),
+               {},
+               []byte("gamma"),
+               []byte("delta"),
+               {},
+       }
+       data := make([]byte, 0, 32)
+       offsets32 := []int32{0}
+       offsets64 := []int64{0}
+       for _, value := range values {
+               data = append(data, value...)
+               offsets32 = append(offsets32, int32(len(data)))
+               offsets64 = append(offsets64, int64(len(data)))
+       }
+       return values, data, offsets32, offsets64
+}
+
+func TestByteArrayStatisticsArrowOffsetsMatchByteArrayInput(t *testing.T) {
+       values, data, offsets32, offsets64 := byteArrayArrowMetadataInput()
+       validBits := make([]byte, bitutil.BytesForBits(int64(len(values)+4)))
+       validBitsOffset := int64(2)
+       for _, index := range []int{0, 1, 3, 4} {
+               bitutil.SetBit(validBits, int(validBitsOffset)+index)
+       }
+
+       column := schema.NewColumn(schema.NewByteArrayNode("value", 
parquet.Repetitions.Optional, -1), 1, 0)
+       want := NewByteArrayStatistics(column, memory.DefaultAllocator)
+       wantSpaced := NewByteArrayStatistics(column, memory.DefaultAllocator)
+       want.Update(values, 0)
+       wantSpaced.UpdateSpaced(values, validBits, validBitsOffset, 2)
+
+       for _, width := range []string{"int32", "int64"} {
+               t.Run(width, func(t *testing.T) {
+                       got := NewByteArrayStatistics(column, 
memory.DefaultAllocator)
+                       gotSpaced := NewByteArrayStatistics(column, 
memory.DefaultAllocator)
+                       if width == "int32" {
+                               got.UpdateFromArrowOffsets(data, offsets32, 0)
+                               gotSpaced.UpdateFromArrowOffsetsSpaced(data, 
offsets32, validBits, validBitsOffset, 2)
+                       } else {
+                               got.UpdateFromArrowOffsets64(data, offsets64, 0)
+                               gotSpaced.UpdateFromArrowOffsetsSpaced64(data, 
offsets64, validBits, validBitsOffset, 2)
+                       }
+                       require.Equal(t, want.Min(), got.Min())
+                       require.Equal(t, want.Max(), got.Max())
+                       require.Equal(t, want.NumValues(), got.NumValues())
+                       require.Equal(t, want.NullCount(), got.NullCount())
+                       require.Equal(t, wantSpaced.Min(), gotSpaced.Min())
+                       require.Equal(t, wantSpaced.Max(), gotSpaced.Max())
+                       require.Equal(t, wantSpaced.NumValues(), 
gotSpaced.NumValues())
+                       require.Equal(t, wantSpaced.NullCount(), 
gotSpaced.NullCount())
+               })
+       }
+}
+
+func TestByteArrayBloomHashesArrowOffsetsMatchByteArrayInput(t *testing.T) {
+       values, data, offsets32, offsets64 := byteArrayArrowMetadataInput()
+       validBits := make([]byte, bitutil.BytesForBits(int64(len(values)+4)))
+       validBitsOffset := int64(2)
+       for _, index := range []int{0, 1, 3, 4} {
+               bitutil.SetBit(validBits, int(validBitsOffset)+index)
+       }
+
+       want := newBatchRecordingBloomFilter(xxhasher{})
+       InsertHashes(want, values)
+       wantSpaced := newBatchRecordingBloomFilter(xxhasher{})
+       InsertSpacedHashes(wantSpaced, 4, values, validBits, validBitsOffset)
+
+       for _, width := range []string{"int32", "int64"} {
+               t.Run(width, func(t *testing.T) {
+                       got := newBatchRecordingBloomFilter(xxhasher{})
+                       gotSpaced := newBatchRecordingBloomFilter(xxhasher{})
+                       if width == "int32" {
+                               InsertArrowOffsetHashes(got, data, offsets32)
+                               InsertSpacedArrowOffsetHashes(gotSpaced, 4, 
data, offsets32, validBits, validBitsOffset)
+                       } else {
+                               InsertArrowOffsetHashes64(got, data, offsets64)
+                               InsertSpacedArrowOffsetHashes64(gotSpaced, 4, 
data, offsets64, validBits, validBitsOffset)
+                       }
+                       require.Equal(t, flattenHashBatches(want.batches), 
flattenHashBatches(got.batches))
+                       require.Equal(t, 
flattenHashBatches(wantSpaced.batches), flattenHashBatches(gotSpaced.batches))
+               })
+       }
+}
diff --git a/parquet/pqarrow/binary_offsets_bench_test.go 
b/parquet/pqarrow/binary_offsets_bench_test.go
new file mode 100644
index 00000000..bdf6eb60
--- /dev/null
+++ b/parquet/pqarrow/binary_offsets_bench_test.go
@@ -0,0 +1,75 @@
+// 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"
+       "strconv"
+       "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/compress"
+       "github.com/apache/arrow-go/v18/parquet/pqarrow"
+)
+
+func benchmarkBinaryTable(mem memory.Allocator, n int) (arrow.Table, int64) {
+       builder := array.NewStringBuilder(mem)
+       builder.Reserve(n)
+       var inputBytes int64
+       for i := 0; i < n; i++ {
+               value := append([]byte("partition/"), strconv.AppendInt(nil, 
int64(i), 10)...)
+               builder.Append(string(value))
+               inputBytes += int64(len(value))
+       }
+       arr := builder.NewArray()
+       builder.Release()
+
+       sch := arrow.NewSchema([]arrow.Field{{Name: "value", Type: 
arrow.BinaryTypes.String}}, nil)
+       col := arrow.NewColumnFromArr(sch.Field(0), arr)
+       arr.Release()
+       tbl := array.NewTable(sch, []arrow.Column{col}, int64(n))
+       col.Release()
+       return tbl, inputBytes
+}
+
+func BenchmarkWriteArrowBinaryOffsets(b *testing.B) {
+       const n = 64 * 1024
+       mem := memory.DefaultAllocator
+       tbl, inputBytes := benchmarkBinaryTable(mem, n)
+       defer tbl.Release()
+
+       for _, stats := range []bool{false, true} {
+               b.Run("stats="+strconv.FormatBool(stats), func(b *testing.B) {
+                       props := parquet.NewWriterProperties(
+                               parquet.WithDictionaryDefault(false),
+                               parquet.WithStats(stats),
+                               
parquet.WithCompression(compress.Codecs.Uncompressed),
+                       )
+                       b.SetBytes(inputBytes)
+                       b.ReportAllocs()
+                       for b.Loop() {
+                               var buf bytes.Buffer
+                               if err := pqarrow.WriteTable(tbl, &buf, 
int64(n), props, pqarrow.DefaultWriterProps()); err != nil {
+                                       b.Fatal(err)
+                               }
+                       }
+               })
+       }
+}
diff --git a/parquet/pqarrow/binary_offsets_test.go 
b/parquet/pqarrow/binary_offsets_test.go
new file mode 100644
index 00000000..a3a6d9ce
--- /dev/null
+++ b/parquet/pqarrow/binary_offsets_test.go
@@ -0,0 +1,234 @@
+// 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/file"
+       "github.com/apache/arrow-go/v18/parquet/pqarrow"
+       "github.com/stretchr/testify/require"
+)
+
+func makeArrowBinaryOffsetsTables(t *testing.T) []struct {
+       name  string
+       table arrow.Table
+} {
+       t.Helper()
+       mem := memory.DefaultAllocator
+       result := make([]struct {
+               name  string
+               table arrow.Table
+       }, 0, 3)
+
+       binaryBuilder := array.NewBinaryBuilder(mem, arrow.BinaryTypes.Binary)
+       for _, value := range [][]byte{[]byte("alpha"), []byte("beta"), nil, 
[]byte("gamma"), {}, []byte("delta")} {
+               if value == nil {
+                       binaryBuilder.AppendNull()
+               } else {
+                       binaryBuilder.Append(value)
+               }
+       }
+       binaryArray := binaryBuilder.NewArray()
+       binaryBuilder.Release()
+       binarySchema := arrow.NewSchema([]arrow.Field{{Name: "value", Type: 
arrow.BinaryTypes.Binary, Nullable: true}}, nil)
+       binaryColumn := arrow.NewColumnFromArr(binarySchema.Field(0), 
binaryArray)
+       binaryArray.Release()
+       result = append(result, struct {
+               name  string
+               table arrow.Table
+       }{"binary", array.NewTable(binarySchema, []arrow.Column{binaryColumn}, 
6)})
+       binaryColumn.Release()
+
+       stringBuilder := array.NewStringBuilder(mem)
+       for _, value := range []string{"alpha", "beta", "", "gamma", "delta", 
"epsilon"} {
+               stringBuilder.Append(value)
+       }
+       stringArray := stringBuilder.NewArray()
+       stringBuilder.Release()
+       stringSchema := arrow.NewSchema([]arrow.Field{{Name: "value", Type: 
arrow.BinaryTypes.String, Nullable: false}}, nil)
+       stringColumn := arrow.NewColumnFromArr(stringSchema.Field(0), 
stringArray)
+       stringArray.Release()
+       result = append(result, struct {
+               name  string
+               table arrow.Table
+       }{"string", array.NewTable(stringSchema, []arrow.Column{stringColumn}, 
6)})
+       stringColumn.Release()
+
+       largeStringBuilder := array.NewLargeStringBuilder(mem)
+       for _, value := range []string{"alpha", "beta", "", "gamma", "delta", 
"epsilon"} {
+               largeStringBuilder.Append(value)
+       }
+       largeStringArray := largeStringBuilder.NewArray()
+       largeStringBuilder.Release()
+       largeStringSchema := arrow.NewSchema([]arrow.Field{{Name: "value", 
Type: arrow.BinaryTypes.LargeString, Nullable: false}}, nil)
+       largeStringColumn := arrow.NewColumnFromArr(largeStringSchema.Field(0), 
largeStringArray)
+       largeStringArray.Release()
+       result = append(result, struct {
+               name  string
+               table arrow.Table
+       }{"large-string", array.NewTable(largeStringSchema, 
[]arrow.Column{largeStringColumn}, 6)})
+       largeStringColumn.Release()
+
+       return result
+}
+
+func TestWriteArrowBinaryOffsets(t *testing.T) {
+       encodings := []parquet.Encoding{
+               parquet.Encodings.Plain,
+               parquet.Encodings.DeltaLengthByteArray,
+               parquet.Encodings.DeltaByteArray,
+       }
+
+       for _, input := range makeArrowBinaryOffsetsTables(t) {
+               input := input
+               t.Run(input.name, func(t *testing.T) {
+                       defer input.table.Release()
+                       for _, encoding := range encodings {
+                               t.Run(encoding.String(), func(t *testing.T) {
+                                       writerProps := 
parquet.NewWriterProperties(
+                                               
parquet.WithDictionaryDefault(false),
+                                               
parquet.WithEncodingFor("value", encoding),
+                                               parquet.WithStats(true),
+                                               parquet.WithBatchSize(2),
+                                               parquet.WithDataPageSize(32),
+                                               
parquet.WithPageIndexEnabled(true),
+                                               
parquet.WithBloomFilterEnabledFor("value", true),
+                                               
parquet.WithBloomFilterNDVFor("value", input.table.NumRows()),
+                                       )
+                                       data := writeParquetTable(t, 
input.table, input.table.NumRows(), writerProps)
+                                       got := readParquetTable(t, data, 
pqarrow.ArrowReadProperties{})
+                                       defer got.Release()
+                                       if input.name == "large-string" {
+                                               wantArr := 
input.table.Column(0).Data().Chunk(0)
+                                               gotArr := 
got.Column(0).Data().Chunk(0)
+                                               for i := 0; i < wantArr.Len(); 
i++ {
+                                                       require.Equal(t, 
wantArr.IsNull(i), gotArr.IsNull(i))
+                                                       if !wantArr.IsNull(i) {
+                                                               
require.Equal(t, binaryOffsetValue(wantArr, i), binaryOffsetValue(gotArr, i))
+                                                       }
+                                               }
+                                       } else {
+                                               assertTableColumnsEqual(t, 
input.table, got)
+                                       }
+                               })
+                       }
+               })
+       }
+}
+
+func binaryOffsetValue(arr arrow.Array, index int) []byte {
+       switch arr := arr.(type) {
+       case *array.Binary:
+               return arr.Value(index)
+       case *array.LargeBinary:
+               return arr.Value(index)
+       case *array.String:
+               return []byte(arr.Value(index))
+       case *array.LargeString:
+               return []byte(arr.Value(index))
+       default:
+               panic("unexpected binary array type")
+       }
+}
+
+func TestWriteArrowBinaryOffsetsWithSlice(t *testing.T) {
+       mem := memory.DefaultAllocator
+       builder := array.NewStringBuilder(mem)
+       for _, value := range []string{"zero", "one", "two", "three", "four", 
"five"} {
+               builder.Append(value)
+       }
+       full := builder.NewArray()
+       builder.Release()
+       defer full.Release()
+
+       sliced := array.NewSlice(full, 1, 5)
+       defer sliced.Release()
+       field := arrow.Field{Name: "value", Type: arrow.BinaryTypes.String, 
Nullable: false}
+       schema := arrow.NewSchema([]arrow.Field{field}, nil)
+       column := arrow.NewColumnFromArr(field, sliced)
+       table := array.NewTable(schema, []arrow.Column{column}, 
int64(sliced.Len()))
+       column.Release()
+       defer table.Release()
+
+       writerProps := parquet.NewWriterProperties(
+               parquet.WithDictionaryDefault(false),
+               parquet.WithBatchSize(2),
+               parquet.WithEncodingFor("value", parquet.Encodings.Plain),
+       )
+       data := writeParquetTable(t, table, table.NumRows(), writerProps)
+       got := readParquetTable(t, data, pqarrow.ArrowReadProperties{})
+       defer got.Release()
+
+       wantArr := table.Column(0).Data().Chunk(0)
+       gotArr := got.Column(0).Data().Chunk(0)
+       for i := 0; i < wantArr.Len(); i++ {
+               require.Equal(t, binaryOffsetValue(wantArr, i), 
binaryOffsetValue(gotArr, i))
+       }
+}
+
+func TestWriteArrowBinaryOffsetsNestedNullPageIndex(t *testing.T) {
+       for _, dtype := range []arrow.DataType{arrow.BinaryTypes.Binary, 
arrow.BinaryTypes.LargeBinary,
+               arrow.BinaryTypes.String, arrow.BinaryTypes.LargeString} {
+               for _, pageVersion := range 
[]parquet.DataPageVersion{parquet.DataPageV1, parquet.DataPageV2} {
+                       for _, enc := range 
[]parquet.Encoding{parquet.Encodings.Plain,
+                               parquet.Encodings.DeltaLengthByteArray, 
parquet.Encodings.DeltaByteArray} {
+                               t.Run(fmt.Sprintf("%s/page-%d/%s", dtype, 
pageVersion, enc), func(t *testing.T) {
+                                       builder := 
array.NewListBuilder(memory.DefaultAllocator, dtype)
+                                       defer builder.Release()
+                                       builder.Append(true)
+                                       value := "aaa"
+                                       if dtype.ID() == arrow.BINARY || 
dtype.ID() == arrow.LARGE_BINARY {
+                                               value = "YWFh"
+                                       }
+                                       require.NoError(t, 
builder.ValueBuilder().AppendValueFromString(value))
+                                       builder.ValueBuilder().AppendNull()
+                                       builder.AppendNull()
+                                       arr := builder.NewListArray()
+                                       defer arr.Release()
+                                       field := arrow.Field{Name: "value", 
Type: arr.DataType(), Nullable: true}
+                                       column := arrow.NewColumnFromArr(field, 
arr)
+                                       defer column.Release()
+                                       tbl := 
array.NewTable(arrow.NewSchema([]arrow.Field{field}, nil), 
[]arrow.Column{column}, int64(arr.Len()))
+                                       defer tbl.Release()
+                                       props := 
parquet.NewWriterProperties(parquet.WithDictionaryDefault(false),
+                                               
parquet.WithDataPageVersion(pageVersion), parquet.WithEncoding(enc),
+                                               
parquet.WithPageIndexEnabled(true))
+                                       data := writeParquetTable(t, tbl, 
tbl.NumRows(), props)
+                                       reader, err := 
file.NewParquetReader(bytes.NewReader(data))
+                                       require.NoError(t, err)
+                                       defer reader.Close()
+                                       rgIndex, err := 
reader.GetPageIndexReader().RowGroup(0)
+                                       require.NoError(t, err)
+                                       index, err := rgIndex.GetColumnIndex(0)
+                                       require.NoError(t, err)
+                                       require.NotNil(t, index)
+                                       require.Equal(t, []bool{false}, 
index.GetNullPages())
+                                       require.Equal(t, []int64{2}, 
index.GetNullCounts())
+                                       require.Equal(t, 
[][]byte{[]byte("aaa")}, index.GetMinValues())
+                                       require.Equal(t, 
[][]byte{[]byte("aaa")}, index.GetMaxValues())
+                               })
+                       }
+               }
+       }
+}
diff --git a/parquet/pqarrow/encode_arrow.go b/parquet/pqarrow/encode_arrow.go
index 3824aa98..dd865c02 100644
--- a/parquet/pqarrow/encode_arrow.go
+++ b/parquet/pqarrow/encode_arrow.go
@@ -661,6 +661,30 @@ func writeDenseArrow(ctx *arrowWriteContext, cw 
file.ColumnChunkWriter, leafArr
                        valueBuf = buffer.Bytes()
                }
 
+               if wr.SupportsArrowOffsets() {
+                       switch leafArr.DataType().ID() {
+                       case arrow.BINARY, arrow.STRING:
+                               offsets := leafArr.(binaryarr).ValueOffsets()
+                               if !maybeParentNulls && noNulls {
+                                       _, err = wr.WriteBatchArrow(valueBuf, 
offsets, defLevels, repLevels)
+                               } else {
+                                       _, err = 
wr.WriteBatchSpacedArrow(valueBuf, offsets, defLevels, repLevels,
+                                               leafArr.NullBitmapBytes(), 
int64(leafArr.Data().Offset()))
+                               }
+                       case arrow.LARGE_BINARY, arrow.LARGE_STRING:
+                               offsets := leafArr.(binary64arr).ValueOffsets()
+                               if !maybeParentNulls && noNulls {
+                                       _, err = wr.WriteBatchArrow64(valueBuf, 
offsets, defLevels, repLevels)
+                               } else {
+                                       _, err = 
wr.WriteBatchSpacedArrow64(valueBuf, offsets, defLevels, repLevels,
+                                               leafArr.NullBitmapBytes(), 
int64(leafArr.Data().Offset()))
+                               }
+                       default:
+                               return fmt.Errorf("%w: invalid column type to 
write to ByteArray: %s", arrow.ErrInvalid, leafArr.DataType().Name())
+                       }
+                       return err
+               }
+
                data := make([]parquet.ByteArray, leafArr.Len())
                switch leafArr.DataType().ID() {
                case arrow.BINARY, arrow.STRING:

Reply via email to